diff options
Diffstat (limited to 'libs')
194 files changed, 5799 insertions, 1761 deletions
diff --git a/libs/WindowManager/Jetpack/src/TEST_MAPPING b/libs/WindowManager/Jetpack/src/TEST_MAPPING index eacfe2520a6a..f8f64001dd24 100644 --- a/libs/WindowManager/Jetpack/src/TEST_MAPPING +++ b/libs/WindowManager/Jetpack/src/TEST_MAPPING @@ -28,5 +28,10 @@ } ] } + ], + "imports": [ + { + "path": "vendor/google_testing/integration/tests/scenarios/src/android/platform/test/scenario/sysui" + } ] -}
\ No newline at end of file +} 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 54d9c55f75c3..575c3f002791 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -32,6 +32,7 @@ import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Instrumentation; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; @@ -46,6 +47,7 @@ import android.util.SparseArray; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; +import androidx.annotation.GuardedBy; import androidx.window.common.EmptyLifecycleCallbacksAdapter; import com.android.internal.annotations.VisibleForTesting; @@ -64,9 +66,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private static final String TAG = "SplitController"; @VisibleForTesting + @GuardedBy("mLock") final SplitPresenter mPresenter; // Currently applied split configuration. + @GuardedBy("mLock") private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); /** * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info @@ -75,15 +79,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * organizer. */ @VisibleForTesting + @GuardedBy("mLock") final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); // Callback to Jetpack to notify about changes to split states. @NonNull private Consumer<List<SplitInfo>> mEmbeddingCallback; private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); + private final Handler mHandler; + private final Object mLock = new Object(); public SplitController() { - mPresenter = new SplitPresenter(new MainThreadExecutor(), this); + final MainThreadExecutor executor = new MainThreadExecutor(); + mHandler = executor.mHandler; + mPresenter = new SplitPresenter(executor, this); ActivityThread activityThread = ActivityThread.currentActivityThread(); // Register a callback to be notified about activities being created. activityThread.getApplication().registerActivityLifecycleCallbacks( @@ -96,146 +105,183 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** Updates the embedding rules applied to future activity launches. */ @Override public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) { - mSplitRules.clear(); - mSplitRules.addAll(rules); - for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - updateAnimationOverride(mTaskContainers.valueAt(i)); + synchronized (mLock) { + mSplitRules.clear(); + mSplitRules.addAll(rules); + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + updateAnimationOverride(mTaskContainers.valueAt(i)); + } } } @NonNull - public List<EmbeddingRule> getSplitRules() { + List<EmbeddingRule> getSplitRules() { return mSplitRules; } /** - * Starts an activity to side of the launchingActivity with the provided split config. - */ - public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, - @Nullable Bundle options, @NonNull SplitRule sideRule, - @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) { - try { - mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule, - isPlaceholder); - } catch (Exception e) { - if (failureCallback != null) { - failureCallback.accept(e); - } - } - } - - /** * Registers the split organizer callback to notify about changes to active splits. */ @Override public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) { - mEmbeddingCallback = callback; - updateCallbackIfNecessary(); + synchronized (mLock) { + mEmbeddingCallback = callback; + updateCallbackIfNecessary(); + } } @Override public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; - } + synchronized (mLock) { + TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container == null) { + return; + } - container.setInfo(taskFragmentInfo); - if (container.isFinished()) { - mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); + container.setInfo(taskFragmentInfo); + if (container.isFinished()) { + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); + } + updateCallbackIfNecessary(); } - updateCallbackIfNecessary(); } @Override public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; - } + synchronized (mLock) { + TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container == null) { + return; + } - final WindowContainerTransaction wct = new WindowContainerTransaction(); - final boolean wasInPip = isInPictureInPicture(container); - container.setInfo(taskFragmentInfo); - final boolean isInPip = isInPictureInPicture(container); - // Check if there are no running activities - consider the container empty if there are no - // non-finishing activities left. - if (!taskFragmentInfo.hasRunningActivity()) { - if (taskFragmentInfo.isTaskFragmentClearedForPip()) { - // Do not finish the dependents if the last activity is reparented to PiP. - // Instead, the original split should be cleanup, and the dependent may be expanded - // to fullscreen. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final boolean wasInPip = isInPictureInPicture(container); + container.setInfo(taskFragmentInfo); + final boolean isInPip = isInPictureInPicture(container); + // Check if there are no running activities - consider the container empty if there are + // no non-finishing activities left. + if (!taskFragmentInfo.hasRunningActivity()) { + if (taskFragmentInfo.isTaskFragmentClearedForPip()) { + // Do not finish the dependents if the last activity is reparented to PiP. + // Instead, the original split should be cleanup, and the dependent may be + // expanded to fullscreen. + cleanupForEnterPip(wct, container); + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct); + } else if (taskFragmentInfo.isTaskClearedForReuse()) { + // Do not finish the dependents if this TaskFragment was cleared due to + // launching activity in the Task. + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct); + } else if (!container.isWaitingActivityAppear()) { + // Do not finish the container before the expected activity appear until + // timeout. + mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct); + } + } else if (wasInPip && isInPip) { + // No update until exit PIP. + return; + } else if (isInPip) { + // Enter PIP. + // All overrides will be cleanup. + container.setLastRequestedBounds(null /* bounds */); + container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED); cleanupForEnterPip(wct, container); - mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct); - } else { - // Do not finish the dependents if this TaskFragment was cleared due to launching - // activity in the Task. - final boolean shouldFinishDependent = !taskFragmentInfo.isTaskClearedForReuse(); - mPresenter.cleanupContainer(container, shouldFinishDependent, wct); + } else if (wasInPip) { + // Exit PIP. + // Updates the presentation of the container. Expand or launch placeholder if + // needed. + updateContainer(wct, container); } - } else if (wasInPip && isInPip) { - // No update until exit PIP. - return; - } else if (isInPip) { - // Enter PIP. - // All overrides will be cleanup. - container.setLastRequestedBounds(null /* bounds */); - container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED); - cleanupForEnterPip(wct, container); - } else if (wasInPip) { - // Exit PIP. - // Updates the presentation of the container. Expand or launch placeholder if needed. - updateContainer(wct, container); + mPresenter.applyTransaction(wct); + updateCallbackIfNecessary(); } - mPresenter.applyTransaction(wct); - updateCallbackIfNecessary(); } @Override public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { - final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container != null) { - // Cleanup if the TaskFragment vanished is not requested by the organizer. - removeContainer(container); - // Make sure the top container is updated. - final TaskFragmentContainer newTopContainer = getTopActiveContainer( - container.getTaskId()); - if (newTopContainer != null) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - updateContainer(wct, newTopContainer); - mPresenter.applyTransaction(wct); + synchronized (mLock) { + final TaskFragmentContainer container = getContainer( + taskFragmentInfo.getFragmentToken()); + if (container != null) { + // Cleanup if the TaskFragment vanished is not requested by the organizer. + removeContainer(container); + // Make sure the top container is updated. + final TaskFragmentContainer newTopContainer = getTopActiveContainer( + container.getTaskId()); + if (newTopContainer != null) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + updateContainer(wct, newTopContainer); + mPresenter.applyTransaction(wct); + } + updateCallbackIfNecessary(); } - updateCallbackIfNecessary(); + cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); } - cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); } @Override public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) { - final TaskFragmentContainer container = getContainer(fragmentToken); - if (container != null) { - onTaskConfigurationChanged(container.getTaskId(), parentConfig); - if (isInPictureInPicture(parentConfig)) { - // No need to update presentation in PIP until the Task exit PIP. - return; + synchronized (mLock) { + final TaskFragmentContainer container = getContainer(fragmentToken); + if (container != null) { + onTaskConfigurationChanged(container.getTaskId(), parentConfig); + if (isInPictureInPicture(parentConfig)) { + // No need to update presentation in PIP until the Task exit PIP. + return; + } + mPresenter.updateContainer(container); + updateCallbackIfNecessary(); } - mPresenter.updateContainer(container); - updateCallbackIfNecessary(); } } @Override public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) { - // If the activity belongs to the current app process, we treat it as a new activity launch. - final Activity activity = ActivityThread.currentActivityThread().getActivity(activityToken); - if (activity != null) { - onActivityCreated(activity); - updateCallbackIfNecessary(); - return; + synchronized (mLock) { + // If the activity belongs to the current app process, we treat it as a new activity + // launch. + final Activity activity = getActivity(activityToken); + if (activity != null) { + // We don't allow split as primary for new launch because we currently only support + // launching to top. We allow split as primary for activity reparent because the + // activity may be split as primary before it is reparented out. In that case, we + // want to show it as primary again when it is reparented back. + if (!resolveActivityToContainer(activity, true /* isOnReparent */)) { + // When there is no embedding rule matched, try to place it in the top container + // like a normal launch. + placeActivityInTopContainer(activity); + } + updateCallbackIfNecessary(); + return; + } + + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.isInPictureInPicture()) { + // We don't embed activity when it is in PIP. + return; + } + + // If the activity belongs to a different app process, we treat it as starting new + // intent, since both actions might result in a new activity that should appear in an + // organized TaskFragment. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId, + activityIntent, null /* launchingActivity */); + if (targetContainer == null) { + // When there is no embedding rule matched, try to place it in the top container + // like a normal launch. + targetContainer = taskContainer.getTopTaskFragmentContainer(); + } + if (targetContainer == null) { + return; + } + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), + activityToken); + mPresenter.applyTransaction(wct); + // Because the activity does not belong to the organizer process, we wait until + // onTaskFragmentAppeared to trigger updateCallbackIfNecessary(). } - // TODO: handle for activity in other process. } /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */ @@ -311,92 +357,278 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + @VisibleForTesting void onActivityCreated(@NonNull Activity launchedActivity) { - handleActivityCreated(launchedActivity); + // TODO(b/229680885): we don't support launching into primary yet because we want to always + // launch the new activity on top. + resolveActivityToContainer(launchedActivity, false /* isOnReparent */); updateCallbackIfNecessary(); } /** - * Checks if the activity start should be routed to a particular container. It can create a new - * container for the activity and a new split container if necessary. + * Checks if the new added activity should be routed to a particular container. It can create a + * new container for the activity and a new split container if necessary. + * @param activity the activity that is newly added to the Task. + * @param isOnReparent whether the activity is reparented to the Task instead of new launched. + * We only support to split as primary for reparented activity for now. + * @return {@code true} if the activity has been handled, such as placed in a TaskFragment, or + * in a state that the caller shouldn't handle. */ - // TODO(b/190433398): Break down into smaller functions. - void handleActivityCreated(@NonNull Activity launchedActivity) { - if (isInPictureInPicture(launchedActivity)) { - // We don't embed activity when it is in PIP. + @VisibleForTesting + boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) { + if (isInPictureInPicture(activity) || activity.isFinishing()) { + // We don't embed activity when it is in PIP, or finishing. Return true since we don't + // want any extra handling. + return true; + } + + if (!isOnReparent && getContainerWithActivity(activity) == null + && getInitialTaskFragmentToken(activity) != null) { + // We can't find the new launched activity in any recorded container, but it is + // currently placed in an embedded TaskFragment. This can happen in two cases: + // 1. the activity is embedded in another app. + // 2. the organizer has already requested to remove the TaskFragment. + // In either case, return true since we don't want any extra handling. + Log.d(TAG, "Activity is in a TaskFragment that is not recorded by the organizer. r=" + + activity); + return true; + } + + /* + * We will check the following to see if there is any embedding rule matched: + * 1. Whether the new launched activity should always expand. + * 2. Whether the new launched activity should launch a placeholder. + * 3. Whether the new launched activity has already been in a split with a rule matched + * (likely done in #onStartActivity). + * 4. Whether the activity below (if any) should be split with the new launched activity. + * 5. Whether the activity split with the activity below (if any) should be split with the + * new launched activity. + */ + + // 1. Whether the new launched activity should always expand. + if (shouldExpand(activity, null /* intent */)) { + expandActivity(activity); + return true; + } + + // 2. Whether the new launched activity should launch a placeholder. + if (launchPlaceholderIfNecessary(activity, !isOnReparent)) { + return true; + } + + // 3. Whether the new launched activity has already been in a split with a rule matched. + if (isNewActivityInSplitWithRuleMatched(activity)) { + return true; + } + + // 4. Whether the activity below (if any) should be split with the new launched activity. + final Activity activityBelow = findActivityBelow(activity); + if (activityBelow == null) { + // Can't find any activity below. + return false; + } + if (putActivitiesIntoSplitIfNecessary(activityBelow, activity)) { + // Have split rule of [ activityBelow | launchedActivity ]. + return true; + } + if (isOnReparent && putActivitiesIntoSplitIfNecessary(activity, activityBelow)) { + // Have split rule of [ launchedActivity | activityBelow]. + return true; + } + + // 5. Whether the activity split with the activity below (if any) should be split with the + // new launched activity. + final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( + activityBelow); + final SplitContainer topSplit = getActiveSplitForContainer(activityBelowContainer); + if (topSplit == null || !isTopMostSplit(topSplit)) { + // Skip if it is not the topmost split. + return false; + } + final TaskFragmentContainer otherTopContainer = + topSplit.getPrimaryContainer() == activityBelowContainer + ? topSplit.getSecondaryContainer() + : topSplit.getPrimaryContainer(); + final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity(); + if (otherTopActivity == null || otherTopActivity == activity) { + // Can't find the top activity on the other split TaskFragment. + return false; + } + if (putActivitiesIntoSplitIfNecessary(otherTopActivity, activity)) { + // Have split rule of [ otherTopActivity | launchedActivity ]. + return true; + } + // Have split rule of [ launchedActivity | otherTopActivity]. + return isOnReparent && putActivitiesIntoSplitIfNecessary(activity, otherTopActivity); + } + + /** + * Places the given activity to the top most TaskFragment in the task if there is any. + */ + @VisibleForTesting + void placeActivityInTopContainer(@NonNull Activity activity) { + if (getContainerWithActivity(activity) != null) { + // The activity has already been put in a TaskFragment. This is likely to be done by + // the server when the activity is started. return; } - final List<EmbeddingRule> splitRules = getSplitRules(); - final TaskFragmentContainer currentContainer = getContainerWithActivity( - launchedActivity.getActivityToken()); - - // Check if the activity is configured to always be expanded. - if (shouldExpand(launchedActivity, null, splitRules)) { - if (shouldContainerBeExpanded(currentContainer)) { - // Make sure that the existing container is expanded - mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); - } else { - // Put activity into a new expanded container - final TaskFragmentContainer newContainer = newContainer(launchedActivity, - launchedActivity.getTaskId()); - mPresenter.expandActivity(newContainer.getTaskFragmentToken(), - launchedActivity); - } + final int taskId = getTaskId(activity); + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null) { return; } - - // Check if activity requires a placeholder - if (launchPlaceholderIfNecessary(launchedActivity)) { + final TaskFragmentContainer targetContainer = taskContainer.getTopTaskFragmentContainer(); + if (targetContainer == null) { return; } + targetContainer.addPendingAppearedActivity(activity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), + activity.getActivityToken()); + mPresenter.applyTransaction(wct); + } + + /** + * Starts an activity to side of the launchingActivity with the provided split config. + */ + private void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, + @Nullable Bundle options, @NonNull SplitRule sideRule, + @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) { + try { + mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule, + isPlaceholder); + } catch (Exception e) { + if (failureCallback != null) { + failureCallback.accept(e); + } + } + } - // TODO(b/190433398): Check if it is a placeholder and there is already another split - // created by the primary activity. This is necessary for the case when the primary activity - // launched another secondary in the split, but the placeholder was still launched by the - // logic above. We didn't prevent the placeholder launcher because we didn't know that - // another secondary activity is coming up. + /** + * Expands the given activity by either expanding the TaskFragment it is currently in or putting + * it into a new expanded TaskFragment. + */ + private void expandActivity(@NonNull Activity activity) { + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (shouldContainerBeExpanded(container)) { + // Make sure that the existing container is expanded. + mPresenter.expandTaskFragment(container.getTaskFragmentToken()); + } else { + // Put activity into a new expanded container. + final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); + mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity); + } + } + + /** Whether the given new launched activity is in a split with a rule matched. */ + private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) { + final TaskFragmentContainer container = getContainerWithActivity(launchedActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(container); + if (splitContainer == null) { + return false; + } - // Check if the activity should form a split with the activity below in the same task - // fragment. + if (container == splitContainer.getPrimaryContainer()) { + // The new launched can be in the primary container when it is starting a new activity + // onCreate. + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + final Intent secondaryIntent = secondaryContainer.getPendingAppearedIntent(); + if (secondaryIntent != null) { + // Check with the pending Intent before it is started on the server side. + // This can happen if the launched Activity start a new Intent to secondary during + // #onCreated(). + return getSplitRule(launchedActivity, secondaryIntent) != null; + } + final Activity secondaryActivity = secondaryContainer.getTopNonFinishingActivity(); + return secondaryActivity != null + && getSplitRule(launchedActivity, secondaryActivity) != null; + } + + // Check if the new launched activity is a placeholder. + if (splitContainer.getSplitRule() instanceof SplitPlaceholderRule) { + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) splitContainer.getSplitRule(); + final ComponentName placeholderName = placeholderRule.getPlaceholderIntent() + .getComponent(); + // TODO(b/232330767): Do we have a better way to check this? + return placeholderName == null + || placeholderName.equals(launchedActivity.getComponentName()) + || placeholderRule.getPlaceholderIntent().equals(launchedActivity.getIntent()); + } + + // Check if the new launched activity should be split with the primary top activity. + final Activity primaryActivity = splitContainer.getPrimaryContainer() + .getTopNonFinishingActivity(); + if (primaryActivity == null) { + return false; + } + /* TODO(b/231845476) we should always respect clearTop. + final SplitPairRule curSplitRule = (SplitPairRule) splitContainer.getSplitRule(); + final SplitPairRule splitRule = getSplitRule(primaryActivity, launchedActivity); + return splitRule != null && haveSamePresentation(splitRule, curSplitRule) + // If the new launched split rule should clear top and it is not the bottom most, + // it means we should create a new split pair and clear the existing secondary. + && (!splitRule.shouldClearTop() + || container.getBottomMostActivity() == launchedActivity); + */ + return getSplitRule(primaryActivity, launchedActivity) != null; + } + + /** Finds the activity below the given activity. */ + @Nullable + private Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; - if (currentContainer != null) { - final List<Activity> containerActivities = currentContainer.collectActivities(); - final int index = containerActivities.indexOf(launchedActivity); + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (container != null) { + final List<Activity> containerActivities = container.collectNonFinishingActivities(); + final int index = containerActivities.indexOf(activity); if (index > 0) { activityBelow = containerActivities.get(index - 1); } } if (activityBelow == null) { - IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( - launchedActivity.getActivityToken()); + final IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( + activity.getActivityToken()); if (belowToken != null) { - activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken); + activityBelow = getActivity(belowToken); } } - if (activityBelow == null) { - return; - } + return activityBelow; + } - // Check if the split is already set. - final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( - activityBelow.getActivityToken()); - if (currentContainer != null && activityBelowContainer != null) { - final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, - activityBelowContainer); - if (existingSplit != null) { - // There is already an active split with the activity below. - return; - } + /** + * Checks if there is a rule to split the two activities. If there is one, puts them into split + * and returns {@code true}. Otherwise, returns {@code false}. + */ + private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); + if (splitRule == null) { + return false; } - - final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity, - splitRules); - if (splitPairRule == null) { - return; + final TaskFragmentContainer primaryContainer = getContainerWithActivity( + primaryActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); + if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() + && canReuseContainer(splitRule, splitContainer.getSplitRule())) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { + // The activity is already in the target TaskFragment. + return true; + } + secondaryContainer.addPendingAppearedActivity(secondaryActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reparentActivityToTaskFragment( + secondaryContainer.getTaskFragmentToken(), + secondaryActivity.getActivityToken()); + mPresenter.applyTransaction(wct); + return true; } - - mPresenter.createNewSplitContainer(activityBelow, launchedActivity, - splitPairRule); + // Create new split pair. + mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); + return true; } private void onActivityConfigurationChanged(@NonNull Activity activity) { @@ -404,8 +636,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // We don't embed activity when it is in PIP. return; } - final TaskFragmentContainer currentContainer = getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer currentContainer = getContainerWithActivity(activity); if (currentContainer != null) { // Changes to activities in controllers are handled in @@ -414,7 +645,159 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Check if activity requires a placeholder - launchPlaceholderIfNecessary(activity); + launchPlaceholderIfNecessary(activity, false /* isOnCreated */); + } + + @VisibleForTesting + void onActivityDestroyed(@NonNull Activity activity) { + // Remove any pending appeared activity, as the server won't send finished activity to the + // organizer. + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + mTaskContainers.valueAt(i).cleanupPendingAppearedActivity(activity); + } + // We didn't trigger the callback if there were any pending appeared activities, so check + // again after the pending is removed. + updateCallbackIfNecessary(); + } + + /** + * Called when we have been waiting too long for the TaskFragment to become non-empty after + * creation. + */ + void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) { + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); + } + + /** + * When we are trying to handle a new activity Intent, returns the {@link TaskFragmentContainer} + * that we should reparent the new activity to if there is any embedding rule matched. + * + * @param wct {@link WindowContainerTransaction} including all the window change + * requests. The caller is responsible to call + * {@link android.window.TaskFragmentOrganizer#applyTransaction}. + * @param taskId The Task to start the activity in. + * @param intent The {@link Intent} for starting the new launched activity. + * @param launchingActivity The {@link Activity} that starts the new activity. We will + * prioritize to split the new activity with it if it is not + * {@code null}. + * @return the {@link TaskFragmentContainer} to start the new activity in. {@code null} if there + * is no embedding rule matched. + */ + @VisibleForTesting + @Nullable + TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { + /* + * We will check the following to see if there is any embedding rule matched: + * 1. Whether the new activity intent should always expand. + * 2. Whether the launching activity (if set) should be split with the new activity intent. + * 3. Whether the top activity (if any) should be split with the new activity intent. + * 4. Whether the top activity (if any) in other split should be split with the new + * activity intent. + */ + + // 1. Whether the new activity intent should always expand. + if (shouldExpand(null /* activity */, intent)) { + return createEmptyExpandedContainer(wct, intent, taskId, launchingActivity); + } + + // 2. Whether the launching activity (if set) should be split with the new activity intent. + if (launchingActivity != null) { + final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, + launchingActivity, intent, true /* respectClearTop */); + if (container != null) { + return container; + } + } + + // 3. Whether the top activity (if any) should be split with the new activity intent. + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.getTopTaskFragmentContainer() == null) { + // There is no other activity in the Task to check split with. + return null; + } + final TaskFragmentContainer topContainer = taskContainer.getTopTaskFragmentContainer(); + final Activity topActivity = topContainer.getTopNonFinishingActivity(); + if (topActivity != null && topActivity != launchingActivity) { + final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, + topActivity, intent, false /* respectClearTop */); + if (container != null) { + return container; + } + } + + // 4. Whether the top activity (if any) in other split should be split with the new + // activity intent. + final SplitContainer topSplit = getActiveSplitForContainer(topContainer); + if (topSplit == null) { + return null; + } + final TaskFragmentContainer otherTopContainer = + topSplit.getPrimaryContainer() == topContainer + ? topSplit.getSecondaryContainer() + : topSplit.getPrimaryContainer(); + final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity(); + if (otherTopActivity != null && otherTopActivity != launchingActivity) { + return getSecondaryContainerForSplitIfAny(wct, otherTopActivity, intent, + false /* respectClearTop */); + } + return null; + } + + /** + * Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into. + */ + @Nullable + private TaskFragmentContainer createEmptyExpandedContainer( + @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, + @Nullable Activity launchingActivity) { + // We need an activity in the organizer process in the same Task to use as the owner + // activity, as well as to get the Task window info. + final Activity activityInTask; + if (launchingActivity != null) { + activityInTask = launchingActivity; + } else { + final TaskContainer taskContainer = getTaskContainer(taskId); + activityInTask = taskContainer != null + ? taskContainer.getTopNonFinishingActivity() + : null; + } + if (activityInTask == null) { + // Can't find any activity in the Task that we can use as the owner activity. + return null; + } + final TaskFragmentContainer expandedContainer = newContainer(intent, activityInTask, + taskId); + mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(), + activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + return expandedContainer; + } + + /** + * Returns a container for the new activity intent to launch into as splitting with the primary + * activity. + */ + @Nullable + private TaskFragmentContainer getSecondaryContainerForSplitIfAny( + @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, + @NonNull Intent intent, boolean respectClearTop) { + final SplitPairRule splitRule = getSplitRule(primaryActivity, intent); + if (splitRule == null) { + return null; + } + final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); + if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() + && (canReuseContainer(splitRule, splitContainer.getSplitRule()) + // TODO(b/231845476) we should always respect clearTop. + || !respectClearTop)) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + return splitContainer.getSecondaryContainer(); + } + // Create a new TaskFragment to split with the primary activity for the new activity. + return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, + splitRule); } /** @@ -422,10 +805,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * container, or no container at all. */ @Nullable - TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { + TaskFragmentContainer getContainerWithActivity(@NonNull Activity activity) { + final IBinder activityToken = activity.getActivityToken(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; - for (TaskFragmentContainer container : containers) { + // Traverse from top to bottom in case an activity is added to top pending, and hasn't + // received update from server yet. + for (int j = containers.size() - 1; j >= 0; j--) { + final TaskFragmentContainer container = containers.get(j); if (container.hasActivity(activityToken)) { return container; } @@ -434,30 +821,43 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } - TaskFragmentContainer newContainer(@NonNull Activity activity, int taskId) { - return newContainer(activity, activity, taskId); + TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, int taskId) { + return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId); + } + + TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, + @NonNull Activity activityInTask, int taskId) { + return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, + activityInTask, taskId); + } + + TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, + @NonNull Activity activityInTask, int taskId) { + return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, + activityInTask, taskId); } /** * Creates and registers a new organized container with an optional activity that will be * re-parented to it in a WCT. * - * @param activity the activity that will be reparented to the TaskFragment. - * @param activityInTask activity in the same Task so that we can get the Task bounds if - * needed. - * @param taskId parent Task of the new TaskFragment. + * @param pendingAppearedActivity the activity that will be reparented to the TaskFragment. + * @param pendingAppearedIntent the Intent that will be started in the TaskFragment. + * @param activityInTask activity in the same Task so that we can get the Task bounds + * if needed. + * @param taskId parent Task of the new TaskFragment. */ - TaskFragmentContainer newContainer(@Nullable Activity activity, - @NonNull Activity activityInTask, int taskId) { + TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, + @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { if (activityInTask == null) { throw new IllegalArgumentException("activityInTask must not be null,"); } - final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId); if (!mTaskContainers.contains(taskId)) { mTaskContainers.put(taskId, new TaskContainer(taskId)); } final TaskContainer taskContainer = mTaskContainers.get(taskId); - taskContainer.mContainers.add(container); + final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, + pendingAppearedIntent, taskContainer, this); if (!taskContainer.isTaskBoundsInitialized()) { // Get the initial bounds before the TaskFragment has appeared. final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask); @@ -487,14 +887,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { removeExistingSecondaryContainers(wct, primaryContainer); } - mTaskContainers.get(primaryContainer.getTaskId()).mSplitContainers.add(splitContainer); + primaryContainer.getTaskContainer().mSplitContainers.add(splitContainer); } /** Cleanups all the dependencies when the TaskFragment is entering PIP. */ private void cleanupForEnterPip(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { - final int taskId = container.getTaskId(); - final TaskContainer taskContainer = mTaskContainers.get(taskId); + final TaskContainer taskContainer = container.getTaskContainer(); if (taskContainer == null) { return; } @@ -532,8 +931,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ void removeContainer(@NonNull TaskFragmentContainer container) { // Remove all split containers that included this one - final int taskId = container.getTaskId(); - final TaskContainer taskContainer = mTaskContainers.get(taskId); + final TaskContainer taskContainer = container.getTaskContainer(); if (taskContainer == null) { return; } @@ -590,7 +988,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } for (int i = taskContainer.mContainers.size() - 1; i >= 0; i--) { final TaskFragmentContainer container = taskContainer.mContainers.get(i); - if (!container.isFinished() && container.getRunningActivityCount() > 0) { + if (!container.isFinished() && (container.getRunningActivityCount() > 0 + // We may be waiting for the top TaskFragment to become non-empty after + // creation. In that case, we don't want to treat the TaskFragment below it as + // top active, otherwise it may incorrectly launch placeholder on top of the + // pending TaskFragment. + || container.isWaitingActivityAppear())) { return container; } } @@ -619,16 +1022,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer == null) { return; } - final List<SplitContainer> splitContainers = mTaskContainers.get(container.getTaskId()) - .mSplitContainers; - if (splitContainers == null - || splitContainer != splitContainers.get(splitContainers.size() - 1)) { + if (!isTopMostSplit(splitContainer)) { // Skip position update - it isn't the topmost split. return; } - if (splitContainer.getPrimaryContainer().isEmpty() - || splitContainer.getSecondaryContainer().isEmpty()) { - // Skip position update - one or both containers are empty. + if (splitContainer.getPrimaryContainer().isFinished() + || splitContainer.getSecondaryContainer().isFinished()) { + // Skip position update - one or both containers are finished. return; } if (dismissPlaceholderIfNecessary(splitContainer)) { @@ -638,14 +1038,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mPresenter.updateSplitContainer(splitContainer, container, wct); } + /** Whether the given split is the topmost split in the Task. */ + private boolean isTopMostSplit(@NonNull SplitContainer splitContainer) { + final List<SplitContainer> splitContainers = splitContainer.getPrimaryContainer() + .getTaskContainer().mSplitContainers; + return splitContainer == splitContainers.get(splitContainers.size() - 1); + } + /** * Returns the top active split container that has the provided container, if available. */ @Nullable - private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) { - final List<SplitContainer> splitContainers = mTaskContainers.get(container.getTaskId()) - .mSplitContainers; - if (splitContainers == null) { + private SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) { + if (container == null) { + return null; + } + final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers; + if (splitContainers.isEmpty()) { return null; } for (int i = splitContainers.size() - 1; i >= 0; i--) { @@ -662,15 +1071,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the active split that has the provided containers as primary and secondary or as * secondary and primary, if available. */ + @VisibleForTesting @Nullable - private SplitContainer getActiveSplitForContainers( + SplitContainer getActiveSplitForContainers( @NonNull TaskFragmentContainer firstContainer, @NonNull TaskFragmentContainer secondContainer) { - final List<SplitContainer> splitContainers = mTaskContainers.get(firstContainer.getTaskId()) + final List<SplitContainer> splitContainers = firstContainer.getTaskContainer() .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(); @@ -692,19 +1099,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } - return launchPlaceholderIfNecessary(topActivity); + return launchPlaceholderIfNecessary(topActivity, false /* isOnCreated */); } - boolean launchPlaceholderIfNecessary(@NonNull Activity activity) { - final TaskFragmentContainer container = getContainerWithActivity( - activity.getActivityToken()); + boolean launchPlaceholderIfNecessary(@NonNull Activity activity, boolean isOnCreated) { + final TaskFragmentContainer container = getContainerWithActivity(activity); // Don't launch placeholder if the container is occluded. if (container != null && container != getTopActiveContainer(container.getTaskId())) { return false; } - SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container) - : null; + final SplitContainer splitContainer = getActiveSplitForContainer(container); if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { // Don't launch placeholder in primary split container return false; @@ -718,12 +1123,35 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // TODO(b/190433398): Handle failed request - startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null /* options */, + final Bundle options = getPlaceholderOptions(activity, isOnCreated); + startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), options, placeholderRule, null /* failureCallback */, true /* isPlaceholder */); return true; } - private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { + /** + * Gets the activity options for starting the placeholder activity. In case the placeholder is + * launched when the Task is in the background, we don't want to bring the Task to the front. + * @param primaryActivity the primary activity to launch the placeholder from. + * @param isOnCreated whether this happens during the primary activity onCreated. + */ + @VisibleForTesting + @Nullable + Bundle getPlaceholderOptions(@NonNull Activity primaryActivity, boolean isOnCreated) { + // Setting avoid move to front will also skip the animation. We only want to do that when + // the Task is currently in background. + // Check if the primary is resumed or if this is called when the primary is onCreated + // (not resumed yet). + if (isOnCreated || primaryActivity.isResumed()) { + return null; + } + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setAvoidMoveToFront(); + return options.toBundle(); + } + + @VisibleForTesting + boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { if (!splitContainer.isPlaceholderContainer()) { return false; } @@ -759,22 +1187,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } - private void updateCallbackIfNecessary() { - updateCallbackIfNecessary(true /* deferCallbackUntilAllActivitiesCreated */); - } - /** * Notifies listeners about changes to split states if necessary. - * - * @param deferCallbackUntilAllActivitiesCreated boolean to indicate whether the split info - * callback should be deferred until all the - * organized activities have been created. */ - private void updateCallbackIfNecessary(boolean deferCallbackUntilAllActivitiesCreated) { + private void updateCallbackIfNecessary() { if (mEmbeddingCallback == null) { return; } - if (deferCallbackUntilAllActivitiesCreated && !allActivitiesCreated()) { + if (!allActivitiesCreated()) { return; } List<SplitInfo> currentSplitStates = getActiveSplitStates(); @@ -830,9 +1250,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen 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()) { + if (!container.taskInfoActivityCountMatchesCreated()) { return false; } } @@ -848,18 +1266,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (container == null) { return false; } - 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; - } - } - return true; + return getActiveSplitForContainer(container) == null; } /** @@ -867,9 +1274,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if available. */ @Nullable - private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, - @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) { - for (EmbeddingRule rule : splitRules) { + private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryActivityIntent) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPairRule)) { continue; } @@ -885,9 +1292,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a split rule for the provided pair of primary and secondary activities if available. */ @Nullable - private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) { - for (EmbeddingRule rule : splitRules) { + private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPairRule)) { continue; } @@ -920,16 +1327,40 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return mTaskContainers.get(taskId); } + Handler getHandler() { + return mHandler; + } + + int getTaskId(@NonNull Activity activity) { + // Prefer to get the taskId from TaskFragmentContainer because Activity.getTaskId() is an + // IPC call. + final TaskFragmentContainer container = getContainerWithActivity(activity); + return container != null ? container.getTaskId() : activity.getTaskId(); + } + + @Nullable + Activity getActivity(@NonNull IBinder activityToken) { + return ActivityThread.currentActivityThread().getActivity(activityToken); + } + + /** + * Gets the token of the initial TaskFragment that embedded this activity. Do not rely on it + * after creation because the activity could be reparented. + */ + @VisibleForTesting + @Nullable + IBinder getInitialTaskFragmentToken(@NonNull Activity activity) { + final ActivityThread.ActivityClientRecord record = ActivityThread.currentActivityThread() + .getActivityClient(activity.getActivityToken()); + return record != null ? record.mInitialTaskFragmentToken : null; + } + /** * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ - private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent, - List<EmbeddingRule> splitRules) { - if (splitRules == null) { - return false; - } - for (EmbeddingRule rule : splitRules) { + private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { continue; } @@ -983,8 +1414,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer, @NonNull Activity associatedActivity) { - TaskFragmentContainer associatedContainer = getContainerWithActivity( - associatedActivity.getActivityToken()); + final TaskFragmentContainer associatedContainer = getContainerWithActivity( + associatedActivity); if (associatedContainer == null) { return false; } @@ -996,29 +1427,28 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) { - final IBinder activityToken = activity.getActivityToken(); - final IBinder initialTaskFragmentToken = ActivityThread.currentActivityThread() - .getActivityClient(activityToken).mInitialTaskFragmentToken; - // If the activity is not embedded, then it will not have an initial task fragment token - // so no further action is needed. - if (initialTaskFragmentToken == null) { - return; - } - for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i) - .mContainers; - for (int j = containers.size() - 1; j >= 0; j--) { - final TaskFragmentContainer container = containers.get(j); - if (!container.hasActivity(activityToken) - && container.getTaskFragmentToken().equals(initialTaskFragmentToken)) { - // The onTaskFragmentInfoChanged callback containing this activity has not - // reached the client yet, so add the activity to the pending appeared - // activities and send a split info callback to the client before - // {@link Activity#onCreate} is called. - container.addPendingAppearedActivity(activity); - updateCallbackIfNecessary( - false /* deferCallbackUntilAllActivitiesCreated */); - return; + synchronized (mLock) { + final IBinder activityToken = activity.getActivityToken(); + final IBinder initialTaskFragmentToken = getInitialTaskFragmentToken(activity); + // If the activity is not embedded, then it will not have an initial task fragment + // token so no further action is needed. + if (initialTaskFragmentToken == null) { + return; + } + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i) + .mContainers; + for (int j = containers.size() - 1; j >= 0; j--) { + final TaskFragmentContainer container = containers.get(j); + if (!container.hasActivity(activityToken) + && container.getTaskFragmentToken() + .equals(initialTaskFragmentToken)) { + // The onTaskFragmentInfoChanged callback containing this activity has + // not reached the client yet, so add the activity to the pending + // appeared activities. + container.addPendingAppearedActivity(activity); + return; + } } } } @@ -1030,12 +1460,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // first. In case of a configured placeholder activity we want to make sure // that we don't launch it if an activity itself already requested something to be // launched to side. - SplitController.this.onActivityCreated(activity); + synchronized (mLock) { + SplitController.this.onActivityCreated(activity); + } } @Override public void onActivityConfigurationChanged(Activity activity) { - SplitController.this.onActivityConfigurationChanged(activity); + synchronized (mLock) { + SplitController.this.onActivityConfigurationChanged(activity); + } + } + + @Override + public void onActivityPostDestroyed(Activity activity) { + synchronized (mLock) { + SplitController.this.onActivityDestroyed(activity); + } } } @@ -1070,130 +1511,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return super.onStartActivity(who, intent, options); } - if (shouldExpand(null, intent, getSplitRules())) { - setLaunchingInExpandedContainer(launchingActivity, options); - } else if (!splitWithLaunchingActivity(launchingActivity, intent, options)) { - setLaunchingInSameSideContainer(launchingActivity, intent, options); + synchronized (mLock) { + final int taskId = getTaskId(launchingActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct, + taskId, intent, launchingActivity); + if (launchedInTaskFragment != null) { + mPresenter.applyTransaction(wct); + // Amend the request to let the WM know that the activity should be placed in + // the dedicated container. + options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + launchedInTaskFragment.getTaskFragmentToken()); + } } return super.onStartActivity(who, intent, options); } - - private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) { - TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer( - launchingActivity); - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - newContainer.getTaskFragmentToken()); - } - - /** - * Returns {@code true} if the activity that is going to be started via the - * {@code intent} should be paired with the {@code launchingActivity} and is set to be - * launched in the side container. - */ - private boolean splitWithLaunchingActivity(Activity launchingActivity, Intent intent, - Bundle options) { - final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent, - getSplitRules()); - if (splitPairRule == null) { - return false; - } - - // Check if there is any existing side container to launch into. - TaskFragmentContainer secondaryContainer = findSideContainerForNewLaunch( - launchingActivity, splitPairRule); - if (secondaryContainer == null) { - // Create a new split with an empty side container. - secondaryContainer = mPresenter - .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule); - } - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - secondaryContainer.getTaskFragmentToken()); - return true; - } - - /** - * Finds if there is an existing split side {@link TaskFragmentContainer} that can be used - * for the new rule. - */ - @Nullable - private TaskFragmentContainer findSideContainerForNewLaunch(Activity launchingActivity, - SplitPairRule splitPairRule) { - final TaskFragmentContainer launchingContainer = getContainerWithActivity( - launchingActivity.getActivityToken()); - if (launchingContainer == null) { - return null; - } - - // We only check if the launching activity is the primary of the split. We will check - // if the launching activity is the secondary in #setLaunchingInSameSideContainer. - final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); - if (splitContainer == null - || splitContainer.getPrimaryContainer() != launchingContainer) { - return null; - } - - if (canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { - return splitContainer.getSecondaryContainer(); - } - return null; - } - - /** - * Checks if the activity that is going to be started via the {@code intent} should be - * paired with the existing top activity which is currently paired with the - * {@code launchingActivity}. If so, set the activity to be launched in the same side - * container of the {@code launchingActivity}. - */ - private void setLaunchingInSameSideContainer(Activity launchingActivity, Intent intent, - Bundle options) { - final TaskFragmentContainer launchingContainer = getContainerWithActivity( - launchingActivity.getActivityToken()); - if (launchingContainer == null) { - return; - } - - final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); - if (splitContainer == null) { - return; - } - - if (splitContainer.getSecondaryContainer() != launchingContainer) { - return; - } - - // The launching activity is on the secondary container. Retrieve the primary - // activity from the other container. - Activity primaryActivity = - splitContainer.getPrimaryContainer().getTopNonFinishingActivity(); - if (primaryActivity == null) { - return; - } - - final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent, - getSplitRules()); - if (splitPairRule == null) { - return; - } - - // Can only launch in the same container if the rules share the same presentation. - if (!canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { - return; - } - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. This is necessary for the case that the activity is started - // into a new Task, or new Task will be escaped from the current host Task and be - // displayed in fullscreen. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - launchingContainer.getTaskFragmentToken()); - } } /** @@ -1202,7 +1535,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @Override public boolean isActivityEmbedded(@NonNull Activity activity) { - return mPresenter.isActivityEmbedded(activity.getActivityToken()); + synchronized (mLock) { + return mPresenter.isActivityEmbedded(activity.getActivityToken()); + } } /** @@ -1213,8 +1548,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } + return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2); + } + + /** Whether the two rules have the same presentation. */ + private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) { + // TODO(b/231655482): add util method to do the comparison in SplitPairRule. return rule1.getSplitRatio() == rule2.getSplitRatio() - && rule1.getLayoutDirection() == rule2.getLayoutDirection(); + && rule1.getLayoutDirection() == rule2.getLayoutDirection() + && rule1.getFinishPrimaryWithSecondary() + == rule2.getFinishPrimaryWithSecondary() + && rule1.getFinishSecondaryWithPrimary() + == rule2.getFinishSecondaryWithPrimary(); } /** 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 06c1d4ec8d32..ac3b05a0e825 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -16,8 +16,6 @@ package androidx.window.extensions.embedding; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - import android.app.Activity; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; @@ -100,10 +98,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Creates a new split with the primary activity and an empty secondary container. * @return The newly created secondary container. */ - TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity, - @NonNull SplitPairRule rule) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - + @NonNull + TaskFragmentContainer createNewSplitWithEmptySideContainer( + @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) { final Rect parentBounds = getParentContainerBounds(primaryActivity); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, isLtr(primaryActivity, rule)); @@ -113,7 +111,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Create new empty task fragment final int taskId = primaryContainer.getTaskId(); final TaskFragmentContainer secondaryContainer = mController.newContainer( - null /* activity */, primaryActivity, taskId); + secondaryIntent, primaryActivity, taskId); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr(primaryActivity, rule)); final int windowingMode = mController.getTaskContainer(taskId) @@ -127,8 +125,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); - applyTransaction(wct); - return secondaryContainer; } @@ -155,8 +151,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr(primaryActivity, rule)); + final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( + secondaryActivity); + TaskFragmentContainer containerToAvoid = primaryContainer; + if (rule.shouldClearTop() && curSecondaryContainer != null) { + // Do not reuse the current TaskFragment if the rule is to clear top. + containerToAvoid = curSecondaryContainer; + } final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, - secondaryActivity, secondaryRectBounds, primaryContainer); + secondaryActivity, secondaryRectBounds, containerToAvoid); // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); @@ -167,21 +170,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } /** - * Creates a new expanded container. - */ - TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { - final TaskFragmentContainer newContainer = mController.newContainer(null /* activity */, - launchingActivity, launchingActivity.getTaskId()); - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - createTaskFragment(wct, newContainer.getTaskFragmentToken(), - launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); - - applyTransaction(wct); - return newContainer; - } - - /** * Creates a new container or resizes an existing container for activity to the provided bounds. * @param activity The activity to be re-parented to the container if necessary. * @param containerToAvoid Re-parent from this container if an activity is already in it. @@ -189,8 +177,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { private TaskFragmentContainer prepareContainerForActivity( @NonNull WindowContainerTransaction wct, @NonNull Activity activity, @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) { - TaskFragmentContainer container = mController.getContainerWithActivity( - activity.getActivityToken()); + TaskFragmentContainer container = mController.getContainerWithActivity(activity); final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); if (container == null || container == containerToAvoid) { container = mController.newContainer(activity, taskId); @@ -230,14 +217,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { isLtr(launchingActivity, rule)); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( - launchingActivity.getActivityToken()); + launchingActivity); if (primaryContainer == null) { primaryContainer = mController.newContainer(launchingActivity, launchingActivity.getTaskId()); } final int taskId = primaryContainer.getTaskId(); - TaskFragmentContainer secondaryContainer = mController.newContainer(null /* activity */, + final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent, launchingActivity, taskId); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(primaryRectBounds); @@ -291,8 +278,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // When placeholder is shown in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); } - final TaskContainer taskContainer = mController.getTaskContainer( - updatedContainer.getTaskId()); + final TaskContainer taskContainer = updatedContainer.getTaskContainer(); final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment( primaryRectBounds); updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode); @@ -456,18 +442,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { - final int taskId = container.getTaskId(); - final TaskContainer taskContainer = mController.getTaskContainer(taskId); - if (taskContainer == null) { - throw new IllegalStateException("Can't find TaskContainer taskId=" + taskId); - } - return taskContainer.getTaskBounds(); + return container.getTaskContainer().getTaskBounds(); } @NonNull Rect getParentContainerBounds(@NonNull Activity activity) { - final TaskFragmentContainer container = mController.getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer container = mController.getContainerWithActivity(activity); if (container != null) { return getParentContainerBounds(container); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index a70755560eb1..0ea5603b1f3d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -16,12 +16,14 @@ package androidx.window.extensions.embedding; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Activity; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; import android.graphics.Rect; @@ -47,9 +49,11 @@ class TaskContainer { private int mWindowingMode = WINDOWING_MODE_UNDEFINED; /** Active TaskFragments in this Task. */ + @NonNull final List<TaskFragmentContainer> mContainers = new ArrayList<>(); /** Active split pairs in this Task. */ + @NonNull final List<SplitContainer> mSplitContainers = new ArrayList<>(); /** @@ -60,6 +64,9 @@ class TaskContainer { final Set<IBinder> mFinishedContainer = new ArraySet<>(); TaskContainer(int taskId) { + if (taskId == INVALID_TASK_ID) { + throw new IllegalArgumentException("Invalid Task id"); + } mTaskId = taskId; } @@ -128,4 +135,30 @@ class TaskContainer { boolean isEmpty() { return mContainers.isEmpty() && mFinishedContainer.isEmpty(); } + + /** Removes the pending appeared activity from all TaskFragments in this Task. */ + void cleanupPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { + for (TaskFragmentContainer container : mContainers) { + container.removePendingAppearedActivity(pendingAppearedActivity); + } + } + + @Nullable + TaskFragmentContainer getTopTaskFragmentContainer() { + if (mContainers.isEmpty()) { + return null; + } + return mContainers.get(mContainers.size() - 1); + } + + @Nullable + Activity getTopNonFinishingActivity() { + for (int i = mContainers.size() - 1; i >= 0; i--) { + final Activity activity = mContainers.get(i).getTopNonFinishingActivity(); + if (activity != null) { + return activity; + } + } + return null; + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java index b3becad3dc5a..cdee9e386b33 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java @@ -17,6 +17,8 @@ package androidx.window.extensions.embedding; import static android.graphics.Matrix.MSCALE_X; +import static android.graphics.Matrix.MTRANS_X; +import static android.graphics.Matrix.MTRANS_Y; import android.graphics.Rect; import android.view.Choreographer; @@ -96,22 +98,20 @@ class TaskFragmentAnimationAdapter { mTarget.localBounds.left, mTarget.localBounds.top); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); - - // Open/close animation may scale up the surface. Apply an inverse scale to the window crop - // so that it will not be covering other windows. - mVecs[1] = mVecs[2] = 0; - mVecs[0] = mVecs[3] = 1; - mTransformation.getMatrix().mapVectors(mVecs); - mVecs[0] = 1.f / mVecs[0]; - mVecs[3] = 1.f / mVecs[3]; - final Rect clipRect = mTarget.localBounds; - mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f); - mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f); - mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f); - mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f); - mRect.offsetTo(Math.round(mTarget.localBounds.width() * (1 - mVecs[0]) / 2.f), - Math.round(mTarget.localBounds.height() * (1 - mVecs[3]) / 2.f)); - t.setWindowCrop(mLeash, mRect); + // Get current animation position. + final int positionX = Math.round(mMatrix[MTRANS_X]); + final int positionY = Math.round(mMatrix[MTRANS_Y]); + // The exiting surface starts at position: mTarget.localBounds and moves with + // positionX varying. Offset our crop region by the amount we have slided so crop + // regions stays exactly on the original container in split. + final int cropOffsetX = mTarget.localBounds.left - positionX; + final int cropOffsetY = mTarget.localBounds.top - positionY; + final Rect cropRect = new Rect(); + cropRect.set(mTarget.localBounds); + // Because window crop uses absolute position. + cropRect.offsetTo(0, 0); + cropRect.offset(cropOffsetX, cropOffsetY); + t.setCrop(mLeash, cropRect); } /** Called after animation finished. */ diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index fe422bf37a69..624cde50ff72 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -16,20 +16,21 @@ package androidx.window.extensions.embedding; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; -import android.app.ActivityThread; import android.app.WindowConfiguration.WindowingMode; +import android.content.Intent; import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -39,25 +40,41 @@ import java.util.List; * on the server side. */ class TaskFragmentContainer { + private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000; + + @NonNull + private final SplitController mController; + /** * Client-created token that uniquely identifies the task fragment container instance. */ @NonNull private final IBinder mToken; - /** Parent leaf Task id. */ - private final int mTaskId; + /** Parent leaf Task. */ + @NonNull + private final TaskContainer mTaskContainer; /** * Server-provided task fragment information. */ - private TaskFragmentInfo mInfo; + @VisibleForTesting + TaskFragmentInfo mInfo; /** * Activities that are being reparented or being started to this container, but haven't been * added to {@link #mInfo} yet. */ - private final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>(); + @VisibleForTesting + final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>(); + + /** + * When this container is created for an {@link Intent} to start within, we store that Intent + * until the container becomes non-empty on the server side, so that we can use it to check + * rules associated with this container. + */ + @Nullable + private Intent mPendingAppearedIntent; /** Containers that are dependent on this one and should be completely destroyed on exit. */ private final List<TaskFragmentContainer> mContainersToFinishOnExit = @@ -81,18 +98,33 @@ class TaskFragmentContainer { private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED; /** + * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment + * if it is still empty after the timeout. + */ + @VisibleForTesting + @Nullable + Runnable mAppearEmptyTimeout; + + /** * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. */ - TaskFragmentContainer(@Nullable Activity activity, int taskId) { - mToken = new Binder("TaskFragmentContainer"); - if (taskId == INVALID_TASK_ID) { - throw new IllegalArgumentException("Invalid Task id"); + TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, + @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, + @NonNull SplitController controller) { + if ((pendingAppearedActivity == null && pendingAppearedIntent == null) + || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { + throw new IllegalArgumentException( + "One and only one of pending activity and intent must be non-null"); } - mTaskId = taskId; - if (activity != null) { - addPendingAppearedActivity(activity); + mController = controller; + mToken = new Binder("TaskFragmentContainer"); + mTaskContainer = taskContainer; + taskContainer.mContainers.add(this); + if (pendingAppearedActivity != null) { + addPendingAppearedActivity(pendingAppearedActivity); } + mPendingAppearedIntent = pendingAppearedIntent; } /** @@ -103,38 +135,68 @@ class TaskFragmentContainer { return mToken; } - /** List of activities that belong to this container and live in this process. */ + /** List of non-finishing activities that belong to this container and live in this process. */ @NonNull - List<Activity> collectActivities() { + List<Activity> collectNonFinishingActivities() { + final List<Activity> allActivities = new ArrayList<>(); + if (mInfo != null) { + // Add activities reported from the server. + for (IBinder token : mInfo.getActivities()) { + final Activity activity = mController.getActivity(token); + if (activity != null && !activity.isFinishing()) { + allActivities.add(activity); + } + } + } + // Add the re-parenting activity, in case the server has not yet reported the task // fragment info update with it placed in this container. We still want to apply rules // in this intermediate state. - List<Activity> allActivities = new ArrayList<>(); - if (!mPendingAppearedActivities.isEmpty()) { - allActivities.addAll(mPendingAppearedActivities); - } - // Add activities reported from the server. - if (mInfo == null) { - return allActivities; - } - ActivityThread activityThread = ActivityThread.currentActivityThread(); - for (IBinder token : mInfo.getActivities()) { - Activity activity = activityThread.getActivity(token); - if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) { + // Place those on top of the list since they will be on the top after reported from the + // server. + for (Activity activity : mPendingAppearedActivities) { + if (!activity.isFinishing()) { allActivities.add(activity); } } return allActivities; } + /** + * Checks if the count of activities from the same process in task fragment info corresponds to + * the ones created and available on the client side. + */ + boolean taskInfoActivityCountMatchesCreated() { + if (mInfo == null) { + return false; + } + return mPendingAppearedActivities.isEmpty() + && mInfo.getActivities().size() == collectNonFinishingActivities().size(); + } + ActivityStack toActivityStack() { - return new ActivityStack(collectActivities(), isEmpty()); + return new ActivityStack(collectNonFinishingActivities(), isEmpty()); } + /** Adds the activity that will be reparented to this container. */ void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { + if (hasActivity(pendingAppearedActivity.getActivityToken())) { + return; + } + // Remove the pending activity from other TaskFragments. + mTaskContainer.cleanupPendingAppearedActivity(pendingAppearedActivity); mPendingAppearedActivities.add(pendingAppearedActivity); } + void removePendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { + mPendingAppearedActivities.remove(pendingAppearedActivity); + } + + @Nullable + Intent getPendingAppearedIntent() { + return mPendingAppearedIntent; + } + boolean hasActivity(@NonNull IBinder token) { if (mInfo != null && mInfo.getActivities().contains(token)) { return true; @@ -155,14 +217,37 @@ class TaskFragmentContainer { return count; } + /** Whether we are waiting for the TaskFragment to appear and become non-empty. */ + boolean isWaitingActivityAppear() { + return !mIsFinished && (mInfo == null || mAppearEmptyTimeout != null); + } + @Nullable TaskFragmentInfo getInfo() { return mInfo; } void setInfo(@NonNull TaskFragmentInfo info) { + if (!mIsFinished && mInfo == null && info.isEmpty()) { + // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is + // still empty after timeout. + mAppearEmptyTimeout = () -> { + mAppearEmptyTimeout = null; + mController.onTaskFragmentAppearEmptyTimeout(this); + }; + mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS); + } else if (mAppearEmptyTimeout != null && !info.isEmpty()) { + mController.getHandler().removeCallbacks(mAppearEmptyTimeout); + mAppearEmptyTimeout = null; + } + mInfo = info; - if (mInfo == null || mPendingAppearedActivities.isEmpty()) { + if (mInfo == null || mInfo.isEmpty()) { + return; + } + // Only track the pending Intent when the container is empty. + mPendingAppearedIntent = null; + if (mPendingAppearedActivities.isEmpty()) { return; } // Cleanup activities that were being re-parented @@ -177,15 +262,14 @@ class TaskFragmentContainer { @Nullable Activity getTopNonFinishingActivity() { - List<Activity> activities = collectActivities(); - if (activities.isEmpty()) { - return null; - } - int i = activities.size() - 1; - while (i >= 0 && activities.get(i).isFinishing()) { - i--; - } - return i >= 0 ? activities.get(i) : null; + final List<Activity> activities = collectNonFinishingActivities(); + return activities.isEmpty() ? null : activities.get(activities.size() - 1); + } + + @Nullable + Activity getBottomMostActivity() { + final List<Activity> activities = collectNonFinishingActivities(); + return activities.isEmpty() ? null : activities.get(0); } boolean isEmpty() { @@ -196,6 +280,9 @@ class TaskFragmentContainer { * Adds a container that should be finished when this container is finished. */ void addContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToFinish) { + if (mIsFinished) { + return; + } mContainersToFinishOnExit.add(containerToFinish); } @@ -203,6 +290,9 @@ class TaskFragmentContainer { * Removes a container that should be finished when this container is finished. */ void removeContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToRemove) { + if (mIsFinished) { + return; + } mContainersToFinishOnExit.remove(containerToRemove); } @@ -210,6 +300,9 @@ class TaskFragmentContainer { * Adds an activity that should be finished when this container is finished. */ void addActivityToFinishOnExit(@NonNull Activity activityToFinish) { + if (mIsFinished) { + return; + } mActivitiesToFinishOnExit.add(activityToFinish); } @@ -217,11 +310,17 @@ class TaskFragmentContainer { * Removes an activity that should be finished when this container is finished. */ void removeActivityToFinishOnExit(@NonNull Activity activityToRemove) { + if (mIsFinished) { + return; + } mActivitiesToFinishOnExit.remove(activityToRemove); } /** Removes all dependencies that should be finished when this container is finished. */ void resetDependencies() { + if (mIsFinished) { + return; + } mContainersToFinishOnExit.clear(); mActivitiesToFinishOnExit.clear(); } @@ -234,6 +333,10 @@ class TaskFragmentContainer { @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { if (!mIsFinished) { mIsFinished = true; + if (mAppearEmptyTimeout != null) { + mController.getHandler().removeCallbacks(mAppearEmptyTimeout); + mAppearEmptyTimeout = null; + } finishActivities(shouldFinishDependent, presenter, wct, controller); } @@ -253,8 +356,11 @@ class TaskFragmentContainer { private void finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { // Finish own activities - for (Activity activity : collectActivities()) { - if (!activity.isFinishing()) { + for (Activity activity : collectNonFinishingActivities()) { + if (!activity.isFinishing() + // In case we have requested to reparent the activity to another container (as + // pendingAppeared), we don't want to finish it with this container. + && mController.getContainerWithActivity(activity) == this) { activity.finish(); } } @@ -324,7 +430,13 @@ class TaskFragmentContainer { /** Gets the parent leaf Task id. */ int getTaskId() { - return mTaskId; + return mTaskContainer.getTaskId(); + } + + /** Gets the parent Task. */ + @NonNull + TaskContainer getTaskContainer() { + return mTaskContainer; } @Override @@ -340,15 +452,17 @@ class TaskFragmentContainer { */ private String toString(boolean includeContainersToFinishOnExit) { return "TaskFragmentContainer{" + + " parentTaskId=" + getTaskId() + " token=" + mToken - + " info=" + mInfo + " topNonFinishingActivity=" + getTopNonFinishingActivity() + + " runningActivityCount=" + getRunningActivityCount() + + " isFinished=" + mIsFinished + + " lastRequestedBounds=" + mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" + containersToFinishOnExitToString() : "") + " activitiesToFinishOnExit=" + mActivitiesToFinishOnExit - + " isFinished=" + mIsFinished - + " lastRequestedBounds=" + mLastRequestedBounds + + " info=" + mInfo + "}"; } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 1f12c4484159..a191e685f651 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -18,6 +18,7 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -27,8 +28,10 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import android.content.Intent; import android.content.res.Configuration; import android.graphics.Point; +import android.os.Handler; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; import android.window.WindowContainerToken; @@ -61,6 +64,10 @@ public class JetpackTaskFragmentOrganizerTest { private WindowContainerTransaction mTransaction; @Mock private JetpackTaskFragmentOrganizer.TaskFragmentCallback mCallback; + @Mock + private SplitController mSplitController; + @Mock + private Handler mHandler; private JetpackTaskFragmentOrganizer mOrganizer; @Before @@ -69,6 +76,7 @@ public class JetpackTaskFragmentOrganizerTest { mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback); mOrganizer.registerOrganizer(); spyOn(mOrganizer); + doReturn(mHandler).when(mSplitController).getHandler(); } @Test @@ -106,7 +114,9 @@ public class JetpackTaskFragmentOrganizerTest { @Test public void testExpandTaskFragment() { - final TaskFragmentContainer container = new TaskFragmentContainer(null, TASK_ID); + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mSplitController); final TaskFragmentInfo info = createMockInfo(container); mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); container.setInfo(info); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index e0fda58fd664..60390eb2b3d2 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -16,25 +16,52 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; +import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import android.annotation.NonNull; import android.app.Activity; +import android.app.ActivityOptions; +import android.content.ComponentName; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.Rect; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.Pair; import android.window.TaskFragmentInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -45,6 +72,10 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Test class for {@link SplitController}. * @@ -57,13 +88,24 @@ import org.mockito.MockitoAnnotations; public class SplitControllerTest { private static final int TASK_ID = 10; private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); + private static final float SPLIT_RATIO = 0.5f; + private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( + new ComponentName("test", "placeholder")); + + /** Default finish behavior in Jetpack. */ + private static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER; + private static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS; - @Mock private Activity mActivity; @Mock private Resources mActivityResources; @Mock private TaskFragmentInfo mInfo; + @Mock + private WindowContainerTransaction mTransaction; + @Mock + private Handler mHandler; + private SplitController mSplitController; private SplitPresenter mSplitPresenter; @@ -74,41 +116,58 @@ public class SplitControllerTest { mSplitPresenter = mSplitController.mPresenter; spyOn(mSplitController); spyOn(mSplitPresenter); + doNothing().when(mSplitPresenter).applyTransaction(any()); final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); - doReturn(mActivityResources).when(mActivity).getResources(); doReturn(activityConfig).when(mActivityResources).getConfiguration(); + doReturn(mHandler).when(mSplitController).getHandler(); + mActivity = createMockActivity(); } @Test public void testGetTopActiveContainer() { - TaskContainer taskContainer = new TaskContainer(TASK_ID); - // tf3 is finished so is not active. - TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class); - doReturn(true).when(tf3).isFinished(); + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + // tf1 has no running activity so is not active. + final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mSplitController); // tf2 has running activity so is active. - TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class); + final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class); doReturn(1).when(tf2).getRunningActivityCount(); - // tf1 has no running activity so is not active. - TaskFragmentContainer tf1 = new TaskFragmentContainer(null, TASK_ID); - - taskContainer.mContainers.add(tf3); taskContainer.mContainers.add(tf2); - taskContainer.mContainers.add(tf1); + // tf3 is finished so is not active. + final TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class); + doReturn(true).when(tf3).isFinished(); + doReturn(false).when(tf3).isWaitingActivityAppear(); + taskContainer.mContainers.add(tf3); mSplitController.mTaskContainers.put(TASK_ID, taskContainer); assertWithMessage("Must return tf2 because tf3 is not active.") .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2); - taskContainer.mContainers.remove(tf1); + taskContainer.mContainers.remove(tf3); assertWithMessage("Must return tf2 because tf2 has running activity.") .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2); taskContainer.mContainers.remove(tf2); - assertWithMessage("Must return null because tf1 has no running activity.") + assertWithMessage("Must return tf because we are waiting for tf1 to appear.") + .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1); + + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + doReturn(new ArrayList<>()).when(info).getActivities(); + doReturn(true).when(info).isEmpty(); + tf1.setInfo(info); + + assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after" + + " creation.") + .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1); + + doReturn(false).when(info).isEmpty(); + tf1.setInfo(info); + + assertWithMessage("Must return null because tf1 becomes empty.") .that(mSplitController.getTopActiveContainer(TASK_ID)).isNull(); } @@ -126,6 +185,26 @@ public class SplitControllerTest { } @Test + public void testOnTaskFragmentAppearEmptyTimeout() { + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.onTaskFragmentAppearEmptyTimeout(tf); + + verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */); + } + + @Test + public void testOnActivityDestroyed() { + doReturn(new Binder()).when(mActivity).getActivityToken(); + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + + assertTrue(tf.hasActivity(mActivity.getActivityToken())); + + mSplitController.onActivityDestroyed(mActivity); + + assertFalse(tf.hasActivity(mActivity.getActivityToken())); + } + + @Test public void testNewContainer() { // Must pass in a valid activity. assertThrows(IllegalArgumentException.class, () -> @@ -133,11 +212,802 @@ public class SplitControllerTest { assertThrows(IllegalArgumentException.class, () -> mSplitController.newContainer(mActivity, null /* launchingActivity */, TASK_ID)); - final TaskFragmentContainer tf = mSplitController.newContainer(null, mActivity, TASK_ID); + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, mActivity, + TASK_ID); final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); assertNotNull(tf); assertNotNull(taskContainer); assertEquals(TASK_BOUNDS, taskContainer.getTaskBounds()); } + + @Test + public void testUpdateContainer() { + // Make SplitController#launchPlaceholderIfNecessary(TaskFragmentContainer) return true + // and verify if shouldContainerBeExpanded() not called. + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + spyOn(tf); + doReturn(mActivity).when(tf).getTopNonFinishingActivity(); + doReturn(true).when(tf).isEmpty(); + doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mActivity, + false /* isOnCreated */); + doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any()); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).shouldContainerBeExpanded(any()); + + // Verify if tf should be expanded, getTopActiveContainer() won't be called + doReturn(null).when(tf).getTopNonFinishingActivity(); + doReturn(true).when(mSplitController).shouldContainerBeExpanded(tf); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).getTopActiveContainer(TASK_ID); + + // Verify if tf is not in split, dismissPlaceholderIfNecessary won't be called. + doReturn(false).when(mSplitController).shouldContainerBeExpanded(tf); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + + // Verify if tf is not in the top splitContainer, + final SplitContainer splitContainer = mock(SplitContainer.class); + doReturn(tf).when(splitContainer).getPrimaryContainer(); + doReturn(tf).when(splitContainer).getSecondaryContainer(); + final List<SplitContainer> splitContainers = + mSplitController.getTaskContainer(TASK_ID).mSplitContainers; + splitContainers.add(splitContainer); + // Add a mock SplitContainer on top of splitContainer + splitContainers.add(1, mock(SplitContainer.class)); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + + // Verify if one or both containers in the top SplitContainer are finished, + // dismissPlaceholder() won't be called. + splitContainers.remove(1); + doReturn(true).when(tf).isFinished(); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + + // Verify if placeholder should be dismissed, updateSplitContainer() won't be called. + doReturn(false).when(tf).isFinished(); + doReturn(true).when(mSplitController) + .dismissPlaceholderIfNecessary(splitContainer); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any()); + + // Verify if the top active split is updated if both of its containers are not finished. + doReturn(false).when(mSplitController) + .dismissPlaceholderIfNecessary(splitContainer); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction); + } + + @Test + public void testOnActivityCreated() { + mSplitController.onActivityCreated(mActivity); + + // Disallow to split as primary because we want the new launch to be always on top. + verify(mSplitController).resolveActivityToContainer(mActivity, false /* isOnReparent */); + } + + @Test + public void testOnActivityReparentToTask_sameProcess() { + mSplitController.onActivityReparentToTask(TASK_ID, new Intent(), + mActivity.getActivityToken()); + + // Treated as on activity created, but allow to split as primary. + verify(mSplitController).resolveActivityToContainer(mActivity, true /* isOnReparent */); + // Try to place the activity to the top TaskFragment when there is no matched rule. + verify(mSplitController).placeActivityInTopContainer(mActivity); + } + + @Test + public void testOnActivityReparentToTask_diffProcess() { + // Create an empty TaskFragment to initialize for the Task. + mSplitController.newContainer(new Intent(), mActivity, TASK_ID); + final IBinder activityToken = new Binder(); + final Intent intent = new Intent(); + + mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken); + + // Treated as starting new intent + verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean()); + verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent), + isNull()); + } + + @Test + public void testResolveStartActivityIntent_withoutLaunchingActivity() { + final Intent intent = new Intent(); + final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + + // No other activity available in the Task. + TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(mTransaction, + TASK_ID, intent, null /* launchingActivity */); + assertNull(container); + + // Task contains another activity that can be used as owner activity. + createMockTaskFragmentContainer(mActivity); + container = mSplitController.resolveStartActivityIntent(mTransaction, + TASK_ID, intent, null /* launchingActivity */); + assertNotNull(container); + } + + @Test + public void testResolveStartActivityIntent_shouldExpand() { + final Intent intent = new Intent(); + setupExpandRule(intent); + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + + assertNotNull(container); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertTrue(container.isLastRequestedWindowingModeEqual(WINDOWING_MODE_UNDEFINED)); + assertFalse(container.hasActivity(mActivity.getActivityToken())); + verify(mSplitPresenter).createTaskFragment(mTransaction, container.getTaskFragmentToken(), + mActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithLaunchingActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopExpandActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + createMockTaskFragmentContainer(mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopSecondaryActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopPrimaryActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testPlaceActivityInTopContainer() { + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter, never()).applyTransaction(any()); + + mSplitController.newContainer(new Intent(), mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter).applyTransaction(any()); + + // Not reparent if activity is in a TaskFragment. + clearInvocations(mSplitPresenter); + mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter, never()).applyTransaction(any()); + } + + @Test + public void testResolveActivityToContainer_noRuleMatched() { + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + } + + @Test + public void testResolveActivityToContainer_expandRule_notInTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + assertNotNull(container); + verify(mSplitController).newContainer(mActivity, TASK_ID); + verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + } + + @Test + public void testResolveActivityToContainer_expandRule_inSingleTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken()); + } + + @Test + public void testResolveActivityToContainer_expandRule_inSplitTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final Activity activity = createMockActivity(); + addSplitTaskFragments(activity, mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + assertNotNull(container); + verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is not in any TaskFragment. + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), + placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inOccludedTaskFragment() { + setupPlaceholderRule(mActivity); + + // Don't launch placeholder if the activity is not in the topmost active TaskFragment. + final Activity activity = createMockActivity(); + mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.newContainer(activity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), + anyBoolean()); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is in the topmost expanded TaskFragment. + mSplitController.newContainer(mActivity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), + placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inPrimarySplit() { + setupPlaceholderRule(mActivity); + + // Don't launch placeholder if the activity is in primary split. + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), + anyBoolean()); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is in secondary split. + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), + placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_splitRule_inPrimarySplitWithRuleMatched() { + final Intent secondaryIntent = new Intent(); + setupSplitRule(mActivity, secondaryIntent); + final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0); + + // Activity is already in primary split, no need to create new split. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( + secondaryIntent, mActivity, TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + splitRule); + clearInvocations(mSplitController); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_splitRule_inPrimarySplitWithNoRuleMatched() { + final Intent secondaryIntent = new Intent(); + setupSplitRule(mActivity, secondaryIntent); + final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0); + + // The new launched activity is in primary split, but there is no rule for it to split with + // the secondary, so return false. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( + secondaryIntent, mActivity, TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + splitRule); + final Activity launchedActivity = createMockActivity(); + primaryContainer.addPendingAppearedActivity(launchedActivity); + + assertFalse(mSplitController.resolveActivityToContainer(launchedActivity, + false /* isOnReparent */)); + } + + @Test + public void testResolveActivityToContainer_splitRule_inSecondarySplitWithRuleMatched() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, mActivity); + + // Activity is already in secondary split, no need to create new split. + addSplitTaskFragments(primaryActivity, mActivity); + clearInvocations(mSplitController); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_splitRule_inSecondarySplitWithNoRuleMatched() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, secondaryActivity); + + // Activity is in secondary split, but there is no rule to split it with primary. + addSplitTaskFragments(primaryActivity, secondaryActivity); + mSplitController.getContainerWithActivity(secondaryActivity) + .addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_isPlaceholderWithRuleMatched() { + final Activity primaryActivity = createMockActivity(); + setupPlaceholderRule(primaryActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + doReturn(PLACEHOLDER_INTENT).when(mActivity).getIntent(); + + // Activity is a placeholder. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer( + primaryActivity, TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + placeholderRule); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsSecondary() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(activityBelow, mActivity); + + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + assertSplitPair(activityBelow, mActivity); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsPrimary() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(mActivity, activityBelow); + + // Disallow to split as primary. + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + assertEquals(container, mSplitController.getContainerWithActivity(mActivity)); + + // Allow to split as primary. + result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */); + + assertTrue(result); + assertSplitPair(mActivity, activityBelow); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsSecondary() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, mActivity); + + final Activity activityBelow = createMockActivity(); + addSplitTaskFragments(primaryActivity, activityBelow); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + primaryActivity); + final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity( + activityBelow); + secondaryContainer.addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + // TODO(b/231845476) we should always respect clearTop. + // assertNotEquals(secondaryContainer, container); + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsPrimary() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(mActivity, primaryActivity); + + final Activity activityBelow = createMockActivity(); + addSplitTaskFragments(primaryActivity, activityBelow); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + primaryActivity); + primaryContainer.addPendingAppearedActivity(mActivity); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity)); + + + result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */); + + assertTrue(result); + assertSplitPair(mActivity, primaryActivity); + } + + @Test + public void testResolveActivityToContainer_inUnknownTaskFragment() { + doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); + + // No need to handle when the new launched activity is in an unknown TaskFragment. + assertTrue(mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */)); + } + + @Test + public void testGetPlaceholderOptions() { + doReturn(true).when(mActivity).isResumed(); + + assertNull(mSplitController.getPlaceholderOptions(mActivity, false /* isOnCreated */)); + + doReturn(false).when(mActivity).isResumed(); + + assertNull(mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */)); + + // Launch placeholder without moving the Task to front if the Task is now in background (not + // resumed or onCreated). + final Bundle options = mSplitController.getPlaceholderOptions(mActivity, + false /* isOnCreated */); + + assertNotNull(options); + final ActivityOptions activityOptions = new ActivityOptions(options); + assertTrue(activityOptions.getAvoidMoveToFront()); + } + + @Test + public void testFinishTwoSplitThatShouldFinishTogether() { + // Setup two split pairs that should finish each other when finishing one. + final Activity secondaryActivity0 = createMockActivity(); + final Activity secondaryActivity1 = createMockActivity(); + final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer(mActivity); + final TaskFragmentContainer secondaryContainer0 = createMockTaskFragmentContainer( + secondaryActivity0); + final TaskFragmentContainer secondaryContainer1 = createMockTaskFragmentContainer( + secondaryActivity1); + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + final SplitRule rule0 = createSplitRule(mActivity, secondaryActivity0, FINISH_ALWAYS, + FINISH_ALWAYS, false /* clearTop */); + final SplitRule rule1 = createSplitRule(mActivity, secondaryActivity1, FINISH_ALWAYS, + FINISH_ALWAYS, false /* clearTop */); + registerSplitPair(primaryContainer, secondaryContainer0, rule0); + registerSplitPair(primaryContainer, secondaryContainer1, rule1); + + primaryContainer.finish(true /* shouldFinishDependent */, mSplitPresenter, + mTransaction, mSplitController); + + // All containers and activities should be finished based on the FINISH_ALWAYS behavior. + assertTrue(primaryContainer.isFinished()); + assertTrue(secondaryContainer0.isFinished()); + assertTrue(secondaryContainer1.isFinished()); + verify(mActivity).finish(); + verify(secondaryActivity0).finish(); + verify(secondaryActivity1).finish(); + assertTrue(taskContainer.mContainers.isEmpty()); + assertTrue(taskContainer.mSplitContainers.isEmpty()); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + doReturn(mActivityResources).when(activity).getResources(); + final IBinder activityToken = new Binder(); + doReturn(activityToken).when(activity).getActivityToken(); + doReturn(activity).when(mSplitController).getActivity(activityToken); + doReturn(TASK_ID).when(activity).getTaskId(); + return activity; + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), + new Configuration(), + 1, + true /* isVisible */, + Collections.singletonList(activity.getActivityToken()), + new Point(), + false /* isTaskClearedForReuse */, + false /* isTaskFragmentClearedForPip */); + } + + /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ + private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { + final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID); + setupTaskFragmentInfo(container, activity); + return container; + } + + /** Setups the given TaskFragment as it has appeared in the server. */ + private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity); + container.setInfo(info); + mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info); + } + + /** Setups a rule to always expand the given intent. */ + private void setupExpandRule(@NonNull Intent expandIntent) { + final ActivityRule expandRule = new ActivityRule.Builder(r -> false, expandIntent::equals) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + } + + /** Setups a rule to always expand the given activity. */ + private void setupExpandRule(@NonNull Activity expandActivity) { + final ActivityRule expandRule = new ActivityRule.Builder(expandActivity::equals, i -> false) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + } + + /** Setups a rule to launch placeholder for the given activity. */ + private void setupPlaceholderRule(@NonNull Activity primaryActivity) { + final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, + primaryActivity::equals, i -> false, w -> true) + .setSplitRatio(SPLIT_RATIO) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent); + mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, + DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, + true /* clearTop */); + mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); + } + + /** Creates a rule to always split the given activity and the given intent. */ + private SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); + return new SplitPairRule.Builder( + activityPair -> false, + targetPair::equals, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setShouldClearTop(true) + .build(); + } + + /** Creates a rule to always split the given activities. */ + private SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + return createSplitRule(primaryActivity, secondaryActivity, + DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, + true /* clearTop */); + } + + /** Creates a rule to always split the given activities with the given finish behaviors. */ + private SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, + int finishSecondaryWithPrimary, boolean clearTop) { + final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity); + return new SplitPairRule.Builder( + targetPair::equals, + activityIntentPair -> false, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary) + .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary) + .setShouldClearTop(clearTop) + .build(); + } + + /** Adds a pair of TaskFragments as split for the given activities. */ + private void addSplitTaskFragments(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + registerSplitPair(createMockTaskFragmentContainer(primaryActivity), + createMockTaskFragmentContainer(secondaryActivity), + createSplitRule(primaryActivity, secondaryActivity)); + } + + /** Registers the two given TaskFragments as split pair. */ + private void registerSplitPair(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule rule) { + mSplitController.registerSplit( + mock(WindowContainerTransaction.class), + primaryContainer, + primaryContainer.getTopNonFinishingActivity(), + secondaryContainer, + rule); + + // We need to set those in case we are not respecting clear top. + // TODO(b/231845476) we should always respect clearTop. + final int windowingMode = mSplitController.getTaskContainer(TASK_ID) + .getWindowingModeForSplitTaskFragment(TASK_BOUNDS); + primaryContainer.setLastRequestedWindowingMode(windowingMode); + secondaryContainer.setLastRequestedWindowingMode(windowingMode); + primaryContainer.setLastRequestedBounds(getSplitBounds(true /* isPrimary */)); + secondaryContainer.setLastRequestedBounds(getSplitBounds(false /* isPrimary */)); + } + + /** Gets the bounds of a TaskFragment that is in split. */ + private Rect getSplitBounds(boolean isPrimary) { + final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO); + return isPrimary + ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, + TASK_BOUNDS.bottom) + : new Rect(TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } + + /** Asserts that the two given activities are in split. */ + private void assertSplitPair(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + assertSplitPair(mSplitController.getContainerWithActivity(primaryActivity), + mSplitController.getContainerWithActivity(secondaryActivity)); + } + + /** Asserts that the two given TaskFragments are in split. */ + private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer) { + assertNotNull(primaryContainer); + assertNotNull(secondaryContainer); + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, + secondaryContainer)); + if (primaryContainer.mInfo != null) { + assertTrue(primaryContainer.areLastRequestedBoundsEqual( + getSplitBounds(true /* isPrimary */))); + assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( + WINDOWING_MODE_MULTI_WINDOW)); + } + if (secondaryContainer.mInfo != null) { + assertTrue(secondaryContainer.areLastRequestedBoundsEqual( + getSplitBounds(false /* isPrimary */))); + assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( + WINDOWING_MODE_MULTI_WINDOW)); + } + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index c7feb7e59de3..ebe202db4e54 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -24,16 +24,24 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import android.app.Activity; +import android.content.Intent; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Test class for {@link TaskContainer}. @@ -48,6 +56,14 @@ public class TaskContainerTest { private static final int TASK_ID = 10; private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); + @Mock + private SplitController mController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + @Test public void testIsTaskBoundsInitialized() { final TaskContainer taskContainer = new TaskContainer(TASK_ID); @@ -126,8 +142,8 @@ public class TaskContainerTest { assertTrue(taskContainer.isEmpty()); - final TaskFragmentContainer tf = new TaskFragmentContainer(null, TASK_ID); - taskContainer.mContainers.add(tf); + final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mController); assertFalse(taskContainer.isEmpty()); @@ -136,4 +152,38 @@ public class TaskContainerTest { assertFalse(taskContainer.isEmpty()); } + + @Test + public void testGetTopTaskFragmentContainer() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + assertNull(taskContainer.getTopTaskFragmentContainer()); + + final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mController); + assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); + + final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mController); + assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); + } + + @Test + public void testGetTopNonFinishingActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + assertNull(taskContainer.getTopNonFinishingActivity()); + + final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class); + taskContainer.mContainers.add(tf0); + final Activity activity0 = mock(Activity.class); + doReturn(activity0).when(tf0).getTopNonFinishingActivity(); + assertEquals(activity0, taskContainer.getTopNonFinishingActivity()); + + final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class); + taskContainer.mContainers.add(tf1); + assertEquals(activity0, taskContainer.getTopNonFinishingActivity()); + + final Activity activity1 = mock(Activity.class); + doReturn(activity1).when(tf1).getTopNonFinishingActivity(); + assertEquals(activity1, taskContainer.getTopNonFinishingActivity()); + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 97896c2c0a57..fcbd8a3ac020 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -18,19 +18,36 @@ package androidx.window.extensions.embedding; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import android.annotation.NonNull; import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Point; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.google.android.collect.Lists; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,6 +55,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Test class for {@link TaskFragmentContainer}. @@ -56,18 +75,39 @@ public class TaskFragmentContainerTest { @Mock private SplitController mController; @Mock - private Activity mActivity; - @Mock private TaskFragmentInfo mInfo; + @Mock + private Handler mHandler; + private Activity mActivity; + private Intent mIntent; @Before public void setup() { MockitoAnnotations.initMocks(this); + doReturn(mHandler).when(mController).getHandler(); + mActivity = createMockActivity(); + mIntent = new Intent(); + } + + @Test + public void testNewContainer() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + + // One of the activity and the intent must be non-null + assertThrows(IllegalArgumentException.class, + () -> new TaskFragmentContainer(null, null, taskContainer, mController)); + + // One of the activity and the intent must be null. + assertThrows(IllegalArgumentException.class, + () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController)); } @Test public void testFinish() { - final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, TASK_ID); + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + doReturn(container).when(mController).getContainerWithActivity(mActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); // Only remove the activity, but not clear the reference until appeared. @@ -94,4 +134,195 @@ public class TaskFragmentContainerTest { verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken()); verify(mController).removeContainer(container); } + + @Test + public void testFinish_notFinishActivityThatIsReparenting() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity); + container0.setInfo(info); + // Request to reparent the activity to a new TaskFragment. + final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + doReturn(container1).when(mController).getContainerWithActivity(mActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + // The activity is requested to be reparented, so don't finish it. + container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + + verify(mActivity, never()).finish(); + verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken()); + verify(mController).removeContainer(container0); + } + + @Test + public void testSetInfo() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + // Pending activity should be cleared when it has appeared on server side. + final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + + assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains(mActivity)); + + final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer, + mActivity); + pendingActivityContainer.setInfo(info0); + + assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty()); + + // Pending intent should be cleared when the container becomes non-empty. + final TaskFragmentContainer pendingIntentContainer = new TaskFragmentContainer( + null /* pendingAppearedActivity */, mIntent, taskContainer, mController); + + assertEquals(mIntent, pendingIntentContainer.getPendingAppearedIntent()); + + final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer, + mActivity); + pendingIntentContainer.setInfo(info1); + + assertNull(pendingIntentContainer.getPendingAppearedIntent()); + } + + @Test + public void testIsWaitingActivityAppear() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + + assertTrue(container.isWaitingActivityAppear()); + + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + doReturn(new ArrayList<>()).when(info).getActivities(); + doReturn(true).when(info).isEmpty(); + container.setInfo(info); + + assertTrue(container.isWaitingActivityAppear()); + + doReturn(false).when(info).isEmpty(); + container.setInfo(info); + + assertFalse(container.isWaitingActivityAppear()); + } + + @Test + public void testAppearEmptyTimeout() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + + assertNull(container.mAppearEmptyTimeout); + + // Not set if it is not appeared empty. + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + doReturn(new ArrayList<>()).when(info).getActivities(); + doReturn(false).when(info).isEmpty(); + container.setInfo(info); + + assertNull(container.mAppearEmptyTimeout); + + // Set timeout if the first info set is empty. + container.mInfo = null; + doReturn(true).when(info).isEmpty(); + container.setInfo(info); + + assertNotNull(container.mAppearEmptyTimeout); + + // Remove timeout after the container becomes non-empty. + doReturn(false).when(info).isEmpty(); + container.setInfo(info); + + assertNull(container.mAppearEmptyTimeout); + + // Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout. + container.mInfo = null; + doReturn(true).when(info).isEmpty(); + container.setInfo(info); + container.mAppearEmptyTimeout.run(); + + assertNull(container.mAppearEmptyTimeout); + verify(mController).onTaskFragmentAppearEmptyTimeout(container); + } + + @Test + public void testCollectNonFinishingActivities() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + List<Activity> activities = container.collectNonFinishingActivities(); + + assertTrue(activities.isEmpty()); + + container.addPendingAppearedActivity(mActivity); + activities = container.collectNonFinishingActivities(); + + assertEquals(1, activities.size()); + + final Activity activity0 = createMockActivity(); + final Activity activity1 = createMockActivity(); + final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(), + activity1.getActivityToken()); + doReturn(runningActivities).when(mInfo).getActivities(); + container.setInfo(mInfo); + activities = container.collectNonFinishingActivities(); + + assertEquals(3, activities.size()); + assertEquals(activity0, activities.get(0)); + assertEquals(activity1, activities.get(1)); + assertEquals(mActivity, activities.get(2)); + } + + @Test + public void testAddPendingActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + + assertEquals(1, container.collectNonFinishingActivities().size()); + + container.addPendingAppearedActivity(mActivity); + + assertEquals(1, container.collectNonFinishingActivities().size()); + } + + @Test + public void testGetBottomMostActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + + assertEquals(mActivity, container.getBottomMostActivity()); + + final Activity activity = createMockActivity(); + final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken()); + doReturn(runningActivities).when(mInfo).getActivities(); + container.setInfo(mInfo); + + assertEquals(activity, container.getBottomMostActivity()); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + final IBinder activityToken = new Binder(); + doReturn(activityToken).when(activity).getActivityToken(); + doReturn(activity).when(mController).getActivity(activityToken); + return activity; + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), + new Configuration(), + 1, + true /* isVisible */, + Collections.singletonList(activity.getActivityToken()), + new Point(), + false /* isTaskClearedForReuse */, + false /* isTaskFragmentClearedForPip */); + } } diff --git a/libs/WindowManager/Shell/res/values-af/strings_tv.xml b/libs/WindowManager/Shell/res/values-af/strings_tv.xml index c87bec093cca..6187ea46769c 100644 --- a/libs/WindowManager/Shell/res/values-af/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-af/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Beeld-in-beeld"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Titellose program)"</string> - <string name="pip_close" msgid="9135220303720555525">"Maak PIP toe"</string> + <string name="pip_close" msgid="2955969519031223530">"Maak toe"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Volskerm"</string> - <string name="pip_move" msgid="1544227837964635439">"Skuif PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Vou PIP uit"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Vou PIP in"</string> + <string name="pip_move" msgid="158770205886688553">"Skuif"</string> + <string name="pip_expand" msgid="1051966011679297308">"Vou uit"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Vou in"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dubbeldruk "<annotation icon="home_icon">" TUIS "</annotation>" vir kontroles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Prent-in-prent-kieslys"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Skuif links"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Skuif regs"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Skuif op"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Skuif af"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klaar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings_tv.xml b/libs/WindowManager/Shell/res/values-am/strings_tv.xml index d23353858de6..74ce49ef078e 100644 --- a/libs/WindowManager/Shell/res/values-am/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ስዕል-ላይ-ስዕል"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ርዕስ የሌለው ፕሮግራም)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIPን ዝጋ"</string> + <string name="pip_close" msgid="2955969519031223530">"ዝጋ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ሙሉ ማያ ገጽ"</string> - <string name="pip_move" msgid="1544227837964635439">"ፒአይፒ ውሰድ"</string> - <string name="pip_expand" msgid="7605396312689038178">"ፒአይፒን ዘርጋ"</string> - <string name="pip_collapse" msgid="5732233773786896094">"ፒአይፒን ሰብስብ"</string> + <string name="pip_move" msgid="158770205886688553">"ውሰድ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ዘርጋ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ሰብስብ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ለመቆጣጠሪያዎች "<annotation icon="home_icon">"መነሻ"</annotation>"ን ሁለቴ ይጫኑ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"የስዕል-ላይ-ስዕል ምናሌ።"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ወደ ግራ ውሰድ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ወደ ቀኝ ውሰድ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ወደ ላይ ውሰድ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ወደ ታች ውሰድ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ተጠናቅቋል"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml index a1ceda5fc987..9c195a7386a9 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"نافذة ضمن النافذة"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ليس هناك عنوان للبرنامج)"</string> - <string name="pip_close" msgid="9135220303720555525">"إغلاق PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"إغلاق"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ملء الشاشة"</string> - <string name="pip_move" msgid="1544227837964635439">"نقل نافذة داخل النافذة (PIP)"</string> - <string name="pip_expand" msgid="7605396312689038178">"توسيع نافذة داخل النافذة (PIP)"</string> - <string name="pip_collapse" msgid="5732233773786896094">"تصغير نافذة داخل النافذة (PIP)"</string> + <string name="pip_move" msgid="158770205886688553">"نقل"</string> + <string name="pip_expand" msgid="1051966011679297308">"توسيع"</string> + <string name="pip_collapse" msgid="3903295106641385962">"تصغير"</string> <string name="pip_edu_text" msgid="3672999496647508701">" انقر مرتين على "<annotation icon="home_icon">" الصفحة الرئيسية "</annotation>" للوصول لعناصر التحكم."</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"قائمة نافذة ضمن النافذة"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"نقل لليسار"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"نقل لليمين"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"نقل للأعلى"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"نقل للأسفل"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"تمّ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings_tv.xml b/libs/WindowManager/Shell/res/values-as/strings_tv.xml index 8d7bd9f6a27e..816b5b1c79dc 100644 --- a/libs/WindowManager/Shell/res/values-as/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-as/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"চিত্ৰৰ ভিতৰত চিত্ৰ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিৰোনামবিহীন কাৰ্যক্ৰম)"</string> - <string name="pip_close" msgid="9135220303720555525">"পিপ বন্ধ কৰক"</string> + <string name="pip_close" msgid="2955969519031223530">"বন্ধ কৰক"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"সম্পূৰ্ণ স্ক্ৰীন"</string> - <string name="pip_move" msgid="1544227837964635439">"পিপ স্থানান্তৰ কৰক"</string> - <string name="pip_expand" msgid="7605396312689038178">"পিপ বিস্তাৰ কৰক"</string> - <string name="pip_collapse" msgid="5732233773786896094">"পিপ সংকোচন কৰক"</string> + <string name="pip_move" msgid="158770205886688553">"স্থানান্তৰ কৰক"</string> + <string name="pip_expand" msgid="1051966011679297308">"বিস্তাৰ কৰক"</string> + <string name="pip_collapse" msgid="3903295106641385962">"সংকোচন কৰক"</string> <string name="pip_edu_text" msgid="3672999496647508701">" নিয়ন্ত্ৰণৰ বাবে "<annotation icon="home_icon">" গৃহপৃষ্ঠা "</annotation>" বুটামত দুবাৰ হেঁচক"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"চিত্ৰৰ ভিতৰৰ চিত্ৰ মেনু।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাওঁফাললৈ নিয়ক"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"সোঁফাললৈ নিয়ক"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ওপৰলৈ নিয়ক"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"তললৈ নিয়ক"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"হ’ল"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings_tv.xml b/libs/WindowManager/Shell/res/values-az/strings_tv.xml index 87c46fa41a01..ccb7a7069ad8 100644 --- a/libs/WindowManager/Shell/res/values-az/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-az/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Şəkil-içində-Şəkil"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Başlıqsız proqram)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP bağlayın"</string> + <string name="pip_close" msgid="2955969519031223530">"Bağlayın"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP tətbiq edin"</string> - <string name="pip_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_move" msgid="158770205886688553">"Köçürün"</string> + <string name="pip_expand" msgid="1051966011679297308">"Genişləndirin"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Yığcamlaşdırın"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Nizamlayıcılar üçün "<annotation icon="home_icon">" ƏSAS SƏHİFƏ "</annotation>" süçimini iki dəfə basın"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Şəkildə şəkil menyusu."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola köçürün"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa köçürün"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Yuxarı köçürün"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Aşağı köçürün"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hazırdır"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml index c87f30611a07..51a1262b1de7 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> - <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ceo ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"Premesti sliku u slici"</string> - <string name="pip_expand" msgid="7605396312689038178">"Proširi sliku u slici"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Skupi sliku u slici"</string> + <string name="pip_move" msgid="158770205886688553">"Premesti"</string> + <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Skupi"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" HOME "</annotation>" za kontrole"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni Slika u slici."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomerite nalevo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomerite nadesno"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomerite nagore"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomerite nadole"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings_tv.xml b/libs/WindowManager/Shell/res/values-be/strings_tv.xml index 3566bc372820..15a353c649d6 100644 --- a/libs/WindowManager/Shell/res/values-be/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-be/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Відарыс у відарысе"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Праграма без назвы)"</string> - <string name="pip_close" msgid="9135220303720555525">"Закрыць PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Закрыць"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Поўнаэкранны рэжым"</string> - <string name="pip_move" msgid="1544227837964635439">"Перамясціць PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Разгарнуць відарыс у відарысе"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Згарнуць відарыс у відарысе"</string> + <string name="pip_move" msgid="158770205886688553">"Перамясціць"</string> + <string name="pip_expand" msgid="1051966011679297308">"Разгарнуць"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Згарнуць"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Двойчы націсніце "<annotation icon="home_icon">" ГАЛОЎНЫ ЭКРАН "</annotation>" для пераходу ў налады"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню рэжыму \"Відарыс у відарысе\"."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перамясціць улева"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перамясціць управа"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Перамясціць уверх"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Перамясціць уніз"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Гатова"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml index 91049fd2cf02..2b27a6927077 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картина в картината"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без заглавие)"</string> - <string name="pip_close" msgid="9135220303720555525">"Затваряне на PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Затваряне"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цял екран"</string> - <string name="pip_move" msgid="1544227837964635439">"„Картина в картина“: Преместв."</string> - <string name="pip_expand" msgid="7605396312689038178">"Разгъване на прозореца за PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Свиване на прозореца за PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Преместване"</string> + <string name="pip_expand" msgid="1051966011679297308">"Разгъване"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Свиване"</string> <string name="pip_edu_text" msgid="3672999496647508701">" За достъп до контролите натиснете 2 пъти "<annotation icon="home_icon">"НАЧАЛО"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню за функцията „Картина в картината“."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Преместване наляво"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Преместване надясно"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Преместване нагоре"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Преместване надолу"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml index 792708d128a5..23c8ffabeede 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ছবির-মধ্যে-ছবি"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিরোনামহীন প্রোগ্রাম)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP বন্ধ করুন"</string> + <string name="pip_close" msgid="2955969519031223530">"বন্ধ করুন"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"পূর্ণ স্ক্রিন"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP সরান"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP বড় করুন"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP আড়াল করুন"</string> + <string name="pip_move" msgid="158770205886688553">"সরান"</string> + <string name="pip_expand" msgid="1051966011679297308">"বড় করুন"</string> + <string name="pip_collapse" msgid="3903295106641385962">"আড়াল করুন"</string> <string name="pip_edu_text" msgid="3672999496647508701">" কন্ট্রোলের জন্য "<annotation icon="home_icon">" হোম "</annotation>" বোতামে ডবল প্রেস করুন"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ছবির-মধ্যে-ছবি মেনু।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাঁদিকে সরান"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ডানদিকে সরান"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"উপরে তুলুন"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"নিচে নামান"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"হয়ে গেছে"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml index b7f0dca1b5a5..443fd620fd65 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> - <string name="pip_close" msgid="9135220303720555525">"Zatvori sliku u slici"</string> + <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"Pokreni sliku u slici"</string> - <string name="pip_expand" msgid="7605396312689038178">"Proširi sliku u slici"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Suzi sliku u slici"</string> + <string name="pip_move" msgid="158770205886688553">"Premjesti"</string> + <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Suzi"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" POČETNI EKRAN "</annotation>" za kontrole"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za način rada slika u slici."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomjeranje ulijevo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomjeranje udesno"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomjeranje nagore"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomjeranje nadolje"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml index 1c560c7afa06..94ba0db7e978 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sense títol)"</string> - <string name="pip_close" msgid="9135220303720555525">"Tanca PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Tanca"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mou pantalla en pantalla"</string> - <string name="pip_expand" msgid="7605396312689038178">"Desplega pantalla en pantalla"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Replega pantalla en pantalla"</string> + <string name="pip_move" msgid="158770205886688553">"Mou"</string> + <string name="pip_expand" msgid="1051966011679297308">"Desplega"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Replega"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Prem dos cops "<annotation icon="home_icon">" INICI "</annotation>" per accedir als controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mou cap a l\'esquerra"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mou cap a la dreta"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mou cap amunt"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mou cap avall"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fet"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml index 9a8cc2b4d70e..3ed85dce0433 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz v obraze"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Bez názvu)"</string> - <string name="pip_close" msgid="9135220303720555525">"Ukončit obraz v obraze (PIP)"</string> + <string name="pip_close" msgid="2955969519031223530">"Zavřít"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> - <string name="pip_move" msgid="1544227837964635439">"Přesunout PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Rozbalit PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Sbalit PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Přesunout"</string> + <string name="pip_expand" msgid="1051966011679297308">"Rozbalit"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Sbalit"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Ovládací prvky zobrazíte dvojitým stisknutím "<annotation icon="home_icon">"tlačítka plochy"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Nabídka režimu obrazu v obraze"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Přesunout doleva"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Přesunout doprava"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Přesunout nahoru"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Přesunout dolů"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings_tv.xml b/libs/WindowManager/Shell/res/values-da/strings_tv.xml index cba660ac723c..09024428a825 100644 --- a/libs/WindowManager/Shell/res/values-da/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-da/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Integreret billede"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uden titel)"</string> - <string name="pip_close" msgid="9135220303720555525">"Luk integreret billede"</string> + <string name="pip_close" msgid="2955969519031223530">"Luk"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Fuld skærm"</string> - <string name="pip_move" msgid="1544227837964635439">"Flyt PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Udvid PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Skjul PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Flyt"</string> + <string name="pip_expand" msgid="1051966011679297308">"Udvid"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Tryk to gange på "<annotation icon="home_icon">" HJEM "</annotation>" for at se indstillinger"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu for integreret billede."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flyt til venstre"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flyt til højre"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flyt op"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flyt ned"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Udfør"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings_tv.xml b/libs/WindowManager/Shell/res/values-de/strings_tv.xml index 02a1b66eb63f..18535c9d9338 100644 --- a/libs/WindowManager/Shell/res/values-de/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-de/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bild im Bild"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Kein Sendungsname gefunden)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP schließen"</string> + <string name="pip_close" msgid="2955969519031223530">"Schließen"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Vollbild"</string> - <string name="pip_move" msgid="1544227837964635439">"BiB verschieben"</string> - <string name="pip_expand" msgid="7605396312689038178">"BiB maximieren"</string> - <string name="pip_collapse" msgid="5732233773786896094">"BiB minimieren"</string> + <string name="pip_move" msgid="158770205886688553">"Bewegen"</string> + <string name="pip_expand" msgid="1051966011679297308">"Maximieren"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Minimieren"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Für Steuerelemente zweimal "<annotation icon="home_icon">"STARTBILDSCHIRMTASTE"</annotation>" drücken"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menü „Bild im Bild“."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Nach links bewegen"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Nach rechts bewegen"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Nach oben bewegen"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Nach unten bewegen"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fertig"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings_tv.xml b/libs/WindowManager/Shell/res/values-el/strings_tv.xml index 24cd030cd754..5f8a004b0a1f 100644 --- a/libs/WindowManager/Shell/res/values-el/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-el/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Δεν υπάρχει τίτλος προγράμματος)"</string> - <string name="pip_close" msgid="9135220303720555525">"Κλείσιμο PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Κλείσιμο"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Πλήρης οθόνη"</string> - <string name="pip_move" msgid="1544227837964635439">"Μετακίνηση PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Ανάπτυξη PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Σύμπτυξη PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Μετακίνηση"</string> + <string name="pip_expand" msgid="1051966011679297308">"Ανάπτυξη"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Σύμπτυξη"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Πατήστε δύο φορές "<annotation icon="home_icon">" ΑΡΧΙΚΗ ΟΘΟΝΗ "</annotation>" για στοιχεία ελέγχου"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Μενού λειτουργίας Picture-in-Picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Μετακίνηση αριστερά"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Μετακίνηση δεξιά"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Μετακίνηση επάνω"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Μετακίνηση κάτω"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Τέλος"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml index 82257b42814d..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml index 82257b42814d..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml index 82257b42814d..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml index 82257b42814d..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml index a6e494cfed3c..507e066e3812 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-Picture menu."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml index 458f6b15b857..a2c27b79e04c 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Sin título de programa)"</string> - <string name="pip_close" msgid="9135220303720555525">"Cerrar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Cerrar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Maximizar PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Minimizar PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expandir"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Presiona dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover hacia arriba"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover hacia abajo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Listo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 6f38ecae674d..39990dc8cb0c 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -63,7 +63,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cerrar burbuja"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar conversación en burbuja"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatea con burbujas"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamadas \"burbujas\". Toca una burbuja para abrirla. Arrástrala para moverla."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamados \"burbujas\". Toca una burbuja para abrirla. Arrástrala para moverla."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla las burbujas"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toca Gestionar para desactivar las burbujas de esta aplicación"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Entendido"</string> diff --git a/libs/WindowManager/Shell/res/values-es/strings_tv.xml b/libs/WindowManager/Shell/res/values-es/strings_tv.xml index 0a690984dac5..7993e03b2464 100644 --- a/libs/WindowManager/Shell/res/values-es/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Imagen en imagen"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sin título)"</string> - <string name="pip_close" msgid="9135220303720555525">"Cerrar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Cerrar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover imagen en imagen"</string> - <string name="pip_expand" msgid="7605396312689038178">"Mostrar imagen en imagen"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Ocultar imagen en imagen"</string> + <string name="pip_move" msgid="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Mostrar"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Ocultar"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Pulsa dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de imagen en imagen."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover hacia arriba"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover hacia abajo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hecho"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings_tv.xml b/libs/WindowManager/Shell/res/values-et/strings_tv.xml index dc0232303a70..e8fcb180c0c4 100644 --- a/libs/WindowManager/Shell/res/values-et/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-et/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pilt pildis"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programmi pealkiri puudub)"</string> - <string name="pip_close" msgid="9135220303720555525">"Sule PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Sule"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Täisekraan"</string> - <string name="pip_move" msgid="1544227837964635439">"Teisalda PIP-režiimi"</string> - <string name="pip_expand" msgid="7605396312689038178">"Laienda PIP-akent"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Ahenda PIP-aken"</string> + <string name="pip_move" msgid="158770205886688553">"Teisalda"</string> + <string name="pip_expand" msgid="1051966011679297308">"Laienda"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Ahenda"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Nuppude nägemiseks vajutage 2 korda nuppu "<annotation icon="home_icon">"AVAKUVA"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menüü Pilt pildis."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Teisalda vasakule"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Teisalda paremale"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Teisalda üles"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Teisalda alla"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Valmis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index caa335a96222..67b9a433dc03 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -23,7 +23,7 @@ <string name="pip_phone_enter_split" msgid="7042877263880641911">"Sartu pantaila zatituan"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menua"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Pantaila txiki gainjarrian dago <xliff:g id="NAME">%s</xliff:g>"</string> - <string name="pip_notification_message" msgid="8854051911700302620">"Ez baduzu nahi <xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea nahi ez baduzu, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string> <string name="pip_play" msgid="3496151081459417097">"Erreproduzitu"</string> <string name="pip_pause" msgid="690688849510295232">"Pausatu"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"Joan hurrengora"</string> diff --git a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml index bce06da2c66f..07d75d2de9cd 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantaila txiki gainjarria"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa izengabea)"</string> - <string name="pip_close" msgid="9135220303720555525">"Itxi PIPa"</string> + <string name="pip_close" msgid="2955969519031223530">"Itxi"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantaila osoa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mugitu pantaila txiki gainjarria"</string> - <string name="pip_expand" msgid="7605396312689038178">"Zabaldu pantaila txiki gainjarria"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Tolestu pantaila txiki gainjarria"</string> + <string name="pip_move" msgid="158770205886688553">"Mugitu"</string> + <string name="pip_expand" msgid="1051966011679297308">"Zabaldu"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Tolestu"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Kontrolatzeko aukerak atzitzeko, sakatu birritan "<annotation icon="home_icon">" HASIERA "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pantaila txiki gainjarriaren menua."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Eraman ezkerrera"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Eraman eskuinera"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Eraman gora"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Eraman behera"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Eginda"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml index ff9a03c6cefb..03f51d01a3a8 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"تصویر در تصویر"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(برنامه بدون عنوان)"</string> - <string name="pip_close" msgid="9135220303720555525">"بستن PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"بستن"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"تمام صفحه"</string> - <string name="pip_move" msgid="1544227837964635439">"انتقال PIP (تصویر در تصویر)"</string> - <string name="pip_expand" msgid="7605396312689038178">"گسترده کردن «تصویر در تصویر»"</string> - <string name="pip_collapse" msgid="5732233773786896094">"جمع کردن «تصویر در تصویر»"</string> + <string name="pip_move" msgid="158770205886688553">"انتقال"</string> + <string name="pip_expand" msgid="1051966011679297308">"گسترده کردن"</string> + <string name="pip_collapse" msgid="3903295106641385962">"جمع کردن"</string> <string name="pip_edu_text" msgid="3672999496647508701">" برای کنترلها، دکمه "<annotation icon="home_icon">"صفحه اصلی"</annotation>" را دوبار فشار دهید"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"منوی تصویر در تصویر."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"انتقال بهچپ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"انتقال بهراست"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"انتقال بهبالا"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"انتقال بهپایین"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"تمام"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml index 3e8bf9032780..24ab7d99e180 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Kuva kuvassa"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nimetön)"</string> - <string name="pip_close" msgid="9135220303720555525">"Sulje PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Sulje"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Koko näyttö"</string> - <string name="pip_move" msgid="1544227837964635439">"Siirrä PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Laajenna PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Tiivistä PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Siirrä"</string> + <string name="pip_expand" msgid="1051966011679297308">"Laajenna"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Tiivistä"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Asetukset: paina "<annotation icon="home_icon">"ALOITUSNÄYTTÖPAINIKETTA"</annotation>" kahdesti"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kuva kuvassa ‑valikko."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Siirrä vasemmalle"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Siirrä oikealle"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Siirrä ylös"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Siirrä alas"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Valmis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml index 66e13b89c64b..87651ec711d9 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Incrustation d\'image"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Aucun programme de titre)"</string> - <string name="pip_close" msgid="9135220303720555525">"Fermer mode IDI"</string> + <string name="pip_close" msgid="2955969519031223530">"Fermer"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string> - <string name="pip_move" msgid="1544227837964635439">"Déplacer l\'image incrustée"</string> - <string name="pip_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_move" msgid="158770205886688553">"Déplacer"</string> + <string name="pip_expand" msgid="1051966011679297308">"Développer"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Appuyez deux fois sur "<annotation icon="home_icon">" ACCUEIL "</annotation>" pour les commandes"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu d\'incrustation d\'image."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Déplacer vers le haut"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Déplacer vers le bas"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index b3e22af0a3e3..07475055f03e 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -63,7 +63,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Fermer la bulle"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher la conversation dans une bulle"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatter en utilisant des bulles"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou de bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Contrôlez les bulles à tout moment"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Appuyez sur \"Gérer\" pour désactiver les bulles de cette application"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> diff --git a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml index ed9baf5b6215..37863fb82295 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programme sans titre)"</string> - <string name="pip_close" msgid="9135220303720555525">"Fermer mode PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Fermer"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string> - <string name="pip_move" msgid="1544227837964635439">"Déplacer le PIP"</string> - <string name="pip_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_move" msgid="158770205886688553">"Déplacer"</string> + <string name="pip_expand" msgid="1051966011679297308">"Développer"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Menu de commandes : appuyez deux fois sur "<annotation icon="home_icon">"ACCUEIL"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu \"Picture-in-picture\"."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Déplacer vers le haut"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Déplacer vers le bas"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml index a057434d7853..5d6de76c4deb 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla superposta"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sen título)"</string> - <string name="pip_close" msgid="9135220303720555525">"Pechar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Pechar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover pantalla superposta"</string> - <string name="pip_expand" msgid="7605396312689038178">"Despregar pantalla superposta"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Contraer pantalla superposta"</string> + <string name="pip_move" msgid="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Despregar"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Preme "<annotation icon="home_icon">"INICIO"</annotation>" dúas veces para acceder aos controis"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla superposta."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover cara á esquerda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover cara á dereita"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover cara arriba"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover cara abaixo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Feito"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml index d9525910e4c6..6c1b9db73582 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ચિત્રમાં-ચિત્ર"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(કોઈ ટાઇટલ પ્રોગ્રામ નથી)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP બંધ કરો"</string> + <string name="pip_close" msgid="2955969519031223530">"બંધ કરો"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"પૂર્ણ સ્ક્રીન"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ખસેડો"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP મોટી કરો"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP નાની કરો"</string> + <string name="pip_move" msgid="158770205886688553">"ખસેડો"</string> + <string name="pip_expand" msgid="1051966011679297308">"મોટું કરો"</string> + <string name="pip_collapse" msgid="3903295106641385962">"નાનું કરો"</string> <string name="pip_edu_text" msgid="3672999496647508701">" નિયંત્રણો માટે "<annotation icon="home_icon">" હોમ "</annotation>" બટન પર બે વાર દબાવો"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ચિત્રમાં ચિત્ર મેનૂ."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ડાબે ખસેડો"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"જમણે ખસેડો"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ઉપર ખસેડો"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"નીચે ખસેડો"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"થઈ ગયું"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 36b11514c7e5..a5fcb97d1418 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -65,9 +65,9 @@ <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल्स का इस्तेमाल करके चैट करें"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नई बातचीत फ़्लोटिंग आइकॉन या बबल्स की तरह दिखेंगी. बबल को खोलने के लिए टैप करें. इसे एक जगह से दूसरी जगह ले जाने के लिए खींचें और छोड़ें."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"जब चाहें, बबल्स को कंट्रोल करें"</string> - <string name="bubbles_user_education_manage" msgid="3460756219946517198">"इस ऐप्लिकेशन पर बबल्स को बंद करने के लिए \'प्रबंधित करें\' पर टैप करें"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"इस ऐप्लिकेशन पर बबल्स को बंद करने के लिए \'मैनेज करें\' पर टैप करें"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ठीक है"</string> - <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"हाल ही के बबल्स मौजूद नहीं हैं"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"हाल ही के कोई बबल्स नहीं हैं"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"हाल ही के बबल्स और हटाए गए बबल्स यहां दिखेंगे"</string> <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"मैनेज करें"</string> diff --git a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml index d897ac73f80d..e0227253b2dc 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"पिक्चर में पिक्चर"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(कोई शीर्षक कार्यक्रम नहीं)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP बंद करें"</string> + <string name="pip_close" msgid="2955969519031223530">"बंद करें"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फ़ुल स्क्रीन"</string> - <string name="pip_move" msgid="1544227837964635439">"पीआईपी को दूसरी जगह लेकर जाएं"</string> - <string name="pip_expand" msgid="7605396312689038178">"पीआईपी विंडो को बड़ा करें"</string> - <string name="pip_collapse" msgid="5732233773786896094">"पीआईपी विंडो को छोटा करें"</string> + <string name="pip_move" msgid="158770205886688553">"ले जाएं"</string> + <string name="pip_expand" msgid="1051966011679297308">"बड़ा करें"</string> + <string name="pip_collapse" msgid="3903295106641385962">"छोटा करें"</string> <string name="pip_edu_text" msgid="3672999496647508701">" कंट्रोल मेन्यू पर जाने के लिए, "<annotation icon="home_icon">" होम बटन"</annotation>" दो बार दबाएं"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"पिक्चर में पिक्चर मेन्यू."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बाईं ओर ले जाएं"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दाईं ओर ले जाएं"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ऊपर ले जाएं"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"नीचे ले जाएं"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"हो गया"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml index 8f5f3164c4d7..a09e6e805f63 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> - <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli zaslon"</string> - <string name="pip_move" msgid="1544227837964635439">"Premjesti PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Proširi PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Sažmi PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Premjesti"</string> + <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Sažmi"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">"POČETNI ZASLON"</annotation>" za kontrole"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izbornik slike u slici."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomaknite ulijevo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomaknite udesno"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomaknite prema gore"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomaknite prema dolje"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml index fc8d79589121..5e065c2ad4e7 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Kép a képben"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Cím nélküli program)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP bezárása"</string> + <string name="pip_close" msgid="2955969519031223530">"Bezárás"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Teljes képernyő"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP áthelyezése"</string> - <string name="pip_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_move" msgid="158770205886688553">"Áthelyezés"</string> + <string name="pip_expand" msgid="1051966011679297308">"Kibontás"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Összecsukás"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Vezérlők: "<annotation icon="home_icon">" KEZDŐKÉPERNYŐ "</annotation>" gomb kétszer megnyomva"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kép a képben menü."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mozgatás balra"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mozgatás jobbra"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mozgatás felfelé"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mozgatás lefelé"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Kész"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml index f5665b8dd166..7963abf8972b 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Նկար նկարի մեջ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Առանց վերնագրի ծրագիր)"</string> - <string name="pip_close" msgid="9135220303720555525">"Փակել PIP-ն"</string> + <string name="pip_close" msgid="2955969519031223530">"Փակել"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Լիէկրան"</string> - <string name="pip_move" msgid="1544227837964635439">"Տեղափոխել PIP-ը"</string> - <string name="pip_expand" msgid="7605396312689038178">"Ծավալել PIP-ը"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Ծալել PIP-ը"</string> + <string name="pip_move" msgid="158770205886688553">"Տեղափոխել"</string> + <string name="pip_expand" msgid="1051966011679297308">"Ծավալել"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Ծալել"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Կարգավորումների համար կրկնակի սեղմեք "<annotation icon="home_icon">"ԳԼԽԱՎՈՐ ԷԿՐԱՆ"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"«Նկար նկարի մեջ» ռեժիմի ընտրացանկ։"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Տեղափոխել ձախ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Տեղափոխել աջ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Տեղափոխել վերև"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Տեղափոխել ներքև"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Պատրաստ է"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings_tv.xml b/libs/WindowManager/Shell/res/values-in/strings_tv.xml index a1535653f679..7d37154bb86c 100644 --- a/libs/WindowManager/Shell/res/values-in/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-in/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tanpa judul)"</string> - <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Tutup"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Layar penuh"</string> - <string name="pip_move" msgid="1544227837964635439">"Pindahkan PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Luaskan PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Ciutkan PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Pindahkan"</string> + <string name="pip_expand" msgid="1051966011679297308">"Luaskan"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Ciutkan"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" HOME "</annotation>" untuk membuka kontrol"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture-in-Picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pindahkan ke kiri"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pindahkan ke kanan"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pindahkan ke atas"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pindahkan ke bawah"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Selesai"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings_tv.xml b/libs/WindowManager/Shell/res/values-is/strings_tv.xml index 70ca1afe3aea..1490cb98e034 100644 --- a/libs/WindowManager/Shell/res/values-is/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-is/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Mynd í mynd"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Efni án titils)"</string> - <string name="pip_close" msgid="9135220303720555525">"Loka mynd í mynd"</string> + <string name="pip_close" msgid="2955969519031223530">"Loka"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Allur skjárinn"</string> - <string name="pip_move" msgid="1544227837964635439">"Færa innfellda mynd"</string> - <string name="pip_expand" msgid="7605396312689038178">"Stækka innfellda mynd"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Minnka innfellda mynd"</string> + <string name="pip_move" msgid="158770205886688553">"Færa"</string> + <string name="pip_expand" msgid="1051966011679297308">"Stækka"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Minnka"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Ýttu tvisvar á "<annotation icon="home_icon">" HEIM "</annotation>" til að opna stillingar"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Valmynd fyrir mynd í mynd."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Færa til vinstri"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Færa til hægri"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Færa upp"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Færa niður"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Lokið"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings_tv.xml b/libs/WindowManager/Shell/res/values-it/strings_tv.xml index cda627517872..a48516f2588e 100644 --- a/libs/WindowManager/Shell/res/values-it/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-it/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture in picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma senza titolo)"</string> - <string name="pip_close" msgid="9135220303720555525">"Chiudi PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Chiudi"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Schermo intero"</string> - <string name="pip_move" msgid="1544227837964635439">"Sposta PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Espandi PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Comprimi PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Sposta"</string> + <string name="pip_expand" msgid="1051966011679297308">"Espandi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Comprimi"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Premi due volte "<annotation icon="home_icon">" HOME "</annotation>" per aprire i controlli"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture in picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sposta a sinistra"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sposta a destra"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Sposta su"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Sposta giù"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fine"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml index 30ce97b998ca..2af1896d3c67 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"תמונה בתוך תמונה"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(תוכנית ללא כותרת)"</string> - <string name="pip_close" msgid="9135220303720555525">"סגירת PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"סגירה"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"מסך מלא"</string> - <string name="pip_move" msgid="1544227837964635439">"העברת תמונה בתוך תמונה (PIP)"</string> - <string name="pip_expand" msgid="7605396312689038178">"הרחבת חלון תמונה-בתוך-תמונה"</string> - <string name="pip_collapse" msgid="5732233773786896094">"כיווץ של חלון תמונה-בתוך-תמונה"</string> + <string name="pip_move" msgid="158770205886688553">"העברה"</string> + <string name="pip_expand" msgid="1051966011679297308">"הרחבה"</string> + <string name="pip_collapse" msgid="3903295106641385962">"כיווץ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" לחיצה כפולה על "<annotation icon="home_icon">" הלחצן הראשי "</annotation>" תציג את אמצעי הבקרה"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"תפריט \'תמונה בתוך תמונה\'."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"הזזה שמאלה"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"הזזה ימינה"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"הזזה למעלה"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"הזזה למטה"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"סיום"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml index e58e7bf6fabc..bc7dcb7aa029 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ピクチャー イン ピクチャー"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無題の番組)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP を閉じる"</string> + <string name="pip_close" msgid="2955969519031223530">"閉じる"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全画面表示"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP を移動"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP を開く"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP を閉じる"</string> + <string name="pip_move" msgid="158770205886688553">"移動"</string> + <string name="pip_expand" msgid="1051966011679297308">"開く"</string> + <string name="pip_collapse" msgid="3903295106641385962">"閉じる"</string> <string name="pip_edu_text" msgid="3672999496647508701">" コントロールにアクセス: "<annotation icon="home_icon">" ホーム "</annotation>" を 2 回押します"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ピクチャー イン ピクチャーのメニューです。"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左に移動"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右に移動"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"上に移動"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"下に移動"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完了"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml index b09686646c8b..898dac2aca88 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ეკრანი ეკრანში"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(პროგრამის სათაურის გარეშე)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP-ის დახურვა"</string> + <string name="pip_close" msgid="2955969519031223530">"დახურვა"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"სრულ ეკრანზე"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP გადატანა"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP-ის გაშლა"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP-ის ჩაკეცვა"</string> + <string name="pip_move" msgid="158770205886688553">"გადაადგილება"</string> + <string name="pip_expand" msgid="1051966011679297308">"გაშლა"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ჩაკეცვა"</string> <string name="pip_edu_text" msgid="3672999496647508701">" მართვის საშუალებებზე წვდომისთვის ორმაგად დააჭირეთ "<annotation icon="home_icon">" მთავარ ღილაკს "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"მენიუ „ეკრანი ეკრანში“."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"მარცხნივ გადატანა"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"მარჯვნივ გადატანა"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ზემოთ გადატანა"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ქვემოთ გადატანა"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"მზადაა"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml index 7bade0dff0d9..cdf564fb4ca0 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Суреттегі сурет"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Атаусыз бағдарлама)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP жабу"</string> + <string name="pip_close" msgid="2955969519031223530">"Жабу"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Толық экран"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP клипін жылжыту"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP терезесін жаю"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP терезесін жию"</string> + <string name="pip_move" msgid="158770205886688553">"Жылжыту"</string> + <string name="pip_expand" msgid="1051966011679297308">"Жаю"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Жию"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Басқару элементтері: "<annotation icon="home_icon">" НЕГІЗГІ ЭКРАН "</annotation>" түймесін екі рет басыңыз."</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"Сурет ішіндегі сурет\" мәзірі."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солға жылжыту"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңға жылжыту"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Жоғары жылжыту"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Төмен жылжыту"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Дайын"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings_tv.xml b/libs/WindowManager/Shell/res/values-km/strings_tv.xml index 721be1fc1650..1a7ae813c1d3 100644 --- a/libs/WindowManager/Shell/res/values-km/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-km/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"រូបក្នុងរូប"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(កម្មវិធីគ្មានចំណងជើង)"</string> - <string name="pip_close" msgid="9135220303720555525">"បិទ PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"បិទ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ពេញអេក្រង់"</string> - <string name="pip_move" msgid="1544227837964635439">"ផ្លាស់ទី PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"ពង្រីក PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"បង្រួម PIP"</string> + <string name="pip_move" msgid="158770205886688553">"ផ្លាស់ទី"</string> + <string name="pip_expand" msgid="1051966011679297308">"ពង្រីក"</string> + <string name="pip_collapse" msgid="3903295106641385962">"បង្រួម"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ចុចពីរដងលើ"<annotation icon="home_icon">"ប៊ូតុងដើម"</annotation>" ដើម្បីបើកផ្ទាំងគ្រប់គ្រង"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ម៉ឺនុយរូបក្នុងរូប"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ផ្លាស់ទីទៅឆ្វេង"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ផ្លាស់ទីទៅស្តាំ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ផ្លាស់ទីឡើងលើ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ផ្លាស់ទីចុះក្រោម"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"រួចរាល់"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml index 8310c8a1169c..45de068c80a0 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ಶೀರ್ಷಿಕೆ ರಹಿತ ಕಾರ್ಯಕ್ರಮ)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP ಮುಚ್ಚಿ"</string> + <string name="pip_close" msgid="2955969519031223530">"ಮುಚ್ಚಿರಿ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ಪೂರ್ಣ ಪರದೆ"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ಅನ್ನು ಸರಿಸಿ"</string> - <string name="pip_expand" msgid="7605396312689038178">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವನ್ನು ವಿಸ್ತರಿಸಿ"</string> - <string name="pip_collapse" msgid="5732233773786896094">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವನ್ನು ಕುಗ್ಗಿಸಿ"</string> + <string name="pip_move" msgid="158770205886688553">"ಸರಿಸಿ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ವಿಸ್ತೃತಗೊಳಿಸಿ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ಕುಗ್ಗಿಸಿ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ಕಂಟ್ರೋಲ್ಗಳಿಗಾಗಿ "<annotation icon="home_icon">" ಹೋಮ್ "</annotation>" ಅನ್ನು ಎರಡು ಬಾರಿ ಒತ್ತಿ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ ಮೆನು."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ಎಡಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ಬಲಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ಮೇಲಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ಕೆಳಗೆ ಸರಿಸಿ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ಮುಗಿದಿದೆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml index a3e055a515a1..9e8f1f1258a5 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"PIP 모드"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(제목 없는 프로그램)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP 닫기"</string> + <string name="pip_close" msgid="2955969519031223530">"닫기"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"전체화면"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP 이동"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP 펼치기"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP 접기"</string> + <string name="pip_move" msgid="158770205886688553">"이동"</string> + <string name="pip_expand" msgid="1051966011679297308">"펼치기"</string> + <string name="pip_collapse" msgid="3903295106641385962">"접기"</string> <string name="pip_edu_text" msgid="3672999496647508701">" 제어 메뉴에 액세스하려면 "<annotation icon="home_icon">" 홈 "</annotation>"을 두 번 누르세요."</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"PIP 모드 메뉴입니다."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"왼쪽으로 이동"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"오른쪽으로 이동"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"위로 이동"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"아래로 이동"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"완료"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml index 887ac52c8e43..19fac5876bb0 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Сүрөттөгү сүрөт"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Аталышы жок программа)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP\'ти жабуу"</string> + <string name="pip_close" msgid="2955969519031223530">"Жабуу"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Толук экран"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP\'ти жылдыруу"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP\'ти жайып көрсөтүү"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP\'ти жыйыштыруу"</string> + <string name="pip_move" msgid="158770205886688553">"Жылдыруу"</string> + <string name="pip_expand" msgid="1051966011679297308">"Жайып көрсөтүү"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Жыйыштыруу"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Башкаруу элементтерин ачуу үчүн "<annotation icon="home_icon">" БАШКЫ БЕТ "</annotation>" баскычын эки жолу басыңыз"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Сүрөт ичиндеги сүрөт менюсу."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солго жылдыруу"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңго жылдыруу"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Жогору жылдыруу"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Төмөн жылдыруу"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Бүттү"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml index 91c4a033356d..6cd0f37c516c 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ການສະແດງຜົນຊ້ອນກັນ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ໂປຣແກຣມບໍ່ມີຊື່)"</string> - <string name="pip_close" msgid="9135220303720555525">"ປິດ PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"ປິດ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ເຕັມໜ້າຈໍ"</string> - <string name="pip_move" msgid="1544227837964635439">"ຍ້າຍ PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"ຂະຫຍາຍ PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"ຫຍໍ້ PIP ລົງ"</string> + <string name="pip_move" msgid="158770205886688553">"ຍ້າຍ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ຂະຫຍາຍ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ຫຍໍ້"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ກົດ "<annotation icon="home_icon">" HOME "</annotation>" ສອງເທື່ອສຳລັບການຄວບຄຸມ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ເມນູການສະແດງຜົນຊ້ອນກັນ."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ຍ້າຍໄປຊ້າຍ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ຍ້າຍໄປຂວາ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ຍ້າຍຂຶ້ນ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ຍ້າຍລົງ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ແລ້ວໆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml index 04265ca01b48..52017dca2b94 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Vaizdas vaizde"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa be pavadinimo)"</string> - <string name="pip_close" msgid="9135220303720555525">"Uždaryti PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Uždaryti"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Visas ekranas"</string> - <string name="pip_move" msgid="1544227837964635439">"Perkelti PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Iškleisti PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Sutraukti PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Perkelti"</string> + <string name="pip_expand" msgid="1051966011679297308">"Išskleisti"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Sutraukti"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Jei reikia valdiklių, dukart paspauskite "<annotation icon="home_icon">"PAGRINDINIS"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Vaizdo vaizde meniu."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Perkelti kairėn"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Perkelti dešinėn"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Perkelti aukštyn"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Perkelti žemyn"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Atlikta"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml index 8c6191e00833..11abac6f6197 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Attēls attēlā"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma bez nosaukuma)"</string> - <string name="pip_close" msgid="9135220303720555525">"Aizvērt PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Aizvērt"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pilnekrāna režīms"</string> - <string name="pip_move" msgid="1544227837964635439">"Pārvietot attēlu attēlā"</string> - <string name="pip_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_move" msgid="158770205886688553">"Pārvietot"</string> + <string name="pip_expand" msgid="1051966011679297308">"Izvērst"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Sakļaut"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Atvērt vadīklas: divreiz nospiediet pogu "<annotation icon="home_icon">"SĀKUMS"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izvēlne attēlam attēlā."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pārvietot pa kreisi"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pārvietot pa labi"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pārvietot augšup"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pārvietot lejup"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gatavs"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml index beef1fef862b..21293223b882 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Слика во слика"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без наслов)"</string> - <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Затвори"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цел екран"</string> - <string name="pip_move" msgid="1544227837964635439">"Премести PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Прошири ја сликата во слика"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Собери ја сликата во слика"</string> + <string name="pip_move" msgid="158770205886688553">"Премести"</string> + <string name="pip_expand" msgid="1051966011679297308">"Прошири"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Собери"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Притиснете двапати на "<annotation icon="home_icon">" HOME "</annotation>" за контроли"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени за „Слика во слика“."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Премести налево"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Премести надесно"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Премести нагоре"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Премести надолу"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml index c2a532d09647..549e39b21101 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ചിത്രത്തിനുള്ളിൽ ചിത്രം"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(പേരില്ലാത്ത പ്രോഗ്രാം)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP അടയ്ക്കുക"</string> + <string name="pip_close" msgid="2955969519031223530">"അടയ്ക്കുക"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"പൂര്ണ്ണ സ്ക്രീന്"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP നീക്കുക"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP വികസിപ്പിക്കുക"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP ചുരുക്കുക"</string> + <string name="pip_move" msgid="158770205886688553">"നീക്കുക"</string> + <string name="pip_expand" msgid="1051966011679297308">"വികസിപ്പിക്കുക"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ചുരുക്കുക"</string> <string name="pip_edu_text" msgid="3672999496647508701">" നിയന്ത്രണങ്ങൾക്കായി "<annotation icon="home_icon">" ഹോം "</annotation>" രണ്ട് തവണ അമർത്തുക"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ചിത്രത്തിനുള്ളിൽ ചിത്രം മെനു."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ഇടത്തേക്ക് നീക്കുക"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"വലത്തേക്ക് നീക്കുക"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"മുകളിലേക്ക് നീക്കുക"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"താഴേക്ക് നീക്കുക"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"പൂർത്തിയായി"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml index bf8c59b57359..9a85d96ca602 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Дэлгэц доторх дэлгэц"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Гарчиггүй хөтөлбөр)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP-г хаах"</string> + <string name="pip_close" msgid="2955969519031223530">"Хаах"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Бүтэн дэлгэц"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP-г зөөх"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP-г дэлгэх"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP-г хураах"</string> + <string name="pip_move" msgid="158770205886688553">"Зөөх"</string> + <string name="pip_expand" msgid="1051966011679297308">"Дэлгэх"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Хураах"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Хяналтад хандах бол "<annotation icon="home_icon">" HOME "</annotation>" дээр хоёр дарна уу"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Дэлгэцэн доторх дэлгэцийн цэс."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Зүүн тийш зөөх"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Баруун тийш зөөх"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Дээш зөөх"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Доош зөөх"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Болсон"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml index 5d519b7afe9a..a9779b3a3e89 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"चित्रात-चित्र"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षक नसलेला कार्यक्रम)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP बंद करा"</string> + <string name="pip_close" msgid="2955969519031223530">"बंद करा"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रीन"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP हलवा"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP चा विस्तार करा"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP कोलॅप्स करा"</string> + <string name="pip_move" msgid="158770205886688553">"हलवा"</string> + <string name="pip_expand" msgid="1051966011679297308">"विस्तार करा"</string> + <string name="pip_collapse" msgid="3903295106641385962">"कोलॅप्स करा"</string> <string name="pip_edu_text" msgid="3672999496647508701">" नियंत्रणांसाठी "<annotation icon="home_icon">" होम "</annotation>" दोनदा दाबा"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"चित्रात-चित्र मेनू."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"डावीकडे हलवा"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"उजवीकडे हलवा"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"वर हलवा"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"खाली हलवा"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"पूर्ण झाले"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml index 08642c47c91a..8fe992d9f3b9 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Gambar dalam Gambar"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tiada tajuk)"</string> - <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Tutup"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Skrin penuh"</string> - <string name="pip_move" msgid="1544227837964635439">"Alihkan PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Kembangkan PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Kuncupkan PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Alih"</string> + <string name="pip_expand" msgid="1051966011679297308">"Kembangkan"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Kuncupkan"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" LAMAN UTAMA "</annotation>" untuk mengakses kawalan"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Gambar dalam Gambar."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Alih ke kiri"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Alih ke kanan"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Alih ke atas"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Alih ke bawah"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Selesai"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings_tv.xml b/libs/WindowManager/Shell/res/values-my/strings_tv.xml index e01daee115ca..105628d8149e 100644 --- a/libs/WindowManager/Shell/res/values-my/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"နှစ်ခုထပ်၍ကြည့်ခြင်း"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ခေါင်းစဉ်မဲ့ အစီအစဉ်)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP ကိုပိတ်ပါ"</string> + <string name="pip_close" msgid="2955969519031223530">"ပိတ်ရန်"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"မျက်နှာပြင် အပြည့်"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ရွှေ့ရန်"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP ကို ချဲ့ရန်"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP ကို လျှော့ပြပါ"</string> + <string name="pip_move" msgid="158770205886688553">"ရွှေ့ရန်"</string> + <string name="pip_expand" msgid="1051966011679297308">"ချဲ့ရန်"</string> + <string name="pip_collapse" msgid="3903295106641385962">"လျှော့ပြရန်"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ထိန်းချုပ်မှုအတွက် "<annotation icon="home_icon">" ပင်မခလုတ် "</annotation>" နှစ်ချက်နှိပ်ပါ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"နှစ်ခုထပ်၍ ကြည့်ခြင်းမီနူး။"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ဘယ်သို့ရွှေ့ရန်"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ညာသို့ရွှေ့ရန်"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"အပေါ်သို့ရွှေ့ရန်"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"အောက်သို့ရွှေ့ရန်"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ပြီးပြီ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 9fd42b2f129c..2f2fea6eb833 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -63,7 +63,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Lukk boblen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ikke vis samtaler i bobler"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat med bobler"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som flytende ikoner eller bobler. Trykk for å åpne bobler. Dra for å flytte dem."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som flytende ikoner eller bobler. Trykk for å åpne en boble. Dra for å flytte den."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrollér bobler når som helst"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Trykk på Administrer for å slå av bobler for denne appen"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Greit"</string> diff --git a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml index 65ed0b7f5bff..ca63518df7a5 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bilde-i-bilde"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uten tittel)"</string> - <string name="pip_close" msgid="9135220303720555525">"Lukk PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Lukk"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Fullskjerm"</string> - <string name="pip_move" msgid="1544227837964635439">"Flytt BIB"</string> - <string name="pip_expand" msgid="7605396312689038178">"Vis BIB"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Skjul BIB"</string> + <string name="pip_move" msgid="158770205886688553">"Flytt"</string> + <string name="pip_expand" msgid="1051966011679297308">"Vis"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dobbelttrykk på "<annotation icon="home_icon">"HJEM"</annotation>" for å åpne kontroller"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bilde-i-bilde-meny."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytt til venstre"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytt til høyre"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flytt opp"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flytt ned"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Ferdig"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml index d33fed67efb6..7cbf9e294e7b 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षकविहीन कार्यक्रम)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP लाई बन्द गर्नुहोस्"</string> + <string name="pip_close" msgid="2955969519031223530">"बन्द गर्नुहोस्"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रिन"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP सार्नुहोस्"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP विन्डो एक्स्पान्ड गर्नु…"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP विन्डो कोल्याप्स गर्नुहोस्"</string> + <string name="pip_move" msgid="158770205886688553">"सार्नुहोस्"</string> + <string name="pip_expand" msgid="1051966011679297308">"एक्स्पान्ड गर्नुहोस्"</string> + <string name="pip_collapse" msgid="3903295106641385962">"कोल्याप्स गर्नुहोस्"</string> <string name="pip_edu_text" msgid="3672999496647508701">" कन्ट्रोल मेनु खोल्न "<annotation icon="home_icon">" होम "</annotation>" बटन दुई पटक थिच्नुहोस्"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"picture-in-picture\" मेनु।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बायाँतिर सार्नुहोस्"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दायाँतिर सार्नुहोस्"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"माथितिर सार्नुहोस्"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"तलतिर सार्नुहोस्"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"सम्पन्न भयो"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml index 9763c5665ab2..2deaeddc4080 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Scherm-in-scherm"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Naamloos programma)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP sluiten"</string> + <string name="pip_close" msgid="2955969519031223530">"Sluiten"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Volledig scherm"</string> - <string name="pip_move" msgid="1544227837964635439">"SIS verplaatsen"</string> - <string name="pip_expand" msgid="7605396312689038178">"SIS uitvouwen"</string> - <string name="pip_collapse" msgid="5732233773786896094">"SIS samenvouwen"</string> + <string name="pip_move" msgid="158770205886688553">"Verplaatsen"</string> + <string name="pip_expand" msgid="1051966011679297308">"Uitvouwen"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Samenvouwen"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Druk twee keer op "<annotation icon="home_icon">" HOME "</annotation>" voor bedieningselementen"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Scherm-in-scherm-menu."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Naar links verplaatsen"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Naar rechts verplaatsen"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Omhoog verplaatsen"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Omlaag verplaatsen"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klaar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings_tv.xml b/libs/WindowManager/Shell/res/values-or/strings_tv.xml index e0344855bd1f..0c1d99e4ca71 100644 --- a/libs/WindowManager/Shell/res/values-or/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-or/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ପିକଚର୍-ଇନ୍-ପିକଚର୍"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(କୌଣସି ଟାଇଟଲ୍ ପ୍ରୋଗ୍ରାମ୍ ନାହିଁ)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP ବନ୍ଦ କରନ୍ତୁ"</string> + <string name="pip_close" msgid="2955969519031223530">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍"</string> - <string name="pip_move" msgid="1544227837964635439">"PIPକୁ ମୁଭ କରନ୍ତୁ"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIPକୁ ବିସ୍ତାର କରନ୍ତୁ"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIPକୁ ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> + <string name="pip_move" msgid="158770205886688553">"ମୁଭ କରନ୍ତୁ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ବିସ୍ତାର କରନ୍ତୁ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ ପାଇଁ "<annotation icon="home_icon">" ହୋମ ବଟନ "</annotation>"କୁ ଦୁଇଥର ଦବାନ୍ତୁ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ପିକଚର-ଇନ-ପିକଚର ମେନୁ।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ବାମକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ଉପରକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ତଳକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ହୋଇଗଲା"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml index 9c01ac3f3cc0..a1edde738775 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ਸਿਰਲੇਖ-ਰਹਿਤ ਪ੍ਰੋਗਰਾਮ)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP ਬੰਦ ਕਰੋ"</string> + <string name="pip_close" msgid="2955969519031223530">"ਬੰਦ ਕਰੋ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ਨੂੰ ਲਿਜਾਓ"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP ਦਾ ਵਿਸਤਾਰ ਕਰੋ"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP ਨੂੰ ਸਮੇਟੋ"</string> + <string name="pip_move" msgid="158770205886688553">"ਲਿਜਾਓ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ਵਿਸਤਾਰ ਕਰੋ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ਸਮੇਟੋ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ਕੰਟਰੋਲਾਂ ਲਈ "<annotation icon="home_icon">" ਹੋਮ ਬਟਨ "</annotation>" ਨੂੰ ਦੋ ਵਾਰ ਦਬਾਓ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ ਮੀਨੂ।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ਖੱਬੇ ਲਿਜਾਓ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ਸੱਜੇ ਲਿਜਾਓ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ਉੱਪਰ ਲਿਜਾਓ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ਹੇਠਾਂ ਲਿਜਾਓ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ਹੋ ਗਿਆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml index b922e2d5a6ba..2bb90addc241 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz w obrazie"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez tytułu)"</string> - <string name="pip_close" msgid="9135220303720555525">"Zamknij PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zamknij"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pełny ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"Przenieś PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Rozwiń PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Zwiń PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Przenieś"</string> + <string name="pip_expand" msgid="1051966011679297308">"Rozwiń"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Zwiń"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Naciśnij dwukrotnie "<annotation icon="home_icon">"EKRAN GŁÓWNY"</annotation>", aby wyświetlić ustawienia"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu funkcji Obraz w obrazie."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Przenieś w lewo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Przenieś w prawo"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Przenieś w górę"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Przenieś w dół"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotowe"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml index cc4eb3c32c1f..14d1c34fd3e8 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(programa sem título)"</string> - <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</string> - <string name="pip_expand" msgid="7605396312689038178">"Abrir picture-in-picture"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Fechar picture-in-picture"</string> + <string name="pip_move" msgid="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Abrir"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml index c4ae78d89ba8..1ada4508714a 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Ecrã no ecrã"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Sem título do programa)"</string> - <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ecrã inteiro"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover Ecrã no ecrã"</string> - <string name="pip_expand" msgid="7605396312689038178">"Expandir Ecrã no ecrã"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Reduzir Ecrã no ecrã"</string> + <string name="pip_move" msgid="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expandir"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Reduzir"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Prima duas vezes "<annotation icon="home_icon">" PÁGINA INICIAL "</annotation>" para controlos"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu de ecrã no ecrã."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml index cc4eb3c32c1f..14d1c34fd3e8 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(programa sem título)"</string> - <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</string> - <string name="pip_expand" msgid="7605396312689038178">"Abrir picture-in-picture"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Fechar picture-in-picture"</string> + <string name="pip_move" msgid="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Abrir"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml index 86a30f49df15..56dadb2e5e65 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program fără titlu)"</string> - <string name="pip_close" msgid="9135220303720555525">"Închideți PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Închideți"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ecran complet"</string> - <string name="pip_move" msgid="1544227837964635439">"Mutați fereastra PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Extindeți fereastra PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Restrângeți fereastra PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Mutați"</string> + <string name="pip_expand" msgid="1051966011679297308">"Extindeți"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Restrângeți"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Apăsați de două ori "<annotation icon="home_icon">"butonul ecran de pornire"</annotation>" pentru comenzi"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meniu picture-in-picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mutați spre stânga"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mutați spre dreapta"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mutați în sus"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mutați în jos"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gata"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml index 08623e1e69c5..e7f55ec1bc57 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картинка в картинке"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Без названия)"</string> - <string name="pip_close" msgid="9135220303720555525">"\"Кадр в кадре\" – выйти"</string> + <string name="pip_close" msgid="2955969519031223530">"Закрыть"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Во весь экран"</string> - <string name="pip_move" msgid="1544227837964635439">"Переместить PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Развернуть PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Свернуть PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Переместить"</string> + <string name="pip_expand" msgid="1051966011679297308">"Развернуть"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Свернуть"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Элементы управления: дважды нажмите "<annotation icon="home_icon">" кнопку главного экрана "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"Картинка в картинке\"."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Переместить влево"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Переместить вправо"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Переместить вверх"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Переместить вниз"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings_tv.xml b/libs/WindowManager/Shell/res/values-si/strings_tv.xml index fbb0ebba0623..5478ce5d3d40 100644 --- a/libs/WindowManager/Shell/res/values-si/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-si/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"පින්තූරය-තුළ-පින්තූරය"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(මාතෘකාවක් නැති වැඩසටහන)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP වසන්න"</string> + <string name="pip_close" msgid="2955969519031223530">"වසන්න"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"සම්පූර්ණ තිරය"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ගෙන යන්න"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP දිග හරින්න"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP හකුළන්න"</string> + <string name="pip_move" msgid="158770205886688553">"ගෙන යන්න"</string> + <string name="pip_expand" msgid="1051966011679297308">"දිග හරින්න"</string> + <string name="pip_collapse" msgid="3903295106641385962">"හකුළන්න"</string> <string name="pip_edu_text" msgid="3672999496647508701">" පාලන සඳහා "<annotation icon="home_icon">" මුල් පිටුව "</annotation>" දෙවරක් ඔබන්න"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"පින්තූරය තුළ පින්තූරය මෙනුව"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"වමට ගෙන යන්න"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"දකුණට ගෙන යන්න"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ඉහළට ගෙන යන්න"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"පහළට ගෙන යන්න"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"නිමයි"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml index 81cb0eafc759..1df43afca2da 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz v obraze"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez názvu)"</string> - <string name="pip_close" msgid="9135220303720555525">"Zavrieť režim PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zavrieť"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> - <string name="pip_move" msgid="1544227837964635439">"Presunúť obraz v obraze"</string> - <string name="pip_expand" msgid="7605396312689038178">"Rozbaliť obraz v obraze"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Zbaliť obraz v obraze"</string> + <string name="pip_move" msgid="158770205886688553">"Presunúť"</string> + <string name="pip_expand" msgid="1051966011679297308">"Rozbaliť"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Zbaliť"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Ovládanie zobraz. dvoj. stlač. "<annotation icon="home_icon">" TLAČIDLA PLOCHY "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Ponuka obrazu v obraze."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Posunúť doľava"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Posunúť doprava"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Posunúť nahor"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Posunúť nadol"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml index 060aaa0ce647..88fc8325aa01 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika v sliki"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program brez naslova)"</string> - <string name="pip_close" msgid="9135220303720555525">"Zapri način PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zapri"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celozaslonsko"</string> - <string name="pip_move" msgid="1544227837964635439">"Premakni sliko v sliki"</string> - <string name="pip_expand" msgid="7605396312689038178">"Razširi sliko v sliki"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Strni sliko v sliki"</string> + <string name="pip_move" msgid="158770205886688553">"Premakni"</string> + <string name="pip_expand" msgid="1051966011679297308">"Razširi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Strni"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Za kontrolnike dvakrat pritisnite gumb za "<annotation icon="home_icon">" ZAČETNI ZASLON "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za sliko v sliki"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Premakni levo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Premakni desno"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Premakni navzgor"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Premakni navzdol"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Končano"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml index 9bfdb6a3edd8..58687e5867fe 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Figurë brenda figurës"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program pa titull)"</string> - <string name="pip_close" msgid="9135220303720555525">"Mbyll PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Mbyll"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ekrani i plotë"</string> - <string name="pip_move" msgid="1544227837964635439">"Zhvendos PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Zgjero PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Palos PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Lëviz"</string> + <string name="pip_expand" msgid="1051966011679297308">"Zgjero"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Palos"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Trokit dy herë "<annotation icon="home_icon">" KREU "</annotation>" për kontrollet"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyja e \"Figurës brenda figurës\"."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Lëviz majtas"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Lëviz djathtas"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Lëviz lart"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Lëviz poshtë"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"U krye"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml index 6bc5c87bab48..e850979174a3 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Слика у слици"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програм без наслова)"</string> - <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Затвори"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цео екран"</string> - <string name="pip_move" msgid="1544227837964635439">"Премести слику у слици"</string> - <string name="pip_expand" msgid="7605396312689038178">"Прошири слику у слици"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Скупи слику у слици"</string> + <string name="pip_move" msgid="158770205886688553">"Премести"</string> + <string name="pip_expand" msgid="1051966011679297308">"Прошири"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Скупи"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Двапут притисните "<annotation icon="home_icon">" HOME "</annotation>" за контроле"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени Слика у слици."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Померите налево"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Померите надесно"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Померите нагоре"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Померите надоле"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml index b3465ab1db85..d3a9c3de66db 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bild-i-bild"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Namnlöst program)"</string> - <string name="pip_close" msgid="9135220303720555525">"Stäng PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Stäng"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Helskärm"</string> - <string name="pip_move" msgid="1544227837964635439">"Flytta BIB"</string> - <string name="pip_expand" msgid="7605396312689038178">"Utöka bild-i-bild"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Komprimera bild-i-bild"</string> + <string name="pip_move" msgid="158770205886688553">"Flytta"</string> + <string name="pip_expand" msgid="1051966011679297308">"Utöka"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Komprimera"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Tryck snabbt två gånger på "<annotation icon="home_icon">" HEM "</annotation>" för kontroller"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bild-i-bild-meny."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytta åt vänster"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytta åt höger"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flytta uppåt"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flytta nedåt"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml index baff49ed821a..7b9a310ff0b6 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pachika Picha Ndani ya Picha Nyingine"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programu isiyo na jina)"</string> - <string name="pip_close" msgid="9135220303720555525">"Funga PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Funga"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Skrini nzima"</string> - <string name="pip_move" msgid="1544227837964635439">"Kuhamisha PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Panua PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Kunja PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Hamisha"</string> + <string name="pip_expand" msgid="1051966011679297308">"Panua"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Kunja"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Bonyeza mara mbili kitufe cha "<annotation icon="home_icon">" UKURASA WA KWANZA "</annotation>" kupata vidhibiti"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyu ya kipengele cha kupachika picha ndani ya picha nyingine."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sogeza kushoto"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sogeza kulia"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Sogeza juu"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Sogeza chini"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Imemaliza"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml index 4439e299c919..e201401e2e35 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"பிக்ச்சர்-இன்-பிக்ச்சர்"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(தலைப்பு இல்லை)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIPஐ மூடு"</string> + <string name="pip_close" msgid="2955969519031223530">"மூடுக"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"முழுத்திரை"</string> - <string name="pip_move" msgid="1544227837964635439">"PIPபை நகர்த்து"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIPபை விரிவாக்கு"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIPபைச் சுருக்கு"</string> + <string name="pip_move" msgid="158770205886688553">"நகர்த்து"</string> + <string name="pip_expand" msgid="1051966011679297308">"விரி"</string> + <string name="pip_collapse" msgid="3903295106641385962">"சுருக்கு"</string> <string name="pip_edu_text" msgid="3672999496647508701">" கட்டுப்பாடுகள்: "<annotation icon="home_icon">" முகப்பு "</annotation>" பட்டனை இருமுறை அழுத்துக"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"பிக்ச்சர்-இன்-பிக்ச்சர் மெனு."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"இடப்புறம் நகர்த்து"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"வலப்புறம் நகர்த்து"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"மேலே நகர்த்து"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"கீழே நகர்த்து"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"முடிந்தது"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings_tv.xml b/libs/WindowManager/Shell/res/values-te/strings_tv.xml index 35579346615f..6284d90cb11f 100644 --- a/libs/WindowManager/Shell/res/values-te/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-te/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"పిక్చర్-ఇన్-పిక్చర్"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(శీర్షిక లేని ప్రోగ్రామ్)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIPని మూసివేయి"</string> + <string name="pip_close" msgid="2955969519031223530">"మూసివేయండి"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ఫుల్-స్క్రీన్"</string> - <string name="pip_move" msgid="1544227837964635439">"PIPను తరలించండి"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIPని విస్తరించండి"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIPని కుదించండి"</string> + <string name="pip_move" msgid="158770205886688553">"తరలించండి"</string> + <string name="pip_expand" msgid="1051966011679297308">"విస్తరించండి"</string> + <string name="pip_collapse" msgid="3903295106641385962">"కుదించండి"</string> <string name="pip_edu_text" msgid="3672999496647508701">" కంట్రోల్స్ కోసం "<annotation icon="home_icon">" HOME "</annotation>" బటన్ రెండుసార్లు నొక్కండి"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"పిక్చర్-ఇన్-పిక్చర్ మెనూ."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ఎడమ వైపుగా జరపండి"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"కుడి వైపుగా జరపండి"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"పైకి జరపండి"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"కిందికి జరపండి"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"పూర్తయింది"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings_tv.xml b/libs/WindowManager/Shell/res/values-th/strings_tv.xml index 0a07d157ec6f..27cf56c6e154 100644 --- a/libs/WindowManager/Shell/res/values-th/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-th/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"การแสดงภาพซ้อนภาพ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ไม่มีชื่อรายการ)"</string> - <string name="pip_close" msgid="9135220303720555525">"ปิด PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"ปิด"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"เต็มหน้าจอ"</string> - <string name="pip_move" msgid="1544227837964635439">"ย้าย PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"ขยาย PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"ยุบ PIP"</string> + <string name="pip_move" msgid="158770205886688553">"ย้าย"</string> + <string name="pip_expand" msgid="1051966011679297308">"ขยาย"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ยุบ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" กดปุ่ม "<annotation icon="home_icon">" หน้าแรก "</annotation>" สองครั้งเพื่อเปิดการควบคุม"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"เมนูการแสดงภาพซ้อนภาพ"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ย้ายไปทางซ้าย"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ย้ายไปทางขวา"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ย้ายขึ้น"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ย้ายลง"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"เสร็จ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml index 9a11a38fa492..4cc050bebe5b 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Walang pamagat na programa)"</string> - <string name="pip_close" msgid="9135220303720555525">"Isara ang PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Isara"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Ilipat ang PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"I-expand ang PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"I-collapse ang PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Ilipat"</string> + <string name="pip_expand" msgid="1051966011679297308">"I-expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"I-collapse"</string> <string name="pip_edu_text" msgid="3672999496647508701">" I-double press ang "<annotation icon="home_icon">" HOME "</annotation>" para sa mga kontrol"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu ng Picture-in-Picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Ilipat pakaliwa"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Ilipat pakanan"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Itaas"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Ibaba"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Tapos na"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml index bf4bc6f1fff7..69bb608061e4 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pencere İçinde Pencere"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Başlıksız program)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP\'yi kapat"</string> + <string name="pip_close" msgid="2955969519031223530">"Kapat"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP\'yi taşı"</string> - <string name="pip_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> + <string name="pip_move" msgid="158770205886688553">"Taşı"</string> + <string name="pip_expand" msgid="1051966011679297308">"Genişlet"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Daralt"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Kontroller için "<annotation icon="home_icon">" ANA SAYFA "</annotation>" düğmesine iki kez basın"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pencere içinde pencere menüsü."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola taşı"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa taşı"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Yukarı taşı"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Aşağı taşı"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Bitti"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml index 7e9f54e68f54..81a8285c58cf 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картинка в картинці"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без назви)"</string> - <string name="pip_close" msgid="9135220303720555525">"Закрити PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Закрити"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"На весь екран"</string> - <string name="pip_move" msgid="1544227837964635439">"Перемістити картинку в картинці"</string> - <string name="pip_expand" msgid="7605396312689038178">"Розгорнути картинку в картинці"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Згорнути картинку в картинці"</string> + <string name="pip_move" msgid="158770205886688553">"Перемістити"</string> + <string name="pip_expand" msgid="1051966011679297308">"Розгорнути"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Згорнути"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Відкрити елементи керування: двічі натисніть "<annotation icon="home_icon">"HOME"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"картинка в картинці\""</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перемістити ліворуч"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перемістити праворуч"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Перемістити вгору"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Перемістити вниз"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml index c2ef69ff1488..e83885772f2d 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"تصویر میں تصویر"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(بلا عنوان پروگرام)"</string> - <string name="pip_close" msgid="9135220303720555525">"PIP بند کریں"</string> + <string name="pip_close" msgid="2955969519031223530">"بند کریں"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"فُل اسکرین"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP کو منتقل کریں"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP کو پھیلائیں"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP کو سکیڑیں"</string> + <string name="pip_move" msgid="158770205886688553">"منتقل کریں"</string> + <string name="pip_expand" msgid="1051966011679297308">"پھیلائیں"</string> + <string name="pip_collapse" msgid="3903295106641385962">"سکیڑیں"</string> <string name="pip_edu_text" msgid="3672999496647508701">" کنٹرولز کے لیے "<annotation icon="home_icon">"ہوم "</annotation>" بٹن کو دو بار دبائیں"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"تصویر میں تصویر کا مینو۔"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"دائیں منتقل کریں"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"بائیں منتقل کریں"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"اوپر منتقل کریں"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"نیچے منتقل کریں"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ہو گیا"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml index 9ab95c80aa25..da953356628c 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Tasvir ustida tasvir"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nomsiz)"</string> - <string name="pip_close" msgid="9135220303720555525">"Kadr ichida kadr – chiqish"</string> + <string name="pip_close" msgid="2955969519031223530">"Yopish"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Butun ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"PIPni siljitish"</string> - <string name="pip_expand" msgid="7605396312689038178">"PIP funksiyasini yoyish"</string> - <string name="pip_collapse" msgid="5732233773786896094">"PIP funksiyasini yopish"</string> + <string name="pip_move" msgid="158770205886688553">"Boshqa joyga olish"</string> + <string name="pip_expand" msgid="1051966011679297308">"Yoyish"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Yopish"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Boshqaruv uchun "<annotation icon="home_icon">"ASOSIY"</annotation>" tugmani ikki marta bosing"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Tasvir ustida tasvir menyusi."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Chapga olish"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Oʻngga olish"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Tepaga olish"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pastga olish"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Tayyor"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml index 146376d3cab6..1f9260fdcff0 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Hình trong hình"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Không có chương trình tiêu đề)"</string> - <string name="pip_close" msgid="9135220303720555525">"Đóng PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Đóng"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Toàn màn hình"</string> - <string name="pip_move" msgid="1544227837964635439">"Di chuyển PIP (Ảnh trong ảnh)"</string> - <string name="pip_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_move" msgid="158770205886688553">"Di chuyển"</string> + <string name="pip_expand" msgid="1051966011679297308">"Mở rộng"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Thu gọn"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Nhấn đúp vào nút "<annotation icon="home_icon">" MÀN HÌNH CHÍNH "</annotation>" để mở trình đơn điều khiển"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Trình đơn hình trong hình."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Di chuyển sang trái"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Di chuyển sang phải"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Di chuyển lên"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Di chuyển xuống"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Xong"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml index 55407d2c699d..399d639fe70f 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"画中画"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(节目没有标题)"</string> - <string name="pip_close" msgid="9135220303720555525">"关闭画中画"</string> + <string name="pip_close" msgid="2955969519031223530">"关闭"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全屏"</string> - <string name="pip_move" msgid="1544227837964635439">"移动画中画窗口"</string> - <string name="pip_expand" msgid="7605396312689038178">"展开 PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"收起 PIP"</string> + <string name="pip_move" msgid="158770205886688553">"移动"</string> + <string name="pip_expand" msgid="1051966011679297308">"展开"</string> + <string name="pip_collapse" msgid="3903295106641385962">"收起"</string> <string name="pip_edu_text" msgid="3672999496647508701">" 按两次"<annotation icon="home_icon">"主屏幕"</annotation>"按钮可查看相关控件"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"画中画菜单。"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左移"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右移"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"上移"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"下移"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml index 15e278d8ecc2..acbc26d033cd 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"畫中畫"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(沒有標題的節目)"</string> - <string name="pip_close" msgid="9135220303720555525">"關閉 PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"關閉"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> - <string name="pip_move" msgid="1544227837964635439">"移動畫中畫"</string> - <string name="pip_expand" msgid="7605396312689038178">"展開畫中畫"</string> - <string name="pip_collapse" msgid="5732233773786896094">"收合畫中畫"</string> + <string name="pip_move" msgid="158770205886688553">"移動"</string> + <string name="pip_expand" msgid="1051966011679297308">"展開"</string> + <string name="pip_collapse" msgid="3903295106641385962">"收合"</string> <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">" 主畫面按鈕"</annotation>"即可顯示控制項"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"畫中畫選單。"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"向上移"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"向下移"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml index 0b17b31d23d0..f8c683ec3a60 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"子母畫面"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無標題的節目)"</string> - <string name="pip_close" msgid="9135220303720555525">"關閉子母畫面"</string> + <string name="pip_close" msgid="2955969519031223530">"關閉"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> - <string name="pip_move" msgid="1544227837964635439">"移動子母畫面"</string> - <string name="pip_expand" msgid="7605396312689038178">"展開子母畫面"</string> - <string name="pip_collapse" msgid="5732233773786896094">"收合子母畫面"</string> + <string name="pip_move" msgid="158770205886688553">"移動"</string> + <string name="pip_expand" msgid="1051966011679297308">"展開"</string> + <string name="pip_collapse" msgid="3903295106641385962">"收合"</string> <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">"主畫面按鈕"</annotation>"即可顯示控制選項"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"子母畫面選單。"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移動"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移動"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"向上移動"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"向下移動"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml index dad8c8128222..20243a9dfc9c 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml @@ -19,10 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Isithombe-esithombeni"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Alukho uhlelo lwesihloko)"</string> - <string name="pip_close" msgid="9135220303720555525">"Vala i-PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Vala"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Iskrini esigcwele"</string> - <string name="pip_move" msgid="1544227837964635439">"Hambisa i-PIP"</string> - <string name="pip_expand" msgid="7605396312689038178">"Nweba i-PIP"</string> - <string name="pip_collapse" msgid="5732233773786896094">"Goqa i-PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Hambisa"</string> + <string name="pip_expand" msgid="1051966011679297308">"Nweba"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Goqa"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Chofoza kabili "<annotation icon="home_icon">" IKHAYA"</annotation>" mayelana nezilawuli"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Imenyu yesithombe-esithombeni"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Yisa kwesokunxele"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Yisa kwesokudla"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Khuphula"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Yehlisa"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Kwenziwe"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 8ba41ab60c87..f03b7f66cdc8 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -100,4 +100,7 @@ <!-- The default gravity for the picture-in-picture window. Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT --> <integer name="config_defaultPictureInPictureGravity">0x55</integer> + + <!-- Whether to dim a split-screen task when the other is the IME target --> + <bool name="config_dimNonImeAttachedSide">true</bool> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml index 09ed9b8e52ee..2b7a13eac6ca 100644 --- a/libs/WindowManager/Shell/res/values/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -26,23 +26,38 @@ <!-- Picture-in-Picture (PIP) menu --> <eat-comment /> <!-- Button to close picture-in-picture (PIP) in PIP menu [CHAR LIMIT=30] --> - <string name="pip_close">Close PIP</string> + <string name="pip_close">Close</string> <!-- Button to move picture-in-picture (PIP) screen to the fullscreen in PIP menu [CHAR LIMIT=30] --> <string name="pip_fullscreen">Full screen</string> <!-- Button to move picture-in-picture (PIP) via DPAD in the PIP menu [CHAR LIMIT=30] --> - <string name="pip_move">Move PIP</string> + <string name="pip_move">Move</string> <!-- Button to expand the picture-in-picture (PIP) window [CHAR LIMIT=30] --> - <string name="pip_expand">Expand PIP</string> + <string name="pip_expand">Expand</string> <!-- Button to collapse/shrink the picture-in-picture (PIP) window [CHAR LIMIT=30] --> - <string name="pip_collapse">Collapse PIP</string> + <string name="pip_collapse">Collapse</string> <!-- Educative text instructing the user to double press the HOME button to access the pip controls menu [CHAR LIMIT=50] --> <string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for controls </string> + + <!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] --> + <string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string> + + <!-- Accessibility action: move the PiP window to the left [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_left">Move left</string> + <!-- Accessibility action: move the PiP window to the right [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_right">Move right</string> + <!-- Accessibility action: move the PiP window up [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_up">Move up</string> + <!-- Accessibility action: move the PiP window down [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_down">Move down</string> + <!-- Accessibility action: done with moving the PiP [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_done">Done</string> + </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index bf074b0337ef..9230c22c5d95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -145,6 +145,8 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { } mDisplayAreasInfo.remove(displayId); + mLeashes.get(displayId).release(); + mLeashes.remove(displayId); ArrayList<RootTaskDisplayAreaListener> listeners = mListeners.get(displayId); if (listeners != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index ced36a705df2..7760df17a8cd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -24,12 +24,18 @@ import android.annotation.Nullable; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.app.WindowConfiguration; +import android.content.ContentResolver; import android.content.Context; +import android.database.ContentObserver; import android.graphics.Point; import android.graphics.PointF; import android.hardware.HardwareBuffer; +import android.net.Uri; +import android.os.Handler; import android.os.RemoteException; import android.os.SystemProperties; +import android.os.UserHandle; +import android.provider.Settings.Global; import android.util.Log; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; @@ -42,22 +48,31 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import java.util.concurrent.atomic.AtomicBoolean; + /** * Controls the window animation run when a user initiates a back gesture. */ public class BackAnimationController implements RemoteCallable<BackAnimationController> { private static final String TAG = "BackAnimationController"; + private static final int SETTING_VALUE_OFF = 0; + private static final int SETTING_VALUE_ON = 1; private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = "persist.wm.debug.predictive_back_progress_threshold"; public static final boolean IS_ENABLED = - SystemProperties.getInt("persist.wm.debug.predictive_back", 1) != 0; + SystemProperties.getInt("persist.wm.debug.predictive_back", + SETTING_VALUE_ON) != SETTING_VALUE_OFF; private static final int PROGRESS_THRESHOLD = SystemProperties .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); - @VisibleForTesting - boolean mEnableAnimations = SystemProperties.getInt( - "persist.wm.debug.predictive_back_anim", 0) != 0; + private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); + /** + * Max duration to wait for a transition to finish before accepting another gesture start + * request. + */ + private static final long MAX_TRANSITION_DURATION = 2000; /** * Location of the initial touch event of the back gesture. @@ -73,6 +88,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont /** True when a back gesture is ongoing */ private boolean mBackGestureStarted = false; + /** Tracks if an uninterruptible transition is in progress */ + private boolean mTransitionInProgress = false; /** @see #setTriggerBack(boolean) */ private boolean mTriggerBack; @@ -85,23 +102,56 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private IOnBackInvokedCallback mBackToLauncherCallback; private float mTriggerThreshold; private float mProgressThreshold; + private final Runnable mResetTransitionRunnable = () -> { + finishAnimation(); + mTransitionInProgress = false; + }; public BackAnimationController( - @ShellMainThread ShellExecutor shellExecutor, + @NonNull @ShellMainThread ShellExecutor shellExecutor, + @NonNull @ShellBackgroundThread Handler backgroundHandler, Context context) { - this(shellExecutor, new SurfaceControl.Transaction(), ActivityTaskManager.getService(), - context); + this(shellExecutor, backgroundHandler, new SurfaceControl.Transaction(), + ActivityTaskManager.getService(), context, context.getContentResolver()); } @VisibleForTesting - BackAnimationController(@NonNull ShellExecutor shellExecutor, + BackAnimationController(@NonNull @ShellMainThread ShellExecutor shellExecutor, + @NonNull @ShellBackgroundThread Handler handler, @NonNull SurfaceControl.Transaction transaction, @NonNull IActivityTaskManager activityTaskManager, - Context context) { + Context context, ContentResolver contentResolver) { mShellExecutor = shellExecutor; mTransaction = transaction; mActivityTaskManager = activityTaskManager; mContext = context; + setupAnimationDeveloperSettingsObserver(contentResolver, handler); + } + + private void setupAnimationDeveloperSettingsObserver( + @NonNull ContentResolver contentResolver, + @NonNull @ShellBackgroundThread final Handler backgroundHandler) { + ContentObserver settingsObserver = new ContentObserver(backgroundHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + updateEnableAnimationFromSetting(); + } + }; + contentResolver.registerContentObserver( + Global.getUriFor(Global.ENABLE_BACK_ANIMATION), + false, settingsObserver, UserHandle.USER_SYSTEM + ); + updateEnableAnimationFromSetting(); + } + + @ShellBackgroundThread + private void updateEnableAnimationFromSetting() { + int settingValue = Global.getInt(mContext.getContentResolver(), + Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF); + boolean isEnabled = settingValue == SETTING_VALUE_ON; + mEnableAnimations.set(isEnabled); + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", + isEnabled); } public BackAnimation getBackAnimationImpl() { @@ -189,7 +239,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mBackToLauncherCallback = null; } - private void onBackToLauncherAnimationFinished() { + @VisibleForTesting + void onBackToLauncherAnimationFinished() { if (mBackNavigationInfo != null) { IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); if (mTriggerBack) { @@ -206,9 +257,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * {@link BackAnimationController} */ public void onMotionEvent(MotionEvent event, int action, @BackEvent.SwipeEdge int swipeEdge) { - if (action == MotionEvent.ACTION_DOWN) { - initAnimation(event); - } else if (action == MotionEvent.ACTION_MOVE) { + if (mTransitionInProgress) { + return; + } + if (action == MotionEvent.ACTION_MOVE) { + if (!mBackGestureStarted) { + // Let the animation initialized here to make sure the onPointerDownOutsideFocus + // could be happened when ACTION_DOWN, it may change the current focus that we + // would access it when startBackNavigation. + initAnimation(event); + } onMove(event, swipeEdge); } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, @@ -228,7 +286,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mBackGestureStarted = true; try { - mBackNavigationInfo = mActivityTaskManager.startBackNavigation(); + boolean requestAnimation = mEnableAnimations.get(); + mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation); onBackNavigationInfoReceived(mBackNavigationInfo); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); @@ -326,6 +385,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont IOnBackInvokedCallback targetCallback = shouldDispatchToLauncher ? mBackToLauncherCallback : mBackNavigationInfo.getOnBackInvokedCallback(); + if (shouldDispatchToLauncher) { + startTransition(); + } if (mTriggerBack) { dispatchOnBackInvoked(targetCallback); } else { @@ -340,12 +402,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private boolean shouldDispatchToLauncher(int backType) { return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME && mBackToLauncherCallback != null - && mEnableAnimations; - } - - @VisibleForTesting - void setEnableAnimations(boolean shouldEnable) { - mEnableAnimations = shouldEnable; + && mEnableAnimations.get(); } private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { @@ -397,6 +454,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ public void setTriggerBack(boolean triggerBack) { + if (mTransitionInProgress) { + return; + } mTriggerBack = triggerBack; } @@ -428,6 +488,23 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTransaction.remove(screenshotSurface); } mTransaction.apply(); + stopTransition(); backNavigationInfo.onBackNavigationFinished(triggerBack); } + + private void startTransition() { + if (mTransitionInProgress) { + return; + } + mTransitionInProgress = true; + mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION); + } + + private void stopTransition() { + if (!mTransitionInProgress) { + return; + } + mShellExecutor.removeCallbacks(mResetTransitionRunnable); + mTransitionInProgress = false; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index 3876533a922e..f1ee8fa38485 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -19,6 +19,7 @@ import android.annotation.DrawableRes; import android.annotation.Nullable; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Path; @@ -182,7 +183,7 @@ public class BadgedImageView extends ConstraintLayout { getDrawingRect(mTempBounds); - mDrawParams.color = mDotColor; + mDrawParams.dotColor = mDotColor; mDrawParams.iconBounds = mTempBounds; mDrawParams.leftAlign = mOnLeft; mDrawParams.scale = mDotScale; @@ -350,16 +351,19 @@ public class BadgedImageView extends ConstraintLayout { } void showBadge() { - if (mBubble.getAppBadge() == null) { + Bitmap appBadgeBitmap = mBubble.getAppBadge(); + if (appBadgeBitmap == null) { mAppIcon.setVisibility(GONE); return; } + int translationX; if (mOnLeft) { - translationX = -(mBubbleIcon.getWidth() - mAppIcon.getWidth()); + translationX = -(mBubble.getBubbleIcon().getWidth() - appBadgeBitmap.getWidth()); } else { translationX = 0; } + mAppIcon.setTranslationX(translationX); mAppIcon.setVisibility(VISIBLE); } 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 227494c04049..31fc6a5be589 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -71,7 +71,7 @@ public class Bubble implements BubbleViewProvider { private long mLastAccessed; @Nullable - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; /** Whether the bubble should show a dot for the notification indicating updated content. */ private boolean mShowBubbleUpdateDot = true; @@ -192,13 +192,13 @@ public class Bubble implements BubbleViewProvider { @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, - @Nullable final Bubbles.SuppressionChangedListener listener, + @Nullable final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor) { mKey = entry.getKey(); mGroupKey = entry.getGroupKey(); mLocusId = entry.getLocusId(); - mSuppressionListener = listener; + mBubbleMetadataFlagListener = listener; mIntentCancelListener = intent -> { if (mIntent != null) { mIntent.unregisterCancelListener(mIntentCancelListener); @@ -606,8 +606,8 @@ public class Bubble implements BubbleViewProvider { mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; } - if (showInShade() != prevShowInShade && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); + if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); } } @@ -626,8 +626,8 @@ public class Bubble implements BubbleViewProvider { } else { mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; } - if (prevSuppressed != suppressBubble && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); + if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); } } @@ -771,12 +771,17 @@ public class Bubble implements BubbleViewProvider { return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } - void setShouldAutoExpand(boolean shouldAutoExpand) { + @VisibleForTesting + public void setShouldAutoExpand(boolean shouldAutoExpand) { + boolean prevAutoExpand = shouldAutoExpand(); if (shouldAutoExpand) { enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } else { disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } + if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); + } } public void setIsBubble(final boolean isBubble) { @@ -799,6 +804,10 @@ public class Bubble implements BubbleViewProvider { return (mFlags & option) != 0; } + public int getFlags() { + return mFlags; + } + @Override public String toString() { return "Bubble{" + mKey + '}'; 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 806c395bf395..f427a2c4bc95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -66,6 +66,7 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.os.UserManager; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; import android.util.ArraySet; @@ -96,6 +97,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.common.annotations.ShellBackgroundThread; +import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; @@ -145,6 +148,7 @@ public class BubbleController { private final FloatingContentCoordinator mFloatingContentCoordinator; private final BubbleDataRepository mDataRepository; private final WindowManagerShellWrapper mWindowManagerShellWrapper; + private final UserManager mUserManager; private final LauncherApps mLauncherApps; private final IStatusBarService mBarService; private final WindowManager mWindowManager; @@ -158,6 +162,8 @@ public class BubbleController { private final ShellExecutor mMainExecutor; private final Handler mMainHandler; + private final ShellExecutor mBackgroundExecutor; + private BubbleLogger mLogger; private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; @@ -227,6 +233,7 @@ public class BubbleController { @Nullable IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, + UserManager userManager, LauncherApps launcherApps, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, @@ -234,8 +241,9 @@ public class BubbleController { DisplayController displayController, Optional<OneHandedController> oneHandedOptional, DragAndDropController dragAndDropController, - ShellExecutor mainExecutor, - Handler mainHandler, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler, + @ShellBackgroundThread ShellExecutor bgExecutor, TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { BubbleLogger logger = new BubbleLogger(uiEventLogger); @@ -243,9 +251,9 @@ public class BubbleController { BubbleData data = new BubbleData(context, logger, positioner, mainExecutor); return new BubbleController(context, data, synchronizer, floatingContentCoordinator, new BubbleDataRepository(context, launcherApps, mainExecutor), - statusBarService, windowManager, windowManagerShellWrapper, launcherApps, - logger, taskStackListener, organizer, positioner, displayController, - oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, + statusBarService, windowManager, windowManagerShellWrapper, userManager, + launcherApps, logger, taskStackListener, organizer, positioner, displayController, + oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor, taskViewTransitions, syncQueue); } @@ -261,6 +269,7 @@ public class BubbleController { @Nullable IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, + UserManager userManager, LauncherApps launcherApps, BubbleLogger bubbleLogger, TaskStackListenerImpl taskStackListener, @@ -269,8 +278,9 @@ public class BubbleController { DisplayController displayController, Optional<OneHandedController> oneHandedOptional, DragAndDropController dragAndDropController, - ShellExecutor mainExecutor, - Handler mainHandler, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler, + @ShellBackgroundThread ShellExecutor bgExecutor, TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { mContext = context; @@ -281,11 +291,13 @@ public class BubbleController { : statusBarService; mWindowManager = windowManager; mWindowManagerShellWrapper = windowManagerShellWrapper; + mUserManager = userManager; mFloatingContentCoordinator = floatingContentCoordinator; mDataRepository = dataRepository; mLogger = bubbleLogger; mMainExecutor = mainExecutor; mMainHandler = mainHandler; + mBackgroundExecutor = bgExecutor; mTaskStackListener = taskStackListener; mTaskOrganizer = organizer; mSurfaceSynchronizer = synchronizer; @@ -323,7 +335,7 @@ public class BubbleController { public void initialize() { mBubbleData.setListener(mBubbleDataListener); - mBubbleData.setSuppressionChangedListener(this::onBubbleNotificationSuppressionChanged); + mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mBubbleData.setPendingIntentCancelledListener(bubble -> { if (bubble.getBubbleIntent() == null) { @@ -440,6 +452,10 @@ public class BubbleController { mOneHandedOptional.ifPresent(this::registerOneHandedState); mDragAndDropController.addListener(this::collapseStack); + + // Clear out any persisted bubbles on disk that no longer have a valid user. + List<UserInfo> users = mUserManager.getAliveUsers(); + mDataRepository.sanitizeBubbles(users); } @VisibleForTesting @@ -554,11 +570,10 @@ public class BubbleController { } @VisibleForTesting - public void onBubbleNotificationSuppressionChanged(Bubble bubble) { + public void onBubbleMetadataFlagChanged(Bubble bubble) { // Make sure NoMan knows suppression state so that anyone querying it can tell. try { - mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(), - !bubble.showInShade(), bubble.isSuppressed()); + mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags()); } catch (RemoteException e) { // Bad things have happened } @@ -584,6 +599,17 @@ public class BubbleController { mCurrentProfiles = currentProfiles; } + /** Called when a user is removed from the device, including work profiles. */ + public void onUserRemoved(int removedUserId) { + UserInfo parent = mUserManager.getProfileParent(removedUserId); + int parentUserId = parent != null ? parent.getUserHandle().getIdentifier() : -1; + mBubbleData.removeBubblesForUser(removedUserId); + // Typically calls from BubbleData would remove bubbles from the DataRepository as well, + // however, this gets complicated when users are removed (mCurrentUserId won't necessarily + // be correct for this) so we update the repo directly. + mDataRepository.removeBubblesForUser(removedUserId, parentUserId); + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL @@ -726,7 +752,8 @@ public class BubbleController { try { mAddedToWindowManager = false; - mContext.unregisterReceiver(mBroadcastReceiver); + // Put on background for this binder call, was causing jank + mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver)); if (mStackView != null) { mWindowManager.removeView(mStackView); mBubbleData.getOverflow().cleanUpExpandedState(); @@ -1038,7 +1065,15 @@ public class BubbleController { } } else { Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); - inflateAndAdd(bubble, suppressFlyout, showInShade); + if (notif.shouldSuppressNotificationList()) { + // If we're suppressing notifs for DND, we don't want the bubbles to randomly + // expand when DND turns off so flip the flag. + if (bubble.shouldAutoExpand()) { + bubble.setShouldAutoExpand(false); + } + } else { + inflateAndAdd(bubble, suppressFlyout, showInShade); + } } } @@ -1070,7 +1105,8 @@ public class BubbleController { } } - private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + @VisibleForTesting + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { // shouldBubbleUp checks canBubble & for bubble metadata boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry); if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { @@ -1096,7 +1132,8 @@ public class BubbleController { } } - private void onRankingUpdated(RankingMap rankingMap, + @VisibleForTesting + public void onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) { if (mTmpRanking == null) { mTmpRanking = new NotificationListenerService.Ranking(); @@ -1107,19 +1144,22 @@ public class BubbleController { Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key); BubbleEntry entry = entryData.first; boolean shouldBubbleUp = entryData.second; - if (entry != null && !isCurrentProfile( entry.getStatusBarNotification().getUser().getIdentifier())) { return; } - + if (entry != null && (entry.shouldSuppressNotificationList() + || entry.getRanking().isSuspended())) { + shouldBubbleUp = false; + } rankingMap.getRanking(key, mTmpRanking); - boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); - if (isActiveBubble && !mTmpRanking.canBubble()) { + boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key); + boolean isActive = mBubbleData.hasBubbleInStackWithKey(key); + if (isActiveOrInOverflow && !mTmpRanking.canBubble()) { // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. // This means that the app or channel's ability to bubble has been revoked. mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED); - } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) { + } else if (isActiveOrInOverflow && !shouldBubbleUp) { // If this entry is allowed to bubble, but cannot currently bubble up or is // suspended, dismiss it. This happens when DND is enabled and configured to hide // bubbles, or focus mode is enabled and the app is designated as distracting. @@ -1127,9 +1167,9 @@ public class BubbleController { // notification, so that the bubble will be re-created if shouldBubbleUp returns // true. mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP); - } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { + } else if (entry != null && mTmpRanking.isBubble() && !isActive) { entry.setFlagBubble(true); - onEntryUpdated(entry, shouldBubbleUp && !entry.getRanking().isSuspended()); + onEntryUpdated(entry, shouldBubbleUp); } } } @@ -1789,6 +1829,13 @@ public class BubbleController { } @Override + public void onUserRemoved(int removedUserId) { + mMainExecutor.execute(() -> { + BubbleController.this.onUserRemoved(removedUserId); + }); + } + + @Override public void onConfigChanged(Configuration newConfig) { mMainExecutor.execute(() -> { BubbleController.this.onConfigChanged(newConfig); 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 c98c0e69de15..fa86c8436647 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -159,7 +159,7 @@ public class BubbleData { private Listener mListener; @Nullable - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; private Bubbles.PendingIntentCanceledListener mCancelledListener; /** @@ -190,9 +190,8 @@ public class BubbleData { mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); } - public void setSuppressionChangedListener( - Bubbles.SuppressionChangedListener listener) { - mSuppressionListener = listener; + public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { + mBubbleMetadataFlagListener = listener; } public void setPendingIntentCancelledListener( @@ -311,7 +310,7 @@ public class BubbleData { bubbleToReturn = mPendingBubbles.get(key); } else if (entry != null) { // New bubble - bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener, + bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener, mMainExecutor); } else { // Persisted bubble being promoted @@ -466,7 +465,7 @@ public class BubbleData { getOverflowBubbles(), invalidBubblesFromPackage, removeBubble); } - /** Dismisses all bubbles from the given package. */ + /** Removes all bubbles from the given package. */ public void removeBubblesWithPackageName(String packageName, int reason) { final Predicate<Bubble> bubbleMatchesPackage = bubble -> bubble.getPackageName().equals(packageName); @@ -478,6 +477,18 @@ public class BubbleData { performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble); } + /** Removes all bubbles for the given user. */ + public void removeBubblesForUser(int userId) { + List<Bubble> removedBubbles = filterAllBubbles(bubble -> + userId == bubble.getUser().getIdentifier()); + for (Bubble b : removedBubbles) { + doRemove(b.getKey(), Bubbles.DISMISS_USER_REMOVED); + } + if (!removedBubbles.isEmpty()) { + dispatchPendingChanges(); + } + } + private void doAdd(Bubble bubble) { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "doAdd: " + bubble); @@ -553,7 +564,8 @@ public class BubbleData { || reason == Bubbles.DISMISS_BLOCKED || reason == Bubbles.DISMISS_SHORTCUT_REMOVED || reason == Bubbles.DISMISS_PACKAGE_REMOVED - || reason == Bubbles.DISMISS_USER_CHANGED; + || reason == Bubbles.DISMISS_USER_CHANGED + || reason == Bubbles.DISMISS_USER_REMOVED; int indexToRemove = indexForKey(key); if (indexToRemove == -1) { @@ -1058,6 +1070,51 @@ public class BubbleData { return null; } + /** + * Get a pending bubble with given notification <code>key</code> + * + * @param key notification key + * @return bubble that matches or null + */ + @VisibleForTesting(visibility = PRIVATE) + public Bubble getPendingBubbleWithKey(String key) { + for (Bubble b : mPendingBubbles.values()) { + if (b.getKey().equals(key)) { + return b; + } + } + return null; + } + + /** + * Returns a list of bubbles that match the provided predicate. This checks all types of + * bubbles (i.e. pending, suppressed, active, and overflowed). + */ + private List<Bubble> filterAllBubbles(Predicate<Bubble> predicate) { + ArrayList<Bubble> matchingBubbles = new ArrayList<>(); + for (Bubble b : mPendingBubbles.values()) { + if (predicate.test(b)) { + matchingBubbles.add(b); + } + } + for (Bubble b : mSuppressedBubbles.values()) { + if (predicate.test(b)) { + matchingBubbles.add(b); + } + } + for (Bubble b : mBubbles) { + if (predicate.test(b)) { + matchingBubbles.add(b); + } + } + for (Bubble b : mOverflowBubbles) { + if (predicate.test(b)) { + matchingBubbles.add(b); + } + } + return matchingBubbles; + } + @VisibleForTesting(visibility = PRIVATE) void setTimeSource(TimeSource timeSource) { mTimeSource = timeSource; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt index 9d9e442affd3..97560f44fb06 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt @@ -22,6 +22,7 @@ import android.content.pm.LauncherApps import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER +import android.content.pm.UserInfo import android.os.UserHandle import android.util.Log import com.android.wm.shell.bubbles.storage.BubbleEntity @@ -73,6 +74,22 @@ internal class BubbleDataRepository( if (entities.isNotEmpty()) persistToDisk() } + /** + * Removes all the bubbles associated with the provided user from memory. Then persists the + * snapshot to disk asynchronously. + */ + fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentId: Int) { + if (volatileRepository.removeBubblesForUser(userId, parentId)) persistToDisk() + } + + /** + * Remove any bubbles that don't have a user id from the provided list of users. + */ + fun sanitizeBubbles(users: List<UserInfo>) { + val userIds = users.map { u -> u.id } + if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk() + } + private fun transform(bubbles: List<Bubble>): List<BubbleEntity> { return bubbles.mapNotNull { b -> BubbleEntity( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index a089585a5a00..b8bf1a8e497e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -706,6 +706,8 @@ public class BubbleExpandedView extends LinearLayout { * @param animate whether the pointer should animate to this position. */ public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) { + final boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() + == LAYOUT_DIRECTION_RTL; // Pointer gets drawn in the padding final boolean showVertically = mPositioner.showBubblesVertically(); final float paddingLeft = (showVertically && onLeft) @@ -732,12 +734,22 @@ public class BubbleExpandedView extends LinearLayout { float pointerX; if (showVertically) { pointerY = bubbleCenter - (mPointerWidth / 2f); - pointerX = onLeft - ? -mPointerHeight + mPointerOverlap - : getWidth() - mPaddingRight - mPointerOverlap; + if (!isRtl) { + pointerX = onLeft + ? -mPointerHeight + mPointerOverlap + : getWidth() - mPaddingRight - mPointerOverlap; + } else { + pointerX = onLeft + ? -(getWidth() - mPaddingLeft - mPointerOverlap) + : mPointerHeight - mPointerOverlap; + } } else { pointerY = mPointerOverlap; - pointerX = bubbleCenter - (mPointerWidth / 2f); + if (!isRtl) { + pointerX = bubbleCenter - (mPointerWidth / 2f); + } else { + pointerX = -(getWidth() - mPaddingLeft - bubbleCenter) + (mPointerWidth / 2f); + } } if (animate) { mPointerView.animate().translationX(pointerX).translationY(pointerY).start(); 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 7cfacbcc92f8..e9729e45731b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -88,6 +88,9 @@ public class BubblePositioner { private int mMaxBubbles; private int mBubbleSize; private int mSpacingBetweenBubbles; + private int mBubblePaddingTop; + private int mBubbleOffscreenAmount; + private int mStackOffset; private int mExpandedViewMinHeight; private int mExpandedViewLargeScreenWidth; @@ -187,6 +190,10 @@ 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); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); + mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + if (mIsSmallTablet) { mExpandedViewLargeScreenWidth = (int) (bounds.width() * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); @@ -329,6 +336,21 @@ public class BubblePositioner { : mBubbleSize; } + /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */ + public int getBubblePaddingTop() { + return mBubblePaddingTop; + } + + /** The amount the stack hang off of the screen when collapsed. */ + public int getStackOffScreenAmount() { + return mBubbleOffscreenAmount; + } + + /** Offset of bubbles in the stack (i.e. how much they overlap). */ + public int getStackOffset() { + return mStackOffset; + } + /** Size of the visible (non-overlapping) part of the pointer. */ public int getPointerSize() { return mPointerHeight - mPointerOverlap; @@ -678,7 +700,28 @@ public class BubblePositioner { return new BubbleStackView.RelativeStackPosition( startOnLeft, startingVerticalOffset / mPositionRect.height()) - .getAbsolutePositionInRegion(new RectF(mPositionRect)); + .getAbsolutePositionInRegion(getAllowableStackPositionRegion( + 1 /* default starts with 1 bubble */)); + } + + + /** + * Returns the region that the stack position must stay within. This goes slightly off the left + * and right sides of the screen, below the status bar/cutout and above the navigation bar. + * While the stack position is not allowed to rest outside of these bounds, it can temporarily + * be animated or dragged beyond them. + */ + public RectF getAllowableStackPositionRegion(int bubbleCount) { + final RectF allowableRegion = new RectF(getAvailableRect()); + final int imeHeight = getImeHeight(); + final float bottomPadding = bubbleCount > 1 + ? mBubblePaddingTop + mStackOffset + : mBubblePaddingTop; + allowableRegion.left -= mBubbleOffscreenAmount; + allowableRegion.top += mBubblePaddingTop; + allowableRegion.right += mBubbleOffscreenAmount - mBubbleSize; + allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize; + return allowableRegion; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index b7c5eb06fbfa..0e8dc63943a6 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 @@ -1296,7 +1296,7 @@ public class BubbleStackView extends FrameLayout public void onOrientationChanged() { mRelativeStackPositionBeforeRotation = new RelativeStackPosition( mPositioner.getRestingPosition(), - mStackAnimationController.getAllowableStackPositionRegion()); + mPositioner.getAllowableStackPositionRegion(getBubbleCount())); addOnLayoutChangeListener(mOrientationChangedListener); hideFlyoutImmediate(); } @@ -1340,7 +1340,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.setStackPosition( new RelativeStackPosition( mPositioner.getRestingPosition(), - mStackAnimationController.getAllowableStackPositionRegion())); + mPositioner.getAllowableStackPositionRegion(getBubbleCount()))); } if (mIsExpanded) { updateExpandedView(); @@ -1440,7 +1440,7 @@ public class BubbleStackView extends FrameLayout if (super.performAccessibilityActionInternal(action, arguments)) { return true; } - final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion(); + final RectF stackBounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); // R constants are not final so we cannot use switch-case here. if (action == AccessibilityNodeInfo.ACTION_DISMISS) { 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 2b2a2f7e35df..8a0db0a12711 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -57,7 +57,7 @@ public interface Bubbles { DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, - DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK}) + DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @@ -76,6 +76,7 @@ public interface Bubbles { int DISMISS_PACKAGE_REMOVED = 13; int DISMISS_NO_BUBBLE_UP = 14; int DISMISS_RELOAD_FROM_DISK = 15; + int DISMISS_USER_REMOVED = 16; /** * @return {@code true} if there is a bubble associated with the provided key and if its @@ -243,6 +244,13 @@ public interface Bubbles { void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles); /** + * Called when a user is removed. + * + * @param removedUserId the id of the removed user. + */ + void onUserRemoved(int removedUserId); + + /** * Called when config changed. * * @param newConfig the new config. @@ -263,10 +271,10 @@ public interface Bubbles { void onBubbleExpandChanged(boolean isExpanding, String key); } - /** Listener to be notified when the flags for notification or bubble suppression changes.*/ - interface SuppressionChangedListener { - /** Called when the notification suppression state of a bubble changes. */ - void onBubbleNotificationSuppressionChange(Bubble bubble); + /** Listener to be notified when the flags on BubbleMetadata have changed. */ + interface BubbleMetadataFlagListener { + /** Called when the flags on BubbleMetadata have changed for the provided bubble. */ + void onBubbleMetadataFlagChanged(Bubble bubble); } /** Listener to be notified when a pending intent has been canceled for a bubble. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt index c09d1e0d189c..e95e8e5cdaea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt @@ -108,8 +108,16 @@ class ManageEducationView constructor(context: Context, positioner: BubblePositi alpha = 0f visibility = View.VISIBLE expandedView.getManageButtonBoundsOnScreen(realManageButtonRect) - manageView.setPadding(realManageButtonRect.left - expandedView.manageButtonMargin, - manageView.paddingTop, manageView.paddingRight, manageView.paddingBottom) + val isRTL = mContext.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL + if (isRTL) { + val rightPadding = positioner.screenRect.right - realManageButtonRect.right - + expandedView.manageButtonMargin + manageView.setPadding(manageView.paddingLeft, manageView.paddingTop, + rightPadding, manageView.paddingBottom) + } else { + manageView.setPadding(realManageButtonRect.left - expandedView.manageButtonMargin, + manageView.paddingTop, manageView.paddingRight, manageView.paddingBottom) + } post { manageButton .setOnClickListener { @@ -122,7 +130,11 @@ class ManageEducationView constructor(context: Context, positioner: BubblePositi val offsetViewBounds = Rect() manageButton.getDrawingRect(offsetViewBounds) manageView.offsetDescendantRectToMyCoords(manageButton, offsetViewBounds) - translationX = 0f + if (isRTL && (positioner.isLargeScreen || positioner.isLandscape)) { + translationX = (positioner.screenRect.right - width).toFloat() + } else { + translationX = 0f + } translationY = (realManageButtonRect.top - offsetViewBounds.top).toFloat() bringToFront() animate() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt index 1ff4be887fb2..627273f093f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt @@ -146,6 +146,12 @@ class StackEducationView constructor( } else { setPadding(paddingLeft, paddingTop, positioner.bubbleSize + stackPadding, paddingBottom) + if (positioner.isLargeScreen || positioner.isLandscape) { + translationX = (positioner.screenRect.right - width - stackPadding) + .toFloat() + } else { + translationX = 0f + } } translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2 } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 04af60dd7a03..0a1b4d70fb2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -185,8 +185,6 @@ public class StackAnimationController extends * stack goes offscreen intentionally. */ private int mBubblePaddingTop; - /** How far offscreen the stack rests. */ - private int mBubbleOffscreen; /** Contains display size, orientation, and inset information. */ private BubblePositioner mPositioner; @@ -212,7 +210,8 @@ public class StackAnimationController extends public Rect getAllowedFloatingBoundsRegion() { final Rect floatingBounds = getFloatingBoundsOnScreen(); final Rect allowableStackArea = new Rect(); - getAllowableStackPositionRegion().roundOut(allowableStackArea); + mPositioner.getAllowableStackPositionRegion(getBubbleCount()) + .roundOut(allowableStackArea); allowableStackArea.right += floatingBounds.width(); allowableStackArea.bottom += floatingBounds.height(); return allowableStackArea; @@ -349,7 +348,7 @@ public class StackAnimationController extends ? velX < ESCAPE_VELOCITY : velX < -ESCAPE_VELOCITY; - final RectF stackBounds = getAllowableStackPositionRegion(); + final RectF stackBounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); // Target X translation (either the left or right side of the screen). final float destinationRelativeX = stackShouldFlingLeft @@ -425,7 +424,7 @@ public class StackAnimationController extends } final PointF stackPos = getStackPosition(); final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); - final RectF bounds = getAllowableStackPositionRegion(); + final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); stackPos.x = onLeft ? bounds.left : bounds.right; return stackPos; @@ -464,7 +463,7 @@ public class StackAnimationController extends StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); final float currentValue = firstBubbleProperty.getValue(this); - final RectF bounds = getAllowableStackPositionRegion(); + final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); final float min = property.equals(DynamicAnimation.TRANSLATION_X) ? bounds.left @@ -525,7 +524,8 @@ public class StackAnimationController extends * of the stack if it's not moving). */ public float animateForImeVisibility(boolean imeVisible) { - final float maxBubbleY = getAllowableStackPositionRegion().bottom; + final float maxBubbleY = mPositioner.getAllowableStackPositionRegion( + getBubbleCount()).bottom; float destinationY = UNSET; if (imeVisible) { @@ -567,25 +567,6 @@ public class StackAnimationController extends mFloatingContentCoordinator.onContentMoved(mStackFloatingContent); } - /** - * Returns the region that the stack position must stay within. This goes slightly off the left - * and right sides of the screen, below the status bar/cutout and above the navigation bar. - * While the stack position is not allowed to rest outside of these bounds, it can temporarily - * be animated or dragged beyond them. - */ - public RectF getAllowableStackPositionRegion() { - final RectF allowableRegion = new RectF(mPositioner.getAvailableRect()); - final int imeHeight = mPositioner.getImeHeight(); - final float bottomPadding = getBubbleCount() > 1 - ? mBubblePaddingTop + mStackOffset - : mBubblePaddingTop; - allowableRegion.left -= mBubbleOffscreen; - allowableRegion.top += mBubblePaddingTop; - allowableRegion.right += mBubbleOffscreen - mBubbleSize; - allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize; - return allowableRegion; - } - /** Moves the stack in response to a touch event. */ public void moveStackFromTouch(float x, float y) { // Begin the spring-to-touch catch up animation if needed. @@ -861,13 +842,12 @@ public class StackAnimationController extends @Override void onActiveControllerForLayout(PhysicsAnimationLayout layout) { Resources res = layout.getResources(); - mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mStackOffset = mPositioner.getStackOffset(); mSwapAnimationOffset = res.getDimensionPixelSize(R.dimen.bubble_swap_animation_offset); mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); mElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); mBubbleSize = mPositioner.getBubbleSize(); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); + mBubblePaddingTop = mPositioner.getBubblePaddingTop(); } /** @@ -958,7 +938,8 @@ public class StackAnimationController extends } public void setStackPosition(BubbleStackView.RelativeStackPosition position) { - setStackPosition(position.getAbsolutePositionInRegion(getAllowableStackPositionRegion())); + setStackPosition(position.getAbsolutePositionInRegion( + mPositioner.getAllowableStackPositionRegion(getBubbleCount()))); } private boolean isStackPositionSet() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt index a5267d8be9fe..1eee0291cb26 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt @@ -15,6 +15,7 @@ */ package com.android.wm.shell.bubbles.storage +import android.annotation.UserIdInt import android.content.pm.LauncherApps import android.os.UserHandle import android.util.SparseArray @@ -95,10 +96,68 @@ class BubbleVolatileRepository(private val launcherApps: LauncherApps) { } @Synchronized - fun removeBubbles(userId: Int, bubbles: List<BubbleEntity>) = + fun removeBubbles(@UserIdInt userId: Int, bubbles: List<BubbleEntity>) = uncache(bubbles.filter { b: BubbleEntity -> getEntities(userId).removeIf { e: BubbleEntity -> b.key == e.key } }) + /** + * Removes all the bubbles associated with the provided userId. + * @return whether bubbles were removed or not. + */ + @Synchronized + fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentUserId: Int): Boolean { + if (parentUserId != -1) { + return removeBubblesForUserWithParent(userId, parentUserId) + } else { + val entities = entitiesByUser.get(userId) + entitiesByUser.remove(userId) + return entities != null + } + } + + /** + * Removes all the bubbles associated with the provided userId when that userId is part of + * a profile (e.g. managed account). + * + * @return whether bubbles were removed or not. + */ + @Synchronized + private fun removeBubblesForUserWithParent( + @UserIdInt userId: Int, + @UserIdInt parentUserId: Int + ): Boolean { + if (entitiesByUser.get(parentUserId) != null) { + return entitiesByUser.get(parentUserId).removeIf { + b: BubbleEntity -> b.userId == userId } + } + return false + } + + /** + * Goes through all the persisted bubbles and removes them if the user is not in the active + * list of users. + * + * @return whether the list of bubbles changed or not (i.e. was a removal made). + */ + @Synchronized + fun sanitizeBubbles(activeUsers: List<Int>): Boolean { + for (i in 0 until entitiesByUser.size()) { + // First check if the user is a parent / top-level user + val parentUserId = entitiesByUser.keyAt(i) + if (!activeUsers.contains(parentUserId)) { + entitiesByUser.remove(parentUserId) + return true + } else if (entitiesByUser.get(parentUserId) != null) { + // Then check if each of the bubbles in the top-level user, still has a valid user + // as it could belong to a profile and have a different id from the parent. + return entitiesByUser.get(parentUserId).removeIf { b: BubbleEntity -> + !activeUsers.contains(b.userId) + } + } + } + return false + } + private fun cache(bubbles: List<BubbleEntity>) { bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) -> launcherApps.cacheShortcuts(key.pkg, bubbles.map { it.shortcutId }, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index 3b83f1586d8c..6a2acf438302 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -498,6 +498,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged dispatchVisibilityChanged(mDisplayId, isShowing); } } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public InsetsSourceControl getImeSourceControl() { + return mImeSourceControl; + } } void removeImeSurface() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index fedb9983a65e..47f1e2e18255 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -423,8 +423,8 @@ public class DisplayLayout { } final DisplayCutout.CutoutPathParserInfo info = cutout.getCutoutPathParserInfo(); final DisplayCutout.CutoutPathParserInfo newInfo = new DisplayCutout.CutoutPathParserInfo( - info.getDisplayWidth(), info.getDisplayHeight(), info.getStableDisplayWidth(), - info.getStableDisplayHeight(), info.getDensity(), info.getCutoutSpec(), rotation, + info.getDisplayWidth(), info.getDisplayHeight(), info.getPhysicalDisplayWidth(), + info.getPhysicalDisplayHeight(), info.getDensity(), info.getCutoutSpec(), rotation, info.getScale(), info.getPhysicalPixelDisplaySizeRatio()); return computeSafeInsets( DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java index 59374a6069c8..b9ddd3650b86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java @@ -38,7 +38,7 @@ public interface TaskStackListenerCallback { default void onTaskStackChanged() { } - default void onTaskProfileLocked(int taskId, int userId) { } + default void onTaskProfileLocked(RunningTaskInfo taskInfo) { } default void onTaskDisplayChanged(int taskId, int newDisplayId) { } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java index 3b670057cb1a..85e2654e4ebe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java @@ -150,8 +150,8 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. } @Override - public void onTaskProfileLocked(int taskId, int userId) { - mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskId, userId).sendToTarget(); + public void onTaskProfileLocked(ActivityManager.RunningTaskInfo taskInfo) { + mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskInfo).sendToTarget(); } @Override @@ -341,8 +341,10 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. break; } case ON_TASK_PROFILE_LOCKED: { + final ActivityManager.RunningTaskInfo + info = (ActivityManager.RunningTaskInfo) msg.obj; for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { - mTaskStackListeners.get(i).onTaskProfileLocked(msg.arg1, msg.arg2); + mTaskStackListeners.get(i).onTaskProfileLocked(info); } break; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 4b125b118ceb..6305959bb6ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -24,6 +24,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.Property; import android.view.GestureDetector; @@ -37,6 +38,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.FrameLayout; import androidx.annotation.NonNull; @@ -80,7 +83,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private final Rect mTempRect = new Rect(); private FrameLayout mDividerBar; - static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY = new Property<DividerView, Integer>(Integer.class, "height") { @Override @@ -109,6 +111,74 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } }; + private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; + if (isLandscape()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_full, + mContext.getString(R.string.accessibility_action_divider_left_full))); + if (snapAlgorithm.isFirstSplitTargetAvailable()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_70, + mContext.getString(R.string.accessibility_action_divider_left_70))); + } + if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { + // Only show the middle target if there are more than 1 split target + info.addAction(new AccessibilityAction(R.id.action_move_tl_50, + mContext.getString(R.string.accessibility_action_divider_left_50))); + } + if (snapAlgorithm.isLastSplitTargetAvailable()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_30, + mContext.getString(R.string.accessibility_action_divider_left_30))); + } + info.addAction(new AccessibilityAction(R.id.action_move_rb_full, + mContext.getString(R.string.accessibility_action_divider_right_full))); + } else { + info.addAction(new AccessibilityAction(R.id.action_move_tl_full, + mContext.getString(R.string.accessibility_action_divider_top_full))); + if (snapAlgorithm.isFirstSplitTargetAvailable()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_70, + mContext.getString(R.string.accessibility_action_divider_top_70))); + } + if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { + // Only show the middle target if there are more than 1 split target + info.addAction(new AccessibilityAction(R.id.action_move_tl_50, + mContext.getString(R.string.accessibility_action_divider_top_50))); + } + if (snapAlgorithm.isLastSplitTargetAvailable()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_30, + mContext.getString(R.string.accessibility_action_divider_top_30))); + } + info.addAction(new AccessibilityAction(R.id.action_move_rb_full, + mContext.getString(R.string.accessibility_action_divider_bottom_full))); + } + } + + @Override + public boolean performAccessibilityAction(@NonNull View host, int action, + @Nullable Bundle args) { + DividerSnapAlgorithm.SnapTarget nextTarget = null; + DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; + if (action == R.id.action_move_tl_full) { + nextTarget = snapAlgorithm.getDismissEndTarget(); + } else if (action == R.id.action_move_tl_70) { + nextTarget = snapAlgorithm.getLastSplitTarget(); + } else if (action == R.id.action_move_tl_50) { + nextTarget = snapAlgorithm.getMiddleTarget(); + } else if (action == R.id.action_move_tl_30) { + nextTarget = snapAlgorithm.getFirstSplitTarget(); + } else if (action == R.id.action_move_rb_full) { + nextTarget = snapAlgorithm.getDismissStartTarget(); + } + if (nextTarget != null) { + mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), nextTarget); + return true; + } + return super.performAccessibilityAction(host, action, args); + } + }; + public DividerView(@NonNull Context context) { super(context); } @@ -179,6 +249,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); mInteractive = true; setOnTouchListener(this); + mHandle.setAccessibilityDelegate(mHandleDelegate); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index de30dbbe7e46..484294ab295b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -160,6 +160,15 @@ public class SplitDecorManager extends WindowlessWindowManager { mBounds.set(newBounds); } + final boolean show = + newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height(); + final boolean animate = show != mShown; + if (animate && mFadeAnimator != null && mFadeAnimator.isRunning()) { + // If we need to animate and animator still running, cancel it before we ensure both + // background and icon surfaces are non null for next animation. + mFadeAnimator.cancel(); + } + if (mBackgroundLeash == null) { mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); @@ -183,11 +192,7 @@ public class SplitDecorManager extends WindowlessWindowManager { newBounds.width() / 2 - mIconSize / 2, newBounds.height() / 2 - mIconSize / 2); - boolean show = newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height(); - if (show != mShown) { - if (mFadeAnimator != null && mFadeAnimator.isRunning()) { - mFadeAnimator.cancel(); - } + if (animate) { startFadeAnimation(show, false /* isResized */); mShown = show; } 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 0b8e631068fc..c94455d9151a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -107,6 +107,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private int mOrientation; private int mRotation; + private final boolean mDimNonImeSide; + public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, @@ -131,6 +133,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mRootBounds.set(configuration.windowConfiguration.getBounds()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); resetDividerPosition(); + + mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide); } private int getDividerInsets(Resources resources, Display display) { @@ -860,10 +864,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Update target dim values mLastDim1 = mDimValue1; mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && mImeShown - ? ADJUSTED_NONFOCUS_DIM : 0.0f; + && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f; mLastDim2 = mDimValue2; mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && mImeShown - ? ADJUSTED_NONFOCUS_DIM : 0.0f; + && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f; // Calculate target bounds offset for IME mLastYOffset = mYOffsetForIme; 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 72c8141c8f2a..1ea5e21a2c1e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -18,6 +18,7 @@ package com.android.wm.shell.dagger; import android.content.Context; import android.os.Handler; +import android.os.SystemClock; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -29,6 +30,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -38,6 +40,7 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; +import com.android.wm.shell.pip.tv.TvPipBoundsController; import com.android.wm.shell.pip.tv.TvPipBoundsState; import com.android.wm.shell.pip.tv.TvPipController; import com.android.wm.shell.pip.tv.TvPipMenuController; @@ -63,6 +66,8 @@ public abstract class TvPipModule { Context context, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, + PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, @@ -72,13 +77,14 @@ public abstract class TvPipModule { PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper windowManagerShellWrapper, - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler) { + @ShellMainThread ShellExecutor mainExecutor) { return Optional.of( TvPipController.create( context, tvPipBoundsState, tvPipBoundsAlgorithm, + tvPipBoundsController, + pipAppOpsListener, pipTaskOrganizer, pipTransitionController, tvPipMenuController, @@ -88,8 +94,22 @@ public abstract class TvPipModule { pipParamsChangedForwarder, displayController, windowManagerShellWrapper, - mainExecutor, - mainHandler)); + mainExecutor)); + } + + @WMSingleton + @Provides + static TvPipBoundsController provideTvPipBoundsController( + Context context, + @ShellMainThread Handler mainHandler, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm) { + return new TvPipBoundsController( + context, + SystemClock::uptimeMillis, + mainHandler, + tvPipBoundsState, + tvPipBoundsAlgorithm); } @WMSingleton @@ -140,8 +160,11 @@ public abstract class TvPipModule { @Provides static TvPipNotificationController provideTvPipNotificationController(Context context, PipMediaController pipMediaController, + PipParamsChangedForwarder pipParamsChangedForwarder, + TvPipBoundsState tvPipBoundsState, @ShellMainThread Handler mainHandler) { - return new TvPipNotificationController(context, pipMediaController, mainHandler); + return new TvPipNotificationController(context, pipMediaController, + pipParamsChangedForwarder, tvPipBoundsState, mainHandler); } @WMSingleton @@ -185,4 +208,12 @@ public abstract class TvPipModule { static PipParamsChangedForwarder providePipParamsChangedForwarder() { return new PipParamsChangedForwarder(); } + + @WMSingleton + @Provides + static PipAppOpsListener providePipAppOpsListener(Context context, + PipTaskOrganizer pipTaskOrganizer, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipAppOpsListener(context, pipTaskOrganizer::removePip, mainExecutor); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 4ad08688bd51..db6131a17114 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -55,6 +55,7 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellAnimationThread; +import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.annotations.ShellSplashscreenThread; import com.android.wm.shell.compatui.CompatUI; @@ -77,7 +78,6 @@ import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipUiEventLogger; -import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; @@ -434,14 +434,6 @@ public abstract class WMShellBaseModule { return new FloatingContentCoordinator(); } - @WMSingleton - @Provides - static PipAppOpsListener providePipAppOpsListener(Context context, - PipTouchHandler pipTouchHandler, - @ShellMainThread ShellExecutor mainExecutor) { - return new PipAppOpsListener(context, pipTouchHandler.getMotionHelper(), mainExecutor); - } - // Needs handler for registering broadcast receivers @WMSingleton @Provides @@ -734,11 +726,12 @@ public abstract class WMShellBaseModule { @Provides static Optional<BackAnimationController> provideBackAnimationController( Context context, - @ShellMainThread ShellExecutor shellExecutor + @ShellMainThread ShellExecutor shellExecutor, + @ShellBackgroundThread Handler backgroundHandler ) { if (BackAnimationController.IS_ENABLED) { return Optional.of( - new BackAnimationController(shellExecutor, context)); + new BackAnimationController(shellExecutor, backgroundHandler, context)); } return Optional.empty(); } 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 7513e5129ade..b3799e2cf8d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -20,6 +20,7 @@ import android.animation.AnimationHandler; import android.content.Context; import android.content.pm.LauncherApps; import android.os.Handler; +import android.os.UserManager; import android.view.WindowManager; import com.android.internal.jank.InteractionJankMonitor; @@ -43,6 +44,7 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ChoreographerSfVsync; +import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.freeform.FreeformTaskListener; @@ -51,6 +53,7 @@ import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; @@ -63,9 +66,7 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PhonePipMenuController; -import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipController; -import com.android.wm.shell.pip.phone.PipKeepClearAlgorithm; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasksController; @@ -106,6 +107,7 @@ public class WMShellModule { IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, + UserManager userManager, LauncherApps launcherApps, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, @@ -115,13 +117,15 @@ public class WMShellModule { DragAndDropController dragAndDropController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, + @ShellBackgroundThread ShellExecutor bgExecutor, TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { return BubbleController.create(context, null /* synchronizer */, floatingContentCoordinator, statusBarService, windowManager, - windowManagerShellWrapper, launcherApps, taskStackListener, + windowManagerShellWrapper, userManager, launcherApps, taskStackListener, uiEventLogger, organizer, displayController, oneHandedOptional, - dragAndDropController, mainExecutor, mainHandler, taskViewTransitions, syncQueue); + dragAndDropController, mainExecutor, mainHandler, bgExecutor, + taskViewTransitions, syncQueue); } // @@ -211,8 +215,7 @@ public class WMShellModule { @Provides static Optional<Pip> providePip(Context context, DisplayController displayController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState, - PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, @@ -221,8 +224,8 @@ public class WMShellModule { Optional<OneHandedController> oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable(PipController.create(context, displayController, - pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, - pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, + pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState, + pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor)); } @@ -241,12 +244,6 @@ public class WMShellModule { @WMSingleton @Provides - static PipKeepClearAlgorithm providePipKeepClearAlgorithm() { - return new PipKeepClearAlgorithm(); - } - - @WMSingleton - @Provides static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context, PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); @@ -333,6 +330,14 @@ public class WMShellModule { @WMSingleton @Provides + static PipAppOpsListener providePipAppOpsListener(Context context, + PipTouchHandler pipTouchHandler, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipAppOpsListener(context, pipTouchHandler.getMotionHelper(), mainExecutor); + } + + @WMSingleton + @Provides static PipMotionHelper providePipMotionHelper(Context context, PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java index f8f9d6b8f8a0..65cb7ac1e5f7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java @@ -16,13 +16,18 @@ package com.android.wm.shell.kidsmode; +import android.annotation.NonNull; +import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; +import android.net.Uri; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; +import java.util.Collection; + /** * A ContentObserver for listening kids mode relative setting keys: * - {@link Settings.Secure#NAVIGATION_MODE} @@ -64,7 +69,11 @@ public class KidsModeSettingsObserver extends ContentObserver { } @Override - public void onChange(boolean selfChange) { + public void onChange(boolean selfChange, @NonNull Collection<Uri> uris, int flags, int userId) { + if (userId != ActivityManager.getCurrentUser()) { + return; + } + if (mOnChangeRunnable != null) { mOnChangeRunnable.run(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java index dc703583a449..b4c87b6cbf95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -23,7 +23,10 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import android.app.ActivityManager; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Binder; @@ -87,6 +90,13 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { private KidsModeSettingsObserver mKidsModeSettingsObserver; private boolean mEnabled; + private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateKidsModeState(); + } + }; + DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener = new DisplayController.OnDisplaysChangedListener() { @Override @@ -169,12 +179,15 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { public void initialize(StartingWindowController startingWindowController) { initStartingWindow(startingWindowController); if (mKidsModeSettingsObserver == null) { - mKidsModeSettingsObserver = new KidsModeSettingsObserver( - mMainHandler, mContext); + mKidsModeSettingsObserver = new KidsModeSettingsObserver(mMainHandler, mContext); } mKidsModeSettingsObserver.setOnChangeRunnable(() -> updateKidsModeState()); updateKidsModeState(); mKidsModeSettingsObserver.register(); + + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_SWITCHED); + mContext.registerReceiverForAllUsers(mUserSwitchIntentReceiver, filter, null, mMainHandler); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index c0734e95ecb7..3b3091a9caf3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -44,11 +44,6 @@ public interface Pip { } /** - * Hides the PIP menu. - */ - default void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {} - - /** * Called when configuration is changed. */ default void onConfigurationChanged(Configuration newConfig) { @@ -125,6 +120,23 @@ public interface Pip { default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { } /** + * Called when the visibility of keyguard is changed. + * @param showing {@code true} if keyguard is now showing, {@code false} otherwise. + * @param animating {@code true} if system is animating between keyguard and surface behind, + * this only makes sense when showing is {@code false}. + */ + default void onKeyguardVisibilityChanged(boolean showing, boolean animating) { } + + /** + * Called when the dismissing animation keyguard and surfaces behind is finished. + * See also {@link #onKeyguardVisibilityChanged(boolean, boolean)}. + * + * TODO(b/206741900) deprecate this path once we're able to animate the PiP window as part of + * keyguard dismiss animation. + */ + default void onKeyguardDismissAnimationFinished() { } + + /** * Dump the current state and information if need. * * @param pw The stream to dump information to. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index d357655882ff..4eba1697b595 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -25,15 +25,14 @@ import android.animation.Animator; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.IntDef; +import android.annotation.NonNull; import android.app.TaskInfo; import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.Rect; import android.view.Choreographer; import android.view.Surface; import android.view.SurfaceControl; -import android.view.SurfaceSession; +import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -197,6 +196,15 @@ public class PipAnimationController { } /** + * Quietly cancel the animator by removing the listeners first. + */ + static void quietCancel(@NonNull ValueAnimator animator) { + animator.removeAllUpdateListeners(); + animator.removeAllListeners(); + animator.cancel(); + } + + /** * Additional callback interface for PiP animation */ public static class PipAnimationCallback { @@ -257,7 +265,7 @@ public class PipAnimationController { mSurfaceControlTransactionFactory; private PipSurfaceTransactionHelper mSurfaceTransactionHelper; private @TransitionDirection int mTransitionDirection; - protected SurfaceControl mContentOverlay; + protected PipContentOverlay mContentOverlay; private PipTransitionAnimator(TaskInfo taskInfo, SurfaceControl leash, @AnimationType int animationType, @@ -335,43 +343,26 @@ public class PipAnimationController { return false; } - SurfaceControl getContentOverlay() { - return mContentOverlay; + SurfaceControl getContentOverlayLeash() { + return mContentOverlay == null ? null : mContentOverlay.mLeash; } - PipTransitionAnimator<T> setUseContentOverlay(Context context) { + void setColorContentOverlay(Context context) { final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); if (mContentOverlay != null) { - // remove existing content overlay if there is any. - tx.remove(mContentOverlay); - tx.apply(); + mContentOverlay.detach(tx); } - mContentOverlay = new SurfaceControl.Builder(new SurfaceSession()) - .setCallsite("PipAnimation") - .setName("PipContentOverlay") - .setColorLayer() - .build(); - tx.show(mContentOverlay); - tx.setLayer(mContentOverlay, Integer.MAX_VALUE); - tx.setColor(mContentOverlay, getContentOverlayColor(context)); - tx.setAlpha(mContentOverlay, 0f); - tx.reparent(mContentOverlay, mLeash); - tx.apply(); - return this; + mContentOverlay = new PipContentOverlay.PipColorOverlay(context); + mContentOverlay.attach(tx, mLeash); } - private float[] getContentOverlayColor(Context context) { - final TypedArray ta = context.obtainStyledAttributes(new int[] { - android.R.attr.colorBackground }); - try { - int colorAccent = ta.getColor(0, 0); - return new float[] { - Color.red(colorAccent) / 255f, - Color.green(colorAccent) / 255f, - Color.blue(colorAccent) / 255f }; - } finally { - ta.recycle(); + void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { + final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); + if (mContentOverlay != null) { + mContentOverlay.detach(tx); } + mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint); + mContentOverlay.attach(tx, mLeash); } /** @@ -575,7 +566,7 @@ public class PipAnimationController { final Rect start = getStartValue(); final Rect end = getEndValue(); if (mContentOverlay != null) { - tx.setAlpha(mContentOverlay, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); + mContentOverlay.onAnimationUpdate(tx, fraction); } if (rotatedEndRect != null) { // Animate the bounds in a different orientation. It only happens when @@ -680,7 +671,7 @@ public class PipAnimationController { .round(tx, leash, shouldApplyCornerRadius()) .shadow(tx, leash, shouldApplyShadowRadius()); // TODO(b/178632364): this is a work around for the black background when - // entering PiP in buttion navigation mode. + // entering PiP in button navigation mode. if (isInPipDirection(direction)) { tx.setWindowCrop(leash, getStartValue()); } @@ -704,6 +695,9 @@ public class PipAnimationController { } else { getSurfaceTransactionHelper().crop(tx, leash, destBounds); } + if (mContentOverlay != null) { + mContentOverlay.onAnimationEnd(tx, destBounds); + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java index d97d2d6ebb4f..48a3fc2460a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.pip; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE; @@ -28,7 +28,6 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.util.Pair; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.pip.PipUtils; public class PipAppOpsListener { private static final String TAG = PipAppOpsListener.class.getSimpleName(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java new file mode 100644 index 000000000000..0e32663955d3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.window.TaskSnapshot; + +/** + * Represents the content overlay used during the entering PiP animation. + */ +public abstract class PipContentOverlay { + protected SurfaceControl mLeash; + + /** Attaches the internal {@link #mLeash} to the given parent leash. */ + public abstract void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash); + + /** Detaches the internal {@link #mLeash} from its parent by removing itself. */ + public void detach(SurfaceControl.Transaction tx) { + if (mLeash != null && mLeash.isValid()) { + tx.remove(mLeash); + tx.apply(); + } + } + + /** + * Animates the internal {@link #mLeash} by a given fraction. + * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly + * call apply on this transaction, it should be applied on the caller side. + * @param fraction progress of the animation ranged from 0f to 1f. + */ + public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction); + + /** + * Callback when reaches the end of animation on the internal {@link #mLeash}. + * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly + * call apply on this transaction, it should be applied on the caller side. + * @param destinationBounds {@link Rect} of the final bounds. + */ + public abstract void onAnimationEnd(SurfaceControl.Transaction atomicTx, + Rect destinationBounds); + + /** A {@link PipContentOverlay} uses solid color. */ + public static final class PipColorOverlay extends PipContentOverlay { + private final Context mContext; + + public PipColorOverlay(Context context) { + mContext = context; + mLeash = new SurfaceControl.Builder(new SurfaceSession()) + .setCallsite("PipAnimation") + .setName(PipColorOverlay.class.getSimpleName()) + .setColorLayer() + .build(); + } + + @Override + public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + tx.show(mLeash); + tx.setLayer(mLeash, Integer.MAX_VALUE); + tx.setColor(mLeash, getContentOverlayColor(mContext)); + tx.setAlpha(mLeash, 0f); + tx.reparent(mLeash, parentLeash); + tx.apply(); + } + + @Override + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) { + atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); + } + + @Override + public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { + // Do nothing. Color overlay should be fully opaque by now. + } + + private float[] getContentOverlayColor(Context context) { + final TypedArray ta = context.obtainStyledAttributes(new int[] { + android.R.attr.colorBackground }); + try { + int colorAccent = ta.getColor(0, 0); + return new float[] { + Color.red(colorAccent) / 255f, + Color.green(colorAccent) / 255f, + Color.blue(colorAccent) / 255f }; + } finally { + ta.recycle(); + } + } + } + + /** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */ + public static final class PipSnapshotOverlay extends PipContentOverlay { + private final TaskSnapshot mSnapshot; + private final Rect mSourceRectHint; + + private float mTaskSnapshotScaleX; + private float mTaskSnapshotScaleY; + + public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { + mSnapshot = snapshot; + mSourceRectHint = new Rect(sourceRectHint); + mLeash = new SurfaceControl.Builder(new SurfaceSession()) + .setCallsite("PipAnimation") + .setName(PipSnapshotOverlay.class.getSimpleName()) + .build(); + } + + @Override + public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + mTaskSnapshotScaleX = (float) mSnapshot.getTaskSize().x + / mSnapshot.getHardwareBuffer().getWidth(); + mTaskSnapshotScaleY = (float) mSnapshot.getTaskSize().y + / mSnapshot.getHardwareBuffer().getHeight(); + tx.show(mLeash); + tx.setLayer(mLeash, Integer.MAX_VALUE); + tx.setBuffer(mLeash, mSnapshot.getHardwareBuffer()); + // Relocate the content to parentLeash's coordinates. + tx.setPosition(mLeash, -mSourceRectHint.left, -mSourceRectHint.top); + tx.setScale(mLeash, mTaskSnapshotScaleX, mTaskSnapshotScaleY); + tx.reparent(mLeash, parentLeash); + tx.apply(); + } + + @Override + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) { + // Do nothing. Keep the snapshot till animation ends. + } + + @Override + public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { + // Work around to make sure the snapshot overlay is aligned with PiP window before + // the atomicTx is committed along with the final WindowContainerTransaction. + final SurfaceControl.Transaction nonAtomicTx = new SurfaceControl.Transaction(); + final float scaleX = (float) destinationBounds.width() + / mSourceRectHint.width(); + final float scaleY = (float) destinationBounds.height() + / mSourceRectHint.height(); + final float scale = Math.max( + scaleX * mTaskSnapshotScaleX, scaleY * mTaskSnapshotScaleY); + nonAtomicTx.setScale(mLeash, scale, scale); + nonAtomicTx.setPosition(mLeash, + -scale * mSourceRectHint.left / mTaskSnapshotScaleX, + -scale * mSourceRectHint.top / mTaskSnapshotScaleY); + nonAtomicTx.apply(); + atomicTx.remove(mLeash); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java index 8a50f2233573..65a12d629c5a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java @@ -32,6 +32,7 @@ import android.content.IntentFilter; import android.graphics.drawable.Icon; import android.media.MediaMetadata; import android.media.session.MediaController; +import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.Handler; @@ -64,7 +65,7 @@ public class PipMediaController { */ public interface ActionListener { /** - * Called when the media actions changes. + * Called when the media actions changed. */ void onMediaActionsChanged(List<RemoteAction> actions); } @@ -74,11 +75,21 @@ public class PipMediaController { */ public interface MetadataListener { /** - * Called when the media metadata changes. + * Called when the media metadata changed. */ void onMediaMetadataChanged(MediaMetadata metadata); } + /** + * A listener interface to receive notification on changes to the media session token. + */ + public interface TokenListener { + /** + * Called when the media session token changed. + */ + void onMediaSessionTokenChanged(MediaSession.Token token); + } + private final Context mContext; private final Handler mMainHandler; private final HandlerExecutor mHandlerExecutor; @@ -133,6 +144,7 @@ public class PipMediaController { private final ArrayList<ActionListener> mActionListeners = new ArrayList<>(); private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>(); + private final ArrayList<TokenListener> mTokenListeners = new ArrayList<>(); public PipMediaController(Context context, Handler mainHandler) { mContext = context; @@ -204,6 +216,31 @@ public class PipMediaController { mMetadataListeners.remove(listener); } + /** + * Adds a new token listener. + */ + public void addTokenListener(TokenListener listener) { + if (!mTokenListeners.contains(listener)) { + mTokenListeners.add(listener); + listener.onMediaSessionTokenChanged(getToken()); + } + } + + /** + * Removes a token listener. + */ + public void removeTokenListener(TokenListener listener) { + listener.onMediaSessionTokenChanged(null); + mTokenListeners.remove(listener); + } + + private MediaSession.Token getToken() { + if (mMediaController == null) { + return null; + } + return mMediaController.getSessionToken(); + } + private MediaMetadata getMediaMetadata() { return mMediaController != null ? mMediaController.getMetadata() : null; } @@ -294,6 +331,7 @@ public class PipMediaController { } notifyActionsChanged(); notifyMetadataChanged(getMediaMetadata()); + notifyTokenChanged(getToken()); // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV) } @@ -317,4 +355,10 @@ public class PipMediaController { mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata)); } } + + private void notifyTokenChanged(MediaSession.Token token) { + if (!mTokenListeners.isEmpty()) { + mTokenListeners.forEach(l -> l.onMediaSessionTokenChanged(token)); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index c6e48f53681c..a017a2674359 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -105,8 +105,10 @@ public class PipSurfaceTransactionHelper { SurfaceControl leash, Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets, boolean isInPipDirection) { - mTmpSourceRectF.set(sourceBounds); mTmpDestinationRect.set(sourceBounds); + // Similar to {@link #scale}, we want to position the surface relative to the screen + // coordinates so offset the bounds to 0,0 + mTmpDestinationRect.offsetTo(0, 0); mTmpDestinationRect.inset(insets); // Scale by the shortest edge and offset such that the top/left of the scaled inset source // rect aligns with the top/left of the destination bounds 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 4690e16bc385..e624de661737 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -66,6 +66,7 @@ import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; import android.window.TaskOrganizer; +import android.window.TaskSnapshot; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -152,8 +153,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final int direction = animator.getTransitionDirection(); final int animationType = animator.getAnimationType(); final Rect destinationBounds = animator.getDestinationBounds(); - if (isInPipDirection(direction) && animator.getContentOverlay() != null) { - fadeOutAndRemoveOverlay(animator.getContentOverlay(), + if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { + fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), animator::clearContentOverlay, true /* withStartDelay*/); } if (mWaitForFixedRotation && animationType == ANIM_TYPE_BOUNDS @@ -186,8 +187,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void onPipAnimationCancel(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); - if (isInPipDirection(direction) && animator.getContentOverlay() != null) { - fadeOutAndRemoveOverlay(animator.getContentOverlay(), + if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { + fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), animator::clearContentOverlay, true /* withStartDelay */); } sendOnPipTransitionCancelled(direction); @@ -363,7 +364,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } mPipBoundsState.setBounds(destinationBounds); mSwipePipToHomeOverlay = overlay; - if (ENABLE_SHELL_TRANSITIONS) { + if (ENABLE_SHELL_TRANSITIONS && overlay != null) { // With Shell transition, the overlay was attached to the remote transition leash, which // will be removed when the current transition is finished, so we need to reparent it // to the actual Task surface now. @@ -430,7 +431,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } } - final Rect destinationBounds = mPipBoundsState.getDisplayBounds(); + final Rect destinationBounds = getExitDestinationBounds(); final int direction = syncWithSplitScreenBounds(destinationBounds, requestEnterSplit) ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN : TRANSITION_DIRECTION_LEAVE_PIP; @@ -456,6 +457,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, wct.setBoundsChangeTransaction(mToken, tx); } + // Cancel the existing animator if there is any. + cancelCurrentAnimator(); + // Set the exiting state first so if there is fixed rotation later, the running animation // won't be interrupted by alpha animation for existing PiP. mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); @@ -485,6 +489,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, }); } + /** Returns the bounds to restore to when exiting PIP mode. */ + public Rect getExitDestinationBounds() { + return mPipBoundsState.getDisplayBounds(); + } + private void exitLaunchIntoPipTask(WindowContainerTransaction wct) { wct.startTask(mTaskInfo.launchIntoPipHostTaskId, null /* ActivityOptions */); mTaskOrganizer.applyTransaction(wct); @@ -795,21 +804,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, "%s: Unrecognized token: %s", TAG, token); return; } + + cancelCurrentAnimator(); onExitPipFinished(info); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mPipTransitionController.forceFinishTransition(); } - final PipAnimationController.PipTransitionAnimator<?> animator = - mPipAnimationController.getCurrentAnimator(); - if (animator != null) { - if (animator.getContentOverlay() != null) { - removeContentOverlay(animator.getContentOverlay(), animator::clearContentOverlay); - } - animator.removeAllUpdateListeners(); - animator.removeAllListeners(); - animator.cancel(); - } } @Override @@ -965,6 +966,22 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mDeferredAnimEndTransaction = null; } + /** Explicitly set the visibility of PiP window. */ + public void setPipVisibility(boolean visible) { + if (!isInPip()) { + return; + } + if (mLeash == null || !mLeash.isValid()) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid leash on setPipVisibility: %s", TAG, mLeash); + return; + } + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper.alpha(tx, mLeash, visible ? 1f : 0f); + tx.apply(); + } + @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { mCurrentRotation = newConfig.windowConfiguration.getRotation(); @@ -1025,9 +1042,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, int direction = TRANSITION_DIRECTION_NONE; if (animator != null) { direction = animator.getTransitionDirection(); - animator.removeAllUpdateListeners(); - animator.removeAllListeners(); - animator.cancel(); + PipAnimationController.quietCancel(animator); // Do notify the listeners that this was canceled sendOnPipTransitionCancelled(direction); sendOnPipTransitionFinished(direction); @@ -1085,11 +1100,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * Handles all changes to the PictureInPictureParams. */ protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { - if (PipUtils.aspectRatioChanged(params.getAspectRatioFloat(), + if (mDeferredTaskInfo != null || PipUtils.aspectRatioChanged(params.getAspectRatioFloat(), mPictureInPictureParams.getAspectRatioFloat())) { mPipParamsChangedForwarder.notifyAspectRatioChanged(params.getAspectRatioFloat()); } - if (PipUtils.remoteActionsChanged(params.getActions(), mPictureInPictureParams.getActions()) + if (mDeferredTaskInfo != null + || PipUtils.remoteActionsChanged(params.getActions(), + mPictureInPictureParams.getActions()) || !PipUtils.remoteActionsMatch(params.getCloseAction(), mPictureInPictureParams.getCloseAction())) { mPipParamsChangedForwarder.notifyActionsChanged(params.getActions(), @@ -1281,7 +1298,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback) { - if (mPipTransitionState.shouldBlockResizeRequest()) { + if (mPipTransitionState.shouldBlockResizeRequest() + || mPipTransitionState.getInSwipePipToHomeTransition()) { return; } if (mWaitForFixedRotation) { @@ -1472,7 +1490,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (isInPipDirection(direction)) { // Similar to auto-enter-pip transition, we use content overlay when there is no // source rect hint to enter PiP use bounds animation. - if (sourceHintRect == null) animator.setUseContentOverlay(mContext); + if (sourceHintRect == null) { + animator.setColorContentOverlay(mContext); + } else { + final TaskSnapshot snapshot = PipUtils.getTaskSnapshot( + mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */); + if (snapshot != null) { + // use the task snapshot during the animation, this is for + // launch-into-pip aka. content-pip use case. + animator.setSnapshotContentOverlay(snapshot, sourceHintRect); + } + } // The destination bounds are used for the end rect of animation and the final bounds // after animation finishes. So after the animation is started, the destination bounds // can be updated to new rotation (computeRotatedBounds has changed the DisplayLayout @@ -1536,7 +1564,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ void fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback, boolean withStartDelay) { - if (surface == null) { + if (surface == null || !surface.isValid()) { return; } @@ -1548,10 +1576,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // set a start delay on this animation. ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Task vanished, skip fadeOutAndRemoveOverlay", TAG); - animation.removeAllListeners(); - animation.removeAllUpdateListeners(); - animation.cancel(); - } else { + PipAnimationController.quietCancel(animation); + } else if (surface.isValid()) { final float alpha = (float) animation.getAnimatedValue(); final SurfaceControl.Transaction transaction = mSurfaceControlTransactionFactory.getTransaction(); @@ -1574,6 +1600,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // Avoid double removal, which is fatal. return; } + if (surface == null || !surface.isValid()) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: trying to remove invalid content overlay (%s)", TAG, surface); + return; + } final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); tx.remove(surface); tx.apply(); @@ -1590,6 +1621,18 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, tx.apply(); } + private void cancelCurrentAnimator() { + final PipAnimationController.PipTransitionAnimator<?> animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null) { + if (animator.getContentOverlayLeash() != null) { + removeContentOverlay(animator.getContentOverlayLeash(), + animator::clearContentOverlay); + } + PipAnimationController.quietCancel(animator); + } + } + @VisibleForTesting public void setSurfaceControlTransactionFactory( PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { 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 48df28ee4cde..36e712459863 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -709,7 +709,7 @@ public class PipTransition extends PipTransitionController { if (sourceHintRect == null) { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. - animator.setUseContentOverlay(mContext); + animator.setColorContentOverlay(mContext); } } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { startTransaction.setAlpha(leash, 0f); 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 24993c621e3c..54f46e0c9938 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -70,8 +70,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { return; } - if (isInPipDirection(direction) && animator.getContentOverlay() != null) { - mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlay(), + if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { + mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), animator::clearContentOverlay, true /* withStartDelay*/); } onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx); @@ -82,8 +82,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void onPipAnimationCancel(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); - if (isInPipDirection(direction) && animator.getContentOverlay() != null) { - mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlay(), + if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { + mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), animator::clearContentOverlay, true /* withStartDelay */); } sendOnPipTransitionCancelled(animator.getTransitionDirection()); 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 c6cf8b8b0566..dc60bcf742ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java @@ -19,13 +19,16 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import android.annotation.Nullable; import android.app.ActivityTaskManager; import android.app.ActivityTaskManager.RootTaskInfo; import android.app.RemoteAction; import android.content.ComponentName; import android.content.Context; import android.os.RemoteException; +import android.util.Log; import android.util.Pair; +import android.window.TaskSnapshot; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -106,4 +109,17 @@ public class PipUtils { } return false; } + + /** @return {@link TaskSnapshot} for a given task id. */ + @Nullable + public static TaskSnapshot getTaskSnapshot(int taskId, boolean isLowResolution) { + if (taskId <= 0) return null; + try { + return ActivityTaskManager.getService().getTaskSnapshot( + taskId, isLowResolution); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get task snapshot, taskId=" + taskId, e); + return null; + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 2e8b5b7979d0..dad261ad9580 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -76,6 +76,7 @@ import com.android.wm.shell.pip.IPipAnimationListener; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; @@ -109,9 +110,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private PipAppOpsListener mAppOpsListener; private PipMediaController mMediaController; private PipBoundsAlgorithm mPipBoundsAlgorithm; - private PipKeepClearAlgorithm mPipKeepClearAlgorithm; private PipBoundsState mPipBoundsState; - private PipMotionHelper mPipMotionHelper; private PipTouchHandler mTouchHandler; private PipTransitionController mPipTransitionController; private TaskStackListenerImpl mTaskStackListener; @@ -130,6 +129,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener = new PipControllerPinnedTaskListener(); + private boolean mIsKeyguardShowingOrAnimating; + private interface PipAnimationListener { /** * Notifies the listener that the Pip animation is started. @@ -247,10 +248,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb Set<Rect> unrestricted) { if (mPipBoundsState.getDisplayId() == displayId) { mPipBoundsState.setKeepClearAreas(restricted, unrestricted); - mPipMotionHelper.moveToBounds(mPipKeepClearAlgorithm.adjust( - mPipBoundsState.getBounds(), - mPipBoundsState.getRestrictedKeepClearAreas(), - mPipBoundsState.getUnrestrictedKeepClearAreas())); } } }; @@ -289,8 +286,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Nullable public static Pip create(Context context, DisplayController displayController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState, - PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, @@ -305,7 +301,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb } return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, - pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController, + pipBoundsState, pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor) @@ -316,9 +312,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb DisplayController displayController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipKeepClearAlgorithm pipKeepClearAlgorithm, @NonNull PipBoundsState pipBoundsState, - PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, @@ -341,9 +335,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mWindowManagerShellWrapper = windowManagerShellWrapper; mDisplayController = displayController; mPipBoundsAlgorithm = pipBoundsAlgorithm; - mPipKeepClearAlgorithm = pipKeepClearAlgorithm; mPipBoundsState = pipBoundsState; - mPipMotionHelper = pipMotionHelper; mPipTaskOrganizer = pipTaskOrganizer; mMainExecutor = mainExecutor; mMediaController = pipMediaController; @@ -603,6 +595,33 @@ public class PipController implements PipTransitionController.PipTransitionCallb } /** + * If {@param keyguardShowing} is {@code false} and {@param animating} is {@code true}, + * we would wait till the dismissing animation of keyguard and surfaces behind to be + * finished first to reset the visibility of PiP window. + * See also {@link #onKeyguardDismissAnimationFinished()} + */ + private void onKeyguardVisibilityChanged(boolean keyguardShowing, boolean animating) { + if (!mPipTaskOrganizer.isInPip()) { + return; + } + if (keyguardShowing) { + mIsKeyguardShowingOrAnimating = true; + hidePipMenu(null /* onStartCallback */, null /* onEndCallback */); + mPipTaskOrganizer.setPipVisibility(false); + } else if (!animating) { + mIsKeyguardShowingOrAnimating = false; + mPipTaskOrganizer.setPipVisibility(true); + } + } + + private void onKeyguardDismissAnimationFinished() { + if (mPipTaskOrganizer.isInPip()) { + mIsKeyguardShowingOrAnimating = false; + mPipTaskOrganizer.setPipVisibility(true); + } + } + + /** * Sets a customized touch gesture that replaces the default one. */ public void setTouchGesture(PipTouchGesture gesture) { @@ -613,7 +632,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb * Sets both shelf visibility and its height. */ private void setShelfHeight(boolean visible, int height) { - setShelfHeightLocked(visible, height); + if (!mIsKeyguardShowingOrAnimating) { + setShelfHeightLocked(visible, height); + } } private void setShelfHeightLocked(boolean visible, int height) { @@ -844,13 +865,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) { - mMainExecutor.execute(() -> { - PipController.this.hidePipMenu(onStartCallback, onEndCallback); - }); - } - - @Override public void expandPip() { mMainExecutor.execute(() -> { PipController.this.expandPip(); @@ -928,6 +942,18 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override + public void onKeyguardVisibilityChanged(boolean showing, boolean animating) { + mMainExecutor.execute(() -> { + PipController.this.onKeyguardVisibilityChanged(showing, animating); + }); + } + + @Override + public void onKeyguardDismissAnimationFinished() { + mMainExecutor.execute(PipController.this::onKeyguardDismissAnimationFinished); + } + + @Override public void dump(PrintWriter pw) { try { mMainExecutor.executeBlocking(() -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithm.java deleted file mode 100644 index a83258f9063b..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithm.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.pip.phone; - -import android.graphics.Rect; - -import java.util.Set; - -/** - * Calculates the adjusted position that does not occlude keep clear areas. - */ -public class PipKeepClearAlgorithm { - - /** Returns a new {@code Rect} that does not occlude the provided keep clear areas. */ - public Rect adjust(Rect defaultBounds, Set<Rect> restrictedKeepClearAreas, - Set<Rect> unrestrictedKeepClearAreas) { - if (restrictedKeepClearAreas.isEmpty()) { - return defaultBounds; - } - // TODO(b/183746978): implement the adjustment algorithm - // naively check if areas intersect, an if so move PiP upwards - Rect outBounds = new Rect(defaultBounds); - for (Rect r : restrictedKeepClearAreas) { - if (r.intersect(outBounds)) { - outBounds.offset(0, r.top - outBounds.bottom); - } - } - return outBounds; - } -} 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 e9b6babfc5fa..5a21e0734277 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -44,6 +44,7 @@ import com.android.wm.shell.animation.FloatProperties; import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java index 21d5d401835d..a2eadcdf6210 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -29,7 +29,6 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.Rect; -import android.os.SystemClock; import android.util.ArraySet; import android.util.Size; import android.view.Gravity; @@ -66,7 +65,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { @NonNull PipSnapAlgorithm pipSnapAlgorithm) { super(context, tvPipBoundsState, pipSnapAlgorithm); this.mTvPipBoundsState = tvPipBoundsState; - this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(SystemClock::uptimeMillis); + this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(); reloadResources(context); } @@ -80,7 +79,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { 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 @@ -104,7 +102,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true); } mTvPipBoundsState.setTvPipExpanded(isPipExpanded); - return getTvPipBounds().getBounds(); + return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); } /** Returns the current bounds adjusted to the new aspect ratio, if valid. */ @@ -114,13 +112,27 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio); } - return getTvPipBounds().getBounds(); + return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); + } + + Rect adjustBoundsForTemporaryDecor(Rect bounds) { + Rect boundsWithDecor = new Rect(bounds); + Insets decorInset = mTvPipBoundsState.getPipMenuTemporaryDecorInsets(); + Insets pipDecorReverseInsets = Insets.subtract(Insets.NONE, decorInset); + boundsWithDecor.inset(decorInset); + Gravity.apply(mTvPipBoundsState.getTvPipGravity(), + boundsWithDecor.width(), boundsWithDecor.height(), bounds, boundsWithDecor); + + // remove temporary decoration again + boundsWithDecor.inset(pipDecorReverseInsets); + return boundsWithDecor; } /** * Calculates the PiP bounds. */ - public Placement getTvPipBounds() { + @NonNull + public Placement getTvPipPlacement() { final Size pipSize = getPipSize(); final Rect displayBounds = mTvPipBoundsState.getDisplayBounds(); final Size screenSize = new Size(displayBounds.width(), displayBounds.height()); @@ -153,8 +165,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset()); mKeepClearAlgorithm.setPipPermanentDecorInsets( mTvPipBoundsState.getPipMenuPermanentDecorInsets()); - mKeepClearAlgorithm.setPipTemporaryDecorInsets( - mTvPipBoundsState.getPipMenuTemporaryDecorInsets()); final Placement placement = mKeepClearAlgorithm.calculatePipPosition( pipSize, @@ -407,8 +417,4 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { TAG, expandedSize.getWidth(), expandedSize.getHeight()); } } - - void keepUnstashedForCurrentKeepClearAreas() { - mKeepClearAlgorithm.keepUnstashedForCurrentKeepClearAreas(); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java new file mode 100644 index 000000000000..3a6ce81821ec --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Handler; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Controller managing the PiP's position. + * Manages debouncing of PiP movements and scheduling of unstashing. + */ +public class TvPipBoundsController { + private static final boolean DEBUG = false; + private static final String TAG = "TvPipBoundsController"; + + /** + * Time the calculated PiP position needs to be stable before PiP is moved there, + * to avoid erratic movement. + * Some changes will cause the PiP to be repositioned immediately, such as changes to + * unrestricted keep clear areas. + */ + @VisibleForTesting + static final long POSITION_DEBOUNCE_TIMEOUT_MILLIS = 300L; + + private final Context mContext; + private final Supplier<Long> mClock; + private final Handler mMainHandler; + private final TvPipBoundsState mTvPipBoundsState; + private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; + + @Nullable + private PipBoundsListener mListener; + + private int mResizeAnimationDuration; + private int mStashDurationMs; + private Rect mCurrentPlacementBounds; + private Rect mPipTargetBounds; + + private final Runnable mApplyPendingPlacementRunnable = this::applyPendingPlacement; + private boolean mPendingStash; + private Placement mPendingPlacement; + private int mPendingPlacementAnimationDuration; + private Runnable mUnstashRunnable; + + public TvPipBoundsController( + Context context, + Supplier<Long> clock, + Handler mainHandler, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm) { + mContext = context; + mClock = clock; + mMainHandler = mainHandler; + mTvPipBoundsState = tvPipBoundsState; + mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; + + loadConfigurations(); + } + + private void loadConfigurations() { + final Resources res = mContext.getResources(); + mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); + mStashDurationMs = res.getInteger(R.integer.config_pipStashDuration); + } + + void setListener(PipBoundsListener listener) { + mListener = listener; + } + + /** + * Update the PiP bounds based on the state of the PiP, decors, and keep clear areas. + * Unless {@code immediate} is {@code true}, the PiP does not move immediately to avoid + * keep clear areas, but waits for a new position to stay uncontested for + * {@link #POSITION_DEBOUNCE_TIMEOUT_MILLIS} before moving to it. + * Temporary decor changes are applied immediately. + * + * @param stayAtAnchorPosition If true, PiP will be placed at the anchor position + * @param disallowStashing If true, PiP will not be placed off-screen in a stashed position + * @param animationDuration Duration of the animation to the new position + * @param immediate If true, PiP will move immediately to avoid keep clear areas + */ + @VisibleForTesting + void recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing, + int animationDuration, boolean immediate) { + final Placement placement = mTvPipBoundsAlgorithm.getTvPipPlacement(); + + final int stashType = disallowStashing ? STASH_TYPE_NONE : placement.getStashType(); + mTvPipBoundsState.setStashed(stashType); + if (stayAtAnchorPosition) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getAnchorBounds(), animationDuration); + } else if (disallowStashing) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getUnstashedBounds(), animationDuration); + } else if (immediate) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getBounds(), animationDuration); + scheduleUnstashIfNeeded(placement); + } else { + applyPlacementBounds(mCurrentPlacementBounds, animationDuration); + schedulePinnedStackPlacement(placement, animationDuration); + } + } + + private void schedulePinnedStackPlacement(@NonNull final Placement placement, + int animationDuration) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: schedulePinnedStackPlacement() - pip bounds: %s", + TAG, placement.getBounds().toShortString()); + } + + if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(), + placement.getBounds())) { + mPendingStash = mPendingStash || placement.getTriggerStash(); + return; + } + + mPendingStash = placement.getStashType() != STASH_TYPE_NONE + && (mPendingStash || placement.getTriggerStash()); + + mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable); + mPendingPlacement = placement; + mPendingPlacementAnimationDuration = animationDuration; + mMainHandler.postAtTime(mApplyPendingPlacementRunnable, + mClock.get() + POSITION_DEBOUNCE_TIMEOUT_MILLIS); + } + + private void scheduleUnstashIfNeeded(final Placement placement) { + if (mUnstashRunnable != null) { + mMainHandler.removeCallbacks(mUnstashRunnable); + mUnstashRunnable = null; + } + if (placement.getUnstashDestinationBounds() != null) { + mUnstashRunnable = () -> { + applyPlacementBounds(placement.getUnstashDestinationBounds(), + mResizeAnimationDuration); + mUnstashRunnable = null; + }; + mMainHandler.postAtTime(mUnstashRunnable, mClock.get() + mStashDurationMs); + } + } + + private void applyPendingPlacement() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: applyPendingPlacement()", TAG); + } + if (mPendingPlacement != null) { + if (mPendingStash) { + mPendingStash = false; + scheduleUnstashIfNeeded(mPendingPlacement); + } + + if (mUnstashRunnable != null) { + // currently stashed, use stashed pos + applyPlacementBounds(mPendingPlacement.getBounds(), + mPendingPlacementAnimationDuration); + } else { + applyPlacementBounds(mPendingPlacement.getUnstashedBounds(), + mPendingPlacementAnimationDuration); + } + } + + mPendingPlacement = null; + } + + void onPipDismissed() { + mCurrentPlacementBounds = null; + mPipTargetBounds = null; + cancelScheduledPlacement(); + } + + private void cancelScheduledPlacement() { + mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable); + mPendingPlacement = null; + + if (mUnstashRunnable != null) { + mMainHandler.removeCallbacks(mUnstashRunnable); + mUnstashRunnable = null; + } + } + + private void applyPlacementBounds(Rect bounds, int animationDuration) { + if (bounds == null) { + return; + } + + mCurrentPlacementBounds = bounds; + Rect adjustedBounds = mTvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(bounds); + movePipTo(adjustedBounds, animationDuration); + } + + /** Animates the PiP to the given bounds with the given animation duration. */ + private void movePipTo(Rect bounds, int animationDuration) { + if (Objects.equals(mPipTargetBounds, bounds)) { + return; + } + + mPipTargetBounds = bounds; + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString()); + } + + if (mListener != null) { + mListener.onPipTargetBoundsChange(bounds, animationDuration); + } + } + + /** + * Interface being notified of changes to the PiP bounds as calculated by + * @link TvPipBoundsController}. + */ + public interface PipBoundsListener { + /** + * Called when the calculated PiP bounds are changing. + * + * @param newTargetBounds The new bounds of the PiP. + * @param animationDuration The animation duration for the PiP movement. + */ + void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 8326588bbbad..fa48def9c7d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -25,12 +25,10 @@ import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.app.RemoteAction; import android.app.TaskInfo; -import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; -import android.os.Handler; import android.os.RemoteException; import android.view.Gravity; @@ -45,25 +43,25 @@ 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.PipAnimationController; -import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.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.List; +import java.util.Objects; import java.util.Set; /** * Manages the picture-in-picture (PIP) UI and states. */ public class TvPipController implements PipTransitionController.PipTransitionCallback, - TvPipMenuController.Delegate, TvPipNotificationController.Delegate, - DisplayController.OnDisplaysChangedListener { + TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate, + TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener { private static final String TAG = "TvPipController"; static final boolean DEBUG = false; @@ -97,18 +95,18 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final TvPipBoundsState mTvPipBoundsState; private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; + private final TvPipBoundsController mTvPipBoundsController; + private final PipAppOpsListener mAppOpsListener; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; private final TvPipNotificationController mPipNotificationController; 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. @@ -121,6 +119,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal Context context, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, + PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, @@ -130,12 +130,13 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShell, - ShellExecutor mainExecutor, - Handler mainHandler) { + ShellExecutor mainExecutor) { return new TvPipController( context, tvPipBoundsState, tvPipBoundsAlgorithm, + tvPipBoundsController, + pipAppOpsListener, pipTaskOrganizer, pipTransitionController, tvPipMenuController, @@ -145,14 +146,15 @@ public class TvPipController implements PipTransitionController.PipTransitionCal pipParamsChangedForwarder, displayController, wmShell, - mainExecutor, - mainHandler).mImpl; + mainExecutor).mImpl; } private TvPipController( Context context, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, + PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, @@ -162,16 +164,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShell, - ShellExecutor mainExecutor, - Handler mainHandler) { + ShellExecutor mainExecutor) { mContext = context; mMainExecutor = mainExecutor; - mMainHandler = mainHandler; mTvPipBoundsState = tvPipBoundsState; mTvPipBoundsState.setDisplayId(context.getDisplayId()); mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; + mTvPipBoundsController = tvPipBoundsController; + mTvPipBoundsController.setListener(this); mPipMediaController = pipMediaController; @@ -181,6 +183,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mTvPipMenuController = tvPipMenuController; mTvPipMenuController.setDelegate(this); + mAppOpsListener = pipAppOpsListener; mPipTaskOrganizer = pipTaskOrganizer; pipTransitionController.registerPipTransitionCallback(this); @@ -221,7 +224,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal /** * Starts the process if bringing up the Pip menu if by issuing a command to move Pip * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip - * task/window is properly positioned in {@link #onPipTransitionFinished(ComponentName, int)}. + * task/window is properly positioned in {@link #onPipTransitionFinished(int)}. */ @Override public void showPictureInPictureMenu() { @@ -250,7 +253,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: closeMenu(), state before=%s", TAG, stateToName(mState)); } setState(STATE_PIP); - mTvPipBoundsAlgorithm.keepUnstashedForCurrentKeepClearAreas(); updatePinnedStackBounds(); } @@ -287,6 +289,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding); mTvPipBoundsState.setTvPipExpanded(expanding); + mPipNotificationController.updateExpansionState(); + updatePinnedStackBounds(); } @@ -323,68 +327,35 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) { if (mTvPipBoundsState.getDisplayId() == displayId) { + boolean unrestrictedAreasChanged = !Objects.equals(unrestricted, + mTvPipBoundsState.getUnrestrictedKeepClearAreas()); mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted); - updatePinnedStackBounds(); + updatePinnedStackBounds(mResizeAnimationDuration, unrestrictedAreasChanged); } } private void updatePinnedStackBounds() { - updatePinnedStackBounds(mResizeAnimationDuration); + updatePinnedStackBounds(mResizeAnimationDuration, true); } /** * 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 updatePinnedStackBounds(int animationDuration) { + private void updatePinnedStackBounds(int animationDuration, boolean immediate) { if (mState == STATE_NO_PIP) { return; } - final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode(); final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition; - final Placement placement = mTvPipBoundsAlgorithm.getTvPipBounds(); - - int stashType = - disallowStashing ? PipBoundsState.STASH_TYPE_NONE : placement.getStashType(); - mTvPipBoundsState.setStashed(stashType); - - 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(), animationDuration); - }; - mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime()); - } + mTvPipBoundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing, + animationDuration, immediate); } - /** Animates the PiP to the given bounds. */ - private void movePinnedStackTo(Rect bounds) { - movePinnedStackTo(bounds, mResizeAnimationDuration); - } - - /** Animates the PiP to the given bounds with the given animation duration. */ - private void movePinnedStackTo(Rect bounds, int animationDuration) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString()); - } - mPipTaskOrganizer.scheduleAnimateResizePip(bounds, - animationDuration, rect -> { - mTvPipMenuController.updateExpansionState(); - }); - mTvPipMenuController.onPipTransitionStarted(bounds); + @Override + public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) { + mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds, + animationDuration, rect -> mTvPipMenuController.updateExpansionState()); + mTvPipMenuController.onPipTransitionStarted(newTargetBounds); } /** @@ -423,7 +394,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void closeEduText() { - updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs); + updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false); } private void registerSessionListenerForCurrentUser() { @@ -465,6 +436,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mPipNotificationController.dismiss(); mTvPipMenuController.closeMenu(); mTvPipBoundsState.resetTvPipState(); + mTvPipBoundsController.onPipDismissed(); setState(STATE_NO_PIP); mPinnedTaskId = NONEXISTENT_TASK_ID; } @@ -521,6 +493,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { checkIfPinnedTaskAppeared(); + mAppOpsListener.onActivityPinned(packageName); + } + + @Override + public void onActivityUnpinned() { + mAppOpsListener.onActivityUnpinned(); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt index 07dccd58abfd..1e54436ebce9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt @@ -33,17 +33,14 @@ 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) { +class TvPipKeepClearAlgorithm() { /** * Result of the positioning algorithm. * @@ -51,17 +48,17 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { * @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] + * @param unstashDestinationBounds If stashed, the PiP should move to this position when + * unstashing. + * @param triggerStash Whether this placement should trigger the PiP to stash, or extend + * the unstash timeout if already stashed. */ data class Placement( val bounds: Rect, val anchorBounds: Rect, @PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE, val unstashDestinationBounds: Rect? = null, - val unstashTime: Long = 0L + val triggerStash: Boolean = false ) { /** Bounds to use if the PiP should not be stashed. */ fun getUnstashedBounds() = unstashDestinationBounds ?: bounds @@ -79,12 +76,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { /** 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 @@ -93,14 +84,10 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { private var transformedMovementBounds = Rect() private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet() - private var lastStashTime: Long = Long.MIN_VALUE /** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP * decorations are relevant for calculating intersecting keep clear areas */ private var pipPermanentDecorInsets = Insets.NONE - /** Spaces around the PiP that we should leave space for when placing the PiP. Temporary PiP - * decorations are not relevant for calculating intersecting keep clear areas */ - private var pipTemporaryDecorInsets = Insets.NONE /** * Calculates the position the PiP should be placed at, taking into consideration the @@ -113,8 +100,8 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { * 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]. + * as [Placement.bounds], along with a position to move to when the PiP unstashes + * as [Placement.unstashDestinationBounds]. * * @param pipSize The size of the PiP window * @param restrictedAreas The restricted keep clear areas @@ -130,13 +117,11 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas) val pipSizeWithAllDecors = addDecors(pipSize) - val pipAnchorBoundsWithAllDecors = + val pipAnchorBoundsWithDecors = getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds) - val pipAnchorBoundsWithPermanentDecors = - removeTemporaryDecorsTransformed(pipAnchorBoundsWithAllDecors) val result = calculatePipPositionTransformed( - pipAnchorBoundsWithPermanentDecors, + pipAnchorBoundsWithDecors, transformedRestrictedAreas, transformedUnrestrictedAreas ) @@ -152,7 +137,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { anchorBounds, getStashType(pipBounds, unstashedDestBounds), unstashedDestBounds, - result.unstashTime + result.triggerStash ) } @@ -213,26 +198,13 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { !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, unstashBounds), unstashBounds, - unstashTime + areasOverlappingUnstashPositionChanged ) } @@ -439,14 +411,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { } /** - * 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 @@ -492,10 +456,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { pipPermanentDecorInsets = insets } - fun setPipTemporaryDecorInsets(insets: Insets) { - pipTemporaryDecorInsets = insets - } - /** * @param open Whether this event marks the opening of an occupied segment * @param pos The coordinate of this event @@ -790,7 +750,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { private fun addDecors(size: Size): Size { val bounds = Rect(0, 0, size.width, size.height) bounds.inset(pipPermanentDecorInsets) - bounds.inset(pipTemporaryDecorInsets) return Size(bounds.width(), bounds.height()) } @@ -805,19 +764,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { return bounds } - /** - * Removes the space that was reserved for temporary decorations around the PiP - * @param bounds the bounds (in base case) to remove the insets from - */ - private fun removeTemporaryDecorsTransformed(bounds: Rect): Rect { - if (pipTemporaryDecorInsets == Insets.NONE) return bounds - - val reverseInsets = Insets.subtract(Insets.NONE, pipTemporaryDecorInsets) - val boundsInScreenSpace = fromTransformedSpace(bounds) - boundsInScreenSpace.inset(reverseInsets) - return toTransformedSpace(boundsInScreenSpace) - } - private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) } private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom 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 132c04481bce..4ce45e142c64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -209,7 +209,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMovementMenuOnly()", TAG); } - mInMoveMode = true; + setInMoveMode(true); mCloseAfterExitMoveMenu = true; showMenuInternal(); } @@ -219,7 +219,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (DEBUG) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); } - mInMoveMode = false; + setInMoveMode(false); mCloseAfterExitMoveMenu = false; showMenuInternal(); } @@ -293,6 +293,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis return mInMoveMode; } + private void setInMoveMode(boolean moveMode) { + if (mInMoveMode == moveMode) { + return; + } + + mInMoveMode = moveMode; + if (mDelegate != null) { + mDelegate.onInMoveModeChanged(); + } + } + @Override public void onEnterMoveMode() { if (DEBUG) { @@ -300,7 +311,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, mCloseAfterExitMoveMenu); } - mInMoveMode = true; + setInMoveMode(true); mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); } @@ -312,13 +323,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mCloseAfterExitMoveMenu); } if (mCloseAfterExitMoveMenu) { - mInMoveMode = false; + setInMoveMode(false); mCloseAfterExitMoveMenu = false; closeMenu(); return true; } if (mInMoveMode) { - mInMoveMode = false; + setInMoveMode(false); mPipMenuView.showButtonsMenu(); return true; } 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 868e45655ba3..320c05c4a415 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -494,6 +494,14 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { setFrameHighlighted(false); } + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (!hasWindowFocus) { + hideAllUserControls(); + } + } + private void animateAlphaTo(float alpha, View view) { if (view.getAlpha() == alpha) { return; 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 4033f030b702..61a609d9755e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -16,36 +16,47 @@ package com.android.wm.shell.pip.tv; +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.RemoteAction; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; -import android.media.MediaMetadata; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.media.session.MediaSession; +import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.ImageUtils; import com.android.wm.shell.R; import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipParamsChangedForwarder; +import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.Objects; +import java.util.ArrayList; +import java.util.List; /** - * A notification that informs users that PIP is running and also provides PIP controls. - * <p>Once it's created, it will manage the PIP notification UI by itself except for handling - * configuration changes. + * A notification that informs users that PiP is running and also provides PiP controls. + * <p>Once it's created, it will manage the PiP notification UI by itself except for handling + * configuration changes and user initiated expanded PiP toggling. */ public class TvPipNotificationController { private static final String TAG = "TvPipNotification"; - private static final boolean DEBUG = TvPipController.DEBUG; // Referenced in com.android.systemui.util.NotificationChannels. public static final String NOTIFICATION_CHANNEL = "TVPIP"; @@ -60,6 +71,8 @@ public class TvPipNotificationController { "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; private static final String ACTION_TOGGLE_EXPANDED_PIP = "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; + private static final String ACTION_FULLSCREEN = + "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; private final Context mContext; private final PackageManager mPackageManager; @@ -68,44 +81,88 @@ public class TvPipNotificationController { private final ActionBroadcastReceiver mActionBroadcastReceiver; private final Handler mMainHandler; private Delegate mDelegate; + private final TvPipBoundsState mTvPipBoundsState; private String mDefaultTitle; + private final List<RemoteAction> mCustomActions = new ArrayList<>(); + private final List<RemoteAction> mMediaActions = new ArrayList<>(); + private RemoteAction mCustomCloseAction; + + private MediaSession.Token mMediaSessionToken; + /** Package name for the application that owns PiP window. */ private String mPackageName; - private boolean mNotified; - private String mMediaTitle; - private Bitmap mArt; + + private boolean mIsNotificationShown; + private String mPipTitle; + private String mPipSubtitle; + + private Bitmap mActivityIcon; public TvPipNotificationController(Context context, PipMediaController pipMediaController, + PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState, Handler mainHandler) { mContext = context; mPackageManager = context.getPackageManager(); mNotificationManager = context.getSystemService(NotificationManager.class); mMainHandler = mainHandler; + mTvPipBoundsState = tvPipBoundsState; mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL) .setLocalOnly(true) - .setOngoing(false) + .setOngoing(true) .setCategory(Notification.CATEGORY_SYSTEM) .setShowWhen(true) .setSmallIcon(R.drawable.pip_icon) + .setAllowSystemGeneratedContextualActions(false) + .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN)) + .setDeleteIntent(getCloseAction().actionIntent) .extend(new Notification.TvExtender() .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU)) .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP))); mActionBroadcastReceiver = new ActionBroadcastReceiver(); - pipMediaController.addMetadataListener(this::onMediaMetadataChanged); + pipMediaController.addActionListener(this::onMediaActionsChanged); + pipMediaController.addTokenListener(this::onMediaSessionTokenChanged); + + pipParamsChangedForwarder.addListener( + new PipParamsChangedForwarder.PipParamsChangedCallback() { + @Override + public void onExpandedAspectRatioChanged(float ratio) { + updateExpansionState(); + } + + @Override + public void onActionsChanged(List<RemoteAction> actions, + RemoteAction closeAction) { + mCustomActions.clear(); + mCustomActions.addAll(actions); + mCustomCloseAction = closeAction; + updateNotificationContent(); + } + + @Override + public void onTitleChanged(String title) { + mPipTitle = title; + updateNotificationContent(); + } + + @Override + public void onSubtitleChanged(String subtitle) { + mPipSubtitle = subtitle; + updateNotificationContent(); + } + }); onConfigurationChanged(context); } void setDelegate(Delegate delegate) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setDelegate(), delegate=%s", TAG, delegate); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s", + TAG, delegate); + if (mDelegate != null) { throw new IllegalStateException( "The delegate has already been set and should not change."); @@ -118,90 +175,181 @@ public class TvPipNotificationController { } void show(String packageName) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName); if (mDelegate == null) { throw new IllegalStateException("Delegate is not set."); } + mIsNotificationShown = true; mPackageName = packageName; - update(); + mActivityIcon = getActivityIcon(); mActionBroadcastReceiver.register(); + + updateNotificationContent(); } void dismiss() { - mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); - mNotified = false; + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: dismiss()", TAG); + + mIsNotificationShown = false; mPackageName = null; mActionBroadcastReceiver.unregister(); + + mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); } - private void onMediaMetadataChanged(MediaMetadata metadata) { - if (updateMediaControllerMetadata(metadata) && mNotified) { - // update notification - update(); + private Notification.Action getToggleAction(boolean expanded) { + if (expanded) { + return createSystemAction(R.drawable.pip_ic_collapse, + R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP); + } else { + return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand, + ACTION_TOGGLE_EXPANDED_PIP); } } - /** - * Called by {@link PipController} when the configuration is changed. - */ - void onConfigurationChanged(Context context) { - mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title); - if (mNotified) { - // Update the notification. - update(); + private Notification.Action createSystemAction(int iconRes, int titleRes, String action) { + Notification.Action.Builder builder = new Notification.Action.Builder( + Icon.createWithResource(mContext, iconRes), + mContext.getString(titleRes), + createPendingIntent(mContext, action)); + builder.setContextual(true); + return builder.build(); + } + + private void onMediaActionsChanged(List<RemoteAction> actions) { + mMediaActions.clear(); + mMediaActions.addAll(actions); + if (mCustomActions.isEmpty()) { + updateNotificationContent(); } } - private void update() { - mNotified = true; - mNotificationBuilder - .setWhen(System.currentTimeMillis()) - .setContentTitle(getNotificationTitle()); - if (mArt != null) { - mNotificationBuilder.setStyle(new Notification.BigPictureStyle() - .bigPicture(mArt)); - } else { - mNotificationBuilder.setStyle(null); + private void onMediaSessionTokenChanged(MediaSession.Token token) { + mMediaSessionToken = token; + updateNotificationContent(); + } + + private Notification.Action remoteToNotificationAction(RemoteAction action) { + return remoteToNotificationAction(action, SEMANTIC_ACTION_NONE); + } + + private Notification.Action remoteToNotificationAction(RemoteAction action, + int semanticAction) { + Notification.Action.Builder builder = new Notification.Action.Builder(action.getIcon(), + action.getTitle(), + action.getActionIntent()); + if (action.getContentDescription() != null) { + Bundle extras = new Bundle(); + extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, + action.getContentDescription()); + builder.addExtras(extras); } - mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, - mNotificationBuilder.build()); + builder.setSemanticAction(semanticAction); + builder.setContextual(true); + return builder.build(); } - private boolean updateMediaControllerMetadata(MediaMetadata metadata) { - String title = null; - Bitmap art = null; - if (metadata != null) { - title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE); - if (TextUtils.isEmpty(title)) { - title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE); - } - art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - if (art == null) { - art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART); + private Notification.Action[] getNotificationActions() { + final List<Notification.Action> actions = new ArrayList<>(); + + // 1. Fullscreen + actions.add(getFullscreenAction()); + // 2. Close + actions.add(getCloseAction()); + // 3. App actions + final List<RemoteAction> appActions = + mCustomActions.isEmpty() ? mMediaActions : mCustomActions; + for (RemoteAction appAction : appActions) { + if (PipUtils.remoteActionsMatch(mCustomCloseAction, appAction) + || !appAction.isEnabled()) { + continue; } + actions.add(remoteToNotificationAction(appAction)); + } + // 4. Move + actions.add(getMoveAction()); + // 5. Toggle expansion (if expanded PiP enabled) + if (mTvPipBoundsState.getDesiredTvExpandedAspectRatio() > 0 + && mTvPipBoundsState.isTvExpandedPipSupported()) { + actions.add(getToggleAction(mTvPipBoundsState.isTvPipExpanded())); } + return actions.toArray(new Notification.Action[0]); + } - if (TextUtils.equals(title, mMediaTitle) && Objects.equals(art, mArt)) { - return false; + private Notification.Action getCloseAction() { + if (mCustomCloseAction == null) { + return createSystemAction(R.drawable.pip_ic_close_white, R.string.pip_close, + ACTION_CLOSE_PIP); + } else { + return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE); } + } + + private Notification.Action getFullscreenAction() { + return createSystemAction(R.drawable.pip_ic_fullscreen_white, + R.string.pip_fullscreen, ACTION_FULLSCREEN); + } - mMediaTitle = title; - mArt = art; + private Notification.Action getMoveAction() { + return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move, + ACTION_MOVE_PIP); + } - return true; + /** + * Called by {@link TvPipController} when the configuration is changed. + */ + void onConfigurationChanged(Context context) { + mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title); + updateNotificationContent(); } + void updateExpansionState() { + updateNotificationContent(); + } - private String getNotificationTitle() { - if (!TextUtils.isEmpty(mMediaTitle)) { - return mMediaTitle; + private void updateNotificationContent() { + if (mPackageManager == null || !mIsNotificationShown) { + return; + } + + Notification.Action[] actions = getNotificationActions(); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: update(), title: %s, subtitle: %s, mediaSessionToken: %s, #actions: %s", TAG, + getNotificationTitle(), mPipSubtitle, mMediaSessionToken, actions.length); + for (Notification.Action action : actions) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: action: %s", TAG, + action.toString()); } + mNotificationBuilder + .setWhen(System.currentTimeMillis()) + .setContentTitle(getNotificationTitle()) + .setContentText(mPipSubtitle) + .setSubText(getApplicationLabel(mPackageName)) + .setActions(actions); + setPipIcon(); + + Bundle extras = new Bundle(); + extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken); + mNotificationBuilder.setExtras(extras); + + // TvExtender not recognized if not set last. + mNotificationBuilder.extend(new Notification.TvExtender() + .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU)) + .setDeleteIntent(createPendingIntent(mContext, ACTION_CLOSE_PIP))); + mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, + mNotificationBuilder.build()); + } + + private String getNotificationTitle() { + if (!TextUtils.isEmpty(mPipTitle)) { + return mPipTitle; + } final String applicationTitle = getApplicationLabel(mPackageName); if (!TextUtils.isEmpty(applicationTitle)) { return applicationTitle; } - return mDefaultTitle; } @@ -214,10 +362,37 @@ public class TvPipNotificationController { } } + private void setPipIcon() { + if (mActivityIcon != null) { + mNotificationBuilder.setLargeIcon(mActivityIcon); + return; + } + // Fallback: Picture-in-Picture icon + mNotificationBuilder.setLargeIcon(Icon.createWithResource(mContext, R.drawable.pip_icon)); + } + + private Bitmap getActivityIcon() { + if (mContext == null) return null; + ComponentName componentName = PipUtils.getTopPipActivity(mContext).first; + if (componentName == null) return null; + + Drawable drawable; + try { + drawable = mPackageManager.getActivityIcon(componentName); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + int width = mContext.getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + int height = mContext.getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_height); + return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true); + } + private static PendingIntent createPendingIntent(Context context, String action) { return PendingIntent.getBroadcast(context, 0, new Intent(action).setPackage(context.getPackageName()), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } private class ActionBroadcastReceiver extends BroadcastReceiver { @@ -228,6 +403,7 @@ public class TvPipNotificationController { mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); mIntentFilter.addAction(ACTION_MOVE_PIP); mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); + mIntentFilter.addAction(ACTION_FULLSCREEN); } boolean mRegistered = false; @@ -249,10 +425,8 @@ public class TvPipNotificationController { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: on(Broadcast)Receive(), action=%s", TAG, action); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: on(Broadcast)Receive(), action=%s", TAG, action); if (ACTION_SHOW_PIP_MENU.equals(action)) { mDelegate.showPictureInPictureMenu(); @@ -262,14 +436,21 @@ public class TvPipNotificationController { mDelegate.enterPipMovementMenu(); } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) { mDelegate.togglePipExpansion(); + } else if (ACTION_FULLSCREEN.equals(action)) { + mDelegate.movePipToFullscreen(); } } } interface Delegate { void showPictureInPictureMenu(); + void closePip(); + void enterPipMovementMenu(); + void togglePipExpansion(); + + void movePipToFullscreen(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 64017e176fc3..d04c34916256 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -40,6 +40,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; 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 9adf1961ebf5..51921e747f1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -98,11 +98,14 @@ interface ISplitScreen { /** * Blocking call that notifies and gets additional split-screen targets when entering * recents (for example: the dividerBar). - * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled). * @param appTargets apps that will be re-parented to display area */ - RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, - in RemoteAnimationTarget[] appTargets) = 13; - + RemoteAnimationTarget[] onGoingToRecentsLegacy(in RemoteAnimationTarget[] appTargets) = 13; + /** + * Blocking call that notifies and gets additional split-screen targets when entering + * recents (for example: the dividerBar). Different than the method above in that this one + * does not expect split to currently be running. + */ + RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index dd2634ca36d9..31b510c38457 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -413,9 +413,29 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); } - RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) { - if (ENABLE_SHELL_TRANSITIONS || !isSplitScreenVisible()) return null; + RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) { + if (isSplitScreenVisible()) { + // Evict child tasks except the top visible one under split root to ensure it could be + // launched as full screen when switching to it on recents. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictInvisibleChildTasks(wct); + mSyncQueue.queue(wct); + } + return reparentSplitTasksForAnimation(apps, true /*splitExpectedToBeVisible*/); + } + + RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { + return reparentSplitTasksForAnimation(apps, false /*splitExpectedToBeVisible*/); + } + + private RemoteAnimationTarget[] reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps, + boolean splitExpectedToBeVisible) { + if (ENABLE_SHELL_TRANSITIONS) return null; // TODO(b/206487881): Integrate this with shell transition. + if (splitExpectedToBeVisible && !isSplitScreenVisible()) return null; + // Split not visible, but not enough apps to have split, also return null + if (!splitExpectedToBeVisible && apps.length < 2) return null; + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); if (mSplitTasksContainerLayer != null) { // Remove the previous layer before recreating @@ -442,7 +462,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, transaction.close(); return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()}; } - /** * Sets drag info to be logged when splitscreen is entered. */ @@ -707,11 +726,19 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, - RemoteAnimationTarget[] apps) { + public RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) { final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy", - (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps), + (controller) -> out[0] = controller.onGoingToRecentsLegacy(apps), + true /* blocking */); + return out[0]; + } + + @Override + public RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { + final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; + executeRemoteCallWithTaskPermission(mController, "onStartingSplitLegacy", + (controller) -> out[0] = controller.onStartingSplitLegacy(apps), true /* blocking */); return out[0]; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 91f9d2522397..7ea32a6d8f86 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 @@ -54,6 +54,9 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_P import static com.android.wm.shell.transition.Transitions.isClosingType; import static com.android.wm.shell.transition.Transitions.isOpeningType; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; @@ -68,6 +71,7 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; +import android.os.Debug; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; @@ -147,7 +151,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final int mDisplayId; private SplitLayout mSplitLayout; + private ValueAnimator mDividerFadeInAnimator; private boolean mDividerVisible; + private boolean mKeyguardShowing; private final SyncTransactionQueue mSyncQueue; private final ShellTaskOrganizer mTaskOrganizer; private final Context mContext; @@ -404,6 +410,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.init(); // Set false to avoid record new bounds with old task still on top; mShouldUpdateRecents = false; + mIsDividerRemoteAnimating = true; final WindowContainerTransaction wct = new WindowContainerTransaction(); final WindowContainerTransaction evictWct = new WindowContainerTransaction(); prepareEvictChildTasks(SPLIT_POSITION_TOP_OR_LEFT, evictWct); @@ -417,7 +424,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, final IRemoteAnimationFinishedCallback finishedCallback) { - mIsDividerRemoteAnimating = true; RemoteAnimationTarget[] augmentedNonApps = new RemoteAnimationTarget[nonApps.length + 1]; for (int i = 0; i < nonApps.length; ++i) { @@ -494,8 +500,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } // Using legacy transitions, so we can't use blast sync since it conflicts. mTaskOrganizer.applyTransaction(wct); - mSyncQueue.runInSync(t -> - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */)); + mSyncQueue.runInSync(t -> { + setDividerVisibility(true, t); + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + }); } private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) { @@ -510,10 +518,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); } else { mSyncQueue.queue(evictWct); - mSyncQueue.runInSync(t -> { - setDividerVisibility(true, t); - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); - }); } } @@ -529,6 +533,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + void prepareEvictInvisibleChildTasks(WindowContainerTransaction wct) { + mMainStage.evictInvisibleChildren(wct); + mSideStage.evictInvisibleChildren(wct); + } + Bundle resolveStartStage(@StageType int stage, @SplitPosition int position, @androidx.annotation.Nullable Bundle options, @androidx.annotation.Nullable WindowContainerTransaction wct) { @@ -623,16 +632,12 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } void onKeyguardVisibilityChanged(boolean showing) { + mKeyguardShowing = showing; if (!mMainStage.isActive()) { return; } - if (ENABLE_SHELL_TRANSITIONS) { - // Update divider visibility so it won't float on top of keyguard. - setDividerVisibility(!showing, null /* transaction */); - } - - if (!showing && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { + if (!mKeyguardShowing && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { if (ENABLE_SHELL_TRANSITIONS) { final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct); @@ -643,7 +648,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, EXIT_REASON_DEVICE_FOLDED); } + return; } + + setDividerVisibility(!mKeyguardShowing, null); } void onFinishedWakingUp() { @@ -727,6 +735,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, setResizingSplits(false /* resizing */); t.setWindowCrop(mMainStage.mRootLeash, null) .setWindowCrop(mSideStage.mRootLeash, null); + setDividerVisibility(false, t); }); // Hide divider and reset its position. @@ -976,6 +985,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateUnfoldBounds(); return; } + // Clear the divider remote animating flag as the divider will be re-rendered to apply + // the new rotation config. + mIsDividerRemoteAnimating = false; mSplitLayout.update(null /* t */); onLayoutSizeChanged(mSplitLayout); } @@ -1055,8 +1067,31 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } private void setDividerVisibility(boolean visible, @Nullable SurfaceControl.Transaction t) { + if (visible == mDividerVisible) { + return; + } + + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Request to %s divider bar from %s.", TAG, + (visible ? "show" : "hide"), Debug.getCaller()); + + // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard + // dismissing animation. + if (visible && mKeyguardShowing) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Defer showing divider bar due to keyguard showing.", TAG); + return; + } + mDividerVisible = visible; sendSplitVisibilityChanged(); + + if (mIsDividerRemoteAnimating) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Skip animating divider bar due to it's remote animating.", TAG); + return; + } + if (t != null) { applyDividerVisibility(t); } else { @@ -1066,15 +1101,56 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void applyDividerVisibility(SurfaceControl.Transaction t) { final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); - if (mIsDividerRemoteAnimating || dividerLeash == null) return; + if (dividerLeash == null) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Skip animating divider bar due to divider leash not ready.", TAG); + return; + } + if (mIsDividerRemoteAnimating) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Skip animating divider bar due to it's remote animating.", TAG); + return; + } + + if (mDividerFadeInAnimator != null && mDividerFadeInAnimator.isRunning()) { + mDividerFadeInAnimator.cancel(); + } if (mDividerVisible) { - t.show(dividerLeash); - t.setAlpha(dividerLeash, 1); - t.setLayer(dividerLeash, Integer.MAX_VALUE); - t.setPosition(dividerLeash, - mSplitLayout.getRefDividerBounds().left, - mSplitLayout.getRefDividerBounds().top); + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + mDividerFadeInAnimator = ValueAnimator.ofFloat(0f, 1f); + mDividerFadeInAnimator.addUpdateListener(animation -> { + if (dividerLeash == null || !dividerLeash.isValid()) { + mDividerFadeInAnimator.cancel(); + return; + } + transaction.setAlpha(dividerLeash, (float) animation.getAnimatedValue()); + transaction.apply(); + }); + mDividerFadeInAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (dividerLeash == null || !dividerLeash.isValid()) { + mDividerFadeInAnimator.cancel(); + return; + } + transaction.show(dividerLeash); + transaction.setAlpha(dividerLeash, 0); + transaction.setLayer(dividerLeash, Integer.MAX_VALUE); + transaction.setPosition(dividerLeash, + mSplitLayout.getRefDividerBounds().left, + mSplitLayout.getRefDividerBounds().top); + transaction.apply(); + } + + @Override + public void onAnimationEnd(Animator animation) { + mTransactionPool.release(transaction); + mDividerFadeInAnimator = null; + } + }); + + mDividerFadeInAnimator.start(); } else { t.hide(dividerLeash); } @@ -1096,10 +1172,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.init(); prepareEnterSplitScreen(wct); mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); - setDividerVisibility(true, t); - }); + mSyncQueue.runInSync(t -> + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */)); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { mShouldUpdateRecents = true; 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 9fd5d2003873..949bf5f55808 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -224,14 +224,12 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { if (mRootTaskInfo.taskId == taskInfo.taskId) { // Inflates split decor view only when the root task is visible. if (mRootTaskInfo.isVisible != taskInfo.isVisible) { - mSyncQueue.runInSync(t -> { - if (taskInfo.isVisible) { - mSplitDecorManager.inflate(mContext, mRootLeash, - taskInfo.configuration.windowConfiguration.getBounds()); - } else { - mSplitDecorManager.release(t); - } - }); + if (taskInfo.isVisible) { + mSplitDecorManager.inflate(mContext, mRootLeash, + taskInfo.configuration.windowConfiguration.getBounds()); + } else { + mSyncQueue.runInSync(t -> mSplitDecorManager.release(t)); + } } mRootTaskInfo = taskInfo; } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { @@ -343,6 +341,15 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + void evictInvisibleChildren(WindowContainerTransaction wct) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) { + final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i); + if (!taskInfo.isVisible) { + wct.reparent(taskInfo.token, null /* parent */, false /* onTop */); + } + } + } + void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, @StageType int stage) { for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { 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 d89ddd2074f0..8cee4f1dc8fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -35,6 +35,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -53,6 +55,7 @@ import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; import android.util.ArrayMap; +import android.util.DisplayMetrics; import android.util.Slog; import android.view.ContextThemeWrapper; import android.view.SurfaceControl; @@ -68,7 +71,6 @@ 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; @@ -102,7 +104,7 @@ public class SplashscreenContentDrawer { */ private static final float NO_BACKGROUND_SCALE = 192f / 160; private final Context mContext; - private final IconProvider mIconProvider; + private final HighResIconProvider mHighResIconProvider; private int mIconSize; private int mDefaultIconSize; @@ -115,12 +117,10 @@ public class SplashscreenContentDrawer { private final Handler mSplashscreenWorkerHandler; @VisibleForTesting final ColorCache mColorCache; - private final ShellExecutor mSplashScreenExecutor; - SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool, - ShellExecutor splashScreenExecutor) { + SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) { mContext = context; - mIconProvider = iconProvider; + mHighResIconProvider = new HighResIconProvider(mContext, iconProvider); mTransactionPool = pool; // Initialize Splashscreen worker thread @@ -131,7 +131,6 @@ public class SplashscreenContentDrawer { shellSplashscreenWorkerThread.start(); mSplashscreenWorkerHandler = shellSplashscreenWorkerThread.getThreadHandler(); mColorCache = new ColorCache(mContext, mSplashscreenWorkerHandler); - mSplashScreenExecutor = splashScreenExecutor; } /** @@ -416,18 +415,16 @@ public class SplashscreenContentDrawer { || mTmpAttrs.mIconBgColor == mThemeColor) { mFinalIconSize *= NO_BACKGROUND_SCALE; } - createIconDrawable(iconDrawable, false); + createIconDrawable(iconDrawable, false /* legacy */, false /* loadInDetail */); } else { final float iconScale = (float) mIconSize / (float) mDefaultIconSize; final int densityDpi = mContext.getResources().getConfiguration().densityDpi; final int scaledIconDpi = (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon"); - iconDrawable = mIconProvider.getIcon(mActivityInfo, scaledIconDpi); + iconDrawable = mHighResIconProvider.getIcon( + mActivityInfo, densityDpi, scaledIconDpi); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - if (iconDrawable == null) { - iconDrawable = mContext.getPackageManager().getDefaultActivityIcon(); - } if (!processAdaptiveIcon(iconDrawable)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "The icon is not an AdaptiveIconDrawable"); @@ -437,7 +434,8 @@ public class SplashscreenContentDrawer { scaledIconDpi, mFinalIconSize); final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - createIconDrawable(new BitmapDrawable(bitmap), true); + createIconDrawable(new BitmapDrawable(bitmap), true, + mHighResIconProvider.mLoadInDetail); } } @@ -450,14 +448,16 @@ public class SplashscreenContentDrawer { } } - private void createIconDrawable(Drawable iconDrawable, boolean legacy) { + private void createIconDrawable(Drawable iconDrawable, boolean legacy, + boolean loadInDetail) { if (legacy) { mFinalIconDrawables = SplashscreenIconDrawableFactory.makeLegacyIconDrawable( - iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler); + iconDrawable, mDefaultIconSize, mFinalIconSize, loadInDetail, + mSplashscreenWorkerHandler); } else { mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable( mTmpAttrs.mIconBgColor, mThemeColor, iconDrawable, mDefaultIconSize, - mFinalIconSize, mSplashscreenWorkerHandler); + mFinalIconSize, loadInDetail, mSplashscreenWorkerHandler); } } @@ -506,11 +506,11 @@ public class SplashscreenContentDrawer { // Using AdaptiveIconDrawable here can help keep the shape consistent with the // current settings. mFinalIconSize = (int) (0.5f + mIconSize * noBgScale); - createIconDrawable(iconForeground, false); + createIconDrawable(iconForeground, false, mHighResIconProvider.mLoadInDetail); } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "processAdaptiveIcon: draw whole icon"); - createIconDrawable(iconDrawable, false); + createIconDrawable(iconDrawable, false, mHighResIconProvider.mLoadInDetail); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); return true; @@ -1015,4 +1015,77 @@ public class SplashscreenContentDrawer { playAnimation.run(); } } + + /** + * When loading a BitmapDrawable object with specific density, there will decode the image based + * on the density from display metrics, so even when load with higher override density, the + * final intrinsic size of a BitmapDrawable can still not big enough to draw on expect size. + * + * So here we use a standalone IconProvider object to load the Drawable object for higher + * density, and the resources object won't affect the entire system. + * + */ + private static class HighResIconProvider { + private final Context mSharedContext; + private final IconProvider mSharedIconProvider; + private boolean mLoadInDetail; + + // only create standalone icon provider when the density dpi is low. + private Context mStandaloneContext; + private IconProvider mStandaloneIconProvider; + + HighResIconProvider(Context context, IconProvider sharedIconProvider) { + mSharedContext = context; + mSharedIconProvider = sharedIconProvider; + } + + Drawable getIcon(ActivityInfo activityInfo, int currentDpi, int iconDpi) { + mLoadInDetail = false; + Drawable drawable; + if (currentDpi < iconDpi && currentDpi < DisplayMetrics.DENSITY_XHIGH) { + drawable = loadFromStandalone(activityInfo, currentDpi, iconDpi); + } else { + drawable = mSharedIconProvider.getIcon(activityInfo, iconDpi); + } + + if (drawable == null) { + drawable = mSharedContext.getPackageManager().getDefaultActivityIcon(); + } + return drawable; + } + + private Drawable loadFromStandalone(ActivityInfo activityInfo, int currentDpi, + int iconDpi) { + if (mStandaloneContext == null) { + final Configuration defConfig = mSharedContext.getResources().getConfiguration(); + mStandaloneContext = mSharedContext.createConfigurationContext(defConfig); + mStandaloneIconProvider = new IconProvider(mStandaloneContext); + } + Resources resources; + try { + resources = mStandaloneContext.getPackageManager() + .getResourcesForApplication(activityInfo.applicationInfo); + } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { + resources = null; + } + if (resources != null) { + updateResourcesDpi(resources, iconDpi); + } + final Drawable drawable = mStandaloneIconProvider.getIcon(activityInfo, iconDpi); + mLoadInDetail = true; + // reset density dpi + if (resources != null) { + updateResourcesDpi(resources, currentDpi); + } + return drawable; + } + + private void updateResourcesDpi(Resources resources, int densityDpi) { + final Configuration config = resources.getConfiguration(); + final DisplayMetrics displayMetrics = resources.getDisplayMetrics(); + config.densityDpi = densityDpi; + displayMetrics.densityDpi = densityDpi; + resources.updateConfiguration(config, displayMetrics); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java index 5f52071bf950..7f6bfd23f72b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java @@ -62,7 +62,7 @@ public class SplashscreenIconDrawableFactory { */ static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor, @NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize, - Handler splashscreenWorkerHandler) { + boolean loadInDetail, Handler splashscreenWorkerHandler) { Drawable foreground; Drawable background = null; boolean drawBackground = @@ -74,13 +74,13 @@ public class SplashscreenIconDrawableFactory { // If the icon is Adaptive, we already use the icon background. drawBackground = false; foreground = new ImmobileIconDrawable(foregroundDrawable, - srcIconSize, iconSize, splashscreenWorkerHandler); + srcIconSize, iconSize, loadInDetail, splashscreenWorkerHandler); } else { // Adaptive icon don't handle transparency so we draw the background of the adaptive // icon with the same color as the window background color instead of using two layers foreground = new ImmobileIconDrawable( new AdaptiveForegroundDrawable(foregroundDrawable), - srcIconSize, iconSize, splashscreenWorkerHandler); + srcIconSize, iconSize, loadInDetail, splashscreenWorkerHandler); } if (drawBackground) { @@ -91,9 +91,9 @@ public class SplashscreenIconDrawableFactory { } static Drawable[] makeLegacyIconDrawable(@NonNull Drawable iconDrawable, int srcIconSize, - int iconSize, Handler splashscreenWorkerHandler) { + int iconSize, boolean loadInDetail, Handler splashscreenWorkerHandler) { return new Drawable[]{new ImmobileIconDrawable(iconDrawable, srcIconSize, iconSize, - splashscreenWorkerHandler)}; + loadInDetail, splashscreenWorkerHandler)}; } /** @@ -106,11 +106,16 @@ public class SplashscreenIconDrawableFactory { private final Matrix mMatrix = new Matrix(); private Bitmap mIconBitmap; - ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, + ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, boolean loadInDetail, Handler splashscreenWorkerHandler) { - final float scale = (float) iconSize / srcIconSize; - mMatrix.setScale(scale, scale); - splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, srcIconSize)); + // This icon has lower density, don't scale it. + if (loadInDetail) { + splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, iconSize)); + } else { + final float scale = (float) iconSize / srcIconSize; + mMatrix.setScale(scale, scale); + splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, srcIconSize)); + } } private void preDrawIcon(Drawable drawable, int size) { 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 464ab1ae2a8c..54d62edf2570 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -153,8 +153,7 @@ public class StartingSurfaceDrawer { mContext = context; mDisplayManager = mContext.getSystemService(DisplayManager.class); mSplashScreenExecutor = splashScreenExecutor; - mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool, - mSplashScreenExecutor); + mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool); mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); mWindowManagerGlobal = WindowManagerGlobal.getInstance(); mDisplayManager.getDisplay(DEFAULT_DISPLAY); 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 cf4ea467a29b..41cd31aabf05 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt @@ -37,7 +37,7 @@ class AppPairsHelper( val displayBounds = WindowUtils.displayBounds val secondaryAppBounds = Region.from(0, dividerBounds.bounds.bottom - WindowUtils.dockedStackDividerInset, - displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) + displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarFrameHeight) return secondaryAppBounds } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt index a510d699387e..e2da1a4565c0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt @@ -171,7 +171,7 @@ class ResizeLegacySplitScreen( val bottomAppBounds = Region.from(0, dividerBounds.bottom - WindowUtils.dockedStackDividerInset, displayBounds.right, - displayBounds.bottom - WindowUtils.navigationBarHeight) + displayBounds.bottom - WindowUtils.navigationBarFrameHeight) visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) .coversExactly(topAppBounds) visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent()) @@ -192,7 +192,7 @@ class ResizeLegacySplitScreen( val bottomAppBounds = Region.from(0, dividerBounds.bottom - WindowUtils.dockedStackDividerInset, displayBounds.right, - displayBounds.bottom - WindowUtils.navigationBarHeight) + displayBounds.bottom - WindowUtils.navigationBarFrameHeight) visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) .coversExactly(topAppBounds) 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 9f3fcea241e4..28b7fc9bd29e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt @@ -123,6 +123,18 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition } } + @Presubmit + @Test + fun pipSameAspectRatio() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.isSameAspectRatio(previous.visibleRegion) + } + } + } + /** * Checks [pipApp] window remains pinned throughout the animation */ diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index fb53e5355c4a..ea10be564351 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -37,12 +37,14 @@ android_test { "androidx.test.ext.junit", "androidx.dynamicanimation_dynamicanimation", "dagger2", + "frameworks-base-testutils", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "mockito-target-extended-minus-junit4", "truth-prebuilt", "testables", "platform-test-annotations", + "frameworks-base-testutils", ], libs: [ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index a905dcaebc6b..fcfcbfa091db 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -20,23 +20,32 @@ import static android.window.BackNavigationInfo.KEY_TRIGGER_BACK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import android.app.IActivityTaskManager; import android.app.WindowConfiguration; -import android.content.Context; +import android.content.pm.ApplicationInfo; import android.graphics.Point; import android.graphics.Rect; import android.hardware.HardwareBuffer; +import android.os.Handler; import android.os.RemoteCallback; import android.os.RemoteException; +import android.provider.Settings; import android.testing.AndroidTestingRunner; +import android.testing.TestableContentResolver; +import android.testing.TestableContext; +import android.testing.TestableLooper; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; @@ -45,12 +54,14 @@ import android.window.BackNavigationInfo; import android.window.IOnBackInvokedCallback; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.util.test.FakeSettingsProvider; import com.android.wm.shell.TestShellExecutor; -import com.android.wm.shell.common.ShellExecutor; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -60,14 +71,17 @@ import org.mockito.MockitoAnnotations; /** * atest WMShellUnitTests:BackAnimationControllerTest */ +@TestableLooper.RunWithLooper @SmallTest @RunWith(AndroidTestingRunner.class) public class BackAnimationControllerTest { - private final ShellExecutor mShellExecutor = new TestShellExecutor(); + private static final String ANIMATION_ENABLED = "1"; + private final TestShellExecutor mShellExecutor = new TestShellExecutor(); - @Mock - private Context mContext; + @Rule + public TestableContext mContext = + new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); @Mock private SurfaceControl.Transaction mTransaction; @@ -80,18 +94,32 @@ public class BackAnimationControllerTest { private BackAnimationController mController; + private int mEventTime = 0; + private TestableContentResolver mContentResolver; + private TestableLooper mTestableLooper; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mContext.getApplicationInfo().privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED; + mContentResolver = new TestableContentResolver(mContext); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, + ANIMATION_ENABLED); + mTestableLooper = TestableLooper.get(this); mController = new BackAnimationController( - mShellExecutor, mTransaction, mActivityTaskManager, mContext); - mController.setEnableAnimations(true); + mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, + mActivityTaskManager, mContext, + mContentResolver); + mEventTime = 0; + mShellExecutor.flushAll(); } private void createNavigationInfo(RemoteAnimationTarget topAnimationTarget, SurfaceControl screenshotSurface, HardwareBuffer hardwareBuffer, - int backType) { + int backType, + IOnBackInvokedCallback onBackInvokedCallback) { BackNavigationInfo navigationInfo = new BackNavigationInfo( backType, topAnimationTarget, @@ -99,9 +127,9 @@ public class BackAnimationControllerTest { hardwareBuffer, new WindowConfiguration(), new RemoteCallback((bundle) -> {}), - null); + onBackInvokedCallback); try { - doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation(); + doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation(anyBoolean()); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } @@ -109,7 +137,7 @@ public class BackAnimationControllerTest { private void createNavigationInfo(BackNavigationInfo.Builder builder) { try { - doReturn(builder.build()).when(mActivityTaskManager).startBackNavigation(); + doReturn(builder.build()).when(mActivityTaskManager).startBackNavigation(anyBoolean()); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } @@ -124,15 +152,10 @@ public class BackAnimationControllerTest { } private void triggerBackGesture() { - MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); - mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT); - - event = MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0); - mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT); - + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + doMotionEvent(MotionEvent.ACTION_MOVE, 0); mController.setTriggerBack(true); - event = MotionEvent.obtain(10, 0, MotionEvent.ACTION_UP, 100, 100, 0); - mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT); + doMotionEvent(MotionEvent.ACTION_UP, 0); } @Test @@ -141,11 +164,8 @@ public class BackAnimationControllerTest { 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), - MotionEvent.ACTION_DOWN, - BackEvent.EDGE_LEFT); + BackNavigationInfo.TYPE_CROSS_ACTIVITY, null); + doMotionEvent(MotionEvent.ACTION_DOWN, 0); verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer); verify(mTransaction).setVisibility(screenshotSurface, true); verify(mTransaction).apply(); @@ -157,19 +177,14 @@ public class BackAnimationControllerTest { 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), - MotionEvent.ACTION_DOWN, - BackEvent.EDGE_LEFT); - mController.onMotionEvent( - MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0), - MotionEvent.ACTION_MOVE, - BackEvent.EDGE_LEFT); + BackNavigationInfo.TYPE_CROSS_ACTIVITY, null); + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + doMotionEvent(MotionEvent.ACTION_MOVE, 100); // b/207481538, we check that the surface is not moved for now, we can re-enable this once // we implement the animation verify(mTransaction, never()).setScale(eq(screenshotSurface), anyInt(), anyInt()); - verify(mTransaction, never()).setPosition(animationTarget.leash, 100, 100); + verify(mTransaction, never()).setPosition( + animationTarget.leash, 100, 100); verify(mTransaction, atLeastOnce()).apply(); } @@ -196,30 +211,96 @@ public class BackAnimationControllerTest { mController.setBackToLauncherCallback(mIOnBackInvokedCallback); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME); + BackNavigationInfo.TYPE_RETURN_TO_HOME, null); - // Check that back start is dispatched. - mController.onMotionEvent( - MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0), - MotionEvent.ACTION_DOWN, - BackEvent.EDGE_LEFT); - verify(mIOnBackInvokedCallback).onBackStarted(); + doMotionEvent(MotionEvent.ACTION_DOWN, 0); - // Check that back progress is dispatched. - mController.onMotionEvent( - MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0), - MotionEvent.ACTION_MOVE, - BackEvent.EDGE_LEFT); + // Check that back start and progress is dispatched when first move. + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + verify(mIOnBackInvokedCallback).onBackStarted(); ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); verify(mIOnBackInvokedCallback).onBackProgressed(backEventCaptor.capture()); assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget()); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back + doMotionEvent(MotionEvent.ACTION_UP, 0); + verify(mIOnBackInvokedCallback).onBackInvoked(); + } + + @Test + public void animationDisabledFromSettings() throws RemoteException { + // Toggle the setting off + Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0"); + mController = new BackAnimationController( + mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, + mActivityTaskManager, mContext, + mContentResolver); + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + + RemoteAnimationTarget animationTarget = createAnimationTarget(); + IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class); + ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback); + + triggerBackGesture(); + + verify(appCallback, never()).onBackStarted(); + verify(appCallback, never()).onBackProgressed(backEventCaptor.capture()); + verify(appCallback, times(1)).onBackInvoked(); + + verify(mIOnBackInvokedCallback, never()).onBackStarted(); + verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture()); + verify(mIOnBackInvokedCallback, never()).onBackInvoked(); + } + + @Test + public void ignoresGesture_transitionInProgress() throws RemoteException { + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + RemoteAnimationTarget animationTarget = createAnimationTarget(); + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + + triggerBackGesture(); + // Check that back invocation is dispatched. + verify(mIOnBackInvokedCallback).onBackInvoked(); + + reset(mIOnBackInvokedCallback); + // Verify that we prevent animation from restarting if another gestures happens before + // the previous transition is finished. + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + verifyNoMoreInteractions(mIOnBackInvokedCallback); + + // Verify that we start accepting gestures again once transition finishes. + mController.onBackToLauncherAnimationFinished(); + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + verify(mIOnBackInvokedCallback).onBackStarted(); + } + + @Test + public void acceptsGesture_transitionTimeout() throws RemoteException { + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + RemoteAnimationTarget animationTarget = createAnimationTarget(); + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + + triggerBackGesture(); + reset(mIOnBackInvokedCallback); + + // Simulate transition timeout. + mShellExecutor.flushAll(); + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + verify(mIOnBackInvokedCallback).onBackStarted(); + } + + private void doMotionEvent(int actionDown, int coordinate) { mController.onMotionEvent( - MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0), - MotionEvent.ACTION_UP, + MotionEvent.obtain(0, mEventTime, actionDown, coordinate, coordinate, 0), + actionDown, BackEvent.EDGE_LEFT); - verify(mIOnBackInvokedCallback).onBackInvoked(); + mEventTime += 10; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 169f03e7bc3e..e6711aca19c1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -115,7 +115,7 @@ public class BubbleDataTest extends ShellTestCase { private ArgumentCaptor<BubbleData.Update> mUpdateCaptor; @Mock - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @Mock private Bubbles.PendingIntentCanceledListener mPendingIntentCanceledListener; @@ -129,37 +129,54 @@ public class BubbleDataTest extends ShellTestCase { mEntryA3 = createBubbleEntry(1, "a3", "package.a", null); mEntryB1 = createBubbleEntry(1, "b1", "package.b", null); mEntryB2 = createBubbleEntry(1, "b2", "package.b", null); - mEntryB3 = createBubbleEntry(1, "b3", "package.b", null); - mEntryC1 = createBubbleEntry(1, "c1", "package.c", null); + mEntryB3 = createBubbleEntry(11, "b3", "package.b", null); + mEntryC1 = createBubbleEntry(11, "c1", "package.c", null); NotificationListenerService.Ranking ranking = mock(NotificationListenerService.Ranking.class); when(ranking.isTextChanged()).thenReturn(true); mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking); - mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null, + mBubbleInterruptive = new Bubble(mEntryInterruptive, mBubbleMetadataFlagListener, null, mMainExecutor); mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null); - mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null, + mBubbleDismissed = new Bubble(mEntryDismissed, mBubbleMetadataFlagListener, null, mMainExecutor); mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null, new LocusId("locusId1")); - mBubbleLocusId = new Bubble(mEntryLocusId, mSuppressionListener, null, mMainExecutor); + mBubbleLocusId = new Bubble(mEntryLocusId, + mBubbleMetadataFlagListener, + null /* pendingIntentCanceledListener */, + mMainExecutor); - mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA1 = new Bubble(mEntryA1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA2 = new Bubble(mEntryA2, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA3 = new Bubble(mEntryA3, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB1 = new Bubble(mEntryB1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB2 = new Bubble(mEntryB2, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB3 = new Bubble(mEntryB3, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleC1 = new Bubble(mEntryC1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); @@ -1041,6 +1058,37 @@ public class BubbleDataTest extends ShellTestCase { assertBubbleListContains(mBubbleA2, mBubbleA1, mBubbleLocusId); } + @Test + public void test_removeBubblesForUser() { + // A is user 1 + sendUpdatedEntryAtTime(mEntryA1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + // B & C belong to user 11 + sendUpdatedEntryAtTime(mEntryB3, 4000); + sendUpdatedEntryAtTime(mEntryC1, 5000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertOverflowChangedTo(ImmutableList.of(mBubbleA1)); + assertBubbleListContains(mBubbleC1, mBubbleB3, mBubbleA2); + + // Remove all the A bubbles + mBubbleData.removeBubblesForUser(1); + verifyUpdateReceived(); + + // Verify the update has the removals. + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.removedBubbles.get(0)).isEqualTo( + Pair.create(mBubbleA2, Bubbles.DISMISS_USER_REMOVED)); + assertThat(update.removedBubbles.get(1)).isEqualTo( + Pair.create(mBubbleA1, Bubbles.DISMISS_USER_REMOVED)); + + // Verify no A bubbles in active or overflow. + assertBubbleListContains(mBubbleC1, mBubbleB3); + assertOverflowChangedTo(ImmutableList.of()); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java index 819a984b4a77..e8f3f69ca64e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -63,7 +63,7 @@ public class BubbleTest extends ShellTestCase { private Bubble mBubble; @Mock - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @Before public void setUp() { @@ -81,7 +81,7 @@ public class BubbleTest extends ShellTestCase { when(mNotif.getBubbleMetadata()).thenReturn(metadata); when(mSbn.getKey()).thenReturn("mock"); mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false); - mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null, mMainExecutor); + mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor); } @Test @@ -144,22 +144,22 @@ public class BubbleTest extends ShellTestCase { } @Test - public void testSuppressionListener_change_notified() { + public void testBubbleMetadataFlagListener_change_notified() { assertThat(mBubble.showInShade()).isTrue(); mBubble.setSuppressNotification(true); assertThat(mBubble.showInShade()).isFalse(); - verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble); + verify(mBubbleMetadataFlagListener).onBubbleMetadataFlagChanged(mBubble); } @Test - public void testSuppressionListener_noChange_doesntNotify() { + public void testBubbleMetadataFlagListener_noChange_doesntNotify() { assertThat(mBubble.showInShade()).isTrue(); mBubble.setSuppressNotification(false); - verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any()); + verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any()); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt index bfdf5208bbf0..9f0d89bc3128 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt @@ -23,14 +23,19 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import org.junit.Test import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito import org.mockito.Mockito.mock -import org.mockito.Mockito.verify +import org.mockito.Mockito.never import org.mockito.Mockito.reset +import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidTestingRunner::class) @@ -41,17 +46,17 @@ class BubbleVolatileRepositoryTest : ShellTestCase() { private val user11 = UserHandle.of(11) // user, package, shortcut, notification key, height, res-height, title, taskId, locusId - private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1", - "0key-1", 120, 0, null, 1, null) - private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob", - "10key-2", 0, 16537428, "title", 2, null) - private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2", - "0key-3", 120, 0, null, INVALID_TASK_ID, null) - - private val bubble11 = BubbleEntity(11, "com.example.messenger", - "shortcut-1", "01key-1", 120, 0, null, 3) - private val bubble12 = BubbleEntity(11, "com.example.chat", "alice and bob", - "11key-2", 0, 16537428, "title", INVALID_TASK_ID) + private val bubble1 = BubbleEntity(user0.identifier, + "com.example.messenger", "shortcut-1", "0key-1", 120, 0, null, 1, null) + private val bubble2 = BubbleEntity(user10_managed.identifier, + "com.example.chat", "alice and bob", "10key-2", 0, 16537428, "title", 2, null) + private val bubble3 = BubbleEntity(user0.identifier, + "com.example.messenger", "shortcut-2", "0key-3", 120, 0, null, INVALID_TASK_ID, null) + + private val bubble11 = BubbleEntity(user11.identifier, + "com.example.messenger", "shortcut-1", "01key-1", 120, 0, null, 3) + private val bubble12 = BubbleEntity(user11.identifier, + "com.example.chat", "alice and bob", "11key-2", 0, 16537428, "title", INVALID_TASK_ID) private val user0bubbles = listOf(bubble1, bubble2, bubble3) private val user11bubbles = listOf(bubble11, bubble12) @@ -151,6 +156,125 @@ class BubbleVolatileRepositoryTest : ShellTestCase() { repository.addBubbles(user0.identifier, listOf(bubbleModified)) assertEquals(bubbleModified, repository.getEntities(user0.identifier).get(0)) } + + @Test + fun testRemoveBubblesForUser() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + + val ret = repository.removeBubblesForUser(user0.identifier, -1) + assertThat(ret).isTrue() // bubbles were removed + + assertThat(repository.getEntities(user0.identifier).toList()).isEmpty() + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + } + + @Test + fun testRemoveBubblesForUser_parentUserRemoved() { + repository.addBubbles(user0.identifier, user0bubbles) + // bubble2 is the work profile bubble + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + + val ret = repository.removeBubblesForUser(user10_managed.identifier, user0.identifier) + assertThat(ret).isTrue() // bubbles were removed + + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble3)) + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + } + + @Test + fun testRemoveBubblesForUser_withoutBubbles() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + + val ret = repository.removeBubblesForUser(user11.identifier, -1) + assertThat(ret).isFalse() // bubbles were NOT removed + + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + } + + @Test + fun testSanitizeBubbles_noChanges() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + repository.addBubbles(user11.identifier, user11bubbles) + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + + val ret = repository.sanitizeBubbles(listOf(user0.identifier, + user10_managed.identifier, + user11.identifier)) + assertThat(ret).isFalse() // bubbles were NOT removed + + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + } + + @Test + fun testSanitizeBubbles_userRemoved() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + repository.addBubbles(user11.identifier, user11bubbles) + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + + val ret = repository.sanitizeBubbles(listOf(user11.identifier)) + assertThat(ret).isTrue() // bubbles were removed + + assertThat(repository.getEntities(user0.identifier).toList()).isEmpty() + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + + // User 11 bubbles should still be here + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + } + + @Test + fun testSanitizeBubbles_userParentRemoved() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + + repository.addBubbles(user11.identifier, user11bubbles) + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + + val ret = repository.sanitizeBubbles(listOf(user0.identifier, user11.identifier)) + assertThat(ret).isTrue() // bubbles were removed + // bubble2 is the work profile bubble and should be removed + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble3)) + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + + // User 11 bubbles should still be here + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + } + + @Test + fun testRemoveBubbleForUser_invalidInputDoesntCrash() { + repository.removeBubblesForUser(-1, 0) + repository.removeBubblesForUser(-1, -1) + } } private const val PKG_MESSENGER = "com.example.messenger" diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java index d8aebc284bf1..96938ebc27df 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java @@ -109,9 +109,10 @@ public class TaskStackListenerImplTest { @Test public void testOnTaskProfileLocked() { - mImpl.onTaskProfileLocked(1, 2); - verify(mCallback).onTaskProfileLocked(eq(1), eq(2)); - verify(mOtherCallback).onTaskProfileLocked(eq(1), eq(2)); + ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class); + mImpl.onTaskProfileLocked(info); + verify(mCallback).onTaskProfileLocked(eq(info)); + verify(mOtherCallback).onTaskProfileLocked(eq(info)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 4b85496f2a7f..e8e6254697c2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -21,10 +21,12 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -77,11 +79,11 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Mock private PipUiEventLogger mMockPipUiEventLogger; @Mock private Optional<SplitScreenController> mMockOptionalSplitScreen; @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; + @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder; private TestShellExecutor mMainExecutor; private PipBoundsState mPipBoundsState; private PipTransitionState mPipTransitionState; private PipBoundsAlgorithm mPipBoundsAlgorithm; - private PipParamsChangedForwarder mPipParamsChangedForwarder; private ComponentName mComponent1; private ComponentName mComponent2; @@ -100,7 +102,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, mMockPipTransitionController, - mPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController, + mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); mMainExecutor.flushAll(); preparePipTaskOrg(); @@ -181,11 +183,12 @@ public class PipTaskOrganizerTest extends ShellTestCase { // It is in entering transition, should defer onTaskInfoChanged callback in this case. mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, createPipParams(newAspectRatio))); - assertEquals(startAspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + verify(mMockPipParamsChangedForwarder, never()).notifyAspectRatioChanged(anyFloat()); // Once the entering transition finishes, the new aspect ratio applies in a deferred manner mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); - assertEquals(newAspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + verify(mMockPipParamsChangedForwarder) + .notifyAspectRatioChanged(newAspectRatio.floatValue()); } @Test @@ -199,7 +202,8 @@ public class PipTaskOrganizerTest extends ShellTestCase { mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, createPipParams(newAspectRatio))); - assertEquals(newAspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + verify(mMockPipParamsChangedForwarder) + .notifyAspectRatioChanged(newAspectRatio.floatValue()); } @Test 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 5368b7db3dc1..df18133adcfb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -45,6 +45,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; @@ -75,7 +76,6 @@ public class PipControllerTest extends ShellTestCase { @Mock private PhonePipMenuController mMockPhonePipMenuController; @Mock private PipAppOpsListener mMockPipAppOpsListener; @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; - @Mock private PipKeepClearAlgorithm mMockPipKeepClearAlgorithm; @Mock private PipSnapAlgorithm mMockPipSnapAlgorithm; @Mock private PipMediaController mMockPipMediaController; @Mock private PipTaskOrganizer mMockPipTaskOrganizer; @@ -100,12 +100,12 @@ public class PipControllerTest extends ShellTestCase { return null; }).when(mMockExecutor).execute(any()); mPipController = new PipController(mContext, mMockDisplayController, - mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, - mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, + mMockPipBoundsState, mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, - mMockTaskStackListener, mPipParamsChangedForwarder, mMockOneHandedController, - mMockExecutor); + mMockTaskStackListener, mPipParamsChangedForwarder, + mMockOneHandedController, mMockExecutor); when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm); when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper); } @@ -133,12 +133,12 @@ public class PipControllerTest extends ShellTestCase { when(spyContext.getPackageManager()).thenReturn(mockPackageManager); assertNull(PipController.create(spyContext, mMockDisplayController, - mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, - mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, + mMockPipBoundsState, mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, - mMockTaskStackListener, mPipParamsChangedForwarder, mMockOneHandedController, - mMockExecutor)); + mMockTaskStackListener, mPipParamsChangedForwarder, + mMockOneHandedController, mMockExecutor)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithmTest.java deleted file mode 100644 index f657b5e62d82..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithmTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.pip.phone; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - -import android.graphics.Rect; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.ShellTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.Set; - -/** - * Unit tests against {@link PipKeepClearAlgorithm}. - */ -@RunWith(AndroidTestingRunner.class) -@SmallTest -@TestableLooper.RunWithLooper(setAsMainLooper = true) -public class PipKeepClearAlgorithmTest extends ShellTestCase { - - private PipKeepClearAlgorithm mPipKeepClearAlgorithm; - - - @Before - public void setUp() throws Exception { - mPipKeepClearAlgorithm = new PipKeepClearAlgorithm(); - } - - @Test - public void adjust_withCollidingRestrictedKeepClearAreas_movesBounds() { - final Rect inBounds = new Rect(0, 0, 100, 100); - final Rect keepClearRect = new Rect(50, 50, 150, 150); - - final Rect outBounds = mPipKeepClearAlgorithm.adjust(inBounds, Set.of(keepClearRect), - Set.of()); - - assertFalse(outBounds.contains(keepClearRect)); - } - - @Test - public void adjust_withNonCollidingRestrictedKeepClearAreas_boundsDoNotChange() { - final Rect inBounds = new Rect(0, 0, 100, 100); - final Rect keepClearRect = new Rect(100, 100, 150, 150); - - final Rect outBounds = mPipKeepClearAlgorithm.adjust(inBounds, Set.of(keepClearRect), - Set.of()); - - assertEquals(inBounds, outBounds); - } - - @Test - public void adjust_withCollidingUnrestrictedKeepClearAreas_boundsDoNotChange() { - // TODO(b/183746978): update this test to accommodate for the updated algorithm - final Rect inBounds = new Rect(0, 0, 100, 100); - final Rect keepClearRect = new Rect(50, 50, 150, 150); - - final Rect outBounds = mPipKeepClearAlgorithm.adjust(inBounds, Set.of(), - Set.of(keepClearRect)); - - assertEquals(inBounds, outBounds); - } - - @Test - public void adjust_withNonCollidingUnrestrictedKeepClearAreas_boundsDoNotChange() { - final Rect inBounds = new Rect(0, 0, 100, 100); - final Rect keepClearRect = new Rect(100, 100, 150, 150); - - final Rect outBounds = mPipKeepClearAlgorithm.adjust(inBounds, Set.of(), - Set.of(keepClearRect)); - - assertEquals(inBounds, outBounds); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt new file mode 100644 index 000000000000..05e472245b4a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.os.Handler +import android.os.test.TestLooper +import android.testing.AndroidTestingRunner + +import com.android.wm.shell.R +import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT +import com.android.wm.shell.pip.tv.TvPipBoundsController.POSITION_DEBOUNCE_TIMEOUT_MILLIS +import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.AdditionalAnswers.returnsFirstArg +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +class TvPipBoundsControllerTest { + val ANIMATION_DURATION = 100 + val STASH_DURATION = 5000 + val FAR_FUTURE = 60 * 60000L + val ANCHOR_BOUNDS = Rect(90, 90, 100, 100) + val STASHED_BOUNDS = Rect(99, 90, 109, 100) + val MOVED_BOUNDS = Rect(90, 80, 100, 90) + val STASHED_MOVED_BOUNDS = Rect(99, 80, 109, 90) + val ANCHOR_PLACEMENT = Placement(ANCHOR_BOUNDS, ANCHOR_BOUNDS) + val STASHED_PLACEMENT = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, ANCHOR_BOUNDS, false) + val STASHED_PLACEMENT_RESTASH = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, ANCHOR_BOUNDS, true) + val MOVED_PLACEMENT = Placement(MOVED_BOUNDS, ANCHOR_BOUNDS) + val STASHED_MOVED_PLACEMENT = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, MOVED_BOUNDS, false) + val STASHED_MOVED_PLACEMENT_RESTASH = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, MOVED_BOUNDS, true) + + lateinit var boundsController: TvPipBoundsController + var time = 0L + lateinit var testLooper: TestLooper + lateinit var mainHandler: Handler + + var inMenu = false + var inMoveMode = false + + @Mock + lateinit var context: Context + @Mock + lateinit var resources: Resources + @Mock + lateinit var tvPipBoundsState: TvPipBoundsState + @Mock + lateinit var tvPipBoundsAlgorithm: TvPipBoundsAlgorithm + @Mock + lateinit var listener: TvPipBoundsController.PipBoundsListener + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + time = 0L + inMenu = false + inMoveMode = false + + testLooper = TestLooper { time } + mainHandler = Handler(testLooper.getLooper()) + + whenever(context.resources).thenReturn(resources) + whenever(resources.getInteger(R.integer.config_pipStashDuration)).thenReturn(STASH_DURATION) + whenever(tvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(any())) + .then(returnsFirstArg<Rect>()) + + boundsController = TvPipBoundsController( + context, + { time }, + mainHandler, + tvPipBoundsState, + tvPipBoundsAlgorithm) + boundsController.setListener(listener) + } + + @Test + fun testPlacement_MovedAfterDebounceTimeout() { + triggerPlacement(MOVED_PLACEMENT) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testStashedPlacement_MovedAfterDebounceTimeout_Unstashes() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testDebounceSamePlacement_MovesDebounceTimeoutAfterFirstPlacement() { + triggerPlacement(MOVED_PLACEMENT) + advanceTimeTo(POSITION_DEBOUNCE_TIMEOUT_MILLIS / 2) + triggerPlacement(MOVED_PLACEMENT) + + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + } + + @Test + fun testNoMovementUntilPlacementStabilizes() { + triggerPlacement(ANCHOR_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(MOVED_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(ANCHOR_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(MOVED_PLACEMENT) + + assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + } + + @Test + fun testUnstashIfStashNoLongerNecessary() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + triggerPlacement(ANCHOR_PLACEMENT) + assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, ANCHOR_BOUNDS) + } + + @Test + fun testRestashingPlacementDelaysUnstash() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + assertNoMovementUpTo(time + STASH_DURATION / 2) + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertNoMovementUpTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS) + assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testNonRestashingPlacementDoesNotDelayUnstash() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + assertNoMovementUpTo(time + STASH_DURATION / 2) + triggerPlacement(STASHED_PLACEMENT) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testImmediatePlacement() { + triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH) + assertMovement(STASHED_BOUNDS) + assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testInMoveMode_KeepAtAnchor() { + startMoveMode() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(ANCHOR_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testInMenu_Unstashed() { + openPipMenu() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(MOVED_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testCloseMenu_DoNotRestash() { + openPipMenu() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(MOVED_BOUNDS) + + closePipMenu() + triggerPlacement(STASHED_MOVED_PLACEMENT) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + fun assertMovement(bounds: Rect) { + verify(listener).onPipTargetBoundsChange(eq(bounds), anyInt()) + reset(listener) + } + + fun assertMovementAt(timeMs: Long, bounds: Rect) { + assertNoMovementUpTo(timeMs - 1) + advanceTimeTo(timeMs) + assertMovement(bounds) + } + + fun assertNoMovementUpTo(timeMs: Long) { + advanceTimeTo(timeMs) + verify(listener, never()).onPipTargetBoundsChange(any(), anyInt()) + } + + fun triggerPlacement(placement: Placement, immediate: Boolean = false) { + whenever(tvPipBoundsAlgorithm.getTvPipPlacement()).thenReturn(placement) + val stayAtAnchorPosition = inMoveMode + val disallowStashing = inMenu || stayAtAnchorPosition + boundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing, + ANIMATION_DURATION, immediate) + } + + fun triggerImmediatePlacement(placement: Placement) { + triggerPlacement(placement, true) + } + + fun openPipMenu() { + inMenu = true + inMoveMode = false + } + + fun closePipMenu() { + inMenu = false + inMoveMode = false + } + + fun startMoveMode() { + inMenu = true + inMoveMode = true + } + + fun advanceTimeTo(ms: Long) { + time = ms + testLooper.dispatchAll() + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt index 46f388d0ce0e..0fcc5cf384c9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt @@ -30,7 +30,9 @@ import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement import org.junit.Before import org.junit.Test import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse import junit.framework.Assert.assertNull +import junit.framework.Assert.assertTrue @RunWith(AndroidTestingRunner::class) class TvPipKeepClearAlgorithmTest { @@ -46,7 +48,6 @@ class TvPipKeepClearAlgorithmTest { 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 @@ -58,16 +59,14 @@ class TvPipKeepClearAlgorithmTest { restrictedAreas.clear() unrestrictedAreas.clear() - currentTime = 0L pipSize = DEFAULT_PIP_SIZE gravity = Gravity.BOTTOM or Gravity.RIGHT - algorithm = TvPipKeepClearAlgorithm({ currentTime }) + algorithm = TvPipKeepClearAlgorithm() algorithm.setScreenSize(SCREEN_SIZE) algorithm.setMovementBounds(movementBounds) algorithm.pipAreaPadding = PADDING algorithm.stashOffset = STASH_OFFSET - algorithm.stashDuration = 5000L algorithm.setGravity(gravity) algorithm.maxRestrictedDistanceFraction = 0.3 } @@ -265,7 +264,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_BOTTOM, placement.stashType) assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) + assertTrue(placement.triggerStash) } @Test @@ -305,7 +304,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) + assertTrue(placement.triggerStash) } @Test @@ -352,9 +351,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) - - currentTime += 1000 + assertTrue(placement.triggerStash) restrictedAreas.remove(sideBar) placement = getActualPlacement() @@ -363,7 +360,7 @@ class TvPipKeepClearAlgorithmTest { } @Test - fun test_Stashed_UnstashBoundsStaysObstructed_UnstashesAfterTimeout() { + fun test_Stashed_UnstashBoundsStaysObstructed_DoesNotTriggerStash() { gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -384,13 +381,13 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) - - currentTime += algorithm.stashDuration + assertTrue(placement.triggerStash) placement = getActualPlacement() - assertEquals(expectedUnstashBounds, placement.bounds) - assertNotStashed(placement) + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) + assertFalse(placement.triggerStash) } @Test @@ -415,9 +412,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) - - currentTime += 1000 + assertTrue(placement.triggerStash) val newObstruction = Rect( 0, @@ -431,7 +426,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime) + assertTrue(placement.triggerStash) } @Test @@ -458,21 +453,9 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_PipInsets() { - val permInsets = Insets.of(-1, -2, -3, -4) - algorithm.setPipPermanentDecorInsets(permInsets) - testInsetsForAllPositions(permInsets) - - val tempInsets = Insets.of(-4, -3, -2, -1) - algorithm.setPipPermanentDecorInsets(Insets.NONE) - algorithm.setPipTemporaryDecorInsets(tempInsets) - testInsetsForAllPositions(tempInsets) - - algorithm.setPipPermanentDecorInsets(permInsets) - algorithm.setPipTemporaryDecorInsets(tempInsets) - testInsetsForAllPositions(Insets.add(permInsets, tempInsets)) - } + val insets = Insets.of(-1, -2, -3, -4) + algorithm.setPipPermanentDecorInsets(insets) - private fun testInsetsForAllPositions(insets: Insets) { gravity = Gravity.BOTTOM or Gravity.RIGHT testAnchorPositionWithInsets(insets) @@ -546,6 +529,6 @@ class TvPipKeepClearAlgorithmTest { private fun assertNotStashed(actual: Placement) { assertEquals(STASH_TYPE_NONE, actual.stashType) assertNull(actual.unstashDestinationBounds) - assertEquals(0L, actual.unstashTime) + assertFalse(actual.triggerStash) } } 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 a55f737f2f25..ffaab652aa99 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -139,6 +139,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testLaunchToSide() { ActivityManager.RunningTaskInfo newTask = new TestRunningTaskInfoBuilder() .setParentTaskId(mSideStage.mRootTaskInfo.taskId).build(); @@ -173,6 +174,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testLaunchPair() { TransitionInfo info = createEnterPairInfo(); @@ -195,6 +197,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testMonitorInSplit() { enterSplit(); @@ -251,6 +254,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testEnterRecents() { enterSplit(); @@ -288,6 +292,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testDismissFromBeingOccluded() { enterSplit(); @@ -325,6 +330,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testDismissFromMultiWindowSupport() { enterSplit(); @@ -346,6 +352,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testDismissSnap() { enterSplit(); @@ -370,6 +377,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testDismissFromAppFinish() { enterSplit(); diff --git a/libs/hwui/AnimatorManager.cpp b/libs/hwui/AnimatorManager.cpp index 4826d5a0c8da..078041411a21 100644 --- a/libs/hwui/AnimatorManager.cpp +++ b/libs/hwui/AnimatorManager.cpp @@ -31,7 +31,8 @@ static void detach(sp<BaseRenderNodeAnimator>& animator) { animator->detach(); } -AnimatorManager::AnimatorManager(RenderNode& parent) : mParent(parent), mAnimationHandle(nullptr) {} +AnimatorManager::AnimatorManager(RenderNode& parent) + : mParent(parent), mAnimationHandle(nullptr), mCancelAllAnimators(false) {} AnimatorManager::~AnimatorManager() { for_each(mNewAnimators.begin(), mNewAnimators.end(), detach); @@ -82,8 +83,16 @@ void AnimatorManager::pushStaging() { } mNewAnimators.clear(); } - for (auto& animator : mAnimators) { - animator->pushStaging(mAnimationHandle->context()); + + if (mCancelAllAnimators) { + for (auto& animator : mAnimators) { + animator->forceEndNow(mAnimationHandle->context()); + } + mCancelAllAnimators = false; + } else { + for (auto& animator : mAnimators) { + animator->pushStaging(mAnimationHandle->context()); + } } } @@ -184,5 +193,9 @@ void AnimatorManager::endAllActiveAnimators() { mAnimationHandle->release(); } +void AnimatorManager::forceEndAnimators() { + mCancelAllAnimators = true; +} + } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/AnimatorManager.h b/libs/hwui/AnimatorManager.h index a0df01d5962c..6002661dc82a 100644 --- a/libs/hwui/AnimatorManager.h +++ b/libs/hwui/AnimatorManager.h @@ -16,11 +16,11 @@ #ifndef ANIMATORMANAGER_H #define ANIMATORMANAGER_H -#include <vector> - #include <cutils/compiler.h> #include <utils/StrongPointer.h> +#include <vector> + #include "utils/Macros.h" namespace android { @@ -56,6 +56,8 @@ public: // Hard-ends all animators. May only be called on the UI thread. void endAllStagingAnimators(); + void forceEndAnimators(); + // Hard-ends all animators that have been pushed. Used for cleanup if // the ActivityContext is being destroyed void endAllActiveAnimators(); @@ -71,6 +73,8 @@ private: // To improve the efficiency of resizing & removing from the vector std::vector<sp<BaseRenderNodeAnimator> > mNewAnimators; std::vector<sp<BaseRenderNodeAnimator> > mAnimators; + + bool mCancelAllAnimators; }; } /* namespace uirenderer */ diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp index fecf26906c04..8191f5e6a83a 100644 --- a/libs/hwui/FrameInfo.cpp +++ b/libs/hwui/FrameInfo.cpp @@ -20,19 +20,33 @@ namespace android { namespace uirenderer { -const std::array FrameInfoNames{ - "Flags", "FrameTimelineVsyncId", "IntendedVsync", - "Vsync", "InputEventId", "HandleInputStart", - "AnimationStart", "PerformTraversalsStart", "DrawStart", - "FrameDeadline", "FrameInterval", "FrameStartTime", - "SyncQueued", "SyncStart", "IssueDrawCommandsStart", - "SwapBuffers", "FrameCompleted", "DequeueBufferDuration", - "QueueBufferDuration", "GpuCompleted", "SwapBuffersCompleted", - "DisplayPresentTime", +const std::array FrameInfoNames{"Flags", + "FrameTimelineVsyncId", + "IntendedVsync", + "Vsync", + "InputEventId", + "HandleInputStart", + "AnimationStart", + "PerformTraversalsStart", + "DrawStart", + "FrameDeadline", + "FrameInterval", + "FrameStartTime", + "SyncQueued", + "SyncStart", + "IssueDrawCommandsStart", + "SwapBuffers", + "FrameCompleted", + "DequeueBufferDuration", + "QueueBufferDuration", + "GpuCompleted", + "SwapBuffersCompleted", + "DisplayPresentTime", + "CommandSubmissionCompleted" }; -static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 22, +static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 23, "Must update value in FrameMetrics.java#FRAME_STATS_COUNT (and here)"); void FrameInfo::importUiThreadInfo(int64_t* info) { diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h index 540a88b16dc9..564ee4f53a54 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -58,6 +58,7 @@ enum class FrameInfoIndex { GpuCompleted, SwapBuffersCompleted, DisplayPresentTime, + CommandSubmissionCompleted, // Must be the last value! // Also must be kept in sync with FrameMetrics.java#FRAME_STATS_COUNT diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 86ae3995eeed..5a67eb9935dd 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -134,7 +134,7 @@ bool Properties::load() { skpCaptureEnabled = debuggingEnabled && base::GetBoolProperty(PROPERTY_CAPTURE_SKP_ENABLED, false); SkAndroidFrameworkTraceUtil::setEnableTracing( - base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, true)); + base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, false)); runningInEmulator = base::GetBoolProperty(PROPERTY_IS_EMULATOR, false); diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index 944393c63ad6..db7639029187 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -543,6 +543,12 @@ static void android_view_RenderNode_endAllAnimators(JNIEnv* env, jobject clazz, renderNode->animators().endAllStagingAnimators(); } +static void android_view_RenderNode_forceEndAnimators(JNIEnv* env, jobject clazz, + jlong renderNodePtr) { + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + renderNode->animators().forceEndAnimators(); +} + // ---------------------------------------------------------------------------- // SurfaceView position callback // ---------------------------------------------------------------------------- @@ -745,6 +751,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}, + {"nForceEndAnimators", "(J)V", (void*)android_view_RenderNode_forceEndAnimators}, {"nRequestPositionUpdates", "(JLjava/lang/ref/WeakReference;)V", (void*)android_view_RenderNode_requestPositionUpdates}, diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index e7432ac5f216..90c4440c8339 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -136,24 +136,59 @@ sk_sp<SkData> ShaderCache::load(const SkData& key) { free(valueBuffer); return nullptr; } + mNumShadersCachedInRam++; + ATRACE_FORMAT("HWUI RAM cache: %d shaders", mNumShadersCachedInRam); return SkData::MakeFromMalloc(valueBuffer, valueSize); } +namespace { +// Helper for BlobCache::set to trace the result. +void set(BlobCache* cache, const void* key, size_t keySize, const void* value, size_t valueSize) { + switch (cache->set(key, keySize, value, valueSize)) { + case BlobCache::InsertResult::kInserted: + // This is what we expect/hope. It means the cache is large enough. + return; + case BlobCache::InsertResult::kDidClean: { + ATRACE_FORMAT("ShaderCache: evicted an entry to fit {key: %lu value %lu}!", keySize, + valueSize); + return; + } + case BlobCache::InsertResult::kNotEnoughSpace: { + ATRACE_FORMAT("ShaderCache: could not fit {key: %lu value %lu}!", keySize, valueSize); + return; + } + case BlobCache::InsertResult::kInvalidValueSize: + case BlobCache::InsertResult::kInvalidKeySize: { + ATRACE_FORMAT("ShaderCache: invalid size {key: %lu value %lu}!", keySize, valueSize); + return; + } + case BlobCache::InsertResult::kKeyTooBig: + case BlobCache::InsertResult::kValueTooBig: + case BlobCache::InsertResult::kCombinedTooBig: { + ATRACE_FORMAT("ShaderCache: entry too big: {key: %lu value %lu}!", keySize, valueSize); + return; + } + } +} +} // namespace + void ShaderCache::saveToDiskLocked() { ATRACE_NAME("ShaderCache::saveToDiskLocked"); if (mInitialized && mBlobCache && mSavePending) { if (mIDHash.size()) { auto key = sIDKey; - mBlobCache->set(&key, sizeof(key), mIDHash.data(), mIDHash.size()); + set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size()); } mBlobCache->writeToFile(); } mSavePending = false; } -void ShaderCache::store(const SkData& key, const SkData& data) { +void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /*description*/) { ATRACE_NAME("ShaderCache::store"); std::lock_guard<std::mutex> lock(mMutex); + mNumShadersCachedInRam++; + ATRACE_FORMAT("HWUI RAM cache: %d shaders", mNumShadersCachedInRam); if (!mInitialized) { return; @@ -187,7 +222,7 @@ void ShaderCache::store(const SkData& key, const SkData& data) { mNewPipelineCacheSize = -1; mTryToStorePipelineCache = true; } - bc->set(key.data(), keySize, value, valueSize); + set(bc, key.data(), keySize, value, valueSize); if (!mSavePending && mDeferredSaveDelay > 0) { mSavePending = true; diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index 4dcc9fb49802..3e0fd5164011 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.h +++ b/libs/hwui/pipeline/skia/ShaderCache.h @@ -73,7 +73,7 @@ public: * "store" attempts to insert a new key/value blob pair into the cache. * This will be called by Skia after it compiled a new SKSL shader */ - void store(const SkData& key, const SkData& data) override; + void store(const SkData& key, const SkData& data, const SkString& description) override; /** * "onVkFrameFlushed" tries to store Vulkan pipeline cache state. @@ -210,6 +210,13 @@ private: */ static constexpr uint8_t sIDKey = 0; + /** + * Most of this class concerns persistent storage for shaders, but it's also + * interesting to keep track of how many shaders are stored in RAM. This + * class provides a convenient entry point for that. + */ + int mNumShadersCachedInRam = 0; + friend class ShaderCacheTestUtils; // used for unit testing }; diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 744739accb2c..2aca41e41905 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -16,8 +16,15 @@ #include "SkiaOpenGLPipeline.h" +#include <GrBackendSurface.h> +#include <SkBlendMode.h> +#include <SkImageInfo.h> +#include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <strings.h> + #include "DeferredLayerUpdater.h" +#include "FrameInfo.h" #include "LayerDrawable.h" #include "LightingInfo.h" #include "SkiaPipeline.h" @@ -27,17 +34,9 @@ #include "renderstate/RenderState.h" #include "renderthread/EglManager.h" #include "renderthread/Frame.h" +#include "renderthread/IRenderPipeline.h" #include "utils/GLUtils.h" -#include <GLES3/gl3.h> - -#include <GrBackendSurface.h> -#include <SkBlendMode.h> -#include <SkImageInfo.h> - -#include <cutils/properties.h> -#include <strings.h> - using namespace android::uirenderer::renderthread; namespace android { @@ -69,12 +68,11 @@ Frame SkiaOpenGLPipeline::getFrame() { return mEglManager.beginFrame(mEglSurface); } -bool SkiaOpenGLPipeline::draw(const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, - const LightGeometry& lightGeometry, - LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, - bool opaque, const LightInfo& lightInfo, - const std::vector<sp<RenderNode>>& renderNodes, - FrameInfoVisualizer* profiler) { +IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( + const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) { if (!isCapturingSkp()) { mEglManager.damageFrame(frame, dirty); } @@ -129,7 +127,7 @@ bool SkiaOpenGLPipeline::draw(const Frame& frame, const SkRect& screenDirty, con dumpResourceCacheUsage(); } - return true; + return {true, IRenderPipeline::DrawResult::kUnknownTime}; } bool SkiaOpenGLPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h index fddd97f1c5b3..186998a01745 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h @@ -36,11 +36,14 @@ public: renderthread::MakeCurrentResult makeCurrent() override; renderthread::Frame getFrame() override; - bool draw(const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, - const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, - const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, - const std::vector<sp<RenderNode> >& renderNodes, - FrameInfoVisualizer* profiler) override; + renderthread::IRenderPipeline::DrawResult draw(const renderthread::Frame& frame, + const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, + LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, + const LightInfo& lightInfo, + const std::vector<sp<RenderNode> >& renderNodes, + FrameInfoVisualizer* profiler) override; GrSurfaceOrigin getSurfaceOrigin() override { return kBottomLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) override; diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 99fd463b0660..905d46e58014 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -16,7 +16,15 @@ #include "SkiaVulkanPipeline.h" +#include <GrDirectContext.h> +#include <GrTypes.h> +#include <SkSurface.h> +#include <SkTypes.h> +#include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <strings.h> +#include <vk/GrVkTypes.h> + #include "DeferredLayerUpdater.h" #include "LightingInfo.h" #include "Readback.h" @@ -26,16 +34,7 @@ #include "VkInteropFunctorDrawable.h" #include "renderstate/RenderState.h" #include "renderthread/Frame.h" - -#include <SkSurface.h> -#include <SkTypes.h> - -#include <GrDirectContext.h> -#include <GrTypes.h> -#include <vk/GrVkTypes.h> - -#include <cutils/properties.h> -#include <strings.h> +#include "renderthread/IRenderPipeline.h" using namespace android::uirenderer::renderthread; @@ -64,15 +63,14 @@ Frame SkiaVulkanPipeline::getFrame() { return vulkanManager().dequeueNextBuffer(mVkSurface); } -bool SkiaVulkanPipeline::draw(const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, - const LightGeometry& lightGeometry, - LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, - bool opaque, const LightInfo& lightInfo, - const std::vector<sp<RenderNode>>& renderNodes, - FrameInfoVisualizer* profiler) { +IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( + const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) { sk_sp<SkSurface> backBuffer = mVkSurface->getCurrentSkSurface(); if (backBuffer.get() == nullptr) { - return false; + return {false, -1}; } // update the coordinates of the global light position based on surface rotation @@ -94,9 +92,10 @@ bool SkiaVulkanPipeline::draw(const Frame& frame, const SkRect& screenDirty, con profiler->draw(profileRenderer); } + nsecs_t submissionTime = IRenderPipeline::DrawResult::kUnknownTime; { ATRACE_NAME("flush commands"); - vulkanManager().finishFrame(backBuffer.get()); + submissionTime = vulkanManager().finishFrame(backBuffer.get()); } layerUpdateQueue->clear(); @@ -105,7 +104,7 @@ bool SkiaVulkanPipeline::draw(const Frame& frame, const SkRect& screenDirty, con dumpResourceCacheUsage(); } - return true; + return {true, submissionTime}; } bool SkiaVulkanPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index 56d42e013f31..ada6af67d4a0 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -33,11 +33,14 @@ public: renderthread::MakeCurrentResult makeCurrent() override; renderthread::Frame getFrame() override; - bool draw(const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, - const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, - const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, - const std::vector<sp<RenderNode> >& renderNodes, - FrameInfoVisualizer* profiler) override; + renderthread::IRenderPipeline::DrawResult draw(const renderthread::Frame& frame, + const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, + LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, + const LightInfo& lightInfo, + const std::vector<sp<RenderNode> >& renderNodes, + FrameInfoVisualizer* profiler) override; GrSurfaceOrigin getSurfaceOrigin() override { return kTopLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) override; diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 122c77f3dadc..976117b9bbd4 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -512,9 +512,9 @@ nsecs_t CanvasContext::draw() { ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty)); - bool drew = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, &mLayerUpdateQueue, - mContentDrawBounds, mOpaque, mLightInfo, mRenderNodes, - &(profiler())); + const auto drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, + &mLayerUpdateQueue, mContentDrawBounds, mOpaque, + mLightInfo, mRenderNodes, &(profiler())); uint64_t frameCompleteNr = getFrameNumber(); @@ -534,8 +534,11 @@ nsecs_t CanvasContext::draw() { bool requireSwap = false; int error = OK; - bool didSwap = - mRenderPipeline->swapBuffers(frame, drew, windowDirty, mCurrentFrameInfo, &requireSwap); + bool didSwap = mRenderPipeline->swapBuffers(frame, drawResult.success, windowDirty, + mCurrentFrameInfo, &requireSwap); + + mCurrentFrameInfo->set(FrameInfoIndex::CommandSubmissionCompleted) = std::max( + drawResult.commandSubmissionTime, mCurrentFrameInfo->get(FrameInfoIndex::SwapBuffers)); mIsDirty = false; @@ -753,7 +756,8 @@ void CanvasContext::onSurfaceStatsAvailable(void* context, int32_t surfaceContro if (frameInfo != nullptr) { frameInfo->set(FrameInfoIndex::FrameCompleted) = std::max(gpuCompleteTime, frameInfo->get(FrameInfoIndex::SwapBuffersCompleted)); - frameInfo->set(FrameInfoIndex::GpuCompleted) = gpuCompleteTime; + frameInfo->set(FrameInfoIndex::GpuCompleted) = std::max( + gpuCompleteTime, frameInfo->get(FrameInfoIndex::CommandSubmissionCompleted)); std::scoped_lock lock(instance->mFrameMetricsReporterMutex); instance->mJankTracker.finishFrame(*frameInfo, instance->mFrameMetricsReporter, frameNumber, surfaceControlId); diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index aceb5a528fc8..ef58bc553c23 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -49,11 +49,21 @@ class IRenderPipeline { public: virtual MakeCurrentResult makeCurrent() = 0; virtual Frame getFrame() = 0; - virtual bool draw(const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, - const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, - const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, - const std::vector<sp<RenderNode>>& renderNodes, - FrameInfoVisualizer* profiler) = 0; + + // Result of IRenderPipeline::draw + struct DrawResult { + // True if draw() succeeded, false otherwise + bool success = false; + // If drawing was successful, reports the time at which command + // submission occurred. -1 if this time is unknown. + static constexpr nsecs_t kUnknownTime = -1; + nsecs_t commandSubmissionTime = kUnknownTime; + }; + virtual DrawResult draw(const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, + FrameInfoVisualizer* profiler) = 0; virtual bool swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) = 0; virtual DeferredLayerUpdater* createTextureLayer() = 0; diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index a9ff2c60fdbe..718d4a16d5c8 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -494,7 +494,7 @@ static void destroy_semaphore(void* context) { } } -void VulkanManager::finishFrame(SkSurface* surface) { +nsecs_t VulkanManager::finishFrame(SkSurface* surface) { ATRACE_NAME("Vulkan finish frame"); ALOGE_IF(mSwapSemaphore != VK_NULL_HANDLE || mDestroySemaphoreContext != nullptr, "finishFrame already has an outstanding semaphore"); @@ -530,6 +530,7 @@ void VulkanManager::finishFrame(SkSurface* surface) { GrDirectContext* context = GrAsDirectContext(surface->recordingContext()); ALOGE_IF(!context, "Surface is not backed by gpu"); context->submit(); + const nsecs_t submissionTime = systemTime(); if (semaphore != VK_NULL_HANDLE) { if (submitted == GrSemaphoresSubmitted::kYes) { mSwapSemaphore = semaphore; @@ -558,6 +559,8 @@ void VulkanManager::finishFrame(SkSurface* surface) { } } skiapipeline::ShaderCache::get().onVkFrameFlushed(context); + + return submissionTime; } void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect) { diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index b816649edf6e..b8c2bdf112f8 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -84,7 +84,9 @@ public: void destroySurface(VulkanSurface* surface); Frame dequeueNextBuffer(VulkanSurface* surface); - void finishFrame(SkSurface* surface); + // Finishes the frame and submits work to the GPU + // Returns the estimated start time for intiating GPU work, -1 otherwise. + nsecs_t finishFrame(SkSurface* surface); void swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect); // Inserts a wait on fence command into the Vulkan command buffer. diff --git a/libs/hwui/tests/unit/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp index 87981f115763..974d85a453db 100644 --- a/libs/hwui/tests/unit/ShaderCacheTests.cpp +++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp @@ -140,9 +140,9 @@ TEST(ShaderCacheTest, testWriteAndRead) { // write to the in-memory cache without storing on disk and verify we read the same values sk_sp<SkData> inVS; setShader(inVS, "sassas"); - ShaderCache::get().store(GrProgramDescTest(100), *inVS.get()); + ShaderCache::get().store(GrProgramDescTest(100), *inVS.get(), SkString()); setShader(inVS, "someVS"); - ShaderCache::get().store(GrProgramDescTest(432), *inVS.get()); + ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString()); ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(100))), sk_sp<SkData>()); ASSERT_TRUE(checkShader(outVS, "sassas")); ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>()); @@ -166,7 +166,7 @@ TEST(ShaderCacheTest, testWriteAndRead) { // change data, store to disk, read back again and verify data has been changed setShader(inVS, "ewData1"); - ShaderCache::get().store(GrProgramDescTest(432), *inVS.get()); + ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString()); ShaderCacheTestUtils::terminate(ShaderCache::get(), true); ShaderCache::get().initShaderDiskCache(); ASSERT_NE((outVS2 = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>()); @@ -177,7 +177,7 @@ TEST(ShaderCacheTest, testWriteAndRead) { std::vector<uint8_t> dataBuffer(dataSize); genRandomData(dataBuffer); setShader(inVS, dataBuffer); - ShaderCache::get().store(GrProgramDescTest(432), *inVS.get()); + ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString()); ShaderCacheTestUtils::terminate(ShaderCache::get(), true); ShaderCache::get().initShaderDiskCache(); ASSERT_NE((outVS2 = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>()); @@ -225,7 +225,7 @@ TEST(ShaderCacheTest, testCacheValidation) { setShader(data, dataBuffer); blob = std::make_pair(key, data); - ShaderCache::get().store(*key.get(), *data.get()); + ShaderCache::get().store(*key.get(), *data.get(), SkString()); } ShaderCacheTestUtils::terminate(ShaderCache::get(), true); diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 1dc74e5f7740..10ea6512c724 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -106,6 +106,7 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::~PointerController() { mDisplayInfoListener->onPointerControllerDestroyed(); mUnregisterWindowInfosListener(mDisplayInfoListener); + mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, 0, 0); } std::mutex& PointerController::getLock() const { @@ -255,6 +256,12 @@ void PointerController::setDisplayViewport(const DisplayViewport& viewport) { getAdditionalMouseResources = true; } mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources); + if (viewport.displayId != mLocked.pointerDisplayId) { + float xPos, yPos; + mCursorController.getPosition(&xPos, &yPos); + mContext.getPolicy()->onPointerDisplayIdChanged(viewport.displayId, xPos, yPos); + mLocked.pointerDisplayId = viewport.displayId; + } } void PointerController::updatePointerIcon(int32_t iconId) { diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index 2e6e851ee15a..eab030f71e1a 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -104,6 +104,7 @@ private: struct Locked { Presentation presentation; + int32_t pointerDisplayId = ADISPLAY_ID_NONE; std::vector<gui::DisplayInfo> mDisplayInfos; std::unordered_map<int32_t /* displayId */, TouchSpotController> spotControllers; diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h index 26a65a47471d..c2bc1e020279 100644 --- a/libs/input/PointerControllerContext.h +++ b/libs/input/PointerControllerContext.h @@ -79,6 +79,7 @@ public: std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId) = 0; virtual int32_t getDefaultPointerIconId() = 0; virtual int32_t getCustomPointerIconId() = 0; + virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) = 0; }; /* diff --git a/libs/input/TEST_MAPPING b/libs/input/TEST_MAPPING index fe74c62d4ec1..9626d8dac787 100644 --- a/libs/input/TEST_MAPPING +++ b/libs/input/TEST_MAPPING @@ -1,7 +1,7 @@ { - "presubmit": [ - { - "name": "libinputservice_test" - } - ] + "imports": [ + { + "path": "frameworks/native/services/inputflinger" + } + ] } diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index dae1fccec804..f9752ed155df 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -56,9 +56,11 @@ public: std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId) override; virtual int32_t getDefaultPointerIconId() override; virtual int32_t getCustomPointerIconId() override; + virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) override; bool allResourcesAreLoaded(); bool noResourcesAreLoaded(); + std::optional<int32_t> getLastReportedPointerDisplayId() { return latestPointerDisplayId; } private: void loadPointerIconForType(SpriteIcon* icon, int32_t cursorType); @@ -66,6 +68,7 @@ private: bool pointerIconLoaded{false}; bool pointerResourcesLoaded{false}; bool additionalMouseResourcesLoaded{false}; + std::optional<int32_t /*displayId*/> latestPointerDisplayId; }; void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, int32_t) { @@ -126,12 +129,19 @@ void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* ic icon->hotSpotX = hotSpot.first; icon->hotSpotY = hotSpot.second; } + +void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId, + float /*xPos*/, + float /*yPos*/) { + latestPointerDisplayId = displayId; +} + class PointerControllerTest : public Test { protected: PointerControllerTest(); ~PointerControllerTest(); - void ensureDisplayViewportIsSet(); + void ensureDisplayViewportIsSet(int32_t displayId = ADISPLAY_ID_DEFAULT); sp<MockSprite> mPointerSprite; sp<MockPointerControllerPolicyInterface> mPolicy; @@ -168,9 +178,9 @@ PointerControllerTest::~PointerControllerTest() { mThread.join(); } -void PointerControllerTest::ensureDisplayViewportIsSet() { +void PointerControllerTest::ensureDisplayViewportIsSet(int32_t displayId) { DisplayViewport viewport; - viewport.displayId = ADISPLAY_ID_DEFAULT; + viewport.displayId = displayId; viewport.logicalRight = 1600; viewport.logicalBottom = 1200; viewport.physicalRight = 800; @@ -255,6 +265,30 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) { ensureDisplayViewportIsSet(); } +TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) { + EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId()) + << "A pointer display change does not occur when PointerController is created."; + + ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT); + + const auto lastReportedPointerDisplayId = mPolicy->getLastReportedPointerDisplayId(); + ASSERT_TRUE(lastReportedPointerDisplayId) + << "The policy is notified of a pointer display change when the viewport is first set."; + EXPECT_EQ(ADISPLAY_ID_DEFAULT, *lastReportedPointerDisplayId) + << "Incorrect pointer display notified."; + + ensureDisplayViewportIsSet(42); + + EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId()) + << "The policy is notified when the pointer display changes."; + + // Release the PointerController. + mPointerController = nullptr; + + EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId()) + << "The pointer display changes to invalid when PointerController is destroyed."; +} + class PointerControllerWindowInfoListenerTest : public Test {}; class TestPointerController : public PointerController { |