diff options
Diffstat (limited to 'libs')
592 files changed, 19968 insertions, 5357 deletions
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/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/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index fc955927f3ed..418ff0e7263a 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -37,6 +37,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.util.SparseArray; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; @@ -58,14 +59,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Currently applied split configuration. 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. + */ + private final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); // Callback to Jetpack to notify about changes to split states. private @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback; private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); // We currently only support split activity embedding within the one root Task. + // TODO(b/207720388): move to TaskContainer private final Rect mParentBounds = new Rect(); public SplitController() { @@ -244,7 +251,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); } else { // Put activity into a new expanded container - final TaskFragmentContainer newContainer = newContainer(launchedActivity); + final TaskFragmentContainer newContainer = newContainer(launchedActivity, + launchedActivity.getTaskId()); mPresenter.expandActivity(newContainer.getTaskFragmentToken(), launchedActivity); } @@ -327,12 +335,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { - for (TaskFragmentContainer container : mContainers) { - if (container.hasActivity(activityToken)) { - 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.hasActivity(activityToken)) { + return container; + } } } - return null; } @@ -340,9 +350,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Creates and registers a new organized container with an optional activity that will be * re-parented to it in a WCT. */ - TaskFragmentContainer newContainer(@Nullable Activity activity) { - TaskFragmentContainer container = new TaskFragmentContainer(activity); - mContainers.add(container); + TaskFragmentContainer newContainer(@Nullable Activity activity, int taskId) { + final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId); + if (!mTaskContainers.contains(taskId)) { + mTaskContainers.put(taskId, new TaskContainer()); + } + mTaskContainers.get(taskId).mContainers.add(container); return container; } @@ -354,13 +367,13 @@ 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); + mTaskContainers.get(primaryContainer.getTaskId()).mSplitContainers.add(splitContainer); } /** @@ -368,15 +381,26 @@ 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 int taskId = container.getTaskId(); + final TaskContainer taskContainer = mTaskContainers.get(taskId); + if (taskContainer == null) { + return; + } + taskContainer.mContainers.remove(container); + if (taskContainer.mContainers.isEmpty()) { + mTaskContainers.remove(taskId); + // No more TaskFragment in this Task, so no need to check split container. + return; + } + + 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); } /** @@ -399,13 +423,17 @@ 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) { return container; } } @@ -434,7 +462,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer == null) { return; } - if (splitContainer != mSplitContainers.get(mSplitContainers.size() - 1)) { + final List<SplitContainer> splitContainers = mTaskContainers.get(container.getTaskId()) + .mSplitContainers; + if (splitContainers == null + || splitContainer != splitContainers.get(splitContainers.size() - 1)) { // Skip position update - it isn't the topmost split. return; } @@ -455,8 +486,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @Nullable private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) { - for (int i = mSplitContainers.size() - 1; i >= 0; i--) { - SplitContainer splitContainer = mSplitContainers.get(i); + final List<SplitContainer> splitContainers = mTaskContainers.get(container.getTaskId()) + .mSplitContainers; + if (splitContainers == null) { + 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; @@ -473,8 +509,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private 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 = mTaskContainers.get(firstContainer.getTaskId()) + .mSplitContainers; + if (splitContainers == null) { + return null; + } + 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) @@ -500,6 +541,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen boolean launchPlaceholderIfNecessary(@NonNull Activity activity) { final TaskFragmentContainer container = getContainerWithActivity( activity.getActivityToken()); + // 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; @@ -584,24 +629,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; + 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. + mPresenter.shouldShowSideBySide(container) + ? container.getSplitRule().getSplitRatio() + : 0.0f); + splitStates.add(splitState); } - 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); } return splitStates; } @@ -611,11 +662,14 @@ 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.getInfo() == null + || container.getInfo().getActivities().size() + != container.collectActivities().size()) { + return false; + } } } return true; @@ -629,7 +683,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (container == null) { return false; } - for (SplitContainer splitContainer : mSplitContainers) { + final List<SplitContainer> splitContainers = mTaskContainers.get(container.getTaskId()) + .mSplitContainers; + if (splitContainers == null) { + return true; + } + for (SplitContainer splitContainer : splitContainers) { if (container.equals(splitContainer.getPrimaryContainer()) || container.equals(splitContainer.getSecondaryContainer())) { return false; @@ -680,9 +739,12 @@ 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; @@ -934,6 +996,7 @@ 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()); } @@ -964,4 +1027,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Not reuse if it needs to destroy the existing. return !pairRule.shouldClearTop(); } + + /** Represents TaskFragments and split pairs below a Task. */ + private static class TaskContainer { + final List<TaskFragmentContainer> mContainers = new ArrayList<>(); + final List<SplitContainer> mSplitContainers = new ArrayList<>(); + } } 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..e7552ff48d52 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -80,7 +80,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { 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); } @@ -103,7 +104,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { primaryActivity, primaryRectBounds, null); // Create new empty task fragment - final TaskFragmentContainer secondaryContainer = mController.newContainer(null); + final TaskFragmentContainer secondaryContainer = mController.newContainer(null, + primaryContainer.getTaskId()); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr(primaryActivity, rule)); createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), @@ -159,7 +161,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Creates a new expanded container. */ TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { - final TaskFragmentContainer newContainer = mController.newContainer(null); + final TaskFragmentContainer newContainer = mController.newContainer(null, + launchingActivity.getTaskId()); final WindowContainerTransaction wct = new WindowContainerTransaction(); createTaskFragment(wct, newContainer.getTaskFragmentToken(), @@ -180,7 +183,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { TaskFragmentContainer container = mController.getContainerWithActivity( activity.getActivityToken()); if (container == null || container == containerToAvoid) { - container = mController.newContainer(activity); + container = mController.newContainer(activity, activity.getTaskId()); final TaskFragmentCreationParams fragmentOptions = createFragmentOptions( @@ -222,10 +225,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( launchingActivity.getActivityToken()); if (primaryContainer == null) { - primaryContainer = mController.newContainer(launchingActivity); + primaryContainer = mController.newContainer(launchingActivity, + launchingActivity.getTaskId()); } - TaskFragmentContainer secondaryContainer = mController.newContainer(null); + TaskFragmentContainer secondaryContainer = mController.newContainer(null, + primaryContainer.getTaskId()); final WindowContainerTransaction wct = new WindowContainerTransaction(); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, rule); 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..e49af41d4eac 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -16,6 +16,8 @@ package androidx.window.extensions.embedding; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; @@ -41,6 +43,9 @@ class TaskFragmentContainer { @NonNull private final IBinder mToken; + /** Parent leaf Task id. */ + private final int mTaskId; + /** * Server-provided task fragment information. */ @@ -71,8 +76,12 @@ class TaskFragmentContainer { * 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 activity, int taskId) { mToken = new Binder("TaskFragmentContainer"); + if (taskId == INVALID_TASK_ID) { + throw new IllegalArgumentException("Invalid Task id"); + } + mTaskId = taskId; if (activity != null) { addPendingAppearedActivity(activity); } @@ -275,6 +284,11 @@ class TaskFragmentContainer { } } + /** Gets the parent leaf Task id. */ + int getTaskId() { + return mTaskId; + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index ee8cb48e3c4c..a4fbdbc493f5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -30,6 +30,7 @@ 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; @@ -41,7 +42,6 @@ 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,7 +60,7 @@ 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; @@ -113,7 +113,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 +170,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 diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp new file mode 100644 index 000000000000..62e8128f9362 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp @@ -0,0 +1,52 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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", + + 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", + ], + + 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..b12b6f6f0ef1 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/AndroidManifest.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. +--> + +<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" /> + </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..b6df876e1956 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions; + +import static com.google.common.truth.Truth.assertThat; + +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; + +@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/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 9d625c089fc2..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", 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_in_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml deleted file mode 100644 index 29d9b257cc59..000000000000 --- a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_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_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/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/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/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_border.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml new file mode 100644 index 000000000000..9bc03112b118 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml @@ -0,0 +1,22 @@ +<?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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/pip_menu_border_radius" /> + <stroke android:width="@dimen/pip_menu_border_width" + android:color="@color/tv_pip_menu_focus_border" /> +</shape>
\ No newline at end of file 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/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 49e2379589a4..b826d03bf765 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -15,38 +15,124 @@ limitations under the License. --> <!-- Layout for TvPipMenuView --> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<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: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" /> - - <com.android.wm.shell.pip.tv.TvPipMenuActionButton - android:id="@+id/tv_pip_menu_close_button" - android:layout_width="@dimen/picture_in_picture_button_width" - 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" /> - - <!-- More TvPipMenuActionButtons may be added here at runtime. --> - - </LinearLayout> - -</FrameLayout> + android:layout_height="match_parent"> + + <ScrollView + android:id="@+id/tv_pip_menu_scroll" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center_horizontal" + android:scrollbars="none" + android:layout_margin="@dimen/pip_menu_outer_space" + android:visibility="gone"/> + + <HorizontalScrollView + android:id="@+id/tv_pip_menu_horizontal_scroll" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:scrollbars="none" + android:layout_margin="@dimen/pip_menu_outer_space"> + + <LinearLayout + android:id="@+id/tv_pip_menu_action_buttons" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center" + android:layout_gravity="center" + 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_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" /> + + <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. --> + + <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_menu_frame" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:alpha="0" + 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" /> + + <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" /> + + <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" /> + + <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..a86a14525022 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,19 @@ 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:layout_margin="@dimen/pip_menu_button_margin" + android:background="@drawable/tv_pip_button_bg" + 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" /> - - <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_additional_action_button.xml deleted file mode 100644 index bf4eb2691ff0..000000000000 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_additional_action_button.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2020 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<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" /> diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index c3ae053d156d..2476f65c7e5b 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Maak toe"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Vou uit"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Instellings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Gaan by verdeelde skerm in"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Kieslys"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in beeld-in-beeld"</string> <string name="pip_notification_message" msgid="8854051911700302620">"As jy nie wil hê dat <xliff:g id="NAME">%s</xliff:g> hierdie kenmerk moet gebruik nie, tik om instellings oop te maak en skakel dit af."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Slaan oor na volgende"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Slaan oor na vorige"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Verander grootte"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Hou vas"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Laat los"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Program sal dalk nie met verdeelde skerm werk nie."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Program steun nie verdeelde skerm nie."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Program sal dalk nie op \'n sekondêre skerm werk nie."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Het dit"</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..c87bec093cca 100644 --- a/libs/WindowManager/Shell/res/values-af/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-af/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Titellose program)"</string> <string name="pip_close" msgid="9135220303720555525">"Maak PIP toe"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Volskerm"</string> + <string name="pip_move" msgid="1544227837964635439">"Skuif PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Vou PIP uit"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Vou PIP in"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dubbeldruk "<annotation icon="home_icon">" TUIS "</annotation>" vir kontroles"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index c889039cffdb..f0c391cd6b99 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ዝጋ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ዘርጋ"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ቅንብሮች"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"የተከፈለ ማያ ገጽን አስገባ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ምናሌ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> በስዕል-ላይ-ስዕል ውስጥ ነው"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ይህን ባህሪ እንዲጠቀም ካልፈለጉ ቅንብሮችን ለመክፈት መታ ያድርጉና ያጥፉት።"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"መተግበሪያ ከተከፈለ ማያ ገጽ ጋር ላይሠራ ይችላል"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"መተግበሪያው የተከፈለ ማያ ገጽን አይደግፍም።"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"መተግበሪያ በሁለተኛ ማሳያ ላይ ላይሠራ ይችላል።"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"ገባኝ"</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..d23353858de6 100644 --- a/libs/WindowManager/Shell/res/values-am/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ርዕስ የሌለው ፕሮግራም)"</string> <string name="pip_close" msgid="9135220303720555525">"PIPን ዝጋ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ሙሉ ማያ ገጽ"</string> + <string name="pip_move" msgid="1544227837964635439">"ፒአይፒ ውሰድ"</string> + <string name="pip_expand" msgid="7605396312689038178">"ፒአይፒን ዘርጋ"</string> + <string name="pip_collapse" msgid="5732233773786896094">"ፒአይፒን ሰብስብ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ለመቆጣጠሪያዎች "<annotation icon="home_icon">"መነሻ"</annotation>"ን ሁለቴ ይጫኑ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 2c89b1d4eb56..aa4b3b704110 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"إغلاق"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"توسيع"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"الإعدادات"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"الدخول في وضع تقسيم الشاشة"</string> <string name="pip_menu_title" msgid="5393619322111827096">"القائمة"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> يظهر في صورة داخل صورة"</string> <string name="pip_notification_message" msgid="8854051911700302620">"إذا كنت لا تريد أن يستخدم <xliff:g id="NAME">%s</xliff:g> هذه الميزة، فانقر لفتح الإعدادات، ثم أوقِف تفعيل هذه الميزة."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"إخفاء"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"إظهار"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"قد لا يعمل التطبيق بشكل سليم في وضع \"تقسيم الشاشة\"."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"التطبيق لا يتيح تقسيم الشاشة."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"قد لا يعمل التطبيق على شاشة عرض ثانوية."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"حسنًا"</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..a1ceda5fc987 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ليس هناك عنوان للبرنامج)"</string> <string name="pip_close" msgid="9135220303720555525">"إغلاق PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ملء الشاشة"</string> + <string name="pip_move" msgid="1544227837964635439">"نقل نافذة داخل النافذة (PIP)"</string> + <string name="pip_expand" msgid="7605396312689038178">"توسيع نافذة داخل النافذة (PIP)"</string> + <string name="pip_collapse" msgid="5732233773786896094">"تصغير نافذة داخل النافذة (PIP)"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" انقر مرتين على "<annotation icon="home_icon">" الصفحة الرئيسية "</annotation>" للوصول لعناصر التحكم."</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 0a74ac6391ce..985d3b9b96fd 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -19,35 +19,38 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="pip_phone_close" msgid="5783752637260411309">"বন্ধ কৰক"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"বিস্তাৰ কৰক"</string> - <string name="pip_phone_settings" msgid="5468987116750491918">"ছেটিংসমূহ"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ছেটিং"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"বিভাজিত স্ক্ৰীন ম’ডলৈ যাওক"</string> <string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> চিত্ৰৰ ভিতৰৰ চিত্ৰত আছে"</string> - <string name="pip_notification_message" msgid="8854051911700302620">"আপুনি যদি <xliff:g id="NAME">%s</xliff:g> সুবিধাটো ব্যৱহাৰ কৰিব নোখোজে, তেন্তে ছেটিংসমূহ খুলিবলৈ টিপক আৰু তালৈ গৈ ইয়াক অফ কৰক।"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"আপুনি যদি <xliff:g id="NAME">%s</xliff:g> সুবিধাটো ব্যৱহাৰ কৰিব নোখোজে, তেন্তে ছেটিং খুলিবলৈ টিপক আৰু তালৈ গৈ ইয়াক অফ কৰক।"</string> <string name="pip_play" msgid="3496151081459417097">"প্লে কৰক"</string> <string name="pip_pause" msgid="690688849510295232">"পজ কৰক"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"পৰৱৰ্তী মিডিয়ালৈ যাওক"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"আগৰটো মিডিয়ালৈ যাওক"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"আকাৰ সলনি কৰক"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"লুকুৱাওক"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"দেখুৱাওক"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"এপ্টোৱে বিভাজিত স্ক্ৰীনৰ সৈতে কাম নকৰিব পাৰে।"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"এপটোৱে বিভাজিত স্ক্ৰীণ সমৰ্থন নকৰে।"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"এপ্টোৱে বিভাজিত স্ক্ৰীন সমৰ্থন নকৰে।"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"গৌণ ডিছপ্লেত এপে সঠিকভাৱে কাম নকৰিব পাৰে।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"গৌণ ডিছপ্লেত এপ্ লঞ্চ কৰিব নোৱাৰি।"</string> - <string name="accessibility_divider" msgid="703810061635792791">"স্প্লিট স্ক্ৰীণৰ বিভাজক"</string> - <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাওঁফালৰ স্ক্ৰীণখন সম্পূৰ্ণ স্ক্ৰীণ কৰক"</string> + <string name="accessibility_divider" msgid="703810061635792791">"স্প্লিট স্ক্ৰীনৰ বিভাজক"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাওঁফালৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"বাওঁফালৰ স্ক্ৰীণখন ৭০% কৰক"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"বাওঁফালৰ স্ক্ৰীণখন ৫০% কৰক"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"বাওঁফালৰ স্ক্ৰীণখন ৩০% কৰক"</string> - <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"সোঁফালৰ স্ক্ৰীণখন সম্পূৰ্ণ স্ক্ৰীণ কৰক"</string> - <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"শীৰ্ষ স্ক্ৰীণখন সম্পূৰ্ণ স্ক্ৰীণ কৰক"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"সোঁফালৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"শীৰ্ষ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ স্ক্ৰীণখন ৭০% কৰক"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ স্ক্ৰীণখন ৫০% কৰক"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"শীর্ষ স্ক্ৰীণখন ৩০% কৰক"</string> - <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"তলৰ স্ক্ৰীণখন সম্পূৰ্ণ স্ক্ৰীণ কৰক"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"তলৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"এখন হাতেৰে ব্যৱহাৰ কৰা ম’ড ব্যৱহাৰ কৰা"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"বাহিৰ হ’বলৈ স্ক্ৰীনখনৰ একেবাৰে তলৰ পৰা ওপৰলৈ ছোৱাইপ কৰক অথবা এপ্টোৰ ওপৰত যিকোনো ঠাইত টিপক"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"এখন হাতেৰে ব্যৱহাৰ কৰা ম\'ডটো আৰম্ভ কৰক"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"এখন হাতেৰে ব্যৱহাৰ কৰা ম\'ডটোৰ পৰা বাহিৰ হওক"</string> - <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g>ৰ bubblesৰ ছেটিংসমূহ"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g>ৰ bubblesৰ ছেটিং"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"অভাৰফ্ল’"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ষ্টেকত পুনৰ যোগ দিয়ক"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>ৰ পৰা <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> @@ -56,7 +59,7 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"শীৰ্ষৰ সোঁফালে নিয়ক"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"বুটামটো বাওঁফালে নিয়ক"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"তলৰ সোঁফালে নিয়ক"</string> - <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ছেটিংসমূহ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ছেটিং"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"বাবল অগ্ৰাহ্য কৰক"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"বাৰ্তালাপ বাবল নকৰিব"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bubbles ব্যৱহাৰ কৰি চাট কৰক"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"বুজি পালোঁ"</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 6c223f45d9b3..8d7bd9f6a27e 100644 --- a/libs/WindowManager/Shell/res/values-as/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-as/strings_tv.xml @@ -20,5 +20,9 @@ <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_fullscreen" msgid="7278047353591302554">"সম্পূৰ্ণ স্ক্ৰীণ"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"সম্পূৰ্ণ স্ক্ৰীন"</string> + <string name="pip_move" msgid="1544227837964635439">"পিপ স্থানান্তৰ কৰক"</string> + <string name="pip_expand" msgid="7605396312689038178">"পিপ বিস্তাৰ কৰক"</string> + <string name="pip_collapse" msgid="5732233773786896094">"পিপ সংকোচন কৰক"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" নিয়ন্ত্ৰণৰ বাবে "<annotation icon="home_icon">" গৃহপৃষ্ঠা "</annotation>" বুটামত দুবাৰ হেঁচক"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 54483bfa730c..8cd9b7a635ab 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Bağlayın"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Genişləndirin"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Bölünmüş ekrana daxil olun"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> şəkil içində şəkildədir"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> tətbiqinin bu funksiyadan istifadə etməyini istəmirsinizsə, ayarları açmaq və deaktiv etmək üçün klikləyin."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Növbətiyə keçin"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Əvvəlkinə keçin"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ölçüsünü dəyişin"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Güvənli məkanda saxlayın"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Güvənli məkandan çıxarın"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Tətbiq bölünmüş ekran ilə işləməyə bilər."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Tətbiq ekran bölünməsini dəstəkləmir."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Tətbiq ikinci ekranda işləməyə bilər."</string> @@ -43,10 +46,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Yuxarı 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Yuxarı 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Aşağı tam ekran"</string> - <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Bir əlli rejimdən istifadə edilir"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Birəlli rejim istifadəsi"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Çıxmaq üçün ekranın aşağısından yuxarıya doğru sürüşdürün və ya tətbiqin yuxarısında istənilən yerə toxunun"</string> - <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Bir əlli rejimi başladın"</string> - <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Bir əlli rejimdən çıxın"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Birəlli rejim başlasın"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Birəlli rejimdən çıxın"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> yumrucuqları üçün ayarlar"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Kənara çıxma"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Yenidən dəstəyə əlavə edin"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Anladım"</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..87c46fa41a01 100644 --- a/libs/WindowManager/Shell/res/values-az/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-az/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP tətbiq edin"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP-ni genişləndirin"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP-ni yığcamlaşdırın"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Nizamlayıcılar üçün "<annotation icon="home_icon">" ƏSAS SƏHİFƏ "</annotation>" süçimini iki dəfə basın"</string> </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 5ed79c4901cd..49524c608543 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Proširi"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Podešavanja"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Uđi na podeljeni ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je slika u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da biste otvorili podešavanja i isključili je."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Pređi na sledeće"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Pređi na prethodno"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promenite veličinu"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stavite u tajnu memoriju"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Uklonite iz tajne memorije"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi sa podeljenim ekranom."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podeljeni ekran."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionisati na sekundarnom ekranu."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Važi"</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..c87f30611a07 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ceo ekran"</string> + <string name="pip_move" msgid="1544227837964635439">"Premesti sliku u slici"</string> + <string name="pip_expand" msgid="7605396312689038178">"Proširi sliku u slici"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Skupi sliku u slici"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" HOME "</annotation>" za kontrole"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index a9a62de6c8f9..1767e0d66241 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Закрыць"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Разгарнуць"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Налады"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Падзяліць экран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> з’яўляецца відарысам у відарысе"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Калі вы не хочаце, каб праграма <xliff:g id="NAME">%s</xliff:g> выкарыстоўвала гэту функцыю, дакраніцеся, каб адкрыць налады і адключыць яе."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Схаваць"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Паказаць"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Праграма можа не працаваць у рэжыме падзеленага экрана."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Праграма не падтрымлівае функцыю дзялення экрана."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Праграма можа не працаваць на дадатковых экранах."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Зразумела"</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..3566bc372820 100644 --- a/libs/WindowManager/Shell/res/values-be/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-be/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Праграма без назвы)"</string> <string name="pip_close" msgid="9135220303720555525">"Закрыць PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Поўнаэкранны рэжым"</string> + <string name="pip_move" msgid="1544227837964635439">"Перамясціць PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Разгарнуць відарыс у відарысе"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Згарнуць відарыс у відарысе"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Двойчы націсніце "<annotation icon="home_icon">" ГАЛОЎНЫ ЭКРАН "</annotation>" для пераходу ў налады"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 80895dc0c032..c22fb86a4d4d 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Затваряне"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Разгъване"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Преминаване към разделен екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> е в режима „Картина в картината“"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ако не искате <xliff:g id="NAME">%s</xliff:g> да използва тази функция, докоснете, за да отворите настройките, и я изключете."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Съхраняване"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Отмяна на съхраняването"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Приложението може да не работи в режим на разделен екран."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложението не поддържа разделен екран."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Възможно е приложението да не работи на алтернативни дисплеи."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Разбрах"</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..91049fd2cf02 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без заглавие)"</string> <string name="pip_close" msgid="9135220303720555525">"Затваряне на PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цял екран"</string> + <string name="pip_move" msgid="1544227837964635439">"„Картина в картина“: Преместв."</string> + <string name="pip_expand" msgid="7605396312689038178">"Разгъване на прозореца за PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Свиване на прозореца за PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" За достъп до контролите натиснете 2 пъти "<annotation icon="home_icon">"НАЧАЛО"</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index bdda799b001f..c0944e0584e6 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"বন্ধ করুন"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"বড় করুন"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"সেটিংস"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"\'স্প্লিট স্ক্রিন\' মোড চালু করুন"</string> <string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</string> <string name="pip_notification_title" msgid="1347104727641353453">"ছবির-মধ্যে-ছবি তে <xliff:g id="NAME">%s</xliff:g> আছেন"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> কে এই বৈশিষ্ট্যটি ব্যবহার করতে দিতে না চাইলে ট্যাপ করে সেটিংসে গিয়ে সেটি বন্ধ করে দিন।"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"স্ট্যাস করুন"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"আনস্ট্যাস করুন"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"অ্যাপটি স্প্লিট স্ক্রিনে কাজ নাও করতে পারে।"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"অ্যাপ্লিকেশান বিভক্ত-স্ক্রিন সমর্থন করে না৷"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"সেকেন্ডারি ডিসপ্লেতে অ্যাপটি কাজ নাও করতে পারে।"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"বুঝেছি"</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..792708d128a5 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিরোনামহীন প্রোগ্রাম)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP বন্ধ করুন"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"পূর্ণ স্ক্রিন"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP সরান"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP বড় করুন"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP আড়াল করুন"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" কন্ট্রোলের জন্য "<annotation icon="home_icon">" হোম "</annotation>" বোতামে ডবল প্রেস করুন"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 759e9b8c415b..ae01c641cc43 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Proširi"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Otvori podijeljeni ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je u načinu priakza Slika u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da otvorite postavke i isključite je."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Preskoči na sljedeći"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Preskoči na prethodni"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promjena veličine"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stavljanje u stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vađenje iz stasha"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi na podijeljenom ekranu."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava dijeljenje ekrana."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće raditi na sekundarnom ekranu."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Razumijem"</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 8e301b0a8f4d..b7f0dca1b5a5 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml @@ -19,6 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> - <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_close" msgid="9135220303720555525">"Zatvori sliku u slici"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli ekran"</string> + <string name="pip_move" msgid="1544227837964635439">"Pokreni sliku u slici"</string> + <string name="pip_expand" msgid="7605396312689038178">"Proširi sliku u slici"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Suzi sliku u slici"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" POČETNI EKRAN "</annotation>" za kontrole"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index 202ea2083b7e..8a522b3e6397 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -20,14 +20,17 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Tanca"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Desplega"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configuració"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Entra al mode de pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> està en pantalla en pantalla"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no vols que <xliff:g id="NAME">%s</xliff:g> utilitzi aquesta funció, toca per obrir la configuració i desactiva-la."</string> <string name="pip_play" msgid="3496151081459417097">"Reprodueix"</string> <string name="pip_pause" msgid="690688849510295232">"Posa en pausa"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"Ves al següent"</string> - <string name="pip_skip_to_prev" msgid="7172158111196394092">"Torna a l\'anterior"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Ves a l\'anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Canvia la mida"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Amaga"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Deixa d\'amagar"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"És possible que l\'aplicació no funcioni amb la pantalla dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'aplicació no admet la pantalla dividida."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"És possible que l\'aplicació no funcioni en una pantalla secundària."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Entesos"</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..1c560c7afa06 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> + <string name="pip_move" msgid="1544227837964635439">"Mou pantalla en pantalla"</string> + <string name="pip_expand" msgid="7605396312689038178">"Desplega pantalla en pantalla"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Replega pantalla en pantalla"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Prem dos cops "<annotation icon="home_icon">" INICI "</annotation>" per accedir als controls"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index 08a4201b2fae..d0cf80aef38c 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zavřít"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Rozbalit"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavení"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aktivovat rozdělenou obrazovku"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Nabídka"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Aplikace <xliff:g id="NAME">%s</xliff:g> je v režimu obraz v obraze"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Pokud nechcete, aby aplikace <xliff:g id="NAME">%s</xliff:g> tuto funkci používala, klepnutím otevřete nastavení a funkci vypněte."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Přeskočit na další"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Přeskočit na předchozí"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Změnit velikost"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Uložit"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušit uložení"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikace v režimu rozdělené obrazovky nemusí fungovat."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikace nepodporuje režim rozdělené obrazovky."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikace na sekundárním displeji nemusí fungovat."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Rozumím"</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..9a8cc2b4d70e 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> + <string name="pip_move" msgid="1544227837964635439">"Přesunout PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Rozbalit PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Sbalit PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Ovládací prvky zobrazíte dvojitým stisknutím "<annotation icon="home_icon">"tlačítka plochy"</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 395f6e75cdbc..bb81c10c6e1b 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Luk"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Udvid"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Indstillinger"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Åbn opdelt skærm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> vises som integreret billede"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke ønsker, at <xliff:g id="NAME">%s</xliff:g> skal benytte denne funktion, kan du åbne indstillingerne og deaktivere den."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Gå videre til næste"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Gå til forrige"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Rediger størrelse"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Skjul"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vis"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen fungerer muligvis ikke i opdelt skærm."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen understøtter ikke opdelt skærm."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer muligvis ikke på sekundære skærme."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..cba660ac723c 100644 --- a/libs/WindowManager/Shell/res/values-da/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-da/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Fuld skærm"</string> + <string name="pip_move" msgid="1544227837964635439">"Flyt PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Udvid PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Skjul PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Tryk to gange på "<annotation icon="home_icon">" HJEM "</annotation>" for at se indstillinger"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index ab3461a1ebf5..3a1107939c9f 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Schließen"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Maximieren"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Einstellungen"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"„Bildschirm teilen“ aktivieren"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ist in Bild im Bild"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Wenn du nicht möchtest, dass <xliff:g id="NAME">%s</xliff:g> diese Funktion verwendet, tippe, um die Einstellungen zu öffnen und die Funktion zu deaktivieren."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Vorwärts springen"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Rückwärts springen"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Größe anpassen"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"In Stash legen"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Aus Stash entfernen"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Die App funktioniert unter Umständen bei geteiltem Bildschirmmodus nicht."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Das Teilen des Bildschirms wird in dieser App nicht unterstützt."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Die App funktioniert auf einem sekundären Display möglicherweise nicht."</string> @@ -60,9 +63,9 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bubble schließen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Unterhaltung nicht als Bubble anzeigen"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bubbles zum Chatten verwenden"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Neue Unterhaltungen erscheinen als unverankerte Symbole, \"Bubbles\" genannt. Wenn du die Bubble öffnen möchtest, tippe sie an. Wenn du sie verschieben möchtest, zieh an ihr."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Neue Unterhaltungen erscheinen als unverankerte Symbole, „Bubbles“ genannt. Wenn du eine Bubble öffnen möchtest, tippe sie an. Wenn du sie verschieben möchtest, zieh an ihr."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Bubble-Einstellungen festlegen"</string> - <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tippe auf \"Verwalten\", um Bubbles für diese App zu deaktivieren"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tippe auf „Verwalten“, um Bubbles für diese App zu deaktivieren"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Keine kürzlich geschlossenen Bubbles"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Hier werden aktuelle und geschlossene Bubbles angezeigt"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Ok"</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..02a1b66eb63f 100644 --- a/libs/WindowManager/Shell/res/values-de/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-de/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Vollbild"</string> + <string name="pip_move" msgid="1544227837964635439">"BiB verschieben"</string> + <string name="pip_expand" msgid="7605396312689038178">"BiB maximieren"</string> + <string name="pip_collapse" msgid="5732233773786896094">"BiB minimieren"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Für Steuerelemente zweimal "<annotation icon="home_icon">"STARTBILDSCHIRMTASTE"</annotation>" drücken"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index 75e4379284b8..70f55058925c 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Κλείσιμο"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Ανάπτυξη"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ρυθμίσεις"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Μετάβαση σε διαχωρισμό οθόνης"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Μενού"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Η λειτουργία picture-in-picture είναι ενεργή σε <xliff:g id="NAME">%s</xliff:g>."</string> <string name="pip_notification_message" msgid="8854051911700302620">"Εάν δεν θέλετε να χρησιμοποιείται αυτή η λειτουργία από την εφαρμογή <xliff:g id="NAME">%s</xliff:g>, πατήστε για να ανοίξετε τις ρυθμίσεις και απενεργοποιήστε την."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Απόκρυψη"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Κατάργηση απόκρυψης"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Η εφαρμογή ενδέχεται να μην λειτουργεί με διαχωρισμό οθόνης."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Η εφαρμογή δεν υποστηρίζει διαχωρισμό οθόνης."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Η εφαρμογή ίσως να μην λειτουργήσει σε δευτερεύουσα οθόνη."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Το κατάλαβα"</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..24cd030cd754 100644 --- a/libs/WindowManager/Shell/res/values-el/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-el/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Δεν υπάρχει τίτλος προγράμματος)"</string> <string name="pip_close" msgid="9135220303720555525">"Κλείσιμο PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Πλήρης οθόνη"</string> + <string name="pip_move" msgid="1544227837964635439">"Μετακίνηση PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Ανάπτυξη PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Σύμπτυξη PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Πατήστε δύο φορές "<annotation icon="home_icon">" ΑΡΧΙΚΗ ΟΘΟΝΗ "</annotation>" για στοιχεία ελέγχου"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 0d7b60ffefc6..0b5aefa5c72e 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..82257b42814d 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 0d7b60ffefc6..0b5aefa5c72e 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..82257b42814d 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 0d7b60ffefc6..0b5aefa5c72e 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..82257b42814d 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 0d7b60ffefc6..0b5aefa5c72e 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..82257b42814d 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 4bff89d68963..5c3d0f65374a 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Got it"</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..a6e494cfed3c 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 90c4d51b995a..e523ae53b0cc 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Cerrar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Introducir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en modo de Pantalla en pantalla"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> use esta función, presiona para abrir la configuración y desactivarla."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Siguiente"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar el tamaño"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Almacenar de manera segura"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Dejar de almacenar de manera segura"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la app no funcione en el modo de pantalla dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La app no es compatible con la función de pantalla dividida."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la app no funcione en una pantalla secundaria."</string> @@ -58,7 +61,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Ubicar abajo a la derecha"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Descartar burbuja"</string> - <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar la conversación en burbujas"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar la conversación en burbuja"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat con burbujas"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como elementos flotantes o burbujas. Presiona para abrir la burbuja. Arrástrala para moverla."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla las burbujas"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Entendido"</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..458f6b15b857 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Maximizar PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Minimizar PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Presiona dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 2207e62e9503..974960708190 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Cerrar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Mostrar"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ajustes"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Introducir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en imagen en imagen"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca la notificación para abrir los ajustes y desactivarla."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Saltar al siguiente"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Volver al anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar tamaño"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Esconder"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"No esconder"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la aplicación no funcione con la pantalla dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La aplicación no admite la pantalla dividida."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la aplicación no funcione en una pantalla secundaria."</string> @@ -60,7 +63,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cerrar burbuja"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar conversación en burbuja"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatea con burbujas"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamadas \"burbujas\". Toca para abrir la burbuja. Arrastra para moverla."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamadas \"burbujas\". Toca una burbuja para abrirla. Arrástrala para moverla."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla las burbujas"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toca Gestionar para desactivar las burbujas de esta aplicación"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Entendido"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Entendido"</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..0a690984dac5 100644 --- a/libs/WindowManager/Shell/res/values-es/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover imagen en imagen"</string> + <string name="pip_expand" msgid="7605396312689038178">"Mostrar imagen en imagen"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Ocultar imagen en imagen"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Pulsa dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index 9222a91b044c..a5f82a6452c4 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Sule"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Laiendamine"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Seaded"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Ava jagatud ekraanikuva"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menüü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on režiimis Pilt pildis"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Kui te ei soovi, et rakendus <xliff:g id="NAME">%s</xliff:g> seda funktsiooni kasutaks, puudutage seadete avamiseks ja lülitage see välja."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Järgmise juurde"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Eelmise juurde"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Suuruse muutmine"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Pane hoidlasse"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Eemalda hoidlast"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Rakendus ei pruugi poolitatud ekraaniga töötada."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Rakendus ei toeta jagatud ekraani."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Rakendus ei pruugi teisesel ekraanil töötada."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Selge"</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..dc0232303a70 100644 --- a/libs/WindowManager/Shell/res/values-et/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-et/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programmi pealkiri puudub)"</string> <string name="pip_close" msgid="9135220303720555525">"Sule PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Täisekraan"</string> + <string name="pip_move" msgid="1544227837964635439">"Teisalda PIP-režiimi"</string> + <string name="pip_expand" msgid="7605396312689038178">"Laienda PIP-akent"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Ahenda PIP-aken"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Nuppude nägemiseks vajutage 2 korda nuppu "<annotation icon="home_icon">"AVAKUVA"</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 4a59b59df27b..caa335a96222 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Itxi"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Zabaldu"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ezarpenak"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Sartu pantaila zatituan"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menua"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Pantaila txiki gainjarrian dago <xliff:g id="NAME">%s</xliff:g>"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ez baduzu nahi <xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Joan hurrengora"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Joan aurrekora"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Aldatu tamaina"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Gorde"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ez gorde"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Baliteke aplikazioak ez funtzionatzea pantaila zatituan."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikazioak ez du onartzen pantaila zatitua"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Baliteke aplikazioak ez funtzionatzea bigarren mailako pantailetan."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Ados"</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..bce06da2c66f 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa izengabea)"</string> <string name="pip_close" msgid="9135220303720555525">"Itxi PIPa"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantaila osoa"</string> + <string name="pip_move" msgid="1544227837964635439">"Mugitu pantaila txiki gainjarria"</string> + <string name="pip_expand" msgid="7605396312689038178">"Zabaldu pantaila txiki gainjarria"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Tolestu pantaila txiki gainjarria"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Kontrolatzeko aukerak atzitzeko, sakatu birritan "<annotation icon="home_icon">" HASIERA "</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index a17f543278f7..9e7257112c6c 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"بستن"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"بزرگ کردن"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"تنظیمات"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ورود به حالت «صفحهٔ دونیمه»"</string> <string name="pip_menu_title" msgid="5393619322111827096">"منو"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> درحالت تصویر در تصویر است"</string> <string name="pip_notification_message" msgid="8854051911700302620">"اگر نمیخواهید <xliff:g id="NAME">%s</xliff:g> از این قابلیت استفاده کند، با ضربه زدن، تنظیمات را باز کنید و آن را خاموش کنید."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"مخفیسازی"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"لغو مخفیسازی"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن است برنامه با «صفحهٔ دونیمه» کار نکند."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"برنامه از تقسیم صفحه پشتیبانی نمیکند."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن است برنامه در نمایشگر ثانویه کار نکند."</string> @@ -43,7 +46,7 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"٪۵۰ بالا"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"٪۳۰ بالا"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"تمامصفحه پایین"</string> - <string name="one_handed_tutorial_title" msgid="4583241688067426350">"استفاده از «حالت تک حرکت»"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"استفاده از حالت یکدستی"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"برای خارج شدن، از پایین صفحهنمایش تند بهطرف بالا بکشید یا در هر جایی از بالای برنامه که میخواهید ضربه بزنید"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"آغاز «حالت تک حرکت»"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"خروج از «حالت تک حرکت»"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"متوجهام"</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..ff9a03c6cefb 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(برنامه بدون عنوان)"</string> <string name="pip_close" msgid="9135220303720555525">"بستن PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"تمام صفحه"</string> + <string name="pip_move" msgid="1544227837964635439">"انتقال PIP (تصویر در تصویر)"</string> + <string name="pip_expand" msgid="7605396312689038178">"گسترده کردن «تصویر در تصویر»"</string> + <string name="pip_collapse" msgid="5732233773786896094">"جمع کردن «تصویر در تصویر»"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" برای کنترلها، دکمه "<annotation icon="home_icon">"صفحه اصلی"</annotation>" را دوبار فشار دهید"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 332dc9b14da2..c809b4879e71 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Sulje"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Laajenna"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Asetukset"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Avaa jaettu näyttö"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Valikko"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on kuva kuvassa ‑tilassa"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jos et halua, että <xliff:g id="NAME">%s</xliff:g> voi käyttää tätä ominaisuutta, avaa asetukset napauttamalla ja poista se käytöstä."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Siirry seuraavaan"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Siirry edelliseen"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Muuta kokoa"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Lisää turvasäilytykseen"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poista turvasäilytyksestä"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Sovellus ei ehkä toimi jaetulla näytöllä."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Sovellus ei tue jaetun näytön tilaa."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Sovellus ei ehkä toimi toissijaisella näytöllä."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..3e8bf9032780 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nimetön)"</string> <string name="pip_close" msgid="9135220303720555525">"Sulje PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Koko näyttö"</string> + <string name="pip_move" msgid="1544227837964635439">"Siirrä PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Laajenna PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Tiivistä PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Asetukset: paina "<annotation icon="home_icon">"ALOITUSNÄYTTÖPAINIKETTA"</annotation>" kahdesti"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index f51fc66b6b83..62b2bb65a603 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Fermer"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Développer"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Entrer dans l\'écran partagé"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode d\'incrustation d\'image"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, touchez l\'écran pour ouvrir les paramètres, puis désactivez-la."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Passer au suivant"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Revenir au précédent"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionner"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ajouter à la réserve"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Retirer de la réserve"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'application n\'est pas compatible avec l\'écran partagé."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..66e13b89c64b 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Plein écran"</string> + <string name="pip_move" msgid="1544227837964635439">"Déplacer l\'image incrustée"</string> + <string name="pip_expand" msgid="7605396312689038178">"Développer l\'image incrustée"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Réduire l\'image incrustée"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Appuyez deux fois sur "<annotation icon="home_icon">" ACCUEIL "</annotation>" pour les commandes"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index 8fa06e88cd50..b3e22af0a3e3 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Fermer"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Développer"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accéder à l\'écran partagé"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode Picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que l\'application <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, appuyez ici pour ouvrir les paramètres et la désactiver."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Passer au contenu suivant"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Passer au contenu précédent"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionner"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Application incompatible avec l\'écran partagé."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> @@ -61,7 +64,7 @@ <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher la conversation dans une bulle"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatter en utilisant des bulles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> - <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Contrôler les paramètres des bulles"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Contrôlez les bulles à tout moment"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Appuyez sur \"Gérer\" pour désactiver les bulles de cette application"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Aucune bulle récente"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..ed9baf5b6215 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Plein écran"</string> + <string name="pip_move" msgid="1544227837964635439">"Déplacer le PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Développer la fenêtre PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Réduire la fenêtre PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Menu de commandes : appuyez deux fois sur "<annotation icon="home_icon">"ACCUEIL"</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index 56188d4e92ba..b8e039602243 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Pechar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Despregar"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Inserir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está na pantalla superposta"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se non queres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca a configuración para abrir as opcións e desactivar a función."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Ir ao seguinte"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Ir ao anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar tamaño"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Esconder"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Non esconder"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Pode que a aplicación non funcione coa pantalla dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A aplicación non é compatible coa función de pantalla dividida."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É posible que a aplicación non funcione nunha pantalla secundaria."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Entendido"</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..a057434d7853 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover pantalla superposta"</string> + <string name="pip_expand" msgid="7605396312689038178">"Despregar pantalla superposta"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Contraer pantalla superposta"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Preme "<annotation icon="home_icon">"INICIO"</annotation>" dúas veces para acceder aos controis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index b76e91010dcc..deda2d755e20 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"બંધ કરો"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"વિસ્તૃત કરો"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"સેટિંગ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"વિભાજિત સ્ક્રીન મોડમાં દાખલ થાઓ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"મેનૂ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ચિત્રમાં-ચિત્રની અંદર છે"</string> <string name="pip_notification_message" msgid="8854051911700302620">"જો તમે નથી ઇચ્છતા કે <xliff:g id="NAME">%s</xliff:g> આ સુવિધાનો ઉપયોગ કરે, તો સેટિંગ ખોલવા માટે ટૅપ કરો અને તેને બંધ કરો."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"છુપાવો"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"બતાવો"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"વિભાજિત-સ્ક્રીન સાથે ઍપ કદાચ કામ ન કરે."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ઍપ્લિકેશન સ્ક્રીન-વિભાજનનું સમર્થન કરતી નથી."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ઍપ્લિકેશન ગૌણ ડિસ્પ્લે પર કદાચ કામ ન કરે."</string> @@ -59,7 +62,7 @@ <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> સેટિંગ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"બબલને છોડી દો"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"વાતચીતને બબલ કરશો નહીં"</string> - <string name="bubbles_user_education_title" msgid="2112319053732691899">"બબલનો ઉપયોગ કરીને ચેટ કરો"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"બબલનો ઉપયોગ કરીને ચૅટ કરો"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"નવી વાતચીત ફ્લોટિંગ આઇકન અથવા બબલ જેવી દેખાશે. બબલને ખોલવા માટે ટૅપ કરો. તેને ખસેડવા માટે ખેંચો."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"બબલને કોઈપણ સમયે નિયંત્રિત કરો"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"આ ઍપમાંથી બબલને બંધ કરવા માટે મેનેજ કરો પર ટૅપ કરો"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"સમજાઈ ગયું"</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..d9525910e4c6 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(કોઈ ટાઇટલ પ્રોગ્રામ નથી)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP બંધ કરો"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"પૂર્ણ સ્ક્રીન"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ખસેડો"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP મોટી કરો"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP નાની કરો"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" નિયંત્રણો માટે "<annotation icon="home_icon">" હોમ "</annotation>" બટન પર બે વાર દબાવો"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index b2c005544c32..36b11514c7e5 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"बंद करें"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तार करें"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रीन मोड में जाएं"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेन्यू"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"पिक्चर में पिक्चर\" के अंदर है"</string> <string name="pip_notification_message" msgid="8854051911700302620">"अगर आप नहीं चाहते कि <xliff:g id="NAME">%s</xliff:g> इस सुविधा का उपयोग करे, तो सेटिंग खोलने के लिए टैप करें और उसे बंद करें ."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"छिपाएं"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"दिखाएं"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ऐप्लिकेशन शायद स्प्लिट स्क्रीन मोड में काम न करे."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ऐप विभाजित स्क्रीन का समर्थन नहीं करता है."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"हो सकता है कि ऐप प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर काम न करे."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"ठीक है"</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..d897ac73f80d 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(कोई शीर्षक कार्यक्रम नहीं)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP बंद करें"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फ़ुल स्क्रीन"</string> + <string name="pip_move" msgid="1544227837964635439">"पीआईपी को दूसरी जगह लेकर जाएं"</string> + <string name="pip_expand" msgid="7605396312689038178">"पीआईपी विंडो को बड़ा करें"</string> + <string name="pip_collapse" msgid="5732233773786896094">"पीआईपी विंडो को छोटा करें"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" कंट्रोल मेन्यू पर जाने के लिए, "<annotation icon="home_icon">" होम बटन"</annotation>" दो बार दबाएं"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 769d1d26aed2..5ecc5585a6e9 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Proširivanje"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Otvorite podijeljeni zaslon"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Izbornik"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> jest na slici u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da aplikacija <xliff:g id="NAME">%s</xliff:g> upotrebljava tu značajku, dodirnite da biste otvorili postavke i isključili je."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Preskoči na sljedeće"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Preskoči na prethodno"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promjena veličine"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Sakrijte"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poništite sakrivanje"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće funkcionirati s podijeljenim zaslonom."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podijeljeni zaslon."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionirati na sekundarnom zaslonu."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Shvaćam"</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..8f5f3164c4d7 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli zaslon"</string> + <string name="pip_move" msgid="1544227837964635439">"Premjesti PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Proširi PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Sažmi PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">"POČETNI ZASLON"</annotation>" za kontrole"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index 05655f1bb607..2295250e2853 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Bezárás"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Kibontás"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Beállítások"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Váltás osztott képernyőre"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"A(z) <xliff:g id="NAME">%s</xliff:g> kép a képben funkciót használ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ha nem szeretné, hogy a(z) <xliff:g id="NAME">%s</xliff:g> használja ezt a funkciót, koppintson a beállítások megnyitásához, és kapcsolja ki."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Ugrás a következőre"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Ugrás az előzőre"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Átméretezés"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Félretevés"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Félretevés megszüntetése"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Lehet, hogy az alkalmazás nem működik osztott képernyős nézetben."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Az alkalmazás nem támogatja az osztott képernyős nézetet."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Előfordulhat, hogy az alkalmazás nem működik másodlagos kijelzőn."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Rendben"</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..fc8d79589121 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Teljes képernyő"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP áthelyezése"</string> + <string name="pip_expand" msgid="7605396312689038178">"Kép a képben kibontása"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Kép a képben összecsukása"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Vezérlők: "<annotation icon="home_icon">" KEZDŐKÉPERNYŐ "</annotation>" gomb kétszer megnyomva"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 5f7495e63613..208936539094 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Փակել"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Ընդարձակել"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Կարգավորումներ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Մտնել տրոհված էկրանի ռեժիմ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Ընտրացանկ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>-ը «Նկար նկարի մեջ» ռեժիմում է"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Եթե չեք ցանկանում, որ <xliff:g id="NAME">%s</xliff:g>-ն օգտագործի այս գործառույթը, հպեք՝ կարգավորումները բացելու և այն անջատելու համար։"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Թաքցնել"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ցուցադրել"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Հավելվածը չի կարող աշխատել տրոհված էկրանի ռեժիմում։"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Հավելվածը չի աջակցում էկրանի տրոհումը:"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Հավելվածը կարող է չաշխատել լրացուցիչ էկրանի վրա"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Եղավ"</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..f5665b8dd166 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Առանց վերնագրի ծրագիր)"</string> <string name="pip_close" msgid="9135220303720555525">"Փակել PIP-ն"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Լիէկրան"</string> + <string name="pip_move" msgid="1544227837964635439">"Տեղափոխել PIP-ը"</string> + <string name="pip_expand" msgid="7605396312689038178">"Ծավալել PIP-ը"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Ծալել PIP-ը"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Կարգավորումների համար կրկնակի սեղմեք "<annotation icon="home_icon">"ԳԼԽԱՎՈՐ ԷԿՐԱՆ"</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index 2cf50c0644f1..1b46b2fe2570 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Tutup"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Luaskan"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Setelan"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Masuk ke mode layar terpisah"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> adalah picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jika Anda tidak ingin <xliff:g id="NAME">%s</xliff:g> menggunakan fitur ini, ketuk untuk membuka setelan dan menonaktifkannya."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Lewati ke berikutnya"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Lewati ke sebelumnya"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ubah ukuran"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Batalkan stash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikasi mungkin tidak berfungsi dengan layar terpisah."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App tidak mendukung layar terpisah."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikasi mungkin tidak berfungsi pada layar sekunder."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Oke"</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..a1535653f679 100644 --- a/libs/WindowManager/Shell/res/values-in/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-in/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tanpa judul)"</string> <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Layar penuh"</string> + <string name="pip_move" msgid="1544227837964635439">"Pindahkan PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Luaskan PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Ciutkan PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" HOME "</annotation>" untuk membuka kontrol"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 7a3b6a693e7a..a201c95137f3 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Loka"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Stækka"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Stillingar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Opna skjáskiptingu"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Valmynd"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er með mynd í mynd"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ef þú vilt ekki að <xliff:g id="NAME">%s</xliff:g> noti þennan eiginleika skaltu ýta til að opna stillingarnar og slökkva á því."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Fara á næsta"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Fara á fyrra"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Breyta stærð"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Geymsla"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Taka úr geymslu"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Hugsanlega virkar forritið ekki með skjáskiptingu."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Forritið styður ekki að skjánum sé skipt."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Hugsanlegt er að forritið virki ekki á öðrum skjá."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Ég skil"</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..70ca1afe3aea 100644 --- a/libs/WindowManager/Shell/res/values-is/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-is/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Allur skjárinn"</string> + <string name="pip_move" msgid="1544227837964635439">"Færa innfellda mynd"</string> + <string name="pip_expand" msgid="7605396312689038178">"Stækka innfellda mynd"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Minnka innfellda mynd"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Ýttu tvisvar á "<annotation icon="home_icon">" HEIM "</annotation>" til að opna stillingar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index b061d1f592c4..dd5416f2e398 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Chiudi"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Espandi"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Impostazioni"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accedi a schermo diviso"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> è in Picture in picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se non desideri che l\'app <xliff:g id="NAME">%s</xliff:g> utilizzi questa funzione, tocca per aprire le impostazioni e disattivarla."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Passa ai contenuti successivi"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Passa ai contenuti precedenti"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ridimensiona"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Accantona"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Annulla accantonamento"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"L\'app potrebbe non funzionare con lo schermo diviso."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'app non supporta la modalità Schermo diviso."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"L\'app potrebbe non funzionare su un display secondario."</string> @@ -60,7 +63,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignora bolla"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Non mettere la conversazione nella bolla"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatta utilizzando le bolle"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Le nuove conversazioni vengono visualizzate come icone mobili o bolle. Tocca per aprire la bolla. Trascinala per spostarla."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Le nuove conversazioni vengono mostrate come icone mobili o bolle. Tocca per aprire la bolla. Trascinala per spostarla."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controlla le bolle quando vuoi"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tocca Gestisci per disattivare le bolle dall\'app"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..cda627517872 100644 --- a/libs/WindowManager/Shell/res/values-it/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-it/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma senza titolo)"</string> <string name="pip_close" msgid="9135220303720555525">"Chiudi PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Schermo intero"</string> + <string name="pip_move" msgid="1544227837964635439">"Sposta PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Espandi PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Comprimi PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Premi due volte "<annotation icon="home_icon">" HOME "</annotation>" per aprire i controlli"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index 4b4310256766..52a6b0676222 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"סגירה"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"הרחבה"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"הגדרות"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"כניסה למסך המפוצל"</string> <string name="pip_menu_title" msgid="5393619322111827096">"תפריט"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> במצב תמונה בתוך תמונה"</string> <string name="pip_notification_message" msgid="8854051911700302620">"אם אינך רוצה שהתכונה הזו תשמש את <xliff:g id="NAME">%s</xliff:g>, יש להקיש כדי לפתוח את ההגדרות ולהשבית את התכונה."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"הסתרה זמנית"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ביטול ההסתרה הזמנית"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ייתכן שהאפליקציה לא תפעל במסך מפוצל."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"האפליקציה אינה תומכת במסך מפוצל."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ייתכן שהאפליקציה לא תפעל במסך משני."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"הבנתי"</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..30ce97b998ca 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(תוכנית ללא כותרת)"</string> <string name="pip_close" msgid="9135220303720555525">"סגירת PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"מסך מלא"</string> + <string name="pip_move" msgid="1544227837964635439">"העברת תמונה בתוך תמונה (PIP)"</string> + <string name="pip_expand" msgid="7605396312689038178">"הרחבת חלון תמונה-בתוך-תמונה"</string> + <string name="pip_collapse" msgid="5732233773786896094">"כיווץ של חלון תמונה-בתוך-תמונה"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" לחיצה כפולה על "<annotation icon="home_icon">" הלחצן הראשי "</annotation>" תציג את אמצעי הבקרה"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index ab693d2e5045..5a25c24ba034 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"閉じる"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"分割画面に切り替え"</string> <string name="pip_menu_title" msgid="5393619322111827096">"メニュー"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>はピクチャー イン ピクチャーで表示中です"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>でこの機能を使用しない場合は、タップして設定を開いて OFF にしてください。"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"非表示"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"表示"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"アプリは分割画面では動作しないことがあります。"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"アプリで分割画面がサポートされていません。"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"アプリはセカンダリ ディスプレイでは動作しないことがあります。"</string> @@ -61,7 +64,7 @@ <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"会話をバブルで表示しない"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"チャットでバブルを使う"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新しい会話はフローティング アイコン(バブル)として表示されます。タップするとバブルが開きます。ドラッグしてバブルを移動できます。"</string> - <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"いつでもバブルを管理"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"バブルはいつでも管理可能"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"このアプリからのバブルを OFF にするには、[管理] をタップしてください"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"最近閉じたバブルはありません"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..e58e7bf6fabc 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無題の番組)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP を閉じる"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全画面表示"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP を移動"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP を開く"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP を閉じる"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" コントロールにアクセス: "<annotation icon="home_icon">" ホーム "</annotation>" を 2 回押します"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index ef9a84f4ce2f..bff86fa6ffe2 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"დახურვა"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"გაშლა"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"პარამეტრები"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"გაყოფილ ეკრანში შესვლა"</string> <string name="pip_menu_title" msgid="5393619322111827096">"მენიუ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> იყენებს რეჟიმს „ეკრანი ეკრანში“"</string> <string name="pip_notification_message" msgid="8854051911700302620">"თუ არ გსურთ, რომ <xliff:g id="NAME">%s</xliff:g> ამ ფუნქციას იყენებდეს, აქ შეხებით შეგიძლიათ გახსნათ პარამეტრები და გამორთოთ ის."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"გადანახვა"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"გადანახვის გაუქმება"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"აპმა შეიძლება არ იმუშაოს გაყოფილი ეკრანის რეჟიმში."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ეკრანის გაყოფა არ არის მხარდაჭერილი აპის მიერ."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"აპმა შეიძლება არ იმუშაოს მეორეულ ეკრანზე."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"გასაგებია"</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..b09686646c8b 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(პროგრამის სათაურის გარეშე)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP-ის დახურვა"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"სრულ ეკრანზე"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP გადატანა"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP-ის გაშლა"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP-ის ჩაკეცვა"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" მართვის საშუალებებზე წვდომისთვის ორმაგად დააჭირეთ "<annotation icon="home_icon">" მთავარ ღილაკს "</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 13f3a4eed8c5..f57f3f581c85 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Жабу"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Жаю"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Параметрлер"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Бөлінген экранға кіру"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Mәзір"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"суреттегі сурет\" режимінде"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> деген пайдаланушының бұл мүмкіндікті пайдалануын қаламасаңыз, параметрлерді түртіп ашыңыз да, оларды өшіріңіз."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Жасыру"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Көрсету"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Қолданба экранды бөлу режимінде жұмыс істемеуі мүмкін."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Қодланба бөлінген экранды қолдамайды."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Қолданба қосымша дисплейде жұмыс істемеуі мүмкін."</string> @@ -68,7 +71,14 @@ <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Соңғы және жабылған қалқыма хабарлар осы жерде көрсетіледі."</string> <string name="notification_bubble_title" msgid="6082910224488253378">"Көпіршік"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Басқару"</string> - <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Қалқымалы анықтама өшірілді."</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Қалқыма хабар жабылды."</string> <string name="restart_button_description" msgid="5887656107651190519">"Бұл қолданбаны қайта қосып, толық экранға өту үшін түртіңіз."</string> - <string name="got_it" msgid="4428750913636945527">"Түсінікті"</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..7bade0dff0d9 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Атаусыз бағдарлама)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP жабу"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Толық экран"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP клипін жылжыту"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP терезесін жаю"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP терезесін жию"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Басқару элементтері: "<annotation icon="home_icon">" НЕГІЗГІ ЭКРАН "</annotation>" түймесін екі рет басыңыз."</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 134d3c2334f0..5c04f881fe0e 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"បិទ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ពង្រីក"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ការកំណត់"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ចូលមុខងារបំបែកអេក្រង់"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ម៉ឺនុយ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ស្ថិតក្នុងមុខងាររូបក្នុងរូប"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ប្រសិនបើអ្នកមិនចង់ឲ្យ <xliff:g id="NAME">%s</xliff:g> ប្រើមុខងារនេះ សូមចុចបើកការកំណត់ រួចបិទវា។"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"លាក់ជាបណ្ដោះអាសន្ន"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ឈប់លាក់ជាបណ្ដោះអាសន្ន"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"កម្មវិធីអាចនឹងមិនដំណើរការជាមួយមុខងារបំបែកអេក្រង់ទេ។"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"កម្មវិធីមិនគាំទ្រអេក្រង់បំបែកជាពីរទេ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"កម្មវិធីនេះប្រហែលជាមិនដំណើរការនៅលើអេក្រង់បន្ទាប់បន្សំទេ។"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"យល់ហើយ"</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..721be1fc1650 100644 --- a/libs/WindowManager/Shell/res/values-km/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-km/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(កម្មវិធីគ្មានចំណងជើង)"</string> <string name="pip_close" msgid="9135220303720555525">"បិទ PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ពេញអេក្រង់"</string> + <string name="pip_move" msgid="1544227837964635439">"ផ្លាស់ទី PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"ពង្រីក PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"បង្រួម PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ចុចពីរដងលើ"<annotation icon="home_icon">"ប៊ូតុងដើម"</annotation>" ដើម្បីបើកផ្ទាំងគ្រប់គ្រង"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index c8b3389a3a60..e91383caa009 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ಮುಚ್ಚಿ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ವಿಸ್ತೃತಗೊಳಿಸು"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ಸ್ಪ್ಲಿಟ್-ಸ್ಕ್ರೀನ್ಗೆ ಪ್ರವೇಶಿಸಿ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ಮೆನು"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವಾಗಿದೆ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ಈ ವೈಶಿಷ್ಟ್ಯ ಬಳಸುವುದನ್ನು ನೀವು ಬಯಸದಿದ್ದರೆ, ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ತೆರೆಯಲು ಮತ್ತು ಅದನ್ನು ಆಫ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"ಸ್ಟ್ಯಾಶ್ ಮಾಡಿ"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ಅನ್ಸ್ಟ್ಯಾಶ್ ಮಾಡಿ"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ವಿಭಜಿಸಿದ ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ಆ್ಯಪ್ ಕೆಲಸ ಮಾಡದೇ ಇರಬಹುದು."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ಅಪ್ಲಿಕೇಶನ್ ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಅಪ್ಲಿಕೇಶನ್ ಕಾರ್ಯ ನಿರ್ವಹಿಸದೇ ಇರಬಹುದು."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"ಅರ್ಥವಾಯಿತು"</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..8310c8a1169c 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ಶೀರ್ಷಿಕೆ ರಹಿತ ಕಾರ್ಯಕ್ರಮ)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP ಮುಚ್ಚಿ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ಪೂರ್ಣ ಪರದೆ"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ಅನ್ನು ಸರಿಸಿ"</string> + <string name="pip_expand" msgid="7605396312689038178">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವನ್ನು ವಿಸ್ತರಿಸಿ"</string> + <string name="pip_collapse" msgid="5732233773786896094">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವನ್ನು ಕುಗ್ಗಿಸಿ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ಕಂಟ್ರೋಲ್ಗಳಿಗಾಗಿ "<annotation icon="home_icon">" ಹೋಮ್ "</annotation>" ಅನ್ನು ಎರಡು ಬಾರಿ ಒತ್ತಿ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index b29612e337f2..104ba3f22c96 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"닫기"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"펼치기"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"설정"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"화면 분할 모드로 전환"</string> <string name="pip_menu_title" msgid="5393619322111827096">"메뉴"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>에서 PIP 사용 중"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>에서 이 기능이 사용되는 것을 원하지 않는 경우 탭하여 설정을 열고 기능을 사용 중지하세요."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"숨기기"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"숨기기 취소"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"앱이 분할 화면에서 작동하지 않을 수 있습니다."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"앱이 화면 분할을 지원하지 않습니다."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"앱이 보조 디스플레이에서 작동하지 않을 수도 있습니다."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"확인"</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..a3e055a515a1 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(제목 없는 프로그램)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP 닫기"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"전체화면"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP 이동"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP 펼치기"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP 접기"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" 제어 메뉴에 액세스하려면 "<annotation icon="home_icon">" 홈 "</annotation>"을 두 번 누르세요."</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 0c64c7639327..8203622a33fc 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Жабуу"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Жайып көрсөтүү"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Жөндөөлөр"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Экранды бөлүү режимине өтүү"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> – сүрөт ичиндеги сүрөт"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Эгер <xliff:g id="NAME">%s</xliff:g> колдонмосу бул функцияны пайдаланбасын десеңиз, жөндөөлөрдү ачып туруп, аны өчүрүп коюңуз."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Сейфке салуу"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Сейфтен чыгаруу"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Колдонмодо экран бөлүнбөшү мүмкүн."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Колдонмодо экран бөлүнбөйт."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Колдонмо кошумча экранда иштебей коюшу мүмкүн."</string> @@ -44,7 +47,7 @@ <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Үстүнкү экранды 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ылдыйкы экранды толук экран режимине өткөрүү"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Бир кол режимин колдонуу"</string> - <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Чыгуу үчүн экранды ылдый жагынан өйдө көздөй сүрүңүз же колдонмонун өйдө жагын басыңыз"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Чыгуу үчүн экранды ылдый жагынан өйдө сүрүңүз же колдонмонун өйдө жагын басыңыз"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Бир кол режимин баштоо"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Бир кол режиминен чыгуу"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> калкып чыкма билдирмелер жөндөөлөрү"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Түшүндүм"</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..887ac52c8e43 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Аталышы жок программа)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP\'ти жабуу"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Толук экран"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP\'ти жылдыруу"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP\'ти жайып көрсөтүү"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP\'ти жыйыштыруу"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Башкаруу элементтерин ачуу үчүн "<annotation icon="home_icon">" БАШКЫ БЕТ "</annotation>" баскычын эки жолу басыңыз"</string> </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 5ccf164b3d67..24396786f9d8 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ປິດ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ຂະຫຍາຍ"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ການຕັ້ງຄ່າ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ເຂົ້າການແບ່ງໜ້າຈໍ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ເມນູ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ແມ່ນເປັນການສະແດງຜົນຫຼາຍຢ່າງພ້ອມກັນ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ຫາກທ່ານບໍ່ຕ້ອງການ <xliff:g id="NAME">%s</xliff:g> ໃຫ້ໃຊ້ຄຸນສົມບັດນີ້, ໃຫ້ແຕະເພື່ອເປີດການຕັ້ງຄ່າ ແລ້ວປິດມັນໄວ້."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"ເກັບໄວ້ບ່ອນເກັບສ່ວນຕົວ"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ເອົາອອກຈາກບ່ອນເກັບສ່ວນຕົວ"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ແອັບອາດໃຊ້ບໍ່ໄດ້ກັບການແບ່ງໜ້າຈໍ."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ແອັບບໍ່ຮອງຮັບໜ້າຈໍແບບແຍກກັນ."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ແອັບອາດບໍ່ສາມາດໃຊ້ໄດ້ໃນໜ້າຈໍທີສອງ."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"ເຂົ້າໃຈແລ້ວ"</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..91c4a033356d 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ໂປຣແກຣມບໍ່ມີຊື່)"</string> <string name="pip_close" msgid="9135220303720555525">"ປິດ PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ເຕັມໜ້າຈໍ"</string> + <string name="pip_move" msgid="1544227837964635439">"ຍ້າຍ PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"ຂະຫຍາຍ PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"ຫຍໍ້ PIP ລົງ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ກົດ "<annotation icon="home_icon">" HOME "</annotation>" ສອງເທື່ອສຳລັບການຄວບຄຸມ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index 1433312ded60..e2ae643ad308 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Uždaryti"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Išskleisti"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Nustatymai"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Įjungti išskaidyto ekrano režimą"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> rodom. vaizdo vaizde"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jei nenorite, kad „<xliff:g id="NAME">%s</xliff:g>“ naudotų šią funkciją, palietę atidarykite nustatymus ir išjunkite ją."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Praleisti ir eiti į kitą"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Praleisti ir eiti į ankstesnį"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Pakeisti dydį"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Paslėpti"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Nebeslėpti"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Programa gali neveikti naudojant išskaidyto ekrano režimą."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programoje nepalaikomas skaidytas ekranas."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Programa gali neveikti antriniame ekrane."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Supratau"</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..04265ca01b48 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Visas ekranas"</string> + <string name="pip_move" msgid="1544227837964635439">"Perkelti PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Iškleisti PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Sutraukti PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Jei reikia valdiklių, dukart paspauskite "<annotation icon="home_icon">"PAGRINDINIS"</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index fb297b8f2990..a77160bc262a 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Aizvērt"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Izvērst"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Iestatījumi"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Piekļūt ekrāna sadalīšanas režīmam"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Izvēlne"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ir attēlā attēlā"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ja nevēlaties lietotnē <xliff:g id="NAME">%s</xliff:g> izmantot šo funkciju, pieskarieties, lai atvērtu iestatījumus un izslēgtu funkciju."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Pāriet uz nākamo"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Pāriet uz iepriekšējo"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Mainīt lielumu"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Paslēpt"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Rādīt"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Iespējams, lietotne nedarbosies ekrāna sadalīšanas režīmā."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Lietotnē netiek atbalstīta ekrāna sadalīšana."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Lietotne, iespējams, nedarbosies sekundārajā displejā."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Labi"</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..8c6191e00833 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Pilnekrāna režīms"</string> + <string name="pip_move" msgid="1544227837964635439">"Pārvietot attēlu attēlā"</string> + <string name="pip_expand" msgid="7605396312689038178">"Izvērst “Attēls attēlā” logu"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Sakļaut “Attēls attēlā” logu"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Atvērt vadīklas: divreiz nospiediet pogu "<annotation icon="home_icon">"SĀKUMS"</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 80b33293af34..bac0c9eee4c2 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Затвори"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Проширете"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Поставки"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Влези во поделен екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Мени"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> е во слика во слика"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ако не сакате <xliff:g id="NAME">%s</xliff:g> да ја користи функцијава, допрете за да ги отворите поставките и да ја исклучите."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Сокријте"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Прикажете"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликацијата може да не работи со поделен екран."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликацијата не поддржува поделен екран."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликацијата може да не функционира на друг екран."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Сфатив"</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..beef1fef862b 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без наслов)"</string> <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цел екран"</string> + <string name="pip_move" msgid="1544227837964635439">"Премести PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Прошири ја сликата во слика"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Собери ја сликата во слика"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Притиснете двапати на "<annotation icon="home_icon">" HOME "</annotation>" за контроли"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index 5d47911137ec..de0f837fcd3f 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"അവസാനിപ്പിക്കുക"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"വികസിപ്പിക്കുക"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ക്രമീകരണം"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"സ്ക്രീൻ വിഭജന മോഡിൽ പ്രവേശിക്കുക"</string> <string name="pip_menu_title" msgid="5393619322111827096">"മെനു"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ചിത്രത്തിനുള്ളിൽ ചിത്രം രീതിയിലാണ്"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ഈ ഫീച്ചർ ഉപയോഗിക്കേണ്ടെങ്കിൽ, ടാപ്പ് ചെയ്ത് ക്രമീകരണം തുറന്ന് അത് ഓഫാക്കുക."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"സ്റ്റാഷ് ചെയ്യൽ"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"അൺസ്റ്റാഷ് ചെയ്യൽ"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"സ്ക്രീൻ വിഭജന മോഡിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"സ്പ്ലിറ്റ്-സ്ക്രീനിനെ ആപ്പ് പിന്തുണയ്ക്കുന്നില്ല."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"രണ്ടാം ഡിസ്പ്ലേയിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല."</string> @@ -66,9 +69,16 @@ <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"മനസ്സിലായി"</string> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"അടുത്തിടെയുള്ള ബബിളുകൾ ഒന്നുമില്ല"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"അടുത്തിടെയുള്ള ബബിളുകൾ, ഡിസ്മിസ് ചെയ്ത ബബിളുകൾ എന്നിവ ഇവിടെ ദൃശ്യമാവും"</string> - <string name="notification_bubble_title" msgid="6082910224488253378">"ബബ്ൾ"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ബബിൾ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"മാനേജ് ചെയ്യുക"</string> - <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ബബ്ൾ ഡിസ്മിസ് ചെയ്തു."</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ബബിൾ ഡിസ്മിസ് ചെയ്തു."</string> <string name="restart_button_description" msgid="5887656107651190519">"ഈ ആപ്പ് റീസ്റ്റാർട്ട് ചെയ്ത് പൂർണ്ണ സ്ക്രീനിലേക്ക് മാറാൻ ടാപ്പ് ചെയ്യുക."</string> - <string name="got_it" msgid="4428750913636945527">"മനസ്സിലായി"</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..c2a532d09647 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(പേരില്ലാത്ത പ്രോഗ്രാം)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP അടയ്ക്കുക"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"പൂര്ണ്ണ സ്ക്രീന്"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP നീക്കുക"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP വികസിപ്പിക്കുക"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP ചുരുക്കുക"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" നിയന്ത്രണങ്ങൾക്കായി "<annotation icon="home_icon">" ഹോം "</annotation>" രണ്ട് തവണ അമർത്തുക"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index a5e7f957b829..1205306e0833 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Хаах"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Дэлгэх"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Тохиргоо"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Хуваасан дэлгэцийг оруулна уу"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Цэс"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> дэлгэцэн доторх дэлгэцэд байна"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Та <xliff:g id="NAME">%s</xliff:g>-д энэ онцлогийг ашиглуулахыг хүсэхгүй байвал тохиргоог нээгээд, үүнийг унтраана уу."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Нуух"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ил гаргах"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Апп хуваагдсан дэлгэц дээр ажиллахгүй байж болзошгүй."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Энэ апп нь дэлгэц хуваах тохиргоог дэмждэггүй."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апп хоёрдогч дэлгэцэд ажиллахгүй."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Ойлголоо"</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..bf8c59b57359 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Гарчиггүй хөтөлбөр)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP-г хаах"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Бүтэн дэлгэц"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP-г зөөх"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP-г дэлгэх"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP-г хураах"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Хяналтад хандах бол "<annotation icon="home_icon">" HOME "</annotation>" дээр хоёр дарна уу"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index 0450189c515d..c91d06fdf3d5 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"बंद करा"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तृत करा"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग्ज"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रीन एंटर करा"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेनू"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> चित्रामध्ये चित्र मध्ये आहे"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>ने हे वैशिष्ट्य वापरू नये असे तुम्हाला वाटत असल्यास, सेटिंग्ज उघडण्यासाठी टॅप करा आणि ते बंद करा."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"स्टॅश करा"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्टॅश करा"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"अॅप कदाचित स्प्लिट स्क्रीनसह काम करू शकत नाही."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अॅप स्क्रीन-विभाजनास समर्थन देत नाही."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"दुसऱ्या डिस्प्लेवर अॅप कदाचित चालणार नाही."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"समजले"</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..5d519b7afe9a 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षक नसलेला कार्यक्रम)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP बंद करा"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रीन"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP हलवा"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP चा विस्तार करा"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP कोलॅप्स करा"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" नियंत्रणांसाठी "<annotation icon="home_icon">" होम "</annotation>" दोनदा दाबा"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index c0c1cbd6ed3c..652a9919d163 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Tutup"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Kembangkan"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Tetapan"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Masuk skrin pisah"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> terdapat dalam gambar dalam gambar"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jika anda tidak mahu <xliff:g id="NAME">%s</xliff:g> menggunakan ciri ini, ketik untuk membuka tetapan dan matikan ciri."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Langkau ke seterusnya"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Langkau ke sebelumnya"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ubah saiz"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Sembunyikan"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Tunjukkan"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Apl mungkin tidak berfungsi dengan skrin pisah."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Apl tidak menyokong skrin pisah."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Apl mungkin tidak berfungsi pada paparan kedua."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..08642c47c91a 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tiada tajuk)"</string> <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Skrin penuh"</string> + <string name="pip_move" msgid="1544227837964635439">"Alihkan PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Kembangkan PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Kuncupkan PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" LAMAN UTAMA "</annotation>" untuk mengakses kawalan"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index 0d78f89d9e54..15d182c6451e 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -20,14 +20,17 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ပိတ်ရန်"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ချဲ့ရန်"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ဆက်တင်များ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းသို့ ဝင်ရန်"</string> <string name="pip_menu_title" msgid="5393619322111827096">"မီနူး"</string> - <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> သည် တစ်ခုပေါ် တစ်ခုထပ်၍ ဖွင့်ထားသည်"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> သည် နှစ်ခုထပ်၍ကြည့်ခြင်း ဖွင့်ထားသည်"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> အား ဤဝန်ဆောင်မှုကို အသုံးမပြုစေလိုလျှင် ဆက်တင်ကိုဖွင့်ရန် တို့ပြီး ၎င်းဝန်ဆောင်မှုကို ပိတ်လိုက်ပါ။"</string> <string name="pip_play" msgid="3496151081459417097">"ဖွင့်ရန်"</string> <string name="pip_pause" msgid="690688849510295232">"ခေတ္တရပ်ရန်"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"နောက်တစ်ခုသို့ ကျော်ရန်"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"ယခင်တစ်ခုသို့ ပြန်သွားရန်"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"အရွယ်အစားပြောင်းရန်"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"သိုဝှက်ရန်"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"မသိုဝှက်ရန်"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းဖြင့် အက်ပ်သည် အလုပ်မလုပ်ပါ။"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"အက်ပ်သည် မျက်နှာပြင်ခွဲပြရန် ပံ့ပိုးထားခြင်းမရှိပါ။"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ဤအက်ပ်အနေဖြင့် ဒုတိယဖန်သားပြင်ပေါ်တွင် အလုပ်လုပ်မည် မဟုတ်ပါ။"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"ရပြီ"</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 9569dc4cbeea..e01daee115ca 100644 --- a/libs/WindowManager/Shell/res/values-my/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml @@ -17,8 +17,12 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="notification_channel_tv_pip" msgid="2576686079160402435">"တစ်ခုပေါ်တစ်ခုထပ်၍ ဖွင့်ခြင်း"</string> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"နှစ်ခုထပ်၍ကြည့်ခြင်း"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ခေါင်းစဉ်မဲ့ အစီအစဉ်)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP ကိုပိတ်ပါ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"မျက်နှာပြင် အပြည့်"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ရွှေ့ရန်"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP ကို ချဲ့ရန်"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP ကို လျှော့ပြပါ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ထိန်းချုပ်မှုအတွက် "<annotation icon="home_icon">" ပင်မခလုတ် "</annotation>" နှစ်ချက်နှိပ်ပါ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index fab0c0cacef4..9fd42b2f129c 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Lukk"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Vis"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Innstillinger"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aktivér delt skjerm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er i bilde-i-bilde"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke vil at <xliff:g id="NAME">%s</xliff:g> skal bruke denne funksjonen, kan du trykke for å åpne innstillingene og slå den av."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Hopp til neste"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Hopp til forrige"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Endre størrelse"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Oppbevar"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Avslutt oppbevaring"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Det kan hende at appen ikke fungerer med delt skjerm."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen støtter ikke delt skjerm."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer kanskje ikke på en sekundær skjerm."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Greit"</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..65ed0b7f5bff 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uten tittel)"</string> <string name="pip_close" msgid="9135220303720555525">"Lukk PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Fullskjerm"</string> + <string name="pip_move" msgid="1544227837964635439">"Flytt BIB"</string> + <string name="pip_expand" msgid="7605396312689038178">"Vis BIB"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Skjul BIB"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dobbelttrykk på "<annotation icon="home_icon">"HJEM"</annotation>" for å åpne kontroller"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index dfa364a763a8..8dfec88cc29d 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"बन्द गर्नुहोस्"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तृत गर्नुहोस्"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिङहरू"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रिन मोड प्रयोग गर्नुहोस्"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेनु"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> Picture-in-picture मा छ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"तपाईं <xliff:g id="NAME">%s</xliff:g> ले सुविधा प्रयोग नगरोस् भन्ने चाहनुहुन्छ भने ट्याप गरेर सेटिङहरू खोल्नुहोस् र यसलाई निष्क्रिय पार्नुहोस्।"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"स्ट्यास गर्नुहोस्"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्ट्यास गर्नुहोस्"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"एप विभाजित स्क्रिनमा काम नगर्न सक्छ।"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अनुप्रयोगले विभाजित-स्क्रिनलाई समर्थन गर्दैन।"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"यो एपले सहायक प्रदर्शनमा काम नगर्नसक्छ।"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"बुझेँ"</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..d33fed67efb6 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षकविहीन कार्यक्रम)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP लाई बन्द गर्नुहोस्"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रिन"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP सार्नुहोस्"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP विन्डो एक्स्पान्ड गर्नु…"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP विन्डो कोल्याप्स गर्नुहोस्"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" कन्ट्रोल मेनु खोल्न "<annotation icon="home_icon">" होम "</annotation>" बटन दुई पटक थिच्नुहोस्"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 0601f153b330..8468b04c66da 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Sluiten"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Uitvouwen"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Instellingen"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Gesplitst scherm openen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in scherm-in-scherm"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Als je niet wilt dat <xliff:g id="NAME">%s</xliff:g> deze functie gebruikt, tik je om de instellingen te openen en zet je de functie uit."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Doorgaan naar volgende"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Teruggaan naar vorige"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Formaat aanpassen"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Verbergen"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Niet meer verbergen"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"De app werkt mogelijk niet met gesplitst scherm."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App biedt geen ondersteuning voor gesplitst scherm."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App werkt mogelijk niet op een secundair scherm."</string> @@ -43,10 +46,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Bovenste scherm 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Bovenste scherm 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Onderste scherm op volledig scherm"</string> - <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Bediening met één hand gebruiken"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Bediening met 1 hand gebruiken"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Als je wilt afsluiten, swipe je omhoog vanaf de onderkant van het scherm of tik je ergens boven de app"</string> - <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Bediening met één hand starten"</string> - <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Bediening met één hand afsluiten"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Bediening met 1 hand starten"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Bediening met 1 hand afsluiten"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Instellingen voor <xliff:g id="APP_NAME">%1$s</xliff:g>-bubbels"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overloop"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Weer toevoegen aan stack"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..9763c5665ab2 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Naamloos programma)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP sluiten"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Volledig scherm"</string> + <string name="pip_move" msgid="1544227837964635439">"SIS verplaatsen"</string> + <string name="pip_expand" msgid="7605396312689038178">"SIS uitvouwen"</string> + <string name="pip_collapse" msgid="5732233773786896094">"SIS samenvouwen"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Druk twee keer op "<annotation icon="home_icon">" HOME "</annotation>" voor bedieningselementen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index 50d20076a26f..a8d8448edf99 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ବଢ଼ାନ୍ତୁ"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ସେଟିଂସ୍"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ମୋଡ ବ୍ୟବହାର କରନ୍ତୁ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ମେନୁ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"ଛବି-ଭିତରେ-ଛବି\"ରେ ଅଛି"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ଏହି ବୈଶିଷ୍ଟ୍ୟ <xliff:g id="NAME">%s</xliff:g> ବ୍ୟବହାର ନକରିବାକୁ ଯଦି ଆପଣ ଚାହାଁନ୍ତି, ସେଟିଙ୍ଗ ଖୋଲିବାକୁ ଟାପ୍ କରନ୍ତୁ ଏବଂ ଏହା ଅଫ୍ କରିଦିଅନ୍ତୁ।"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"ଲୁଚାନ୍ତୁ"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ଦେଖାନ୍ତୁ"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରିନରେ ଆପ୍ କାମ କରିନପାରେ।"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ଆପ୍ ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରୀନକୁ ସପୋର୍ଟ କରେ ନାହିଁ।"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ କାମ ନକରିପାରେ।"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"ବୁଝିଗଲି"</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..e0344855bd1f 100644 --- a/libs/WindowManager/Shell/res/values-or/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-or/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(କୌଣସି ଟାଇଟଲ୍ ପ୍ରୋଗ୍ରାମ୍ ନାହିଁ)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP ବନ୍ଦ କରନ୍ତୁ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍"</string> + <string name="pip_move" msgid="1544227837964635439">"PIPକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIPକୁ ବିସ୍ତାର କରନ୍ତୁ"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIPକୁ ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ ପାଇଁ "<annotation icon="home_icon">" ହୋମ ବଟନ "</annotation>"କୁ ଦୁଇଥର ଦବାନ୍ତୁ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index dd3d26e56b4c..f99176cb682d 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ਬੰਦ ਕਰੋ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ਵਿਸਤਾਰ ਕਰੋ"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ਸੈਟਿੰਗਾਂ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਿੱਚ ਦਾਖਲ ਹੋਵੋ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ਮੀਨੂ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ਤਸਵੀਰ-ਅੰਦਰ-ਤਸਵੀਰ ਵਿੱਚ ਹੈ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ਜੇਕਰ ਤੁਸੀਂ ਨਹੀਂ ਚਾਹੁੰਦੇ ਕਿ <xliff:g id="NAME">%s</xliff:g> ਐਪ ਇਸ ਵਿਸ਼ੇਸ਼ਤਾ ਦੀ ਵਰਤੋਂ ਕਰੇ, ਤਾਂ ਸੈਟਿੰਗਾਂ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ ਅਤੇ ਇਸਨੂੰ ਬੰਦ ਕਰੋ।"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"ਸਟੈਸ਼"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ਅਣਸਟੈਸ਼"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ।"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨੂੰ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ।"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇ \'ਤੇ ਕੰਮ ਨਾ ਕਰੇ।"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"ਸਮਝ ਲਿਆ"</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..9c01ac3f3cc0 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ਸਿਰਲੇਖ-ਰਹਿਤ ਪ੍ਰੋਗਰਾਮ)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP ਬੰਦ ਕਰੋ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ਨੂੰ ਲਿਜਾਓ"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP ਦਾ ਵਿਸਤਾਰ ਕਰੋ"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP ਨੂੰ ਸਮੇਟੋ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ਕੰਟਰੋਲਾਂ ਲਈ "<annotation icon="home_icon">" ਹੋਮ ਬਟਨ "</annotation>" ਨੂੰ ਦੋ ਵਾਰ ਦਬਾਓ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index ca2bdcb270da..f2147c04d335 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zamknij"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Rozwiń"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ustawienia"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Włącz podzielony ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Aplikacja <xliff:g id="NAME">%s</xliff:g> działa w trybie obraz w obrazie"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jeśli nie chcesz, by aplikacja <xliff:g id="NAME">%s</xliff:g> korzystała z tej funkcji, otwórz ustawienia i wyłącz ją."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Dalej"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Wstecz"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Zmień rozmiar"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Przenieś do schowka"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zabierz ze schowka"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacja może nie działać przy podzielonym ekranie."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacja nie obsługuje dzielonego ekranu."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacja może nie działać na dodatkowym ekranie."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..b922e2d5a6ba 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Pełny ekran"</string> + <string name="pip_move" msgid="1544227837964635439">"Przenieś PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Rozwiń PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Zwiń PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Naciśnij dwukrotnie "<annotation icon="home_icon">"EKRAN GŁÓWNY"</annotation>", aby wyświetlić ustawienia"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index bdd0d4b8baec..2efc5543dd87 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -18,8 +18,9 @@ <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">"Fechar"</string> - <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Abrir"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Dividir tela"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Pular para a próxima"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Pular para a anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ocultar"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Exibir"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"É possível que o app não funcione com a tela dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"O app não é compatível com a divisão de tela."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É possível que o app não funcione em uma tela secundária."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Ok"</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..cc4eb3c32c1f 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</string> + <string name="pip_expand" msgid="7605396312689038178">"Abrir picture-in-picture"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Fechar picture-in-picture"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 6661b0506c5e..c68a6934dead 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Fechar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Definições"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aceder ao ecrã dividido"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"A app <xliff:g id="NAME">%s</xliff:g> está no modo de ecrã no ecrã"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se não pretende que a app <xliff:g id="NAME">%s</xliff:g> utilize esta funcionalidade, toque para abrir as definições e desative-a."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Mudar para o seguinte"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Mudar para o anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Armazenar"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Remover do armazenamento"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"A app pode não funcionar com o ecrã dividido."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A app não é compatível com o ecrã dividido."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"A app pode não funcionar num ecrã secundário."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..c4ae78d89ba8 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Ecrã inteiro"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover Ecrã no ecrã"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expandir Ecrã no ecrã"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Reduzir Ecrã no ecrã"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Prima duas vezes "<annotation icon="home_icon">" PÁGINA INICIAL "</annotation>" para controlos"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index bdd0d4b8baec..2efc5543dd87 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -18,8 +18,9 @@ <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">"Fechar"</string> - <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Abrir"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Dividir tela"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Pular para a próxima"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Pular para a anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ocultar"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Exibir"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"É possível que o app não funcione com a tela dividida."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"O app não é compatível com a divisão de tela."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É possível que o app não funcione em uma tela secundária."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Ok"</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..cc4eb3c32c1f 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</string> + <string name="pip_expand" msgid="7605396312689038178">"Abrir picture-in-picture"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Fechar picture-in-picture"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index 9112543c8f60..804d34f980ff 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Închideți"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Extindeți"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Setări"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accesați ecranul împărțit"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> este în modul picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Dacă nu doriți ca <xliff:g id="NAME">%s</xliff:g> să utilizeze această funcție, atingeți pentru a deschide setările și dezactivați-o."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Treceți la următorul"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Treceți la cel anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionați"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stocați"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulați stocarea"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Este posibil ca aplicația să nu funcționeze cu ecranul împărțit."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplicația nu acceptă ecranul împărțit."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Este posibil ca aplicația să nu funcționeze pe un ecran secundar."</string> @@ -70,5 +73,12 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionați"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string> <string name="restart_button_description" msgid="5887656107651190519">"Atingeți ca să reporniți aplicația și să treceți în modul ecran complet."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> + <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Aveți probleme cu camera foto?\nAtingeți pentru a reîncadra"</string> + <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nu ați remediat problema?\nAtingeți pentru a reveni"</string> + <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nu aveți probleme cu camera foto? Atingeți pentru a închide."</string> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Unele aplicații funcționează cel mai bine în orientarea portret"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Încercați una dintre aceste opțiuni pentru a profita din plin de spațiu"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotiți dispozitivul pentru a trece în modul ecran complet"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Atingeți de două ori lângă o aplicație pentru a o repoziționa"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml index 9438e4955b68..86a30f49df15 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Ecran complet"</string> + <string name="pip_move" msgid="1544227837964635439">"Mutați fereastra PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Extindeți fereastra PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Restrângeți fereastra PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Apăsați de două ori "<annotation icon="home_icon">"butonul ecran de pornire"</annotation>" pentru comenzi"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 5120136e3c68..95bf1cf11435 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Закрыть"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Развернуть"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Включить разделение экрана"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> находится в режиме \"Картинка в картинке\""</string> <string name="pip_notification_message" msgid="8854051911700302620">"Чтобы отключить эту функцию для приложения \"<xliff:g id="NAME">%s</xliff:g>\", перейдите в настройки."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Скрыть"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показать"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"В режиме разделения экрана приложение может работать нестабильно."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложение не поддерживает разделение экрана."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Приложение может не работать на дополнительном экране"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"ОК"</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..08623e1e69c5 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Без названия)"</string> <string name="pip_close" msgid="9135220303720555525">"\"Кадр в кадре\" – выйти"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Во весь экран"</string> + <string name="pip_move" msgid="1544227837964635439">"Переместить PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Развернуть PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Свернуть PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Элементы управления: дважды нажмите "<annotation icon="home_icon">" кнопку главного экрана "</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index e1d9a825a004..23dd65ad7b31 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"වසන්න"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"දිග හරින්න"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"සැකසීම්"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"බෙදුම් තිරයට ඇතුළු වන්න"</string> <string name="pip_menu_title" msgid="5393619322111827096">"මෙනුව"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> පින්තූරය-තුළ-පින්තූරය තුළ වේ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ඔබට <xliff:g id="NAME">%s</xliff:g> මෙම විශේෂාංගය භාවිත කිරීමට අවශ්ය නැති නම්, සැකසීම් විවෘත කිරීමට තට්ටු කර එය ක්රියාවිරහිත කරන්න."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"සඟවා තබන්න"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"සඟවා තැබීම ඉවත් කරන්න"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"යෙදුම බෙදුම් තිරය සමග ක්රියා නොකළ හැකිය"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"යෙදුම බෙදුණු-තිරය සඳහා සහාය නොදක්වයි."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"යෙදුම ද්විතියික සංදර්ශකයක ක්රියා නොකළ හැකිය."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"තේරුණා"</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..fbb0ebba0623 100644 --- a/libs/WindowManager/Shell/res/values-si/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-si/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(මාතෘකාවක් නැති වැඩසටහන)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP වසන්න"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"සම්පූර්ණ තිරය"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ගෙන යන්න"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP දිග හරින්න"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP හකුළන්න"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" පාලන සඳහා "<annotation icon="home_icon">" මුල් පිටුව "</annotation>" දෙවරක් ඔබන්න"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index c88099b35c0d..a231cacefb20 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zavrieť"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Rozbaliť"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavenia"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Prejsť na rozdelenú obrazovku"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Ponuka"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v režime obraz v obraze"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ak nechcete, aby aplikácia <xliff:g id="NAME">%s</xliff:g> používala túto funkciu, klepnutím otvorte nastavenia a vypnite ju."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Preskočiť na ďalšie"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Preskočiť na predchádzajúce"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Zmeniť veľkosť"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Skryť"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušiť skrytie"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikácia nemusí fungovať s rozdelenou obrazovkou."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikácia nepodporuje rozdelenú obrazovku."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikácia nemusí fungovať na sekundárnej obrazovke."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Dobre"</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..81cb0eafc759 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> + <string name="pip_move" msgid="1544227837964635439">"Presunúť obraz v obraze"</string> + <string name="pip_expand" msgid="7605396312689038178">"Rozbaliť obraz v obraze"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Zbaliť obraz v obraze"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Ovládanie zobraz. dvoj. stlač. "<annotation icon="home_icon">" TLAČIDLA PLOCHY "</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 42d7be7a146d..adeaae978eaa 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zapri"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Razširi"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavitve"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Vklopi razdeljen zaslon"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v načinu slika v sliki"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Če ne želite, da aplikacija <xliff:g id="NAME">%s</xliff:g> uporablja to funkcijo, se dotaknite, da odprete nastavitve, in funkcijo izklopite."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Preskoči na naslednjega"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Preskoči na prejšnjega"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Spremeni velikost"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Zakrij"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Razkrij"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija morda ne deluje v načinu razdeljenega zaslona."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podpira načina razdeljenega zaslona."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija morda ne bo delovala na sekundarnem zaslonu."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Razumem"</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..060aaa0ce647 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Celozaslonsko"</string> + <string name="pip_move" msgid="1544227837964635439">"Premakni sliko v sliki"</string> + <string name="pip_expand" msgid="7605396312689038178">"Razširi sliko v sliki"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Strni sliko v sliki"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Za kontrolnike dvakrat pritisnite gumb za "<annotation icon="home_icon">" ZAČETNI ZASLON "</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index 1f373b53d0a4..2839b4bae7e4 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Mbyll"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Zgjero"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Cilësimet"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Hyr në ekranin e ndarë"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyja"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> është në figurë brenda figurës"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Nëse nuk dëshiron që <xliff:g id="NAME">%s</xliff:g> ta përdorë këtë funksion, trokit për të hapur cilësimet dhe për ta çaktivizuar."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Kalo te tjetra"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Kalo tek e mëparshmja"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ndrysho përmasat"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Fshih"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Mos e fshih"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacioni mund të mos funksionojë me ekranin e ndarë."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacioni nuk mbështet ekranin e ndarë."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacioni mund të mos funksionojë në një ekran dytësor."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"E kuptova"</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..9bfdb6a3edd8 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program pa titull)"</string> <string name="pip_close" msgid="9135220303720555525">"Mbyll PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ekrani i plotë"</string> + <string name="pip_move" msgid="1544227837964635439">"Zhvendos PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Zgjero PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Palos PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Trokit dy herë "<annotation icon="home_icon">" KREU "</annotation>" për kontrollet"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index 2bbbbf9e0714..9db6b7c63610 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Затвори"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Прошири"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Подешавања"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Уђи на подељени екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Мени"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> је слика у слици"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ако не желите да <xliff:g id="NAME">%s</xliff:g> користи ову функцију, додирните да бисте отворили подешавања и искључили је."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Ставите у тајну меморију"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Уклоните из тајне меморије"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликација можда неће радити са подељеним екраном."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликација не подржава подељени екран."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликација можда неће функционисати на секундарном екрану."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Важи"</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..6bc5c87bab48 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програм без наслова)"</string> <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цео екран"</string> + <string name="pip_move" msgid="1544227837964635439">"Премести слику у слици"</string> + <string name="pip_expand" msgid="7605396312689038178">"Прошири слику у слици"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Скупи слику у слици"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Двапут притисните "<annotation icon="home_icon">" HOME "</annotation>" за контроле"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 692b5ed8576f..f6bd55423cdc 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Stäng"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Utöka"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Inställningar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Starta delad skärm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> visas i bild-i-bild"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Om du inte vill att den här funktionen används i <xliff:g id="NAME">%s</xliff:g> öppnar du inställningarna genom att trycka. Sedan inaktiverar du funktionen."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Hoppa till nästa"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Hoppa till föregående"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ändra storlek"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Utför stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Återställ stash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen kanske inte fungerar med delad skärm."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen har inte stöd för delad skärm."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen kanske inte fungerar på en sekundär skärm."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..b3465ab1db85 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Helskärm"</string> + <string name="pip_move" msgid="1544227837964635439">"Flytta BIB"</string> + <string name="pip_expand" msgid="7605396312689038178">"Utöka bild-i-bild"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Komprimera bild-i-bild"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Tryck snabbt två gånger på "<annotation icon="home_icon">" HEM "</annotation>" för kontroller"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 61c95ee183a0..f6e558527ee5 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Funga"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Panua"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Mipangilio"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Weka skrini iliyogawanywa"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> iko katika hali ya picha ndani ya picha nyingine"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ikiwa hutaki <xliff:g id="NAME">%s</xliff:g> itumie kipengele hiki, gusa ili ufungue mipangilio na uizime."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Ruka ufikie inayofuata"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Ruka ufikie iliyotangulia"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Badilisha ukubwa"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ficha"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Fichua"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Huenda programu isifanye kazi kwenye skrini inayogawanywa."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programu haiwezi kutumia skrini iliyogawanywa."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Huenda programu isifanye kazi kwenye dirisha lingine."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Nimeelewa"</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..baff49ed821a 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Skrini nzima"</string> + <string name="pip_move" msgid="1544227837964635439">"Kuhamisha PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Panua PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Kunja PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Bonyeza mara mbili kitufe cha "<annotation icon="home_icon">" UKURASA WA KWANZA "</annotation>" kupata vidhibiti"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 32a925a7994e..d8334adfe5ef 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"மூடு"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"விரி"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"அமைப்புகள்"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"திரைப் பிரிப்பு பயன்முறைக்குச் செல்"</string> <string name="pip_menu_title" msgid="5393619322111827096">"மெனு"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> தற்போது பிக்ச்சர்-இன்-பிக்ச்சரில் உள்ளது"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> இந்த அம்சத்தைப் பயன்படுத்த வேண்டாம் என நினைத்தால் இங்கு தட்டி அமைப்புகளைத் திறந்து இதை முடக்கவும்."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"திரைப் பிரிப்பு அம்சத்தில் ஆப்ஸ் செயல்படாமல் போகக்கூடும்."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"திரையைப் பிரிப்பதைப் ஆப்ஸ் ஆதரிக்கவில்லை."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"இரண்டாம்நிலைத் திரையில் ஆப்ஸ் வேலை செய்யாமல் போகக்கூடும்."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"சரி"</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..4439e299c919 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(தலைப்பு இல்லை)"</string> <string name="pip_close" msgid="9135220303720555525">"PIPஐ மூடு"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"முழுத்திரை"</string> + <string name="pip_move" msgid="1544227837964635439">"PIPபை நகர்த்து"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIPபை விரிவாக்கு"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIPபைச் சுருக்கு"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" கட்டுப்பாடுகள்: "<annotation icon="home_icon">" முகப்பு "</annotation>" பட்டனை இருமுறை அழுத்துக"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 3db12e7d5661..733075550007 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"మూసివేయి"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"విస్తరింపజేయి"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"సెట్టింగ్లు"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"స్ప్లిట్ స్క్రీన్ను ఎంటర్ చేయండి"</string> <string name="pip_menu_title" msgid="5393619322111827096">"మెనూ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> చిత్రంలో చిత్రం రూపంలో ఉంది"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ఈ లక్షణాన్ని ఉపయోగించకూడదు అని మీరు అనుకుంటే, సెట్టింగ్లను తెరవడానికి ట్యాప్ చేసి, దీన్ని ఆఫ్ చేయండి."</string> @@ -28,21 +29,23 @@ <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_stash" msgid="4060775037619702641">"స్టాచ్"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ఆన్స్టాచ్"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"స్క్రీన్ విభజనతో యాప్ పని చేయకపోవచ్చు."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"అనువర్తనంలో స్క్రీన్ విభజనకు మద్దతు లేదు."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"యాప్లో స్క్రీన్ విభజనకు మద్దతు లేదు."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ప్రత్యామ్నాయ డిస్ప్లేలో యాప్ పని చేయకపోవచ్చు."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ప్రత్యామ్నాయ డిస్ప్లేల్లో ప్రారంభానికి యాప్ మద్దతు లేదు."</string> <string name="accessibility_divider" msgid="703810061635792791">"విభజన స్క్రీన్ విభాగిని"</string> - <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ఎడమవైపు పూర్తి స్క్రీన్"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ఎడమవైపు ఫుల్-స్క్రీన్"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ఎడమవైపు 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ఎడమవైపు 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ఎడమవైపు 30%"</string> - <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"కుడివైపు పూర్తి స్క్రీన్"</string> - <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ఎగువ పూర్తి స్క్రీన్"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"కుడివైపు ఫుల్-స్క్రీన్"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ఎగువ ఫుల్-స్క్రీన్"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ఎగువ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ఎగువ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ఎగువ 30%"</string> - <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"దిగువ పూర్తి స్క్రీన్"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"దిగువ ఫుల్-స్క్రీన్"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"వన్-హ్యాండెడ్ మోడ్ను ఉపయోగించడం"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"నిష్క్రమించడానికి, స్క్రీన్ కింది భాగం నుండి పైకి స్వైప్ చేయండి లేదా యాప్ పైన ఎక్కడైనా ట్యాప్ చేయండి"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"వన్-హ్యాండెడ్ మోడ్ను ప్రారంభిస్తుంది"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"అర్థమైంది"</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 47489efbc4c2..35579346615f 100644 --- a/libs/WindowManager/Shell/res/values-te/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-te/strings_tv.xml @@ -20,5 +20,9 @@ <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_fullscreen" msgid="7278047353591302554">"పూర్తి స్క్రీన్"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"ఫుల్-స్క్రీన్"</string> + <string name="pip_move" msgid="1544227837964635439">"PIPను తరలించండి"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIPని విస్తరించండి"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIPని కుదించండి"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" కంట్రోల్స్ కోసం "<annotation icon="home_icon">" HOME "</annotation>" బటన్ రెండుసార్లు నొక్కండి"</string> </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..86ca65526336 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-television/config.xml @@ -0,0 +1,46 @@ +<?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> +</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 7df76e84cbb7..cfee8ea3242e 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ปิด"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ขยาย"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"การตั้งค่า"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"เข้าสู่โหมดแบ่งหน้าจอ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"เมนู"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ใช้การแสดงภาพซ้อนภาพ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"หากคุณไม่ต้องการให้ <xliff:g id="NAME">%s</xliff:g> ใช้ฟีเจอร์นี้ ให้แตะเพื่อเปิดการตั้งค่าแล้วปิดฟีเจอร์"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"เก็บเข้าที่เก็บส่วนตัว"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"เอาออกจากที่เก็บส่วนตัว"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"แอปอาจใช้ไม่ได้กับโหมดแบ่งหน้าจอ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"แอปไม่สนับสนุนการแยกหน้าจอ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"แอปอาจไม่ทำงานในจอแสดงผลรอง"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"รับทราบ"</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..0a07d157ec6f 100644 --- a/libs/WindowManager/Shell/res/values-th/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-th/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ไม่มีชื่อรายการ)"</string> <string name="pip_close" msgid="9135220303720555525">"ปิด PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"เต็มหน้าจอ"</string> + <string name="pip_move" msgid="1544227837964635439">"ย้าย PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"ขยาย PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"ยุบ PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" กดปุ่ม "<annotation icon="home_icon">" หน้าแรก "</annotation>" สองครั้งเพื่อเปิดการควบคุม"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index d6c2784f764f..eed624dd5069 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Isara"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Palawakin"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Mga Setting"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Pumasok sa split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Nasa picture-in-picture ang <xliff:g id="NAME">%s</xliff:g>"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Kung ayaw mong magamit ni <xliff:g id="NAME">%s</xliff:g> ang feature na ito, i-tap upang buksan ang mga setting at i-off ito."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Lumaktaw sa susunod"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Lumaktaw sa nakaraan"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"I-resize"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"I-stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"I-unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Posibleng hindi gumana ang app sa split screen."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Hindi sinusuportahan ng app ang split-screen."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Maaaring hindi gumana ang app sa pangalawang display."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..9a11a38fa492 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Full screen"</string> + <string name="pip_move" msgid="1544227837964635439">"Ilipat ang PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"I-expand ang PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"I-collapse ang PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" I-double press ang "<annotation icon="home_icon">" HOME "</annotation>" para sa mga kontrol"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 47d5966549ef..2b4a2d0550f0 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Kapat"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Genişlet"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Bölünmüş ekrana geç"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>, pencere içinde pencere özelliğini kullanıyor"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> uygulamasının bu özelliği kullanmasını istemiyorsanız dokunarak ayarları açın ve söz konusu özelliği kapatın."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Sonrakine atla"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Öncekine atla"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Yeniden boyutlandır"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Depola"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Depolama"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Uygulama bölünmüş ekranda çalışmayabilir."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uygulama bölünmüş ekranı desteklemiyor."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uygulama ikincil ekranda çalışmayabilir."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Anladım"</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..bf4bc6f1fff7 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP\'yi taşı"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP penceresini genişlet"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP penceresini daralt"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Kontroller için "<annotation icon="home_icon">" ANA SAYFA "</annotation>"\'ya iki kez basın"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 7920fd237a08..558ec51752b9 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -16,7 +16,19 @@ --> <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">40dp</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> + <dimen name="pip_menu_border_radius">4dp</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> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index c57f16f059df..c3411a837c78 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Закрити"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Розгорнути"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Налаштування"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Розділити екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> <string name="pip_notification_title" msgid="1347104727641353453">"У додатку <xliff:g id="NAME">%s</xliff:g> є функція \"Картинка в картинці\""</string> <string name="pip_notification_message" msgid="8854051911700302620">"Щоб додаток <xliff:g id="NAME">%s</xliff:g> не використовував цю функцію, вимкніть її в налаштуваннях."</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Сховати"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показати"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Додаток може не працювати в режимі розділеного екрана."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Додаток не підтримує розділення екрана."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Додаток може не працювати на додатковому екрані."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Зрозуміло"</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..7e9f54e68f54 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без назви)"</string> <string name="pip_close" msgid="9135220303720555525">"Закрити PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"На весь екран"</string> + <string name="pip_move" msgid="1544227837964635439">"Перемістити картинку в картинці"</string> + <string name="pip_expand" msgid="7605396312689038178">"Розгорнути картинку в картинці"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Згорнути картинку в картинці"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Відкрити елементи керування: двічі натисніть "<annotation icon="home_icon">"HOME"</annotation></string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 97a22e77c069..a31c2be25643 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"بند کریں"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"پھیلائیں"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ترتیبات"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"اسپلٹ اسکرین تک رسائی"</string> <string name="pip_menu_title" msgid="5393619322111827096">"مینو"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> تصویر میں تصویر میں ہے"</string> <string name="pip_notification_message" msgid="8854051911700302620">"اگر آپ نہیں چاہتے ہیں کہ <xliff:g id="NAME">%s</xliff:g> اس خصوصیت کا استعمال کرے تو ترتیبات کھولنے کے لیے تھپتھپا کر اسے آف کرے۔"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"Stash"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن ہے کہ ایپ اسپلٹ اسکرین کے ساتھ کام نہ کرے۔"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ایپ سپلٹ اسکرین کو سپورٹ نہیں کرتی۔"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن ہے ایپ ثانوی ڈسپلے پر کام نہ کرے۔"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"سمجھ آ گئی"</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..c2ef69ff1488 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(بلا عنوان پروگرام)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP بند کریں"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"فُل اسکرین"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP کو منتقل کریں"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP کو پھیلائیں"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP کو سکیڑیں"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" کنٹرولز کے لیے "<annotation icon="home_icon">"ہوم "</annotation>" بٹن کو دو بار دبائیں"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 4e91e7624434..2e3222560dde 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Yopish"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Yoyish"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Sozlamalar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Ajratilgan ekranga kirish"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> tasvir ustida tasvir rejimida"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ilovasi uchun bu funksiyani sozlamalar orqali faolsizlantirish mumkin."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Keyingisiga o‘tish"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Avvalgisiga qaytish"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Oʻlchamini oʻzgartirish"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Berkitish"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Chiqarish"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Bu ilova ekranni ikkiga ajratish rejimini dastaklamaydi."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Bu ilova ekranni bo‘lish xususiyatini qo‘llab-quvvatlamaydi."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Bu ilova qo‘shimcha ekranda ishlamasligi mumkin."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"OK"</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..9ab95c80aa25 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nomsiz)"</string> <string name="pip_close" msgid="9135220303720555525">"Kadr ichida kadr – chiqish"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Butun ekran"</string> + <string name="pip_move" msgid="1544227837964635439">"PIPni siljitish"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP funksiyasini yoyish"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP funksiyasini yopish"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Boshqaruv uchun "<annotation icon="home_icon">"ASOSIY"</annotation>" tugmani ikki marta bosing"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 169e986f7721..8f3cffecc952 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Đóng"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Mở rộng"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Cài đặt"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Truy cập chế độ chia đôi màn hình"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> đang ở chế độ ảnh trong ảnh"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Nếu bạn không muốn <xliff:g id="NAME">%s</xliff:g> sử dụng tính năng này, hãy nhấn để mở cài đặt và tắt tính năng này."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Chuyển tới mục tiếp theo"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Chuyển về mục trước"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Đổi kích thước"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ẩn"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Hiện"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Ứng dụng có thể không hoạt động với tính năng chia đôi màn hình."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Ứng dụng không hỗ trợ chia đôi màn hình."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Ứng dụng có thể không hoạt động trên màn hình phụ."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Tôi hiểu"</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..146376d3cab6 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Toàn màn hình"</string> + <string name="pip_move" msgid="1544227837964635439">"Di chuyển PIP (Ảnh trong ảnh)"</string> + <string name="pip_expand" msgid="7605396312689038178">"Mở rộng PIP (Ảnh trong ảnh)"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Thu gọn PIP (Ảnh trong ảnh)"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Nhấn đúp vào nút "<annotation icon="home_icon">" MÀN HÌNH CHÍNH "</annotation>" để mở trình đơn điều khiển"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index 1999703ddd2a..19a9d371e435 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"关闭"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"展开"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"设置"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"进入分屏模式"</string> <string name="pip_menu_title" msgid="5393619322111827096">"菜单"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>目前位于“画中画”中"</string> <string name="pip_notification_message" msgid="8854051911700302620">"如果您不想让“<xliff:g id="NAME">%s</xliff:g>”使用此功能,请点按以打开设置,然后关闭此功能。"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"隐藏"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消隐藏"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"应用可能无法在分屏模式下正常运行。"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"应用不支持分屏。"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"应用可能无法在辅显示屏上正常运行。"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"知道了"</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..55407d2c699d 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(节目没有标题)"</string> <string name="pip_close" msgid="9135220303720555525">"关闭画中画"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全屏"</string> + <string name="pip_move" msgid="1544227837964635439">"移动画中画窗口"</string> + <string name="pip_expand" msgid="7605396312689038178">"展开 PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"收起 PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" 按两次"<annotation icon="home_icon">"主屏幕"</annotation>"按钮可查看相关控件"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index f82d6d5e771b..0c40e963f2e4 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"關閉"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"進入分割螢幕"</string> <string name="pip_menu_title" msgid="5393619322111827096">"選單"</string> <string name="pip_notification_title" msgid="1347104727641353453">"「<xliff:g id="NAME">%s</xliff:g>」目前在畫中畫模式"</string> <string name="pip_notification_message" msgid="8854051911700302620">"如果您不想「<xliff:g id="NAME">%s</xliff:g>」使用此功能,請輕按以開啟設定,然後停用此功能。"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"保護"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消保護"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"應用程式不支援分割畫面。"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示屏上運作。"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"知道了"</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..15e278d8ecc2 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(沒有標題的節目)"</string> <string name="pip_close" msgid="9135220303720555525">"關閉 PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> + <string name="pip_move" msgid="1544227837964635439">"移動畫中畫"</string> + <string name="pip_expand" msgid="7605396312689038178">"展開畫中畫"</string> + <string name="pip_collapse" msgid="5732233773786896094">"收合畫中畫"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">" 主畫面按鈕"</annotation>"即可顯示控制項"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index 596e7c717aa2..8691352cf94a 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"關閉"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"進入分割畫面"</string> <string name="pip_menu_title" msgid="5393619322111827096">"選單"</string> <string name="pip_notification_title" msgid="1347104727641353453">"「<xliff:g id="NAME">%s</xliff:g>」目前在子母畫面中"</string> <string name="pip_notification_message" msgid="8854051911700302620">"如果你不想讓「<xliff:g id="NAME">%s</xliff:g>」使用這項功能,請輕觸開啟設定頁面,然後停用此功能。"</string> @@ -28,6 +29,8 @@ <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_stash" msgid="4060775037619702641">"暫時隱藏"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消暫時隱藏"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"這個應用程式不支援分割畫面。"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示器上運作。"</string> @@ -44,7 +47,7 @@ <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"以 30% 的螢幕空間顯示頂端畫面"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"以全螢幕顯示底部畫面"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"使用單手模式"</string> - <string name="one_handed_tutorial_description" msgid="3486582858591353067">"如要退出,請從螢幕底部向上滑動,或輕觸應用程式上的任何位置"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"如要退出,請從螢幕底部向上滑動,或輕觸應用程式上方的任何位置"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"啟動單手模式"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"結束單手模式"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」對話框的設定"</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"我知道了"</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..0b17b31d23d0 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml @@ -21,4 +21,8 @@ <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無標題的節目)"</string> <string name="pip_close" msgid="9135220303720555525">"關閉子母畫面"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> + <string name="pip_move" msgid="1544227837964635439">"移動子母畫面"</string> + <string name="pip_expand" msgid="7605396312689038178">"展開子母畫面"</string> + <string name="pip_collapse" msgid="5732233773786896094">"收合子母畫面"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">"主畫面按鈕"</annotation>"即可顯示控制選項"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 3fed41f27c0a..44ffbc6afa45 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Vala"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Nweba"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Faka ukuhlukanisa isikrini"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"U-<xliff:g id="NAME">%s</xliff:g> ungaphakathi kwesithombe esiphakathi kwesithombe"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Uma ungafuni i-<xliff:g id="NAME">%s</xliff:g> ukuthi isebenzise lesi sici, thepha ukuze uvule izilungiselelo uphinde uyivale."</string> @@ -28,6 +29,8 @@ <string name="pip_skip_to_next" msgid="8403429188794867653">"Yeqela kokulandelayo"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Yeqela kokwangaphambilini"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Shintsha usayizi"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Yenza isiteshi"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Susa isiteshi"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Izinhlelo zokusebenza kungenzeka zingasebenzi ngesikrini esihlukanisiwe."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uhlelo lokusebenza alusekeli isikrini esihlukanisiwe."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uhlelo lokusebenza kungenzeka lungasebenzi kusibonisi sesibili."</string> @@ -70,5 +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="got_it" msgid="4428750913636945527">"Ngiyezwa"</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..dad8c8128222 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml @@ -21,4 +21,8 @@ <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_fullscreen" msgid="7278047353591302554">"Iskrini esigcwele"</string> + <string name="pip_move" msgid="1544227837964635439">"Hambisa i-PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Nweba i-PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Goqa i-PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Chofoza kabili "<annotation icon="home_icon">" IKHAYA"</annotation>" mayelana nezilawuli"</string> </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..4606d24d1716 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -34,6 +34,10 @@ <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> 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..64b146ec3a83 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/colors_tv.xml @@ -0,0 +1,26 @@ +<?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> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 1b8032b7077b..d416c060c86c 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -70,4 +70,30 @@ <!-- 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> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index af78293eb3ea..59d03c738723 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> @@ -129,6 +129,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 +139,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 +205,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 +228,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 +266,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..09ed9b8e52ee 100644 --- a/libs/WindowManager/Shell/res/values/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -30,5 +30,19 @@ <!-- 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 PIP</string> + + <!-- Button to expand the picture-in-picture (PIP) window [CHAR LIMIT=30] --> + <string name="pip_expand">Expand PIP</string> + + <!-- Button to collapse/shrink the picture-in-picture (PIP) window [CHAR LIMIT=30] --> + <string name="pip_collapse">Collapse PIP</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> </resources> 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..b729fe1e55dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -29,6 +29,7 @@ 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; @@ -48,6 +49,7 @@ 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; @@ -68,6 +70,7 @@ public class ShellInitImpl { DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<BubbleController> bubblesOptional, Optional<SplitScreenController> splitScreenOptional, Optional<AppPairsController> appPairsOptional, @@ -84,6 +87,7 @@ public class ShellInitImpl { mDisplayInsetsController = displayInsetsController; mDragAndDropController = dragAndDropController; mShellTaskOrganizer = shellTaskOrganizer; + mKidsModeTaskOrganizer = kidsModeTaskOrganizer; mBubblesOptional = bubblesOptional; mSplitScreenOptional = splitScreenOptional; mAppPairsOptional = appPairsOptional; @@ -136,6 +140,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/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java index e344c3bea42b..e528df8c89b4 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)); @@ -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); } 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..7cf359729ee8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java @@ -0,0 +1,53 @@ +/* + * 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.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. + */ + void onBackMotion(MotionEvent event, @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..08cb252cdf43 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -0,0 +1,496 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; +import android.app.WindowConfiguration; +import android.content.Context; +import android.graphics.Point; +import android.graphics.PointF; +import android.hardware.HardwareBuffer; +import android.os.RemoteException; +import android.os.SystemProperties; +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.ShellMainThread; + +/** + * Controls the window animation run when a user initiates a back gesture. + */ +public class BackAnimationController implements RemoteCallable<BackAnimationController> { + + private static final String BACK_PREDICTABILITY_PROGRESS_THRESHOLD_PROP = + "persist.debug.back_predictability_progress_threshold"; + // By default, enable new back dispatching without any animations. + private static final int BACK_PREDICTABILITY_PROP = + SystemProperties.getInt("persist.debug.back_predictability", 1); + public static final boolean IS_ENABLED = BACK_PREDICTABILITY_PROP > 0; + private static final int PROGRESS_THRESHOLD = SystemProperties + .getInt(BACK_PREDICTABILITY_PROGRESS_THRESHOLD_PROP, -1); + private static final String TAG = "BackAnimationController"; + @VisibleForTesting + boolean mEnableAnimations = (BACK_PREDICTABILITY_PROP & (1 << 1)) != 0; + + /** + * 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; + + /** @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; + + public BackAnimationController( + @ShellMainThread ShellExecutor shellExecutor, + Context context) { + this(shellExecutor, new SurfaceControl.Transaction(), ActivityTaskManager.getService(), + context); + } + + @VisibleForTesting + BackAnimationController(@NonNull ShellExecutor shellExecutor, + @NonNull SurfaceControl.Transaction transaction, + @NonNull IActivityTaskManager activityTaskManager, + Context context) { + mShellExecutor = shellExecutor; + mTransaction = transaction; + mActivityTaskManager = activityTaskManager; + mContext = context; + } + + 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(MotionEvent event, @BackEvent.SwipeEdge int swipeEdge) { + mShellExecutor.execute(() -> onMotionEvent(event, 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) -> mController.setBackToLauncherCallback(callback)); + } + + @Override + public void clearBackToLauncherCallback() { + executeRemoteCallWithTaskPermission(mController, "clearBackToLauncherCallback", + (controller) -> mController.clearBackToLauncherCallback()); + } + + @Override + public void onBackToLauncherAnimationFinished() { + executeRemoteCallWithTaskPermission(mController, "onBackToLauncherAnimationFinished", + (controller) -> mController.onBackToLauncherAnimationFinished()); + } + + void invalidate() { + mController = null; + } + } + + @VisibleForTesting + void setBackToLauncherCallback(IOnBackInvokedCallback callback) { + mBackToLauncherCallback = callback; + } + + private void clearBackToLauncherCallback() { + mBackToLauncherCallback = null; + } + + private 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(MotionEvent event, @BackEvent.SwipeEdge int swipeEdge) { + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + initAnimation(event); + } else if (action == MotionEvent.ACTION_MOVE) { + onMove(event, swipeEdge); + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + onGestureFinished(); + } + } + + private void initAnimation(MotionEvent event) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); + if (mBackGestureStarted) { + Log.e(TAG, "Animation is being initialized but is already started."); + return; + } + + if (mBackNavigationInfo != null) { + finishAnimation(); + } + mInitTouchLocation.set(event.getX(), event.getY()); + mBackGestureStarted = true; + + try { + mBackNavigationInfo = mActivityTaskManager.startBackNavigation(); + 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(MotionEvent event, @BackEvent.SwipeEdge int swipeEdge) { + if (!mBackGestureStarted || mBackNavigationInfo == null) { + return; + } + int deltaX = Math.round(event.getX() - mInitTouchLocation.x); + int deltaY = Math.round(event.getY() - mInitTouchLocation.y); + ProtoLog.v(WM_SHELL_BACK_PREVIEW, "Runner move: %d %d", deltaX, deltaY); + 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(0, 0, progress, swipeEdge, animationTarget); + IOnBackInvokedCallback targetCallback = null; + if (shouldDispatchToLauncher(backType)) { + targetCallback = mBackToLauncherCallback; + } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK + || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { + if (animationTarget != null) { + mTransaction.setPosition(animationTarget.leash, deltaX, deltaY); + mTouchEventDelta.set(deltaX, deltaY); + mTransaction.apply(); + } + } 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 (mTriggerBack) { + dispatchOnBackInvoked(targetCallback); + } else { + dispatchOnBackCancelled(targetCallback); + } + if (backType == BackNavigationInfo.TYPE_CALLBACK) { + finishAnimation(); + } else if (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME + && !shouldDispatchToLauncher) { + // Launcher callback missing. Simply finish animation. + finishAnimation(); + } else if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY + || backType == BackNavigationInfo.TYPE_CROSS_TASK) { + if (mTriggerBack) { + prepareTransition(); + } else { + resetPositionAnimated(); + } + } + } + + private boolean shouldDispatchToLauncher(int backType) { + return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME + && mBackToLauncherCallback != null + && mEnableAnimations; + } + + @VisibleForTesting + void setEnableAnimations(boolean shouldEnable) { + mEnableAnimations = shouldEnable; + } + + 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); + } + } + + /** + * Animate the top window leash to its initial position. + */ + private void resetPositionAnimated() { + mBackGestureStarted = false; + // TODO(208786853) Handle overlap with a new coming gesture. + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Runner: Back not triggered, cancelling animation " + + "mLastPos=%s mInitTouch=%s", mTouchEventDelta, mInitTouchLocation); + + // TODO(208427216) : Replace placeholder animation with an actual one. + ValueAnimator animation = ValueAnimator.ofFloat(0f, 1f).setDuration(200); + animation.addUpdateListener(animation1 -> { + if (mBackNavigationInfo == null) { + return; + } + float fraction = animation1.getAnimatedFraction(); + int deltaX = Math.round(mTouchEventDelta.x - (mTouchEventDelta.x * fraction)); + int deltaY = Math.round(mTouchEventDelta.y - (mTouchEventDelta.y * fraction)); + RemoteAnimationTarget animationTarget = + mBackNavigationInfo.getDepartingAnimationTarget(); + if (animationTarget != null) { + mTransaction.setPosition(animationTarget.leash, deltaX, deltaY); + mTransaction.apply(); + } + }); + + animation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onAnimationEnd"); + finishAnimation(); + } + }); + animation.start(); + } + + private void prepareTransition() { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "prepareTransition()"); + mTriggerBack = false; + mBackGestureStarted = false; + } + + /** + * Sets to true when the back gesture has passed the triggering threshold, false otherwise. + */ + public void setTriggerBack(boolean triggerBack) { + 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(); + backNavigationInfo.onBackNavigationFinished(triggerBack); + } +} 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..3876533a922e 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,27 @@ */ 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.graphics.Bitmap; +import android.content.res.TypedArray; 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 +47,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 +72,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 +87,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 +104,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 +148,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 +161,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 +173,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; @@ -176,6 +190,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 +307,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 +350,26 @@ public class BadgedImageView extends ImageView { } void showBadge() { - Bitmap badge = mBubble.getAppBadge(); - if (badge == null) { - setImageBitmap(mBubble.getBubbleIcon()); + if (mBubble.getAppBadge() == 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 = -(mBubbleIcon.getWidth() - mAppIcon.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..cf4647a09a58 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 @@ -108,6 +108,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; @@ -248,6 +250,11 @@ public class Bubble implements BubbleViewProvider { } @Override + public Bitmap getRawAppBadge() { + return mRawBadgeBitmap; + } + + @Override public int getDotColor() { return mDotColor; } @@ -357,13 +364,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 +382,7 @@ public class Bubble implements BubbleViewProvider { controller, stackView, iconFactory, + badgeIconFactory, skipInflation, callback, mMainExecutor); @@ -409,6 +419,7 @@ public class Bubble implements BubbleViewProvider { mFlyoutMessage = info.flyoutMessage; mBadgeBitmap = info.badgeBitmap; + mRawBadgeBitmap = info.mRawBadgeBitmap; mBubbleBitmap = info.bubbleBitmap; mDotColor = info.dotColor; 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..d2a1c55d1c29 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; @@ -80,6 +87,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,6 +96,8 @@ 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.onehanded.OneHandedController; +import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import java.io.FileDescriptor; @@ -97,6 +107,7 @@ 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 +134,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; @@ -136,6 +151,7 @@ public class BubbleController { 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 @@ -146,6 +162,7 @@ public class BubbleController { private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; private BubbleIconFactory mBubbleIconFactory; + private BubbleBadgeIconFactory mBubbleBadgeIconFactory; private BubblePositioner mBubblePositioner; private Bubbles.SysuiProxy mSysuiProxy; @@ -196,6 +213,9 @@ 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; + /** * Creates an instance of the BubbleController. */ @@ -210,8 +230,10 @@ public class BubbleController { UiEventLogger uiEventLogger, ShellTaskOrganizer organizer, DisplayController displayController, + Optional<OneHandedController> oneHandedOptional, ShellExecutor mainExecutor, Handler mainHandler, + TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { BubbleLogger logger = new BubbleLogger(uiEventLogger); BubblePositioner positioner = new BubblePositioner(context, windowManager); @@ -219,8 +241,9 @@ public class BubbleController { return new BubbleController(context, data, synchronizer, floatingContentCoordinator, new BubbleDataRepository(context, launcherApps, mainExecutor), statusBarService, windowManager, windowManagerShellWrapper, launcherApps, - logger, taskStackListener, organizer, positioner, displayController, mainExecutor, - mainHandler, syncQueue); + logger, taskStackListener, organizer, positioner, displayController, + oneHandedOptional, mainExecutor, mainHandler, taskViewTransitions, + syncQueue); } /** @@ -241,8 +264,10 @@ public class BubbleController { ShellTaskOrganizer organizer, BubblePositioner positioner, DisplayController displayController, + Optional<OneHandedController> oneHandedOptional, ShellExecutor mainExecutor, Handler mainHandler, + TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { mContext = context; mLauncherApps = launcherApps; @@ -265,10 +290,32 @@ public class BubbleController { mBubbleData = data; mSavedBubbleKeysPerUser = new SparseSetArray<>(); mBubbleIconFactory = new BubbleIconFactory(context); + mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context); mDisplayController = displayController; + mTaskViewTransitions = taskViewTransitions; + mOneHandedOptional = oneHandedOptional; 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); @@ -336,23 +383,15 @@ 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()) { + expandedId = mStackView.getExpandedBubble().getTaskId(); + } + if (expandedId != INVALID_TASK_ID && expandedId != taskId) { + mBubbleData.setExpanded(false); + } }); } @@ -383,7 +422,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 +430,8 @@ public class BubbleController { } } }); + + mOneHandedOptional.ifPresent(this::registerOneHandedState); } @VisibleForTesting @@ -464,6 +504,7 @@ public class BubbleController { } mStackView.updateStackPosition(); mBubbleIconFactory = new BubbleIconFactory(mContext); + mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext); mStackView.onDisplaySizeChanged(); } if (b.getBoolean(EXTRA_BUBBLE_OVERFLOW_OPENED, false)) { @@ -570,8 +611,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 +659,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 +674,7 @@ public class BubbleController { try { mAddedToWindowManager = true; + registerBroadcastReceiver(); mBubbleData.getOverflow().initialize(this); mWindowManager.addView(mStackView, mWmLayoutParams); mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> { @@ -670,6 +717,7 @@ public class BubbleController { try { mAddedToWindowManager = false; + mContext.unregisterReceiver(mBroadcastReceiver); if (mStackView != null) { mWindowManager.removeView(mStackView); mBubbleData.getOverflow().cleanUpExpandedState(); @@ -683,11 +731,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 +775,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 +809,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 +835,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) { @@ -921,7 +997,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 +1007,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,11 +1017,17 @@ 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); @@ -956,7 +1039,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 */); } /** @@ -1042,6 +1126,24 @@ public class BubbleController { } } + @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); + } + } + } + } + /** * Retrieves any bubbles that are part of the notification group represented by the provided * group key. @@ -1099,6 +1201,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 @@ -1178,6 +1292,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) { @@ -1192,14 +1314,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 +1393,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 +1411,8 @@ public class BubbleController { } mStackView.updateContentDescription(); + + mStackView.updateBubblesAcessibillityStates(); } @VisibleForTesting @@ -1324,7 +1441,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 +1574,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 +1734,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); 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..9961ad71e64a 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 @@ -224,7 +224,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 +236,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; @@ -356,11 +371,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(); @@ -532,16 +547,19 @@ 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; + 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 +573,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 +591,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 +608,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 +702,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 +710,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 +722,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 +739,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 +813,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 +962,9 @@ public class BubbleData { if (b == null) { b = getOverflowBubbleWithKey(key); } + if (b == null) { + b = getSuppressedBubbleWithKey(key); + } return b; } @@ -953,6 +1042,23 @@ 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; + } + @VisibleForTesting(visibility = PRIVATE) void setTimeSource(TimeSource timeSource) { mTimeSource = timeSource; 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..10ff2fb31b0d 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,6 +60,7 @@ 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; @@ -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; } /** @@ -689,6 +692,7 @@ 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) { // Pointer gets drawn in the padding 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..97e5ee3a35d3 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,6 +81,7 @@ public class BubblePositioner { private boolean mImeVisible; private int mImeHeight; private boolean mIsLargeScreen; + private boolean mIsSmallTablet; private Rect mPositionRect; private int mDefaultMaxBubbles; @@ -85,7 +91,8 @@ public class BubblePositioner { private int mExpandedViewMinHeight; private int mExpandedViewLargeScreenWidth; - private int mExpandedViewLargeScreenInset; + private int mExpandedViewLargeScreenInsetClosestEdge; + private int mExpandedViewLargeScreenInsetFurthestEdge; private int mOverflowWidth; private int mExpandedViewPadding; @@ -112,10 +119,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 +133,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 +187,32 @@ 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); + 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 +302,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. */ @@ -315,6 +345,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. * @@ -329,16 +368,21 @@ public class BubblePositioner { */ public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) { final int pointerTotalHeight = mPointerHeight - mPointerOverlap; + 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 +540,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 @@ -534,11 +579,10 @@ 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 bottomHeight = getImeHeight() + mInsets.bottom - (mSpacingBetweenBubbles * 2); + final float bottomInset = mScreenRect.bottom - bottomHeight; final float expandedStackSize = getExpandedStackSize(numberOfBubbles); - final float centerPosition = showBubblesVertically() - ? mPositionRect.centerY() - : mPositionRect.centerX(); + final float centerPosition = mPositionRect.centerY(); final float rowBottom = centerPosition + (expandedStackSize / 2f); final float rowTop = centerPosition - (expandedStackSize / 2f); float rowTopForIme = rowTop; 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..331941103028 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; @@ -167,26 +168,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(); @@ -780,12 +782,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 +824,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 +899,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.updateResources(); mBubbleOverflow.updateResources(); - if (mRelativeStackPositionBeforeRotation != null) { + if (!isStackEduShowing() && mRelativeStackPositionBeforeRotation != null) { mStackAnimationController.setStackPosition( mRelativeStackPositionBeforeRotation); mRelativeStackPositionBeforeRotation = null; @@ -939,7 +943,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 +1049,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 +1139,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 +1201,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 +1217,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 +1249,7 @@ public class BubbleStackView extends FrameLayout b.getExpandedView().updateFontSize(); } } - if (mBubbleOverflow != null) { + if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) { mBubbleOverflow.getExpandedView().updateFontSize(); } } @@ -1300,7 +1316,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 +1335,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(), + mStackAnimationController.getAllowableStackPositionRegion())); + } if (mIsExpanded) { updateExpandedView(); } + setUpManageMenu(); } @Override @@ -1482,6 +1500,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); @@ -1541,7 +1622,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 +1637,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 +1667,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 +1867,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 } /** @@ -2125,7 +2235,7 @@ public class BubbleStackView extends FrameLayout PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) .spring(DynamicAnimation.TRANSLATION_Y, mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize, - mTranslateSpringConfig) + mTranslateSpringConfig) .start(); } @@ -2614,11 +2724,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,7 +2809,14 @@ 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) { mSurfaceSynchronizer.syncSurfaceAndRun(() -> { @@ -2707,6 +2826,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, @@ -2972,14 +3101,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 +3141,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..af403d23d69c 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; @@ -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). */ 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..c09d1e0d189c 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,9 +101,8 @@ 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 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..1ff4be887fb2 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,28 +122,29 @@ 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) } translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2 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..3ba056a6b4a2 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 @@ -750,6 +750,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 +791,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 +816,7 @@ public class StackAnimationController extends } else { moveToFinalIndex(view, newIndex, finishReorder); } + return true; } } 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/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/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/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 36e55bae18c3..5dc6bd19853a 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,11 @@ 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; + public SplitDecorManager(Configuration configuration, IconProvider iconProvider, SurfaceSession surfaceSession) { super(configuration, null /* rootSurface */, null /* hostInputToken */); @@ -113,6 +123,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 +141,8 @@ public class SplitDecorManager extends WindowlessWindowManager { mHostLeash = null; mIcon = null; mResizingIconView = null; + mIsResizing = false; + mShown = false; } /** Showing resizing hint. */ @@ -137,16 +152,19 @@ public class SplitDecorManager extends WindowlessWindowManager { return; } + if (!mIsResizing) { + mIsResizing = true; + mBounds.set(newBounds); + } + 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); @@ -156,20 +174,99 @@ public class SplitDecorManager extends WindowlessWindowManager { lp.width = mIcon.getIntrinsicWidth(); lp.height = mIcon.getIntrinsicHeight(); 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); + + boolean show = newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height(); + if (show != mShown) { + if (mFadeAnimator != null && mFadeAnimator.isRunning()) { + mFadeAnimator.cancel(); + } + 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..116d3524e711 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; @@ -93,11 +92,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange 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; @@ -123,7 +123,7 @@ 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(); } @@ -144,21 +144,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 +201,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) { @@ -260,20 +293,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 +335,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 +348,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 +391,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 +410,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 +419,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 */); } @@ -381,7 +439,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - setDividePosition(to); if (flingFinishedCallback != null) { flingFinishedCallback.run(); } @@ -389,7 +446,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange @Override public void onAnimationCancel(Animator animation) { - setDividePosition(to); + setDividePosition(to, true /* applyLayoutChange */); } }); animator.start(); @@ -432,14 +489,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { 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)) { @@ -452,22 +512,28 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange /** 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. @@ -687,6 +753,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 +776,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 + mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && mImeShown ? ADJUSTED_NONFOCUS_DIM : 0.0f; mLastDim2 = mDimValue2; - mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && showing + mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && mImeShown ? 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 +820,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 +835,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 +847,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 +881,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 +889,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..833d9d50701c 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 @@ -58,6 +58,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,22 +133,38 @@ 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); 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..1dd5ebcd993e 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 @@ -27,11 +27,8 @@ 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.PipMediaController; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; @@ -39,6 +36,8 @@ 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.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; @@ -60,29 +59,33 @@ public abstract class TvPipModule { @Provides static Optional<Pip> providePip( Context context, - PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, PipTransitionController pipTransitionController, TvPipNotificationController tvPipNotificationController, TaskStackListenerImpl taskStackListener, + DisplayController displayController, WindowManagerShellWrapper windowManagerShellWrapper, - @ShellMainThread ShellExecutor mainExecutor) { + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { return Optional.of( TvPipController.create( context, - pipBoundsState, - pipBoundsAlgorithm, + tvPipBoundsState, + tvPipBoundsAlgorithm, pipTaskOrganizer, pipTransitionController, tvPipMenuController, pipMediaController, tvPipNotificationController, taskStackListener, + displayController, windowManagerShellWrapper, - mainExecutor)); + mainExecutor, + mainHandler)); } @WMSingleton @@ -93,15 +96,15 @@ public abstract class TvPipModule { @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 +112,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); } @@ -154,21 +158,20 @@ 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, 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, + syncTransactionQueue, pipTransitionState, tvPipBoundsState, tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, - pipTransitionController, splitScreenOptional, newSplitScreenOptional, + pipTransitionController, splitScreenControllerOptional, displayController, pipUiEventLogger, shellTaskOrganizer, 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..026eeb09d741 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; @@ -65,6 +68,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; @@ -92,6 +96,7 @@ import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; import java.util.Optional; import dagger.BindsOptionalOf; +import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -165,8 +170,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 +186,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 +209,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 +256,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 +283,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); + } } // @@ -352,7 +391,6 @@ public abstract class WMShellBaseModule { return Optional.empty(); } - // // Task to Surface communication // @@ -449,9 +487,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 +630,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,6 +653,7 @@ public abstract class WMShellBaseModule { DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<BubbleController> bubblesOptional, Optional<SplitScreenController> splitScreenOptional, Optional<AppPairsController> appPairsOptional, @@ -622,6 +670,7 @@ public abstract class WMShellBaseModule { displayInsetsController, dragAndDropController, shellTaskOrganizer, + kidsModeTaskOrganizer, bubblesOptional, splitScreenOptional, appPairsOptional, @@ -649,6 +698,7 @@ public abstract class WMShellBaseModule { @Provides static ShellCommandHandlerImpl provideShellCommandHandlerImpl( ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<LegacySplitScreenController> legacySplitScreenOptional, Optional<SplitScreenController> splitScreenOptional, Optional<Pip> pipOptional, @@ -657,8 +707,21 @@ 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 + ) { + if (BackAnimationController.IS_ENABLED) { + return Optional.of( + new BackAnimationController(shellExecutor, 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..8f9636c0bb30 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 @@ -27,7 +27,10 @@ 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; @@ -35,7 +38,6 @@ import com.android.wm.shell.common.annotations.ExternalMainThread; import com.android.wm.shell.common.annotations.ShellAnimationThread; 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 +55,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 +87,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; } 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..73f393140cbc 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 @@ -22,11 +22,13 @@ import android.content.pm.LauncherApps; import android.os.Handler; 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; @@ -106,13 +108,16 @@ public class WMShellModule { UiEventLogger uiEventLogger, ShellTaskOrganizer organizer, DisplayController displayController, + @DynamicOverride Optional<OneHandedController> oneHandedOptional, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, + TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { return BubbleController.create(context, null /* synchronizer */, floatingContentCoordinator, statusBarService, windowManager, windowManagerShellWrapper, launcherApps, taskStackListener, - uiEventLogger, organizer, displayController, mainExecutor, mainHandler, syncQueue); + uiEventLogger, organizer, displayController, oneHandedOptional, + mainExecutor, mainHandler, taskViewTransitions, syncQueue); } // @@ -139,12 +144,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 +162,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); } @@ -242,10 +246,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 +285,14 @@ public class WMShellModule { PipAnimationController pipAnimationController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, PipTransitionController pipTransitionController, - 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, menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper, - pipTransitionController, splitScreenOptional, newSplitScreenOptional, + pipTransitionController, splitScreenControllerOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); } @@ -305,9 +309,12 @@ 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 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..11ecc9197be7 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 @@ -35,6 +35,9 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.content.ClipDescription; import android.content.Context; import android.content.res.Configuration; @@ -54,6 +57,7 @@ import com.android.internal.logging.UiEventLogger; 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.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -205,6 +209,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange break; case ACTION_DRAG_ENTERED: pd.dragLayout.show(); + pd.dragLayout.update(event); break; case ACTION_DRAG_LOCATION: pd.dragLayout.update(event); @@ -250,10 +255,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..25fe8b9e0da3 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. @@ -139,6 +138,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 +159,6 @@ public class DragLayout extends LinearLayout { } } - public boolean hasDropTarget() { - return mCurrentTarget != null; - } - public boolean hasDropped() { return mHasDropped; } @@ -171,22 +172,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 +213,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 +285,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 +298,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 +319,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 +337,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 +420,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..38870bcb3383 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,6 @@ 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.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -43,8 +43,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 +61,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,19 +104,14 @@ 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); 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.setAlpha(0f); @@ -157,10 +126,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 +142,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 +160,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 +202,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 +266,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/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java new file mode 100644 index 000000000000..003c55977841 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Context; +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.internal.policy.ForceShowNavigationBarSettingsObserver; +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 ForceShowNavigationBarSettingsObserver mForceShowNavigationBarSettingsObserver; + private boolean mEnabled; + + 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, + ForceShowNavigationBarSettingsObserver forceShowNavigationBarSettingsObserver) { + super(taskOrganizerController, mainExecutor, context, /* compatUI= */ null, recentTasks); + mContext = context; + mMainHandler = mainHandler; + mSyncQueue = syncTransactionQueue; + mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; + mForceShowNavigationBarSettingsObserver = forceShowNavigationBarSettingsObserver; + } + + 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 (mForceShowNavigationBarSettingsObserver == null) { + mForceShowNavigationBarSettingsObserver = new ForceShowNavigationBarSettingsObserver( + mMainHandler, mContext); + } + mForceShowNavigationBarSettingsObserver.setOnChangeRunnable(() -> updateKidsModeState()); + updateKidsModeState(); + mForceShowNavigationBarSettingsObserver.register(); + } + + @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 = mForceShowNavigationBarSettingsObserver.isEnabled(); + if (mEnabled == enabled) { + return; + } + mEnabled = enabled; + if (mEnabled) { + enable(); + } else { + disable(); + } + } + + @VisibleForTesting + void enable() { + 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() { + 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/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..c20b7d9b2747 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.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) { + // One-handed mode is only available 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()); + } + } + + 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..48acfc1c76e7 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,12 +29,10 @@ 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; @@ -48,6 +45,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 +68,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 +99,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 +162,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, public void onStopFinished(Rect bounds) { mState.setState(STATE_NONE); notifyShortcutStateChanged(STATE_NONE); - mBackgroundPanelOrganizer.onStopFinished(); } }; @@ -200,37 +193,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 +228,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, OneHandedAccessibilityUtil oneHandedAccessibilityUtil, OneHandedTimeoutHandler timeoutHandler, OneHandedState state, + InteractionJankMonitor jankMonitor, OneHandedUiEventLogger uiEventsLogger, IOverlayManager overlayManager, TaskStackListenerImpl taskStackListener, @@ -246,7 +237,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mContext = context; mOneHandedSettingsUtil = settingsUtil; mOneHandedAccessibilityUtil = oneHandedAccessibilityUtil; - mBackgroundPanelOrganizer = backgroundPanelOrganizer; mDisplayAreaOrganizer = displayAreaOrganizer; mDisplayController = displayController; mTouchHandler = touchHandler; @@ -282,14 +272,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); } @@ -371,7 +360,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 +387,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 +443,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 +511,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 +575,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 +583,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 +597,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 +633,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 +658,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..04e8cf9d2c44 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; } @@ -145,6 +157,7 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, } mTutorialAreaHeight = Math.round(mDisplayBounds.height() * mTutorialHeightRatio); mAlphaTransitionStart = mTutorialAreaHeight * START_TRANSITION_FRACTION; + mBackgroundWindowManager.onDisplayChanged(displayLayout); } @VisibleForTesting @@ -168,6 +181,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 +199,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 +231,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 +268,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 +311,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..ef627647794e 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 @@ -32,4 +32,9 @@ oneway interface IPipAnimationListener { * @param cornerRadius the pixel value of the corner radius, zero means it's disabled. */ void onPipCornerRadiusChanged(int cornerRadius); + + /** + * 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..87eca74acd0b 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 @@ -72,9 +72,10 @@ public class PinnedStackListenerForwarder { } } - private void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + private void onActionsChanged(ParceledListSlice<RemoteAction> actions, + RemoteAction closeAction) { for (PinnedTaskListener listener : mListeners) { - listener.onActionsChanged(actions); + listener.onActionsChanged(actions, closeAction); } } @@ -90,6 +91,12 @@ public class PinnedStackListenerForwarder { } } + private void onExpandedAspectRatioChanged(float aspectRatio) { + for (PinnedTaskListener listener : mListeners) { + listener.onExpandedAspectRatioChanged(aspectRatio); + } + } + @BinderThread private class PinnedTaskListenerImpl extends IPinnedTaskListener.Stub { @Override @@ -107,9 +114,10 @@ public class PinnedStackListenerForwarder { } @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + public void onActionsChanged(ParceledListSlice<RemoteAction> actions, + RemoteAction closeAction) { mMainExecutor.execute(() -> { - PinnedStackListenerForwarder.this.onActionsChanged(actions); + PinnedStackListenerForwarder.this.onActionsChanged(actions, closeAction); }); } @@ -126,6 +134,15 @@ public class PinnedStackListenerForwarder { PinnedStackListenerForwarder.this.onAspectRatioChanged(aspectRatio); }); } + + @Override + public void onExpandedAspectRatioChanged(float aspectRatio) { + mMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onExpandedAspectRatioChanged(aspectRatio); + }); + } + + } /** @@ -137,10 +154,13 @@ public class PinnedStackListenerForwarder { public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {} - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {} + public void onActionsChanged(ParceledListSlice<RemoteAction> actions, + RemoteAction closeAction) {} public void onActivityHidden(ComponentName componentName) {} public void onAspectRatioChanged(float aspectRatio) {} + + public void onExpandedAspectRatioChanged(float aspectRatio) {} } } 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..77fd228af286 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 @@ -584,9 +584,11 @@ 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) + getSurfaceTransactionHelper().crop(tx, leash, base) + .scale(tx, leash, base, bounds, angle) .round(tx, leash, base, bounds); } } else { @@ -618,17 +620,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 +648,10 @@ 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); + } tx.apply(); } 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/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java index 97139626a3d2..8a50f2233573 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 @@ -144,7 +144,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, 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..f6ff294b4328 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 @@ -66,7 +66,7 @@ public interface PipMenuController { /** * Given a set of actions, update the menu. */ - void setAppActions(ParceledListSlice<RemoteAction> appActions); + void setAppActions(ParceledListSlice<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/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index 180e3fb48c9d..d7322ce7beda 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 @@ -138,8 +138,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); 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..5d6b041f9006 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,7 +62,6 @@ 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; @@ -67,6 +71,7 @@ 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; @@ -127,7 +132,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, 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; @@ -243,7 +247,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 +259,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @NonNull PipTransitionController pipTransitionController, - Optional<LegacySplitScreenController> legacySplitScreenOptional, Optional<SplitScreenController> splitScreenOptional, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @@ -277,7 +281,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 +307,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 +353,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) { + // 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 +382,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 +404,62 @@ 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(); + 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 = mPipBoundsState.getDisplayBounds(); 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); + } + // 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 +483,30 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, }); } + 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 +529,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 +543,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); } } @@ -521,7 +574,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 +584,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 +604,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 +622,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); @@ -626,7 +681,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void onEndOfSwipePipToHomeTransition() { if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mSwipePipToHomeOverlay = null; return; } @@ -698,7 +752,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,24 +764,19 @@ 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); - } + onExitPipFinished(info); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mPipTransitionController.forceFinishTransition(); @@ -749,8 +798,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; @@ -761,7 +811,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipBoundsAlgorithm.getMinimalSize(info.topActivityInfo)); final PictureInPictureParams newParams = info.pictureInPictureParams; if (newParams == null || !applyPictureInPictureParams(newParams)) { - Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Ignored onTaskInfoChanged with PiP param: %s", TAG, newParams); return; } // Aspect ratio changed, re-calculate bounds if valid. @@ -784,10 +835,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 +878,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 +907,30 @@ 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; + } + 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; @@ -873,11 +977,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 +992,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) { @@ -962,13 +1071,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) { final Rational currentAspectRatio = - mPictureInPictureParams != null ? mPictureInPictureParams.getAspectRatioRational() + mPictureInPictureParams != null ? mPictureInPictureParams.getAspectRatio() : null; final boolean aspectRatioChanged = !Objects.equals(currentAspectRatio, - params.getAspectRatioRational()); + params.getAspectRatio()); mPictureInPictureParams = params; if (aspectRatioChanged) { - mPipBoundsState.setAspectRatio(params.getAspectRatio()); + mPipBoundsState.setAspectRatio(params.getAspectRatioFloat()); } return aspectRatioChanged; } @@ -989,7 +1098,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 +1113,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 +1152,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 +1188,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; } @@ -1156,7 +1270,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, 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 +1284,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 +1397,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 +1430,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 @@ -1380,43 +1498,28 @@ 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()) { - return false; - } - - LegacySplitScreenController legacySplitScreen = mLegacySplitScreenOptional.get(); - if (!legacySplitScreen.isDividerVisible()) { - // fail early if system is not in split screen mode + if (!enterSplit || !mSplitScreenOptional.isPresent()) { 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) { return; @@ -1428,7 +1531,8 @@ 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"); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Task vanished, skip fadeOutAndRemoveOverlay", TAG); animation.removeAllListeners(); animation.removeAllUpdateListeners(); animation.cancel(); 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..48df28ee4cde 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.setUseContentOverlay(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..24993c621e3c 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.getContentOverlay() != null) { + mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlay(), + 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.getContentOverlay() != null) { + mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlay(), + 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..9c23a32a7d2b 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,10 @@ 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); 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..d7b69adf1241 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 @@ -24,9 +24,11 @@ import android.app.ActivityTaskManager.RootTaskInfo; import android.content.ComponentName; import android.content.Context; import android.os.RemoteException; -import android.util.Log; import android.util.Pair; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + /** A class that includes convenience methods. */ public class PipUtils { private static final String TAG = "PipUtils"; @@ -51,7 +53,8 @@ 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); } 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..5e4c12ed37ed 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 @@ -28,9 +28,7 @@ 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 +36,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,13 +119,13 @@ public class PhonePipMenuController implements PipMenuController { private final ArrayList<Listener> mListeners = new ArrayList<>(); private final SystemWindows mSystemWindows; private final Optional<SplitScreenController> mSplitScreenController; + private final PipUiEventLogger mPipUiEventLogger; private ParceledListSlice<RemoteAction> mAppActions; private ParceledListSlice<RemoteAction> mMediaActions; private SyncRtSurfaceTransactionApplier mApplier; private int mMenuState; private PipMenuView mPipMenuView; - private IBinder mPipMenuInputToken; private ActionListener mMediaActionListener = new ActionListener() { @Override @@ -150,6 +151,7 @@ public class PhonePipMenuController implements PipMenuController { 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 +160,7 @@ public class PhonePipMenuController implements PipMenuController { mMainExecutor = mainExecutor; mMainHandler = mainHandler; mSplitScreenController = splitScreenOptional; + mPipUiEventLogger = pipUiEventLogger; } public boolean isMenuVisible() { @@ -187,7 +190,7 @@ public class PhonePipMenuController implements PipMenuController { 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); @@ -202,7 +205,6 @@ public class PhonePipMenuController implements PipMenuController { mApplier = null; mSystemWindows.removeView(mPipMenuView); mPipMenuView = null; - mPipMenuInputToken = null; } /** @@ -284,13 +286,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()) { @@ -382,13 +386,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 +404,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 +415,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 +442,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,7 +474,8 @@ 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(ParceledListSlice<RemoteAction> appActions, + RemoteAction closeAction) { mAppActions = appActions; updateMenuActions(); } @@ -516,9 +526,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 +547,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 +597,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/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index a41fd8429e35..ad5d85cc083a 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 @@ -46,10 +46,8 @@ 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 +59,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; @@ -85,10 +84,12 @@ 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.Optional; +import java.util.Set; import java.util.function.Consumer; /** @@ -136,6 +137,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb * @param cornerRadius the pixel value of the corner radius, zero means it's disabled. */ void onPipCornerRadiusChanged(int cornerRadius); + + /** + * Notifies the listener that user leaves PiP by tapping on the expand button. + */ + void onExpandPip(); } /** @@ -143,6 +149,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 +234,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,8 +263,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - mMenuController.setAppActions(actions); + public void onActionsChanged(ParceledListSlice<RemoteAction> actions, + RemoteAction closeAction) { + mMenuController.setAppActions(actions, closeAction); } @Override @@ -282,7 +300,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb 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; } @@ -309,7 +328,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb 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."); } @@ -375,7 +394,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 +407,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(); } @@ -592,9 +613,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 +657,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 +748,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(); @@ -870,7 +895,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb 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); } } } @@ -893,6 +919,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb public void onPipCornerRadiusChanged(int cornerRadius) { mListener.call(l -> l.onPipCornerRadiusChanged(cornerRadius)); } + + @Override + public void onExpandPip() { + mListener.call(l -> l.onExpandPip()); + } }; IPipImpl(PipController controller) { @@ -923,11 +954,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..11633a91e8c6 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); @@ -261,7 +230,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ public void createOrUpdateDismissTarget() { if (!mTargetViewContainer.isAttachedToWindow()) { - mMagneticTargetAnimator.cancel(); + mTargetViewContainer.cancelAnimators(); mTargetViewContainer.setVisibility(View.INVISIBLE); mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); @@ -284,11 +253,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 +281,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 +291,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/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index e1475efcdb57..c0fa8c0a8898 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 @@ -47,7 +47,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,10 +59,13 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; +import com.android.internal.protolog.common.ProtoLog; import com.android.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; @@ -119,8 +121,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() { @@ -150,13 +153,15 @@ public class PipMenuView extends FrameLayout { 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); @@ -419,7 +424,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); @@ -501,7 +506,8 @@ public class PipMenuView extends FrameLayout { try { action.getActionIntent().send(); } catch (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); } }); } @@ -539,6 +545,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 +555,7 @@ 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); } } @@ -566,6 +575,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..fa0f0925a08a 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,12 +34,12 @@ 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 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; @@ -49,6 +49,7 @@ 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; @@ -94,6 +95,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, 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()); } @@ -354,8 +357,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 +372,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 +557,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 +678,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 +692,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..147a272f4645 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,7 +35,6 @@ 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.InputEvent; import android.view.MotionEvent; @@ -46,6 +45,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 +54,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 +150,6 @@ public class PipTouchHandler { @Override public void onPipDismiss() { - mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); mTouchState.removeDoubleTapTimeoutCallback(); mMotionHelper.dismissPip(); } @@ -1011,7 +1011,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/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..1aefd77419aa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -0,0 +1,406 @@ +/* + * 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.Rect; +import android.os.SystemClock; +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(SystemClock::uptimeMillis); + 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)); + mKeepClearAlgorithm.setStashDuration(res.getInteger(R.integer.config_pipStashDuration)); + } + + @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 getTvPipBounds().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 getTvPipBounds().getBounds(); + } + + /** + * Calculates the PiP bounds. + */ + public Placement getTvPipBounds() { + 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()); + + 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 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); + 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); + 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()); + } + } + + void keepUnstashedForCurrentKeepClearAreas() { + mKeepClearAlgorithm.keepUnstashedForCurrentKeepClearAreas(); + } +} 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..986554853034 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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; + + + 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); + 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 || aspectRatio == 0) { + mDesiredTvExpandedAspectRatio = aspectRatio; + resetTvPipState(); + return; + } + if ((aspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL) + || (aspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL)) { + mDesiredTvExpandedAspectRatio = aspectRatio; + } + } + + /** Get the PIP aspect ratio for the expanded PIP (TV) that is desired 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; + } + +} 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..46b8e6098273 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,6 +22,7 @@ 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; @@ -30,42 +31,49 @@ import android.content.pm.ParceledListSlice; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.Handler; 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.PipAnimationController; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Set; /** * Manages the picture-in-picture (PIP) UI and states. */ public class TvPipController implements PipTransitionController.PipTransitionCallback, - TvPipMenuController.Delegate, TvPipNotificationController.Delegate { + 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 double EPS = 1e-7; 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,65 +95,79 @@ 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 PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; private final TvPipNotificationController mPipNotificationController; private final TvPipMenuController mTvPipMenuController; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; 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 Runnable mUnstashRunnable; + + 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; public static Pip create( Context context, - PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, + DisplayController displayController, WindowManagerShellWrapper wmShell, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, + Handler mainHandler) { return new TvPipController( context, - pipBoundsState, - pipBoundsAlgorithm, + tvPipBoundsState, + tvPipBoundsAlgorithm, pipTaskOrganizer, pipTransitionController, tvPipMenuController, pipMediaController, pipNotificationController, taskStackListener, + displayController, wmShell, - mainExecutor).mImpl; + mainExecutor, + mainHandler).mImpl; } private TvPipController( Context context, - PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, + DisplayController displayController, WindowManagerShellWrapper wmShell, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, + Handler mainHandler) { mContext = context; mMainExecutor = mainExecutor; + mMainHandler = mainHandler; - 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; mPipMediaController = pipMediaController; @@ -162,18 +184,26 @@ public class TvPipController implements PipTransitionController.PipTransitionCal 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); } /** @@ -190,26 +220,38 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ @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); + mTvPipBoundsAlgorithm.keepUnstashedForCurrentKeepClearAreas(); + updatePinnedStackBounds(); + } + + @Override + public void onInMoveModeChanged() { + updatePinnedStackBounds(); } /** @@ -217,59 +259,153 @@ 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); + updatePinnedStackBounds(); + } - removeTask(mPinnedTaskId); - onPipDisappeared(); + @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) { + mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted); + updatePinnedStackBounds(); + } } /** - * 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. + * Animates to the current PiP bounds, and schedules unstashing the PiP if necessary. */ - private void resizePinnedStack(@State int state) { - if (state != mState) { - throw new IllegalArgumentException("The passed state should match the current state!"); + private void updatePinnedStackBounds() { + if (mState == STATE_NO_PIP) { + return; } - if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + stateToName(mState)); - final Rect newBounds; - switch (mState) { - case STATE_PIP_MENU: - newBounds = mPipBoundsState.getExpandedBounds(); - break; + final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode(); + final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition; + final Placement placement = mTvPipBoundsAlgorithm.getTvPipBounds(); - 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; + int stashType = + disallowStashing ? PipBoundsState.STASH_TYPE_NONE : placement.getStashType(); + mTvPipBoundsState.setStashed(stashType); - case STATE_NO_PIP: - default: - return; + if (stayAtAnchorPosition) { + movePinnedStackTo(placement.getAnchorBounds()); + } else if (disallowStashing) { + movePinnedStackTo(placement.getUnstashedBounds()); + } else { + movePinnedStackTo(placement.getBounds()); + } + + if (mUnstashRunnable != null) { + mMainHandler.removeCallbacks(mUnstashRunnable); + mUnstashRunnable = null; + } + if (!disallowStashing && placement.getUnstashDestinationBounds() != null) { + mUnstashRunnable = () -> movePinnedStackTo(placement.getUnstashDestinationBounds()); + mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime()); + } + } + + /** Animates the PiP to the given bounds. */ + private void movePinnedStackTo(Rect bounds) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString()); + } + mPipTaskOrganizer.scheduleAnimateResizePip(bounds, + mResizeAnimationDuration, rect -> { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePinnedStack() animation done", TAG); + } + mTvPipMenuController.updateExpansionState(); + }); + } + + /** + * 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); } - mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null); + 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(); } private void registerSessionListenerForCurrentUser() { @@ -278,57 +414,75 @@ 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(); 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)); + } } @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)); + } } @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)); } } 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 +490,7 @@ 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); } private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) { @@ -365,7 +509,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. @@ -381,21 +528,86 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @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); + mTvPipBoundsState.setImeVisibility(imeVisible, imeHeight); + + if (mState != STATE_NO_PIP) { + updatePinnedStackBounds(); + } + } + + @Override + public void onAspectRatioChanged(float ratio) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onAspectRatioChanged: %f", TAG, ratio); + } + + boolean ratioChanged = mTvPipBoundsState.getAspectRatio() != ratio; + mTvPipBoundsState.setAspectRatio(ratio); + + if (!mTvPipBoundsState.isTvPipExpanded() && ratioChanged) { + updatePinnedStackBounds(); + } + } + + @Override + public void onExpandedAspectRatioChanged(float ratio) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onExpandedAspectRatioChanged: %f", TAG, ratio); + } + + // 0) No update to the ratio --> don't do anything + + if (Math.abs(mTvPipBoundsState.getDesiredTvExpandedAspectRatio() - ratio) + < EPS) { + return; + } + + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false); + + // 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(); } } @@ -403,36 +615,53 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public void onMovementBoundsChanged(boolean fromImeAdjustment) {} @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - if (DEBUG) Log.d(TAG, "onActionsChanged()"); + public void onActionsChanged(ParceledListSlice<RemoteAction> actions, + RemoteAction closeAction) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onActionsChanged()", TAG); + } - mTvPipMenuController.setAppActions(actions); + mTvPipMenuController.setAppActions(actions, closeAction); + mCloseAction = closeAction; } }); } 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..5ac7a7200494 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt @@ -0,0 +1,741 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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 DEFAULT_STASH_DURATION = 5000L +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. + * + * @param clock A function returning a current timestamp (in milliseconds) + */ +class TvPipKeepClearAlgorithm(private val clock: () -> Long) { + /** + * 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 after + * [stashDuration] has passed. + * @param unstashTime If stashed, the time at which the PiP should move + * to [unstashDestinationBounds] + */ + data class Placement( + val bounds: Rect, + val anchorBounds: Rect, + @PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE, + val unstashDestinationBounds: Rect? = null, + val unstashTime: Long = 0L + ) { + /** 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 + + /** + * How long (in milliseconds) the PiP should stay stashed for after the last time the + * keep clear areas causing the PiP to stash have changed. + */ + var stashDuration = DEFAULT_STASH_DURATION + + /** 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() + private var lastStashTime: Long = Long.MIN_VALUE + + /** + * 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 once [Placement.unstashTime] has + * passed 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 pipAnchorBounds = getNormalPipAnchorBounds(pipSize, transformedMovementBounds) + + val result = calculatePipPositionTransformed( + pipAnchorBounds, + transformedRestrictedAreas, + transformedUnrestrictedAreas + ) + + val screenSpaceBounds = fromTransformedSpace(result.bounds) + return Placement( + screenSpaceBounds, + fromTransformedSpace(result.anchorBounds), + getStashType(screenSpaceBounds, movementBounds), + result.unstashDestinationBounds?.let { fromTransformedSpace(it) }, + result.unstashTime + ) + } + + /** + * 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 (restrictedAreas.isEmpty() && unrestrictedAreas.isEmpty()) { + 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 keepClearAreas = restrictedAreas + unrestrictedAreas + val areasOverlappingUnstashPosition = + keepClearAreas.filter { Rect.intersects(it, unstashBounds) }.toSet() + val areasOverlappingUnstashPositionChanged = + !lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition) + lastAreasOverlappingUnstashPosition = areasOverlappingUnstashPosition + + val now = clock() + if (areasOverlappingUnstashPositionChanged) { + lastStashTime = now + } + + // If overlapping areas haven't changed and the stash duration has passed, we can + // place the PiP at the unstash position + val unstashTime = lastStashTime + stashDuration + if (now >= unstashTime) { + return Placement(unstashBounds, pipAnchorBounds) + } + + // Otherwise, we'll stash it close to the unstash position + val stashedBounds = getNearbyStashedPosition(unstashBounds, keepClearAreas) + return Placement( + stashedBounds, + pipAnchorBounds, + getStashType(stashedBounds, transformedMovementBounds), + unstashBounds, + unstashTime + ) + } + + @PipBoundsState.StashType + private fun getStashType(stashedBounds: Rect, movementBounds: Rect): Int { + return when { + stashedBounds.left < movementBounds.left -> STASH_TYPE_LEFT + stashedBounds.right > movementBounds.right -> STASH_TYPE_RIGHT + stashedBounds.top < movementBounds.top -> STASH_TYPE_TOP + stashedBounds.bottom > movementBounds.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 = Array(2) { Rect(bounds) } + val areasOverlappingPipX = keepClearAreas.filter { it.intersectsX(bounds) } + val areasOverlappingPipY = keepClearAreas.filter { it.intersectsY(bounds) } + + if (screenBounds.bottom - bounds.bottom <= bounds.top - screenBounds.top) { + // bottom is closer than top, stash downwards + val fullStashTop = screenBounds.bottom - stashOffset + + val maxBottom = areasOverlappingPipX.maxByOrNull { it.bottom }!!.bottom + val partialStashTop = maxBottom + pipAreaPadding + + val downPosition = stashCandidates[0] + downPosition.offsetTo(bounds.left, min(fullStashTop, partialStashTop)) + } else { + // top is closer than bottom, stash upwards + val fullStashY = screenBounds.top - bounds.height() + stashOffset + + val minTop = areasOverlappingPipX.minByOrNull { it.top }!!.top + val partialStashY = minTop - bounds.height() - pipAreaPadding + + val upPosition = stashCandidates[0] + upPosition.offsetTo(bounds.left, max(fullStashY, partialStashY)) + } + + if (screenBounds.right - bounds.right <= bounds.left - screenBounds.left) { + // right is closer than left, stash rightwards + val fullStashLeft = screenBounds.right - stashOffset + + val maxRight = areasOverlappingPipY.maxByOrNull { it.right }!!.right + val partialStashLeft = maxRight + pipAreaPadding + + val rightPosition = stashCandidates[1] + rightPosition.offsetTo(min(fullStashLeft, partialStashLeft), bounds.top) + } else { + // left is closer than right, stash leftwards + val fullStashLeft = screenBounds.left - bounds.width() + stashOffset + + val minLeft = areasOverlappingPipY.minByOrNull { it.left }!!.left + val partialStashLeft = minLeft - bounds.width() - pipAreaPadding + + val rightPosition = stashCandidates[1] + rightPosition.offsetTo(max(fullStashLeft, partialStashLeft), bounds.top) + } + + return stashCandidates.minByOrNull { + val dx = abs(it.left - bounds.left) + val dy = abs(it.top - bounds.top) + dx * bounds.height() + dy * bounds.width() + }!! + } + + /** + * Prevents the PiP from being stashed for the current set of keep clear areas. + * The PiP may stash again if keep clear areas change. + */ + fun keepUnstashedForCurrentKeepClearAreas() { + lastStashTime = Long.MIN_VALUE + } + + /** + * 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) + } + + /** + * @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 + } + + private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) } + private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom + private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right +} 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..abbc614b4b4f 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,7 @@ 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 mButtonView; private OnClickListener mOnClickListener; public TvPipMenuActionButton(Context context) { @@ -64,8 +56,7 @@ 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); final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; final TypedArray typedArray = context.obtainStyledAttributes(attrs, values, defStyleAttr, @@ -74,45 +65,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 +107,34 @@ 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)); + mButtonView.setBackgroundTintList(getResources() + .getColorStateList(isCustomCloseAction ? R.color.tv_pip_menu_close_icon_bg + : R.color.tv_pip_menu_icon_bg)); } + } 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..35c34ac8315f 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,34 @@ 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.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; import android.os.Handler; -import android.util.Log; +import android.os.RemoteException; import android.view.SurfaceControl; +import android.view.SyncRtSurfaceTransactionApplier; +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. @@ -47,21 +56,47 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private final Context mContext; private final SystemWindows mSystemWindows; - private final PipBoundsState mPipBoundsState; + private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; private Delegate mDelegate; private SurfaceControl mLeash; - private TvPipMenuView mMenuView; + private TvPipMenuView mPipMenuView; + + // 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; + RectF mTmpSourceRectF = new RectF(); + RectF mTmpDestinationRectF = new RectF(); + Matrix mMoveTransform = new Matrix(); + + 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) { + if (DEBUG) e.printStackTrace(); + } + }; - 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 +105,21 @@ 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); } 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."); @@ -104,94 +142,190 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void attachPipMenuView() { - if (DEBUG) Log.d(TAG, "attachPipMenuView()"); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: attachPipMenuView()", TAG); + } - if (mMenuView != null) { + if (mPipMenuView != null) { detachPipMenuView(); } - mMenuView = new TvPipMenuView(mContext); - mMenuView.setListener(this); - mSystemWindows.addView(mMenuView, + mPipMenuView = new TvPipMenuView(mContext); + mPipMenuView.setListener(this); + mSystemWindows.addView(mPipMenuView, 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); + } + mInMoveMode = 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); } + mInMoveMode = false; + mCloseAfterExitMoveMenu = false; + showMenuInternal(); } - void hideMenu() { - hideMenu(true); + private void showMenuInternal() { + if (mPipMenuView == null) { + return; + } + Rect menuBounds = getMenuBounds(mTvPipBoundsState.getBounds()); + mSystemWindows.updateViewLayout(mPipMenuView, getPipMenuLayoutParams( + MENU_WINDOW_TITLE, menuBounds.width(), menuBounds.height())); + maybeUpdateMenuViewActions(); + updateExpansionState(); + + SurfaceControl menuSurfaceControl = getSurfaceControl(); + if (menuSurfaceControl != null) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setRelativeLayer(mPipMenuView.getWindowSurfaceControl(), mLeash, 1); + t.setPosition(menuSurfaceControl, menuBounds.left, menuBounds.top); + t.apply(); + } + grantPipMenuFocus(true); + if (mInMoveMode) { + mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); + } else { + mPipMenuView.showButtonMenu(); + } + } + + void updateGravity(int gravity) { + mPipMenuView.showMovementHints(gravity); } - void hideMenu(boolean movePipWindow) { - if (DEBUG) Log.d(TAG, "hideMenu(), movePipWindow=" + movePipWindow); + void updateExpansionState() { + mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported() + && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); + mPipMenuView.setIsExpanded(mTvPipBoundsState.isTvPipExpanded()); + } + + private Rect getMenuBounds(Rect pipBounds) { + int extraSpaceInPx = mContext.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_outer_space); + Rect menuBounds = new Rect(pipBounds); + menuBounds.inset(-extraSpaceInPx, -extraSpaceInPx); + return menuBounds; + } - if (!isMenuVisible()) { + void closeMenu() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: closeMenu()", TAG); + } + if (mPipMenuView == null) { return; } - mMenuView.hide(); - if (movePipWindow) { - mDelegate.movePipToNormalPosition(); - } + mPipMenuView.hideAll(); + grantPipMenuFocus(false); + mDelegate.onMenuClosed(); + } + + boolean isInMoveMode() { + return mInMoveMode; } @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); + } + mInMoveMode = 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) { + mInMoveMode = false; + mCloseAfterExitMoveMenu = false; + closeMenu(); + return true; + } + if (mInMoveMode) { + mInMoveMode = false; + mPipMenuView.showButtonMenu(); + 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(); + detachPipMenuView(); + mLeash = null; } @Override - public void setAppActions(ParceledListSlice<RemoteAction> actions) { - if (DEBUG) Log.d(TAG, "setAppActions()"); - updateAdditionalActionsList(mAppActions, actions.getList()); + public void setAppActions(ParceledListSlice<RemoteAction> actions, RemoteAction closeAction) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setAppActions()", TAG); + } + updateAdditionalActionsList(mAppActions, actions.getList(), 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 +334,180 @@ 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(); + boolean isVisible = mPipMenuView != null && mPipMenuView.isVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: isMenuVisible: %b", TAG, isVisible); + } + return isVisible; + } + + /** + * Does an immediate window crop of the PiP menu. + */ + @Override + public void resizePipMenu(@android.annotation.Nullable SurfaceControl pipLeash, + @android.annotation.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; + } + + SurfaceControl surfaceControl = getSurfaceControl(); + SyncRtSurfaceTransactionApplier.SurfaceParams + params = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(surfaceControl) + .withWindowCrop(getMenuBounds(destinationBounds)) + .build(); + if (pipLeash != null && t != null) { + SyncRtSurfaceTransactionApplier.SurfaceParams + pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash) + .withMergeTransaction(t) + .build(); + mApplier.scheduleApply(params, pipParams); + } else { + mApplier.scheduleApply(params); + } + } + + private SurfaceControl getSurfaceControl() { + return mSystemWindows.getViewSurface(mPipMenuView); + } + + @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; + } + + Rect menuDestBounds = getMenuBounds(pipDestBounds); + Rect mTmpSourceBounds = 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: mTmpSourceBounds based on mPipMenuView.getBoundsOnScreen()", TAG); + } + mPipMenuView.getBoundsOnScreen(mTmpSourceBounds); + } else { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: mTmpSourceBounds based on menu width and height", TAG); + } + mTmpSourceBounds.set(0, 0, menuDestBounds.width(), menuDestBounds.height()); + } + + mTmpSourceRectF.set(mTmpSourceBounds); + mTmpDestinationRectF.set(menuDestBounds); + mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); + + SurfaceControl surfaceControl = getSurfaceControl(); + SyncRtSurfaceTransactionApplier.SurfaceParams params = + new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( + surfaceControl) + .withMatrix(mMoveTransform) + .build(); + + if (pipLeash != null && transaction != null) { + SyncRtSurfaceTransactionApplier.SurfaceParams + pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash) + .withMergeTransaction(transaction) + .build(); + mApplier.scheduleApply(params, pipParams); + } else { + mApplier.scheduleApply(params); + } + + if (mPipMenuView.getViewRootImpl() != null) { + mPipMenuView.getHandler().removeCallbacks(mUpdateEmbeddedMatrix); + mPipMenuView.getHandler().post(mUpdateEmbeddedMatrix); + } + + 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); + } + return true; + } + + private void detachPipMenuView() { + if (mPipMenuView == null) { + return; + } + + mApplier = null; + mSystemWindows.removeView(mPipMenuView); + mPipMenuView = null; + } + + @Override + public void updateMenuBounds(Rect destinationBounds) { + Rect menuBounds = getMenuBounds(destinationBounds); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateMenuBounds: %s", TAG, menuBounds.toShortString()); + } + mSystemWindows.updateViewLayout(mPipMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, menuBounds.width(), + menuBounds.height())); + if (mPipMenuView != null) { + mPipMenuView.updateLayout(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 +520,39 @@ 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 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); + } + } } 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..9529d04fe185 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,54 +16,71 @@ 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 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.Animator; import android.app.PendingIntent; import android.app.RemoteAction; import android.content.Context; -import android.graphics.Color; +import android.graphics.Rect; import android.os.Handler; -import android.os.Looper; 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.ImageView; import android.widget.LinearLayout; 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.protolog.ShellProtoLogGroup; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** - * 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 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 ImageView mArrowUp; + private final ImageView mArrowRight; + private final ImageView mArrowDown; + private final ImageView mArrowLeft; + + private final ViewGroup mScrollView; + private final ViewGroup mHorizontalScrollView; + + private Rect mCurrentBounds; + + private final TvPipMenuActionButton mExpandButton; + private final TvPipMenuActionButton mCloseButton; + public TvPipMenuView(@NonNull Context context) { this(context, null); } @@ -85,67 +102,172 @@ 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); - mFadeInAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_in_animation); - mFadeInAnimation.setTarget(mActionButtonsContainer); + mScrollView = findViewById(R.id.tv_pip_menu_scroll); + mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll); - mFadeOutAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_out_animation); - mFadeOutAnimation.setTarget(mActionButtonsContainer); + mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); + + 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); + } + + void updateLayout(Rect updatedBounds) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: update menu layout: %s", TAG, updatedBounds.toShortString()); + boolean previouslyVertical = + mCurrentBounds != null && mCurrentBounds.height() > mCurrentBounds.width(); + boolean vertical = updatedBounds.height() > updatedBounds.width(); + + mCurrentBounds = updatedBounds; + if (previouslyVertical == vertical) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: no update for menu layout", TAG); + } + return; + } else { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: change menu layout to vertical: %b", TAG, vertical); + } + } + + if (vertical) { + mHorizontalScrollView.removeView(mActionButtonsContainer); + mScrollView.addView(mActionButtonsContainer); + } else { + mScrollView.removeView(mActionButtonsContainer); + mHorizontalScrollView.addView(mActionButtonsContainer); + } + mActionButtonsContainer.setOrientation(vertical ? LinearLayout.VERTICAL + : LinearLayout.HORIZONTAL); + + mScrollView.setVisibility(vertical ? VISIBLE : GONE); + mHorizontalScrollView.setVisibility(vertical ? GONE : VISIBLE); } void setListener(@Nullable Listener listener) { mListener = listener; } - void show() { - if (DEBUG) Log.d(TAG, "show()"); - - mFadeInAnimation.start(); - setAlpha(1.0f); - grantWindowFocus(true); + void setExpandedModeEnabled(boolean enabled) { + mExpandButton.setVisibility(enabled ? VISIBLE : GONE); } - void hide() { - if (DEBUG) Log.d(TAG, "hide()"); + 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); + } - mFadeOutAnimation.start(); - setAlpha(0.0f); - grantWindowFocus(false); + /** + * @param gravity for the arrow hints + */ + void showMoveMenu(int gravity) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); + } + showMenuButtons(false); + showMovementHints(gravity); + showMenuFrame(true); } - boolean isVisible() { - return getAlpha() == 1.0f; + void showButtonMenu() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showButtonMenu()", TAG); + } + showMenuButtons(true); + hideMovementHints(); + showMenuFrame(true); } - private void grantWindowFocus(boolean grantFocus) { - if (DEBUG) Log.d(TAG, "grantWindowFocus(" + grantFocus + ")"); + /** + * Hides all menu views, including the menu frame. + */ + void hideAll() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAll()", TAG); + } + showMenuButtons(false); + hideMovementHints(); + showMenuFrame(false); + } - 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(500) + .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()"); + boolean isVisible() { + return mMenuFrameView.getAlpha() != 0f + || mActionButtonsContainer.getAlpha() != 0f + || mArrowUp.getAlpha() != 0f + || mArrowRight.getAlpha() != 0f + || mArrowDown.getAlpha() != 0f + || mArrowLeft.getAlpha() != 0f; + } + + 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. 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, + mActionButtonsContainer.getChildCount() - 1); mAdditionalButtons.add(button); buttonsNumber++; @@ -165,19 +287,37 @@ 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 (actionsMatch(action, closeAction)) { + button.setVisibility(GONE); + continue; + } + setActionForButton(action, button, mainHandler); } } + /** + * Checks whether title, description and intent match. + * Comparing icons would be good, but using equals causes false negatives + */ + private boolean actionsMatch(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()); + } + + private void setActionForButton(RemoteAction action, TvPipMenuActionButton button, + Handler mainHandler) { + button.setVisibility(View.VISIBLE); // Ensure the button is visible. + button.setTextAndDescription(action.getContentDescription()); + button.setEnabled(action.isEnabled()); + button.setTag(action); + action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler); + } + @Nullable SurfaceControl getWindowSurfaceControl() { final ViewRootImpl root = getViewRootImpl(); @@ -198,8 +338,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 +351,115 @@ 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 (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: dispatchKeyEvent, action: %d, keycode: %d", + TAG, event.getAction(), event.getKeyCode()); + } + if (mListener != null && event.getAction() == ACTION_UP) { + 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 user actions. + */ + public void showMenuButtons(boolean show) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMenuButtons: %b", TAG, show); + } + animateAlphaTo(show ? 1 : 0, mActionButtonsContainer); + } + + private void showMenuFrame(boolean show) { + animateAlphaTo(show ? 1 : 0, mMenuFrameView); + } + 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..4033f030b702 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 @@ -28,13 +28,13 @@ import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.media.MediaMetadata; 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.wm.shell.R; import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.Objects; @@ -56,6 +56,10 @@ 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 final Context mContext; private final PackageManager mPackageManager; @@ -98,7 +102,10 @@ public class TvPipNotificationController { } 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."); @@ -219,6 +226,8 @@ 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); } boolean mRegistered = false; @@ -240,12 +249,19 @@ 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); + if (DEBUG) { + 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(); } } } @@ -253,5 +269,7 @@ public class TvPipNotificationController { interface Delegate { void showPictureInPictureMenu(); void closePip(); + void enterPipMovementMenu(); + void togglePipExpansion(); } } 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..64017e176fc3 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,14 @@ 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), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; @@ -91,6 +99,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..9adf1961ebf5 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,9 +84,16 @@ 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 @@ -100,5 +102,7 @@ interface ISplitScreen { * @param appTargets apps that will be re-parented to display area */ RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, - in RemoteAnimationTarget[] appTargets) = 12; + in RemoteAnimationTarget[] appTargets) = 13; + + } 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..f20870ff0b2d 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,7 +188,7 @@ 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); } @@ -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,32 @@ 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); + 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 +399,22 @@ 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; + if (ENABLE_SHELL_TRANSITIONS || apps.length < 2) return null; + // TODO(b/206487881): Integrate this with shell transition. SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); if (mSplitTasksContainerLayer != null) { // Remove the previous layer before recreating @@ -487,13 +535,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 +575,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, SplitScreenController.this.onFinishedWakingUp(); }); } - - @Override - public void onFinishedGoingToSleep() { - mMainExecutor.execute(() -> { - SplitScreenController.this.onFinishedGoingToSleep(); - }); - } } /** @@ -607,14 +641,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 +667,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, 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..d30d0cc95f46 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,36 +167,69 @@ 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; mOnFinish.run(); if (mFinishTransaction != null) { @@ -201,14 +237,25 @@ class SplitScreenTransitions { mTransactionPool.release(mFinishTransaction); mFinishTransaction = null; } - mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); - mFinishCallback = null; + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(wct /* wct */, wctCB /* wctCB */); + mFinishCallback = null; + } if (mAnimatingTransition == mPendingEnter) { mPendingEnter = null; } - if (mAnimatingTransition == mPendingDismiss) { + if (mPendingDismiss != null && mPendingDismiss.mTransition == mAnimatingTransition) { mPendingDismiss = null; } + if (mAnimatingTransition == mPendingRecent) { + // If the wct is not null while finishing recent transition, it indicates it's not + // dismissing split and thus need to reorder split task so they can be on top again. + final boolean dismissSplit = wct == null; + mStageCoordinator.finishRecentAnimation(dismissSplit); + mPendingRecent = null; + } + mPendingRemoteHandler = null; + mActiveRemoteHandler = null; mAnimatingTransition = null; } @@ -230,7 +277,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 +325,7 @@ class SplitScreenTransitions { mTransactionPool.release(transaction); mTransitions.getMainExecutor().execute(() -> { mAnimations.remove(va); - onFinish(); + onFinish(null /* wct */, null /* wctCB */); }); }; va.addListener(new AnimatorListenerAdapter() { @@ -295,4 +342,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..b10d50da10bf 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,13 +18,17 @@ 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.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; @@ -32,23 +36,24 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT 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.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -58,6 +63,7 @@ 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; @@ -72,7 +78,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 +88,11 @@ 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.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; @@ -115,18 +121,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,6 +139,7 @@ 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; @@ -142,47 +147,40 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private SplitLayout mSplitLayout; private boolean mDividerVisible; private final SyncTransactionQueue mSyncQueue; - private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; private final ShellTaskOrganizer mTaskOrganizer; - private DisplayAreaInfo mDisplayAreaInfo; private final Context mContext; private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); + private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; private final DisplayInsetsController mDisplayInsetsController; + private final TransactionPool mTransactionPool; private final SplitScreenTransitions mSplitTransitions; private final SplitscreenEventLogger mLogger; 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; - /** 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 +190,21 @@ 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, - Optional<RecentTasksController> recentTasks, + IconProvider iconProvider, Optional<RecentTasksController> recentTasks, Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; - mRootTDAOrganizer = rootTDAOrganizer; mTaskOrganizer = taskOrganizer; mLogger = logger; 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,22 +224,25 @@ 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, @@ -251,20 +251,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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; mRecentTasks = recentTasks; + mDisplayController.addDisplayWindowListener(this); + mDisplayLayout = new DisplayLayout(); transitions.addHandler(this); } @@ -316,7 +318,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 +352,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,8 +375,23 @@ 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) { // 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; @@ -398,9 +423,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onAnimationFinished() throws RemoteException { mIsDividerRemoteAnimating = false; mShouldUpdateRecents = true; - setDividerVisibility(true /* visible */); mSyncQueue.queue(evictWct); - mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + mSyncQueue.runInSync(t -> setDividerVisibility(true, t)); finishedCallback.onAnimationFinished(); } }; @@ -423,9 +447,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onAnimationCancelled() { mIsDividerRemoteAnimating = false; mShouldUpdateRecents = true; - setDividerVisibility(true /* visible */); mSyncQueue.queue(evictWct); - mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + mSyncQueue.runInSync(t -> setDividerVisibility(true, t)); try { adapter.getRunner().onAnimationCancelled(); } catch (RemoteException e) { @@ -448,36 +471,30 @@ 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); addActivityOptions(sideOptions, mSideStage); // Add task launch requests - wct.startTask(mainTaskId, mainOptions); - wct.startTask(sideTaskId, sideOptions); + if (pendingIntent != null && fillInIntent != null) { + wct.startTask(mainTaskId, mainOptions); + wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions); + } else { + wct.startTask(mainTaskId, mainOptions); + wct.startTask(sideTaskId, sideOptions); + } // Using legacy transitions, so we can't use blast sync since it conflicts. mTaskOrganizer.applyTransaction(wct); - } - - public void startIntent(PendingIntent intent, Intent fillInIntent, - @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); + mSyncQueue.runInSync(t -> updateSurfaceBounds(mSplitLayout, t)); } /** @@ -508,8 +525,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 +602,54 @@ 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 onKeyguardVisibilityChanged(boolean showing) { + if (!mMainStage.isActive()) { + return; + } - 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; - } + if (ENABLE_SHELL_TRANSITIONS) { + // Update divider visibility so it won't float on top of keyguard. + setDividerVisibility(!showing, null /* transaction */); + } - void onKeyguardVisibilityChanged(boolean showing) { - if (!showing && mMainStage.isActive() - && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { - exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, - EXIT_REASON_DEVICE_FOLDED); + if (!showing && 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); + } } } 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 +673,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 +699,17 @@ 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)); // 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 +733,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 +748,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); + 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 +907,151 @@ 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, false /* applyDismissingParallax */); + 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; + } + 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) { + mDividerVisible = visible; + sendSplitVisibilityChanged(); + 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 (mIsDividerRemoteAnimating || dividerLeash == null) return; if (mDividerVisible) { - t.show(dividerLeash) - .setAlpha(dividerLeash, 1) - .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER) - .setPosition(dividerLeash, - mSplitLayout.getDividerBounds().left, - mSplitLayout.getDividerBounds().top); + t.show(dividerLeash); + t.setAlpha(dividerLeash, 1); + t.setLayer(dividerLeash, Integer.MAX_VALUE); + t.setPosition(dividerLeash, + mSplitLayout.getRefDividerBounds().left, + mSplitLayout.getRefDividerBounds().top); } else { t.hide(dividerLeash); } @@ -942,13 +1068,15 @@ 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); + setDividerVisibility(true, t); + }); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { mShouldUpdateRecents = true; @@ -963,23 +1091,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); + + 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 @@ -1012,19 +1139,25 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { updateSurfaceBounds(layout, t); - mMainStage.onResized(getMainStageBounds(), t); - mSideStage.onResized(getSideStageBounds(), t); + 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 @@ -1052,9 +1185,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(); } @@ -1074,34 +1207,31 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @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 +1282,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 +1318,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 +1432,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 +1446,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 +1541,80 @@ 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); + setDividerVisibility(false, t); + mSplitLayout.release(); 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 finishRecentAnimation(boolean dismissSplit) { + // 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 (dismissSplit) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + mSplitTransitions.startDismissTransition(null /* transition */, wct, this, + STAGE_TYPE_UNDEFINED, EXIT_REASON_RETURN_HOME); + } else { + setDividerVisibility(true, null /* t */); + } + } + private void addDividerBarToTransition(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, boolean show) { final SurfaceControl leash = mSplitLayout.getDividerLeash(); @@ -1386,7 +1630,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 +1713,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onRootTaskAppeared() { mHasRootTask = true; - StageCoordinator.this.onStageRootTaskAppeared(this); + StageCoordinator.this.onRootTaskAppeared(); } @Override @@ -1499,13 +1743,21 @@ 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 */, + if (!ENABLE_SHELL_TRANSITIONS) { + StageCoordinator.this.exitSplitScreen(null /* childrenToTop */, + EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); + } + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + mSplitTransitions.startDismissTransition(null /* transition */, wct, + StageCoordinator.this, STAGE_TYPE_UNDEFINED, 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..9fd5d2003873 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( @@ -280,10 +288,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 +313,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*/); - } - - void moveToTop(Rect rootBounds, WindowContainerTransaction wct) { - final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setBounds(rootToken, rootBounds).reorder(rootToken, 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 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,10 +343,6 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } - void setVisibility(boolean visible, WindowContainerTransaction wct) { - wct.reorder(mRootTaskInfo.token, visible /* onTop */); - } - void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, @StageType int stage) { for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { 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/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..6ef20e37d5bc 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; @@ -722,7 +721,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 +737,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); } @@ -1190,7 +1189,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..b6c8cffb9699 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; @@ -46,6 +49,7 @@ 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; @@ -54,6 +58,7 @@ 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 +66,12 @@ 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.ShellExecutor; 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 +86,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 @@ -109,8 +116,10 @@ public class SplashscreenContentDrawer { private final Handler mSplashscreenWorkerHandler; @VisibleForTesting final ColorCache mColorCache; + private final ShellExecutor mSplashScreenExecutor; - SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) { + SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool, + ShellExecutor splashScreenExecutor) { mContext = context; mIconProvider = iconProvider; mTransactionPool = pool; @@ -123,6 +132,7 @@ public class SplashscreenContentDrawer { shellSplashscreenWorkerThread.start(); mSplashscreenWorkerHandler = shellSplashscreenWorkerThread.getThreadHandler(); mColorCache = new ColorCache(mContext, mSplashscreenWorkerHandler); + mSplashScreenExecutor = splashScreenExecutor; } /** @@ -137,8 +147,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 +159,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 +250,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 +259,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 +270,7 @@ public class SplashscreenContentDrawer { .overlayDrawable(legacyDrawable) .chooseStyle(suggestType) .setUiThreadInitConsumer(uiThreadInitConsumer) + .setAllowHandleSolidColor(info.allowHandleSolidColorSplashScreen()) .build(); } @@ -287,20 +301,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 +324,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 +363,7 @@ public class SplashscreenContentDrawer { private Drawable[] mFinalIconDrawables; private int mFinalIconSize = mIconSize; private Consumer<Runnable> mUiThreadInitTask; + private boolean mAllowHandleSolidColor; StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) { mContext = context; @@ -354,18 +390,20 @@ public class SplashscreenContentDrawer { return this; } + 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 @@ -385,23 +423,19 @@ public class SplashscreenContentDrawer { 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); } - animationDuration = 0; } - return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration, - mUiThreadInitTask); + return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, mUiThreadInitTask); } private class ShapeIconFactory extends BaseIconFactory { @@ -416,8 +450,8 @@ public class SplashscreenContentDrawer { iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler); } else { mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable( - mTmpAttrs.mIconBgColor, mThemeColor, - iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler); + mTmpAttrs.mIconBgColor, mThemeColor, iconDrawable, mDefaultIconSize, + mFinalIconSize, mSplashscreenWorkerHandler); } } @@ -435,14 +469,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 +490,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 = @@ -469,9 +502,8 @@ public class SplashscreenContentDrawer { mFinalIconSize = (int) (0.5f + mIconSize * noBgScale); createIconDrawable(iconForeground, false); } else { - if (DEBUG) { - Slog.d(TAG, "makeSplashScreenContentView: draw whole icon"); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "processAdaptiveIcon: draw whole icon"); createIconDrawable(iconDrawable, false); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); @@ -479,7 +511,7 @@ public class SplashscreenContentDrawer { } 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 +527,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,9 +536,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() { @@ -536,10 +565,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 +588,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 +623,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 +829,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 +1002,27 @@ 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(); + } } } 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..5f52071bf950 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 @@ -266,99 +268,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..04d6ef7f9505 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; @@ -61,10 +62,12 @@ 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.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 +109,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; @@ -121,6 +122,25 @@ public class StartingSurfaceDrawer { private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo(); /** + * 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. */ public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, @@ -128,7 +148,8 @@ public class StartingSurfaceDrawer { mContext = context; mDisplayManager = mContext.getSystemService(DisplayManager.class); mSplashScreenExecutor = splashScreenExecutor; - mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool); + mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool, + mSplashScreenExecutor); mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); mWindowManagerGlobal = WindowManagerGlobal.getInstance(); mDisplayManager.getDisplay(DEFAULT_DISPLAY); @@ -179,11 +200,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 +227,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 +240,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 +290,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 +299,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 +349,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)) { @@ -349,6 +365,12 @@ public class StartingSurfaceDrawer { final StartingWindowRecord record = mStartingWindowRecords.get(taskId); final SplashScreenView contentView = viewSupplier.get(); 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 +483,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 +493,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 +522,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 +550,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,9 +610,8 @@ 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) { if (immediately || record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { @@ -604,7 +620,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 @@ -619,9 +636,8 @@ public class StartingSurfaceDrawer { 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 { @@ -653,6 +669,7 @@ public class StartingSurfaceDrawer { private boolean mSetSplashScreen; private @StartingWindowType int mSuggestType; private int mBGColor; + private final long mCreateTime; StartingWindowRecord(IBinder appToken, View decorView, TaskSnapshotWindow taskSnapshotWindow, @StartingWindowType int suggestType) { @@ -663,6 +680,7 @@ public class StartingSurfaceDrawer { mBGColor = mTaskSnapshotWindow.getBackgroundColor(); } mSuggestType = suggestType; + mCreateTime = SystemClock.uptimeMillis(); } private void setSplashScreenView(SplashScreenView splashScreenView) { 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..2debcf296ea8 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 @@ -62,6 +62,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; @@ -85,8 +86,10 @@ 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; /** * This class represents a starting window that shows a snapshot. @@ -113,8 +116,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 +127,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; @@ -158,9 +157,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; @@ -244,9 +242,9 @@ 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, + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, - tmpControls, TMP_SURFACE_SIZE); + tmpControls, new Bundle()); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); @@ -327,17 +325,15 @@ public class TaskSnapshotWindow { ? 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 +359,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 @@ -383,9 +378,7 @@ public class TaskSnapshotWindow { } 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 +424,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,7 +518,7 @@ public class TaskSnapshotWindow { private void reportDrawn() { try { - mSession.finishDrawing(mWindow, null /* postDrawTransaction */); + mSession.finishDrawing(mWindow, null /* postDrawTransaction */, Integer.MAX_VALUE); } catch (RemoteException e) { clearWindowSynced(); } @@ -541,8 +534,9 @@ public class TaskSnapshotWindow { @Override public void resized(ClientWindowFrames frames, boolean reportDraw, - MergedConfiguration mergedConfiguration, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId) { + MergedConfiguration mergedConfiguration, InsetsState insetsState, + boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId, + int resizeMode) { if (mOuter != null) { mOuter.mSplashScreenExecutor.execute(() -> { if (mergedConfiguration != null diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java index bde2b5ff4d60..51722c46a7ae 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,32 @@ 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,43 +51,57 @@ 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 (isSnapshotCompatible(windowInfo)) { + 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; } + 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; + } /** * Returns {@code true} if the task snapshot is compatible with this activity (at least the @@ -99,26 +110,24 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor 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); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "isSnapshotCompatible no snapshot, taskId=%d", + 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); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "isSnapshotCompatible obsoleted snapshot for %s", + 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); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "isSnapshotCompatible taskRotation=%d, snapshotRotation=%d", + taskRotation, snapshotRotation); return taskRotation == snapshotRotation; } } 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..542ddeea769c 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,249 @@ 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) { + // 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.getShowBackground()) { + if (info.getAnimationOptions().getBackgroundColor() != 0) { + // If available use the background color provided through AnimationOptions + backgroundColorForTransition = + info.getAnimationOptions().getBackgroundColor(); + } else if (a.getBackgroundColor() != 0) { + // Otherwise fallback on the background color provided through the animation + // definition. + backgroundColorForTransition = a.getBackgroundColor(); + } 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 +680,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 +700,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 +709,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 +807,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 +819,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 +849,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 +895,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 +920,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 +953,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/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/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 f49e80ae16b1..a244d14b0e14 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -2,3 +2,7 @@ # 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..b7c80df03ce2 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 @@ -52,9 +52,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 @@ -80,7 +80,7 @@ class AppPairsTestCannotPairNonResizeableApps( @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() 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..1ac664eb1f62 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 @@ -46,9 +46,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( @@ -65,7 +65,7 @@ class AppPairsTestPairPrimaryAndSecondaryApps( @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() 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..57bcbc093a62 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.flicker.apppairs import android.platform.test.annotations.Presubmit +import android.view.Display import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory @@ -24,6 +25,7 @@ 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 @@ -52,15 +54,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()) } } @@ -84,7 +97,7 @@ class AppPairsTestSupportPairNonResizeableApps( @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() 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..12910dd74271 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 @@ -47,9 +47,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( @@ -69,7 +69,7 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() 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..863c3aff63a2 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 @@ -25,14 +25,11 @@ 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 @@ -50,7 +47,6 @@ 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 +78,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) 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..f2f4877a44c4 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 @@ -25,7 +25,6 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.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 @@ -50,14 +49,14 @@ 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) } } @@ -85,15 +84,19 @@ class RotateTwoLaunchedAppsInAppPairsMode( @Presubmit @Test fun appPairsPrimaryBoundsIsVisibleAtEnd() = - testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.endRotation, primaryApp.component) - @FlakyTest + @Presubmit @Test fun appPairsSecondaryBoundsIsVisibleAtEnd() = - testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.endRotation, secondaryApp.component) + @FlakyTest(bugId = 206753786) + @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..2a173d16004f 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 @@ -25,7 +25,6 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.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 @@ -50,11 +49,11 @@ 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) @@ -81,6 +80,10 @@ class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( @Test override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + @FlakyTest(bugId = 206753786) + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + @Presubmit @Test fun bothAppWindowsVisible() { @@ -90,16 +93,16 @@ class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( } } - @FlakyTest(bugId = 172776659) + @Presubmit @Test fun appPairsPrimaryBoundsIsVisibleAtEnd() = - testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.endRotation, primaryApp.component) - @FlakyTest(bugId = 172776659) + @Presubmit @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..670fbd810907 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 @@ -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 { 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..f6abc75037ed 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 @@ -27,7 +28,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 /** @@ -42,24 +47,38 @@ 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() { + @Before + open fun before() { + Assume.assumeFalse(isShellTransitionsEnabled) + } + + 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/DismissBubbleScreenShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreenShellTransit.kt new file mode 100644 index 000000000000..dd744b3c45ab --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreenShellTransit.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.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 DismissBubbleScreenShellTransit( + testSpec: FlickerTestParameter +) : DismissBubbleScreen(testSpec) { + @Before + override fun before() { + Assume.assumeTrue(isShellTransitionsEnabled) + } +} 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..2ec743c10413 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 @@ -23,7 +24,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 /** @@ -40,20 +45,33 @@ 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() { + @Before + open fun before() { + Assume.assumeFalse(isShellTransitionsEnabled) + } + + 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/ExpandBubbleScreenShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreenShellTransit.kt new file mode 100644 index 000000000000..d92ec7781005 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreenShellTransit.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 ExpandBubbleScreenShellTransit( + testSpec: FlickerTestParameter +) : ExpandBubbleScreen(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/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt new file mode 100644 index 000000000000..fb404b913465 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt @@ -0,0 +1,92 @@ +/* + * 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 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 { + 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..c43230e77683 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,17 @@ package com.android.wm.shell.flicker.bubble -import androidx.test.filters.RequiresDevice +import android.platform.test.annotations.Presubmit +import androidx.test.filters.FlakyTest +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 com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import org.junit.Assume +import org.junit.Before +import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -37,12 +43,36 @@ 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() { + @Before + open fun before() { + Assume.assumeFalse(isShellTransitionsEnabled) + } + + 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() { + Assume.assumeFalse(isShellTransitionsEnabled) + testSpec.assertLayers { + this.isVisible(testApp.component) + } + } + + @FlakyTest(bugId = 218642026) + @Test + open fun testAppIsAlwaysVisible_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + testSpec.assertLayers { + this.isVisible(testApp.component) + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreenShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreenShellTransit.kt new file mode 100644 index 000000000000..9350868a99f4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreenShellTransit.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 LaunchBubbleScreenShellTransit( + testSpec: FlickerTestParameter +) : LaunchBubbleScreen(testSpec) { + @Before + override fun before() { + Assume.assumeTrue(isShellTransitionsEnabled) + } +} 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/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt index 623055f659b9..cf4ea467a29b 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,25 +17,25 @@ 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) 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/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..a510d699387e 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,9 +166,9 @@ 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) @@ -187,9 +187,9 @@ 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) @@ -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..274d34ba3c5b 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 @@ -26,7 +26,9 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.rules.WMFlickerServiceRuleForTestSpec import org.junit.FixMethodOrder +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -55,16 +57,35 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + @get:Rule + val flickerRule = WMFlickerServiceRuleForTestSpec(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 +115,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 +127,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 +174,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 +197,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..e2d08346efb6 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.waitForSurfaceAppeared(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..3fe6f02eccf7 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 @@ -17,13 +17,17 @@ 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.rules.WMFlickerServiceRuleForTestSpec import org.junit.FixMethodOrder +import org.junit.Rule +import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized @@ -52,11 +56,13 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransition(testSpec) { + @get:Rule + val flickerRule = WMFlickerServiceRuleForTestSpec(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 = true) { setup { eachRun { @@ -66,12 +72,22 @@ 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() = super.pipLayerExpands() + companion object { /** * Creates the test configurations. @@ -83,7 +99,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..9c095a2a039f 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,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.rules.WMFlickerServiceRuleForTestSpec import org.junit.FixMethodOrder +import org.junit.Rule +import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized @@ -52,14 +57,34 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + @get:Rule + val flickerRule = WMFlickerServiceRuleForTestSpec(testSpec) + + 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 +97,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..5fc80db6c617 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) } } @@ -148,7 +148,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 +156,10 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition } } + @FlakyTest(bugId = 206753786) + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + companion object { /** * Creates the test configurations. @@ -168,7 +172,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..87e927fd50ea 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,19 @@ 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.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 @@ -53,13 +58,18 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class MovePipDownShelfHeightChangeTest( +open class MovePipDownShelfHeightChangeTest( testSpec: FlickerTestParameter ) : MovePipShelfHeightTransition(testSpec) { + @Before + open 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 MovePipDownShelfHeightChangeTest( current.isHigherOrEqual(previous.region) } + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + companion object { /** * Creates the test configurations. @@ -89,7 +104,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/MovePipDownShelfHeightChangeTest_ShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest_ShellTransit.kt new file mode 100644 index 000000000000..0ff260b94dc8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest_ShellTransit.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import androidx.test.filters.FlakyTest +import android.platform.test.annotations.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip movement with Launcher shelf height change (decrease). + * + * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest` + * + * Actions: + * Launch [pipApp] in pip mode + * Launch [testApp] + * Press home + * Check if pip window moves down (visually) + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +@FlakyTest(bugId = 219693385) +class MovePipDownShelfHeightChangeTest_ShellTransit( + testSpec: FlickerTestParameter +) : MovePipDownShelfHeightChangeTest(testSpec) { + @Before + override fun before() { + Assume.assumeTrue(isShellTransitionsEnabled) + } +} 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/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..c8ced1c9df12 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,15 @@ 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.Before import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -47,7 +48,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 +64,29 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group4 -class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { +@FlakyTest(bugId = 218604389) +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 -> + @Before + open fun before() { + Assume.assumeFalse(isShellTransitionsEnabled) + } + + 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 +107,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() @@ -184,7 +191,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/PipRotationTestShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTestShellTransit.kt new file mode 100644 index 000000000000..a017f56af5bd --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTestShellTransit.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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 PipRotationTestShellTransit(testSpec: FlickerTestParameter) : PipRotationTest(testSpec) { + @Before + override fun before() { + Assume.assumeTrue(isShellTransitionsEnabled) + } +} 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..f7384e742a04 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,16 @@ 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.helpers.isShellTransitionsEnabled +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.Assume +import org.junit.Before import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -36,75 +42,91 @@ 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( +@FlakyTest(bugId = 218604389) +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) + @Before + open fun before() { + Assume.assumeFalse(isShellTransitionsEnabled) + } + 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 +134,7 @@ class SetRequestedOrientationWhilePinnedTest( } } - @FlakyTest + @Presubmit @Test fun pipAppShowsOnTop() { testSpec.assertWmEnd { @@ -120,7 +142,7 @@ class SetRequestedOrientationWhilePinnedTest( } } - @FlakyTest + @Presubmit @Test fun pipLayerInsideDisplay() { testSpec.assertLayersStart { @@ -128,13 +150,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 +166,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/SetRequestedOrientationWhilePinnedTestShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTestShellTransit.kt new file mode 100644 index 000000000000..8d764a8d0e69 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTestShellTransit.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.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test exiting Pip with orientation changes. + * To run this test: `atest WMShellFlickerTests:SetRequestedOrientationWhilePinnedTestShellTransit` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group4 +@FlakyTest(bugId = 217777115) +class SetRequestedOrientationWhilePinnedTestShellTransit( + testSpec: FlickerTestParameter +) : SetRequestedOrientationWhilePinnedTest(testSpec) { + @Before + override fun before() { + Assume.assumeTrue(isShellTransitionsEnabled) + } +} 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/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..05230a9417bb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.IActivityTaskManager; +import android.app.WindowConfiguration; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.HardwareBuffer; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +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 com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.ShellExecutor; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * atest WMShellUnitTests:BackAnimationControllerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class BackAnimationControllerTest { + + private final ShellExecutor mShellExecutor = new TestShellExecutor(); + + @Mock + private Context mContext; + + @Mock + private SurfaceControl.Transaction mTransaction; + + @Mock + private IActivityTaskManager mActivityTaskManager; + + @Mock + private IOnBackInvokedCallback mIOnBackInvokedCallback; + + private BackAnimationController mController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mController = new BackAnimationController( + mShellExecutor, mTransaction, mActivityTaskManager, mContext); + mController.setEnableAnimations(true); + } + + private void createNavigationInfo(RemoteAnimationTarget topAnimationTarget, + SurfaceControl screenshotSurface, + HardwareBuffer hardwareBuffer, + int backType) { + BackNavigationInfo navigationInfo = new BackNavigationInfo( + backType, + topAnimationTarget, + screenshotSurface, + hardwareBuffer, + new WindowConfiguration(), + new RemoteCallback((bundle) -> {}), + null); + try { + doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation(); + } 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); + } + + @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); + mController.onMotionEvent( + MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0), + BackEvent.EDGE_LEFT); + 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); + mController.onMotionEvent( + MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0), + BackEvent.EDGE_LEFT); + mController.onMotionEvent( + MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0), + BackEvent.EDGE_LEFT); + verify(mTransaction).setPosition(animationTarget.leash, 100, 100); + verify(mTransaction, atLeastOnce()).apply(); + } + + @Test + public void backToHome_dispatchesEvents() throws RemoteException { + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + RemoteAnimationTarget animationTarget = createAnimationTarget(); + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME); + + // Check that back start is dispatched. + mController.onMotionEvent( + MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0), + BackEvent.EDGE_LEFT); + verify(mIOnBackInvokedCallback).onBackStarted(); + + // Check that back progress is dispatched. + mController.onMotionEvent( + MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0), + BackEvent.EDGE_LEFT); + 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 + mController.onMotionEvent( + MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0), + BackEvent.EDGE_LEFT); + verify(mIOnBackInvokedCallback).onBackInvoked(); + } +} 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..169f03e7bc3e 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; @@ -141,6 +143,10 @@ public class BubbleDataTest extends ShellTestCase { mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null, mMainExecutor); + mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null, + new LocusId("locusId1")); + mBubbleLocusId = new Bubble(mEntryLocusId, mSuppressionListener, null, mMainExecutor); + mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener, mMainExecutor); mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener, @@ -794,7 +800,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 +810,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 +945,102 @@ 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); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); @@ -995,9 +1097,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 +1132,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/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 453050fcfab4..8b4e1f8bfdb7 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; @@ -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/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..ff6dfdb748c4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.internal.policy.ForceShowNavigationBarSettingsObserver; +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 ForceShowNavigationBarSettingsObserver 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..2886b97a3020 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; @@ -72,8 +73,6 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Mock DisplayController mMockDisplayController; @Mock - OneHandedBackgroundPanelOrganizer mMockBackgroundOrganizer; - @Mock OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; @Mock OneHandedEventCallback mMockEventCallback; @@ -86,6 +85,8 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Mock OneHandedUiEventLogger mMockUiEventLogger; @Mock + InteractionJankMonitor mMockJankMonitor; + @Mock IOverlayManager mMockOverlayManager; @Mock TaskStackListenerImpl mMockTaskStackListener; @@ -109,9 +110,9 @@ public class OneHandedControllerTest extends OneHandedTestCase { 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( @@ -130,7 +131,6 @@ public class OneHandedControllerTest extends OneHandedTestCase { mSpiedOneHandedController = spy(new OneHandedController( mContext, mMockDisplayController, - mMockBackgroundOrganizer, mMockDisplayAreaOrganizer, mMockTouchHandler, mMockTutorialHandler, @@ -138,6 +138,7 @@ public class OneHandedControllerTest extends OneHandedTestCase { mOneHandedAccessibilityUtil, mSpiedTimeoutHandler, mSpiedTransitionState, + mMockJankMonitor, mMockUiEventLogger, mMockOverlayManager, mMockTaskStackListener, @@ -153,6 +154,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); 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/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/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 0172cf324eea..14d9fb9babc4 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 @@ -48,7 +48,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,7 +75,6 @@ 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; private TestShellExecutor mMainExecutor; @@ -101,8 +99,8 @@ public class PipTaskOrganizerTest extends ShellTestCase { mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, - mMockPipTransitionController, mMockOptionalLegacySplitScreen, - mMockOptionalSplitScreen, mMockDisplayController, mMockPipUiEventLogger, + mMockPipTransitionController, mMockOptionalSplitScreen, + mMockDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); mMainExecutor.flushAll(); preparePipTaskOrg(); 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..aef298ed478a 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 @@ -59,6 +59,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Optional; +import java.util.Set; /** * Unit tests for {@link PipController} @@ -209,4 +210,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/TvPipKeepClearAlgorithmTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt new file mode 100644 index 000000000000..e6ba70e1b60e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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.tv.TvPipKeepClearAlgorithm.Placement +import org.junit.Before +import org.junit.Test +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertNull + +@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 currentTime = 0L + 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() + currentTime = 0L + pipSize = DEFAULT_PIP_SIZE + gravity = Gravity.BOTTOM or Gravity.RIGHT + + algorithm = TvPipKeepClearAlgorithm({ currentTime }) + algorithm.setScreenSize(SCREEN_SIZE) + algorithm.setMovementBounds(movementBounds) + algorithm.pipAreaPadding = PADDING + algorithm.stashOffset = STASH_OFFSET + algorithm.stashDuration = 5000L + 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) + assertEquals(algorithm.stashDuration, placement.unstashTime) + } + + @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) + assertEquals(algorithm.stashDuration, placement.unstashTime) + } + + @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) + assertEquals(algorithm.stashDuration, placement.unstashTime) + + currentTime += 1000 + + restrictedAreas.remove(sideBar) + placement = getActualPlacement() + assertEquals(expectedUnstashBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_Stashed_UnstashBoundsStaysObstructed_UnstashesAfterTimeout() { + 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) + assertEquals(algorithm.stashDuration, placement.unstashTime) + + currentTime += algorithm.stashDuration + + placement = getActualPlacement() + assertEquals(expectedUnstashBounds, placement.bounds) + assertNotStashed(placement) + } + + @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) + assertEquals(algorithm.stashDuration, placement.unstashTime) + + currentTime += 1000 + + 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) + assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime) + } + + 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) + assertEquals(0L, actual.unstashTime) + } +} 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..49f36a4be35f 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,21 +16,19 @@ 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.SyncTransactionQueue; @@ -50,6 +48,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 +64,24 @@ 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, - Optional<RecentTasksController> recentTasks, + SplitscreenEventLogger logger, 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, 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..f847e6eb5e96 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; @@ -119,7 +124,7 @@ 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); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); @@ -133,6 +138,40 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + 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 public void testLaunchPair() { TransitionInfo info = createEnterPairInfo(); @@ -211,18 +250,20 @@ public class SplitTransitionTests extends ShellTestCase { } @Test - public void testDismissToHome() { + 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,6 +283,64 @@ public class SplitTransitionTests extends ShellTestCase { mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); + assertTrue(mStageCoordinator.isSplitScreenVisible()); + } + + @Test + 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 + 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()); } @@ -256,8 +355,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, @@ -320,8 +420,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..061136c65daf 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,23 +34,24 @@ 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.SyncTransactionQueue; @@ -79,8 +80,6 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock - private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; - @Mock private MainStage mMainStage; @Mock private SideStage mSideStage; @@ -91,6 +90,8 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private SplitLayout mSplitLayout; @Mock + private DisplayController mDisplayController; + @Mock private DisplayImeController mDisplayImeController; @Mock private DisplayInsetsController mDisplayInsetsController; @@ -104,16 +105,27 @@ public class StageCoordinatorTests extends ShellTestCase { 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, + 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 +165,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 +305,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()); } 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/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/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/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/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 c7b3eba1451f..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_str.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_str.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 504082db41bb..ece150a4bd45 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -106,6 +106,8 @@ cc_defaults { target: { android: { shared_libs: [ + "android.hardware.graphics.common-V3-ndk", + "android.hardware.graphics.common@1.2", "liblog", "libcutils", "libutils", @@ -125,9 +127,11 @@ cc_defaults { static_libs: [ "libEGL_blobCache", "libprotoutil", + "libshaders", "libstatslog_hwui", "libstatspull_lazy", "libstatssocket_lazy", + "libtonemap", ], }, host: { @@ -255,7 +259,6 @@ cc_defaults { "apex/android_bitmap.cpp", "apex/android_canvas.cpp", "apex/jni_runtime.cpp", - "apex/renderthread.cpp", ], }, host: { @@ -342,6 +345,7 @@ cc_defaults { "jni/PathEffect.cpp", "jni/PathMeasure.cpp", "jni/Picture.cpp", + "jni/Region.cpp", "jni/Shader.cpp", "jni/RenderEffect.cpp", "jni/Typeface.cpp", @@ -391,7 +395,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", @@ -532,7 +535,10 @@ cc_defaults { target: { android: { - header_libs: ["libandroid_headers_private"], + header_libs: [ + "libandroid_headers_private", + "libtonemap_headers", + ], srcs: [ "hwui/AnimatedImageThread.cpp", @@ -571,6 +577,7 @@ cc_defaults { "HardwareBitmapUploader.cpp", "HWUIProperties.sysprop", "JankTracker.cpp", + "FrameMetricsReporter.cpp", "Layer.cpp", "LayerUpdateQueue.cpp", "ProfileData.cpp", @@ -676,6 +683,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", 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/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 bcfe9c3ecab4..86ae3995eeed 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; @@ -135,7 +134,7 @@ bool Properties::load() { skpCaptureEnabled = debuggingEnabled && base::GetBoolProperty(PROPERTY_CAPTURE_SKP_ENABLED, false); SkAndroidFrameworkTraceUtil::setEnableTracing( - base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, false)); + base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, true)); runningInEmulator = base::GetBoolProperty(PROPERTY_IS_EMULATOR, false); 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/renderthread.h deleted file mode 100644 index 50280a6dd1fb..000000000000 --- a/libs/hwui/apex/include/android/graphics/renderthread.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef ANDROID_GRAPHICS_RENDERTHREAD_H -#define ANDROID_GRAPHICS_RENDERTHREAD_H - -#include <cutils/compiler.h> -#include <sys/cdefs.h> - -__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. - */ -ANDROID_API void ARenderThread_dumpGraphicsMemory(int fd); - -__END_DECLS - -#endif // ANDROID_GRAPHICS_RENDERTHREAD_H diff --git a/libs/hwui/apex/renderthread.cpp b/libs/hwui/apex/renderthread.cpp deleted file mode 100644 index 5d26afe7a2ab..000000000000 --- a/libs/hwui/apex/renderthread.cpp +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "android/graphics/renderthread.h" - -#include <renderthread/RenderProxy.h> - -using namespace android; - -void ARenderThread_dumpGraphicsMemory(int fd) { - uirenderer::renderthread::RenderProxy::dumpGraphicsMemory(fd); -} 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 3ca457f1a6a7..08fc80fbdafd 100644 --- a/libs/hwui/jni/NinePatch.cpp +++ b/libs/hwui/jni/NinePatch.cpp @@ -64,7 +64,7 @@ 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 0; 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 86d4742b949e..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); } 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..944393c63ad6 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -547,9 +547,12 @@ static void android_view_RenderNode_endAllAnimators(JNIEnv* env, jobject clazz, // 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 +560,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 +612,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 +621,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 +685,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 +707,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 +720,7 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, } JavaVM* mVm; - jobject mWeakRef; + jobject mListener; uirenderer::Rect mPreviousPosition; }; @@ -754,7 +745,7 @@ 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", + {"nRequestPositionUpdates", "(JLjava/lang/ref/WeakReference;)V", (void*)android_view_RenderNode_requestPositionUpdates}, // ---------------------------------------------------------------------------- @@ -852,12 +843,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/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..087c006a8680 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,6 @@ LIBHWUI { ARegionIterator_next; ARegionIterator_getRect; ARegionIterator_getTotalBounds; - ARenderThread_dumpGraphicsMemory; 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/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/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 9bca4df577c9..744739accb2c 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -91,6 +91,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."); } 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/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..122c77f3dadc 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); + ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty)); + bool drew = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, &mLayerUpdateQueue, mContentDrawBounds, mOpaque, mLightInfo, mRenderNodes, &(profiler())); - int64_t frameCompleteNr = getFrameNumber(); + uint64_t frameCompleteNr = getFrameNumber(); waitOnFences(); @@ -521,8 +526,9 @@ 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)); } } @@ -579,7 +585,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 +618,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 +667,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 +693,70 @@ 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; + } + } + + return nullptr; +} - CanvasContext* instance = static_cast<CanvasContext*>(context); +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; std::scoped_lock lock(instance->mFrameMetricsReporterMutex); - instance->mJankTracker.finishFrame(*frameInfo, instance->mFrameMetricsReporter); + instance->mJankTracker.finishFrame(*frameInfo, instance->mFrameMetricsReporter, frameNumber, + surfaceControlId); } } @@ -853,9 +897,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 389546ea7973..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. + // Need at least 4 because we do quad buffer. Add a few more for good measure. RingBuffer<SwapHistory, 7> mSwapHistory; - int64_t mFrameNumber = -1; + // 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/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..a9ff2c60fdbe 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 { 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/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..2293ace0bd16 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; @@ -219,6 +225,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 +240,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: 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..1dc74e5f7740 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,36 @@ 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); +} + +std::mutex& PointerController::getLock() const { + return mDisplayInfoListener->mLock; } bool PointerController::getBounds(float* outMinX, float* outMinY, float* outMaxX, @@ -74,7 +118,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 +137,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 +164,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 +194,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 +232,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,7 +248,7 @@ 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) { @@ -179,12 +258,12 @@ void PointerController::setDisplayViewport(const DisplayViewport& viewport) { } 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 +273,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 +293,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..2e6e851ee15a 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; @@ -85,12 +105,30 @@ private: struct Locked { Presentation presentation; + 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/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/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index b67088a389b6..dae1fccec804 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -255,4 +255,36 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) { ensureDisplayViewportIsSet(); } +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", ], |