diff options
Diffstat (limited to 'libs')
48 files changed, 987 insertions, 295 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index fdcb7be597d5..a20e7179daf1 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -22,7 +22,6 @@ import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; import static androidx.window.common.CommonFoldingFeature.parseListFromString; import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.Context; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; @@ -30,22 +29,25 @@ import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; +import androidx.window.util.AcceptOnceConsumer; import androidx.window.util.BaseDataProducer; -import androidx.window.util.DataProducer; import com.android.internal.R; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; /** - * An implementation of {@link androidx.window.util.DataProducer} that returns the device's posture - * by mapping the state returned from {@link DeviceStateManager} to values provided in the resources - * config at {@link R.array#config_device_state_postures}. + * An implementation of {@link androidx.window.util.BaseDataProducer} that returns + * the device's posture by mapping the state returned from {@link DeviceStateManager} to + * values provided in the resources' config at {@link R.array#config_device_state_postures}. */ -public final class DeviceStateManagerFoldingFeatureProducer extends - BaseDataProducer<List<CommonFoldingFeature>> { +public final class DeviceStateManagerFoldingFeatureProducer + extends BaseDataProducer<List<CommonFoldingFeature>> { private static final String TAG = DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); private static final boolean DEBUG = false; @@ -54,15 +56,11 @@ public final class DeviceStateManagerFoldingFeatureProducer extends private int mCurrentDeviceState = INVALID_DEVICE_STATE; - private final DeviceStateCallback mDeviceStateCallback = (state) -> { - mCurrentDeviceState = state; - notifyDataChanged(); - }; @NonNull - private final DataProducer<String> mRawFoldSupplier; + private final BaseDataProducer<String> mRawFoldSupplier; public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context, - @NonNull DataProducer<String> rawFoldSupplier) { + @NonNull BaseDataProducer<String> rawFoldSupplier) { mRawFoldSupplier = rawFoldSupplier; String[] deviceStatePosturePairs = context.getResources() .getStringArray(R.array.config_device_state_postures); @@ -70,7 +68,8 @@ public final class DeviceStateManagerFoldingFeatureProducer extends String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); if (deviceStatePostureMapping.length != 2) { if (DEBUG) { - Log.e(TAG, "Malformed device state posture pair: " + deviceStatePosturePair); + Log.e(TAG, "Malformed device state posture pair: " + + deviceStatePosturePair); } continue; } @@ -82,7 +81,8 @@ public final class DeviceStateManagerFoldingFeatureProducer extends posture = Integer.parseInt(deviceStatePostureMapping[1]); } catch (NumberFormatException e) { if (DEBUG) { - Log.e(TAG, "Failed to parse device state or posture: " + deviceStatePosturePair, + Log.e(TAG, "Failed to parse device state or posture: " + + deviceStatePosturePair, e); } continue; @@ -92,32 +92,92 @@ public final class DeviceStateManagerFoldingFeatureProducer extends } if (mDeviceStateToPostureMap.size() > 0) { - context.getSystemService(DeviceStateManager.class) - .registerCallback(context.getMainExecutor(), mDeviceStateCallback); + DeviceStateCallback deviceStateCallback = (state) -> { + mCurrentDeviceState = state; + mRawFoldSupplier.getData(this::notifyFoldingFeatureChange); + }; + Objects.requireNonNull(context.getSystemService(DeviceStateManager.class)) + .registerCallback(context.getMainExecutor(), deviceStateCallback); } } - @Override - @Nullable - public Optional<List<CommonFoldingFeature>> getData() { - final int globalHingeState = globalHingeState(); - Optional<String> displayFeaturesString = mRawFoldSupplier.getData(); - if (displayFeaturesString.isEmpty() || TextUtils.isEmpty(displayFeaturesString.get())) { - return Optional.empty(); + /** + * Add a callback to mCallbacks if there is no device state. This callback will be run + * once a device state is set. Otherwise,run the callback immediately. + */ + private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback, + String displayFeaturesString) { + if (isCurrentStateValid()) { + callback.accept(calculateFoldingFeature(displayFeaturesString)); + } else { + // This callback will be added to mCallbacks and removed once it runs once. + AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = + new AcceptOnceConsumer<>(this, callback); + addDataChangedCallback(singleRunCallback); } - return Optional.of(parseListFromString(displayFeaturesString.get(), globalHingeState)); + } + + /** + * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the + * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was + * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}. + * Returns a boolean value of whether the device state is valid. + */ + private boolean isCurrentStateValid() { + // If the device state is not found in the map, indexOfKey returns a negative number. + return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState) >= 0; } @Override - protected void onListenersChanged(Set<Runnable> callbacks) { + protected void onListenersChanged( + @NonNull Set<Consumer<List<CommonFoldingFeature>>> callbacks) { super.onListenersChanged(callbacks); if (callbacks.isEmpty()) { - mRawFoldSupplier.removeDataChangedCallback(this::notifyDataChanged); + mCurrentDeviceState = INVALID_DEVICE_STATE; + mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); } else { - mRawFoldSupplier.addDataChangedCallback(this::notifyDataChanged); + mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); } } + @NonNull + @Override + public Optional<List<CommonFoldingFeature>> getCurrentData() { + Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); + if (!isCurrentStateValid()) { + return Optional.empty(); + } else { + return displayFeaturesString.map(this::calculateFoldingFeature); + } + } + + /** + * Adds the data to the storeFeaturesConsumer when the data is ready. + * @param storeFeaturesConsumer a consumer to collect the data when it is first available. + */ + public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) { + mRawFoldSupplier.getData((String displayFeaturesString) -> { + if (TextUtils.isEmpty(displayFeaturesString)) { + storeFeaturesConsumer.accept(new ArrayList<>()); + } else { + runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString); + } + }); + } + + private void notifyFoldingFeatureChange(String displayFeaturesString) { + if (TextUtils.isEmpty(displayFeaturesString)) { + notifyDataChanged(new ArrayList<>()); + } else { + notifyDataChanged(calculateFoldingFeature(displayFeaturesString)); + } + } + + private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { + final int globalHingeState = globalHingeState(); + return parseListFromString(displayFeaturesString, globalHingeState); + } + private int globalHingeState() { return mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java index 69ad1badce60..7906342d445d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java @@ -32,6 +32,7 @@ import com.android.internal.R; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; /** * Implementation of {@link androidx.window.util.DataProducer} that produces a @@ -40,7 +41,7 @@ import java.util.Set; * settings where the {@link String} property is saved with the key * {@link RawFoldingFeatureProducer#DISPLAY_FEATURES}. If this value is null or empty then the * value in {@link android.content.res.Resources} is used. If both are empty then - * {@link RawFoldingFeatureProducer#getData()} returns an empty object. + * {@link RawFoldingFeatureProducer#getData} returns an empty object. * {@link RawFoldingFeatureProducer} listens to changes in the setting so that it can override * the system {@link CommonFoldingFeature} data. */ @@ -63,12 +64,13 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { @Override @NonNull - public Optional<String> getData() { + public void getData(Consumer<String> dataConsumer) { String displayFeaturesString = getFeatureString(); if (displayFeaturesString == null) { - return Optional.empty(); + dataConsumer.accept(""); + } else { + dataConsumer.accept(displayFeaturesString); } - return Optional.of(displayFeaturesString); } /** @@ -84,7 +86,7 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { } @Override - protected void onListenersChanged(Set<Runnable> callbacks) { + protected void onListenersChanged(Set<Consumer<String>> callbacks) { if (callbacks.isEmpty()) { unregisterObserversIfNeeded(); } else { @@ -92,6 +94,12 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { } } + @NonNull + @Override + public Optional<String> getCurrentData() { + return Optional.of(getFeatureString()); + } + /** * Registers settings observers, if needed. When settings observers are registered for this * producer callbacks for changes in data will be triggered. @@ -125,8 +133,8 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { @Override public void onChange(boolean selfChange, Uri uri) { if (mDisplayFeaturesUri.equals(uri)) { - notifyDataChanged(); + notifyDataChanged(getFeatureString()); } } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index f4e91bae54ee..e50b9a1cd469 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -70,6 +70,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo); void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, @NonNull Configuration parentConfig); + void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, + @NonNull IBinder activityToken); } /** @@ -300,4 +302,12 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { mCallback.onTaskFragmentParentInfoChanged(fragmentToken, parentConfig); } } + + @Override + public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, + @NonNull IBinder activityToken) { + if (mCallback != null) { + mCallback.onActivityReparentToTask(taskId, activityIntent, activityToken); + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index e20cef2bec4e..9f33cbcbcbd5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -40,6 +40,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.util.ArraySet; +import android.util.Log; import android.util.SparseArray; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; @@ -59,6 +60,7 @@ import java.util.function.Consumer; */ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, ActivityEmbeddingComponent { + private static final String TAG = "SplitController"; @VisibleForTesting final SplitPresenter mPresenter; @@ -220,6 +222,19 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } + @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; + } + // TODO: handle for activity in other process. + } + /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */ private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { @@ -229,8 +244,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } if (taskContainer.isEmpty()) { // Cleanup the TaskContainer if it becomes empty. - mPresenter.stopOverrideSplitAnimation(taskContainer.mTaskId); - mTaskContainers.remove(taskContainer.mTaskId); + mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId()); + mTaskContainers.remove(taskContainer.getTaskId()); } return; } @@ -241,13 +256,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (taskContainer == null) { return; } - final boolean wasInPip = isInPictureInPicture(taskContainer.mConfiguration); + final boolean wasInPip = isInPictureInPicture(taskContainer.getConfiguration()); final boolean isInPIp = isInPictureInPicture(config); - taskContainer.mConfiguration = config; + taskContainer.setConfiguration(config); // We need to check the animation override when enter/exit PIP or has bounds changed. boolean shouldUpdateAnimationOverride = wasInPip != isInPIp; - if (onTaskBoundsMayChange(taskContainer, config.windowConfiguration.getBounds()) + if (taskContainer.setTaskBounds(config.windowConfiguration.getBounds()) && !isInPIp) { // We don't care the bounds change when it has already entered PIP. shouldUpdateAnimationOverride = true; @@ -257,16 +272,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } - /** Returns {@code true} if the bounds is changed. */ - private boolean onTaskBoundsMayChange(@NonNull TaskContainer taskContainer, - @NonNull Rect taskBounds) { - if (!taskBounds.isEmpty() && !taskContainer.mTaskBounds.equals(taskBounds)) { - taskContainer.mTaskBounds.set(taskBounds); - return true; - } - return false; - } - /** * Updates if we should override transition animation. We only want to override if the Task * bounds is large enough for at least one split rule. @@ -279,15 +284,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // We only want to override if it supports split. if (supportSplit(taskContainer)) { - mPresenter.startOverrideSplitAnimation(taskContainer.mTaskId); + mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId()); } else { - mPresenter.stopOverrideSplitAnimation(taskContainer.mTaskId); + mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId()); } } private boolean supportSplit(@NonNull TaskContainer taskContainer) { // No split inside PIP. - if (isInPictureInPicture(taskContainer.mConfiguration)) { + if (isInPictureInPicture(taskContainer.getConfiguration())) { return false; } // Check if the parent container bounds can support any split rule. @@ -295,7 +300,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!(rule instanceof SplitRule)) { continue; } - if (mPresenter.shouldShowSideBySide(taskContainer.mTaskBounds, (SplitRule) rule)) { + if (mPresenter.shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) { return true; } } @@ -425,21 +430,36 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } + TaskFragmentContainer newContainer(@NonNull Activity activity, int taskId) { + return newContainer(activity, activity, 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. */ - TaskFragmentContainer newContainer(@Nullable Activity activity, int taskId) { + TaskFragmentContainer newContainer(@Nullable Activity activity, + @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); - if (activity != null && !taskContainer.isTaskBoundsInitialized() - && onTaskBoundsMayChange(taskContainer, - SplitPresenter.getTaskBoundsFromActivity(activity))) { - // Initial check before any TaskFragment has appeared. + if (!taskContainer.isTaskBoundsInitialized()) { + // Get the initial bounds before the TaskFragment has appeared. + final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask); + if (!taskContainer.setTaskBounds(taskBounds)) { + Log.w(TAG, "Can't find bounds from activity=" + activityInTask); + } updateAnimationOverride(taskContainer); } return container; @@ -887,6 +907,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } + @Nullable + TaskContainer getTaskContainer(int taskId) { + return mTaskContainers.get(taskId); + } + /** * 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. @@ -1211,37 +1236,4 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return configuration != null && configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED; } - - /** Represents TaskFragments and split pairs below a Task. */ - @VisibleForTesting - static class TaskContainer { - /** The unique task id. */ - final int mTaskId; - /** Active TaskFragments in this Task. */ - final List<TaskFragmentContainer> mContainers = new ArrayList<>(); - /** Active split pairs in this Task. */ - final List<SplitContainer> mSplitContainers = new ArrayList<>(); - /** - * TaskFragments that the organizer has requested to be closed. They should be removed when - * the organizer receives {@link #onTaskFragmentVanished(TaskFragmentInfo)} event for them. - */ - final Set<IBinder> mFinishedContainer = new ArraySet<>(); - /** Available window bounds of this Task. */ - final Rect mTaskBounds = new Rect(); - /** Configuration of the Task. */ - @Nullable - Configuration mConfiguration; - - TaskContainer(int taskId) { - mTaskId = taskId; - } - - boolean isEmpty() { - return mContainers.isEmpty() && mFinishedContainer.isEmpty(); - } - - boolean isTaskBoundsInitialized() { - return !mTaskBounds.isEmpty(); - } - } } 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 1b49585ed7dc..716a087203d3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -19,9 +19,9 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import android.app.Activity; +import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; -import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -111,8 +111,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { primaryActivity, primaryRectBounds, null); // Create new empty task fragment - final TaskFragmentContainer secondaryContainer = mController.newContainer(null, - primaryContainer.getTaskId()); + final TaskFragmentContainer secondaryContainer = mController.newContainer( + null /* activity */, primaryActivity, primaryContainer.getTaskId()); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr(primaryActivity, rule)); createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), @@ -168,8 +168,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Creates a new expanded container. */ TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { - final TaskFragmentContainer newContainer = mController.newContainer(null, - launchingActivity.getTaskId()); + final TaskFragmentContainer newContainer = mController.newContainer(null /* activity */, + launchingActivity, launchingActivity.getTaskId()); final WindowContainerTransaction wct = new WindowContainerTransaction(); createTaskFragment(wct, newContainer.getTaskFragmentToken(), @@ -236,8 +236,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { launchingActivity.getTaskId()); } - TaskFragmentContainer secondaryContainer = mController.newContainer(null, - primaryContainer.getTaskId()); + TaskFragmentContainer secondaryContainer = mController.newContainer(null /* activity */, + launchingActivity, primaryContainer.getTaskId()); final WindowContainerTransaction wct = new WindowContainerTransaction(); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, rule); @@ -398,20 +398,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { - final Configuration parentConfig = mFragmentParentConfigs.get( - container.getTaskFragmentToken()); - if (parentConfig != null) { - return parentConfig.windowConfiguration.getBounds(); + final int taskId = container.getTaskId(); + final TaskContainer taskContainer = mController.getTaskContainer(taskId); + if (taskContainer == null) { + throw new IllegalStateException("Can't find TaskContainer taskId=" + taskId); } - - // If there is no parent yet - then assuming that activities are running in full task bounds - final Activity topActivity = container.getTopNonFinishingActivity(); - final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null; - - if (bounds == null) { - throw new IllegalStateException("Unknown parent bounds"); - } - return bounds; + return taskContainer.getTaskBounds(); } @NonNull @@ -419,22 +411,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final TaskFragmentContainer container = mController.getContainerWithActivity( activity.getActivityToken()); if (container != null) { - final Configuration parentConfig = mFragmentParentConfigs.get( - container.getTaskFragmentToken()); - if (parentConfig != null) { - return parentConfig.windowConfiguration.getBounds(); - } + return getParentContainerBounds(container); } - return getTaskBoundsFromActivity(activity); } @NonNull static Rect getTaskBoundsFromActivity(@NonNull Activity activity) { + final WindowConfiguration windowConfiguration = + activity.getResources().getConfiguration().windowConfiguration; if (!activity.isInMultiWindowMode()) { // In fullscreen mode the max bounds should correspond to the task bounds. - return activity.getResources().getConfiguration().windowConfiguration.getMaxBounds(); + return windowConfiguration.getMaxBounds(); } - return activity.getResources().getConfiguration().windowConfiguration.getBounds(); + return windowConfiguration.getBounds(); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java new file mode 100644 index 000000000000..be793018d969 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.ArraySet; +import android.window.TaskFragmentInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** Represents TaskFragments and split pairs below a Task. */ +class TaskContainer { + + /** The unique task id. */ + private final int mTaskId; + + /** Available window bounds of this Task. */ + private final Rect mTaskBounds = new Rect(); + + /** Configuration of the Task. */ + @Nullable + private Configuration mConfiguration; + + /** Active TaskFragments in this Task. */ + final List<TaskFragmentContainer> mContainers = new ArrayList<>(); + + /** Active split pairs in this Task. */ + final List<SplitContainer> mSplitContainers = new ArrayList<>(); + + /** + * TaskFragments that the organizer has requested to be closed. They should be removed when + * the organizer receives {@link SplitController#onTaskFragmentVanished(TaskFragmentInfo)} event + * for them. + */ + final Set<IBinder> mFinishedContainer = new ArraySet<>(); + + TaskContainer(int taskId) { + mTaskId = taskId; + } + + int getTaskId() { + return mTaskId; + } + + @NonNull + Rect getTaskBounds() { + return mTaskBounds; + } + + /** Returns {@code true} if the bounds is changed. */ + boolean setTaskBounds(@NonNull Rect taskBounds) { + if (!taskBounds.isEmpty() && !mTaskBounds.equals(taskBounds)) { + mTaskBounds.set(taskBounds); + return true; + } + return false; + } + + /** Whether the Task bounds has been initialized. */ + boolean isTaskBoundsInitialized() { + return !mTaskBounds.isEmpty(); + } + + @Nullable + Configuration getConfiguration() { + return mConfiguration; + } + + void setConfiguration(@Nullable Configuration configuration) { + mConfiguration = configuration; + } + + /** Whether there is any {@link TaskFragmentContainer} below this Task. */ + boolean isEmpty() { + return mContainers.isEmpty() && mFinishedContainer.isEmpty(); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index a6f638822d10..792176b6d029 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -43,7 +43,6 @@ import androidx.window.util.DataProducer; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -63,7 +62,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; - public WindowLayoutComponentImpl(Context context) { + public WindowLayoutComponentImpl(@NonNull Context context) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); RawFoldingFeatureProducer foldingFeatureProducer = new RawFoldingFeatureProducer(context); @@ -81,7 +80,6 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { public void addWindowLayoutInfoListener(@NonNull Activity activity, @NonNull Consumer<WindowLayoutInfo> consumer) { mWindowLayoutChangeListeners.put(activity, consumer); - onDisplayFeaturesChanged(); } /** @@ -89,18 +87,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} */ - public void removeWindowLayoutInfoListener( - @NonNull Consumer<WindowLayoutInfo> consumer) { + public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) { mWindowLayoutChangeListeners.values().remove(consumer); - onDisplayFeaturesChanged(); - } - - void updateWindowLayout(@NonNull Activity activity, - @NonNull WindowLayoutInfo newLayout) { - Consumer<WindowLayoutInfo> consumer = mWindowLayoutChangeListeners.get(activity); - if (consumer != null) { - consumer.accept(newLayout); - } } @NonNull @@ -108,7 +96,6 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { return mWindowLayoutChangeListeners.keySet(); } - @NonNull private boolean isListeningForLayoutChanges(IBinder token) { for (Activity activity: getActivitiesListeningForLayoutChanges()) { if (token.equals(activity.getWindow().getAttributes().token)) { @@ -125,12 +112,12 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { /** * A convenience method to translate from the common feature state to the extensions feature * state. More specifically, translates from {@link CommonFoldingFeature.State} to - * {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED}. If it is not + * {@link FoldingFeature#STATE_FLAT} or {@link FoldingFeature#STATE_HALF_OPENED}. If it is not * possible to translate, then we will return a {@code null} value. * * @param state if it matches a value in {@link CommonFoldingFeature.State}, {@code null} - * otherwise. @return a {@link FoldingFeature.STATE_FLAT} or - * {@link FoldingFeature.STATE_HALF_OPENED} if the given state matches a value in + * otherwise. @return a {@link FoldingFeature#STATE_FLAT} or + * {@link FoldingFeature#STATE_HALF_OPENED} if the given state matches a value in * {@link CommonFoldingFeature.State} and {@code null} otherwise. */ @Nullable @@ -144,17 +131,24 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } } - private void onDisplayFeaturesChanged() { + private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { for (Activity activity : getActivitiesListeningForLayoutChanges()) { - WindowLayoutInfo newLayout = getWindowLayoutInfo(activity); - updateWindowLayout(activity, newLayout); + // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer. + Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(activity); + WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(activity, storedFeatures); + layoutConsumer.accept(newWindowLayout); } } - @NonNull - private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { - List<DisplayFeature> displayFeatures = getDisplayFeatures(activity); - return new WindowLayoutInfo(displayFeatures); + /** + * Translates the {@link DisplayFeature} into a {@link WindowLayoutInfo} when a + * valid state is found. + * @param activity a proxy for the {@link android.view.Window} that contains the + */ + private WindowLayoutInfo getWindowLayoutInfo( + @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> displayFeatureList = getDisplayFeatures(activity, storedFeatures); + return new WindowLayoutInfo(displayFeatureList); } /** @@ -172,26 +166,21 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * * @param activity a proxy for the {@link android.view.Window} that contains the * {@link DisplayFeature}. - * @return a {@link List} of valid {@link DisplayFeature} that * are within the {@link android.view.Window} of the {@link Activity} */ - private List<DisplayFeature> getDisplayFeatures(@NonNull Activity activity) { + private List<DisplayFeature> getDisplayFeatures( + @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) { List<DisplayFeature> features = new ArrayList<>(); int displayId = activity.getDisplay().getDisplayId(); if (displayId != DEFAULT_DISPLAY) { Log.w(TAG, "This sample doesn't support display features on secondary displays"); return features; - } - - if (activity.isInMultiWindowMode()) { + } else if (activity.isInMultiWindowMode()) { // It is recommended not to report any display features in multi-window mode, since it // won't be possible to synchronize the display feature positions with window movement. return features; - } - - Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData(); - if (storedFeatures.isPresent()) { - for (CommonFoldingFeature baseFeature : storedFeatures.get()) { + } else { + for (CommonFoldingFeature baseFeature : storedFeatures) { Integer state = convertToExtensionState(baseFeature.getState()); if (state == null) { continue; @@ -205,8 +194,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); } } + return features; } - return features; } /** @@ -233,7 +222,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private void onDisplayFeaturesChangedIfListening(Activity activity) { IBinder token = activity.getWindow().getAttributes().token; if (token == null || isListeningForLayoutChanges(token)) { - onDisplayFeaturesChanged(); + mFoldingFeatureProducer.getData( + WindowLayoutComponentImpl.this::onDisplayFeaturesChanged); } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java index 970f0a2af632..5bfb0ebdcaa8 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java @@ -28,41 +28,42 @@ import android.content.Context; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; -import android.util.Log; import androidx.annotation.NonNull; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; import androidx.window.common.RawFoldingFeatureProducer; -import androidx.window.util.DataProducer; +import androidx.window.util.BaseDataProducer; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; /** * Reference implementation of androidx.window.sidecar OEM interface for use with * WindowManager Jetpack. */ class SampleSidecarImpl extends StubSidecar { - private static final String TAG = "SampleSidecar"; - - private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; - + private List<CommonFoldingFeature> mStoredFeatures = new ArrayList<>(); SampleSidecarImpl(Context context) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); - DataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context); - mFoldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context, - settingsFeatureProducer); + BaseDataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context); + BaseDataProducer<List<CommonFoldingFeature>> foldingFeatureProducer = + new DeviceStateManagerFoldingFeatureProducer(context, + settingsFeatureProducer); - mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); + foldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } - private void onDisplayFeaturesChanged() { + private void setStoredFeatures(List<CommonFoldingFeature> storedFeatures) { + mStoredFeatures = storedFeatures; + } + + private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { + setStoredFeatures(storedFeatures); updateDeviceState(getDeviceState()); for (IBinder windowToken : getWindowsListeningForLayoutChanges()) { SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken); @@ -79,16 +80,16 @@ class SampleSidecarImpl extends StubSidecar { } private int deviceStateFromFeature() { - List<CommonFoldingFeature> storedFeatures = mFoldingFeatureProducer.getData() - .orElse(Collections.emptyList()); - for (int i = 0; i < storedFeatures.size(); i++) { - CommonFoldingFeature feature = storedFeatures.get(i); + for (int i = 0; i < mStoredFeatures.size(); i++) { + CommonFoldingFeature feature = mStoredFeatures.get(i); final int state = feature.getState(); switch (state) { case CommonFoldingFeature.COMMON_STATE_FLAT: return SidecarDeviceState.POSTURE_OPENED; case CommonFoldingFeature.COMMON_STATE_HALF_OPENED: return SidecarDeviceState.POSTURE_HALF_OPENED; + case CommonFoldingFeature.COMMON_STATE_UNKNOWN: + return SidecarDeviceState.POSTURE_UNKNOWN; } } return SidecarDeviceState.POSTURE_UNKNOWN; @@ -109,7 +110,6 @@ class SampleSidecarImpl extends StubSidecar { private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) { int displayId = activity.getDisplay().getDisplayId(); if (displayId != DEFAULT_DISPLAY) { - Log.w(TAG, "This sample doesn't support display features on secondary displays"); return Collections.emptyList(); } @@ -119,18 +119,15 @@ class SampleSidecarImpl extends StubSidecar { return Collections.emptyList(); } - Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData(); List<SidecarDisplayFeature> features = new ArrayList<>(); - if (storedFeatures.isPresent()) { - for (CommonFoldingFeature baseFeature : storedFeatures.get()) { - SidecarDisplayFeature feature = new SidecarDisplayFeature(); - Rect featureRect = baseFeature.getRect(); - rotateRectToDisplayRotation(displayId, featureRect); - transformToWindowSpaceRect(activity, featureRect); - feature.setRect(featureRect); - feature.setType(baseFeature.getType()); - features.add(feature); - } + for (CommonFoldingFeature baseFeature : mStoredFeatures) { + SidecarDisplayFeature feature = new SidecarDisplayFeature(); + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(activity, featureRect); + feature.setRect(featureRect); + feature.setType(baseFeature.getType()); + features.add(feature); } return Collections.unmodifiableList(features); } @@ -138,7 +135,7 @@ class SampleSidecarImpl extends StubSidecar { @Override protected void onListenersChanged() { if (hasListeners()) { - onDisplayFeaturesChanged(); + onDisplayFeaturesChanged(mStoredFeatures); } } @@ -158,7 +155,7 @@ class SampleSidecarImpl extends StubSidecar { private void onDisplayFeaturesChangedForActivity(@NonNull Activity activity) { IBinder token = activity.getWindow().getAttributes().token; if (token == null || mWindowLayoutChangeListenerTokens.contains(token)) { - onDisplayFeaturesChanged(); + onDisplayFeaturesChanged(mStoredFeatures); } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java new file mode 100644 index 000000000000..7624b693ac43 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.util; + +import android.annotation.NonNull; + +import java.util.function.Consumer; + +/** + * A base class that works with {@link BaseDataProducer} to add/remove a consumer that should + * only be used once when {@link BaseDataProducer#notifyDataChanged} is called. + * @param <T> The type of data this producer returns through {@link DataProducer#getData}. + */ +public class AcceptOnceConsumer<T> implements Consumer<T> { + private final Consumer<T> mCallback; + private final DataProducer<T> mProducer; + + public AcceptOnceConsumer(@NonNull DataProducer<T> producer, @NonNull Consumer<T> callback) { + mProducer = producer; + mCallback = callback; + } + + @Override + public void accept(@NonNull T t) { + mCallback.accept(t); + mProducer.removeDataChangedCallback(this); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java index 930db3b701b7..0da44ac36a6e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java @@ -19,38 +19,48 @@ package androidx.window.util; import androidx.annotation.NonNull; import java.util.LinkedHashSet; +import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; /** * Base class that provides the implementation for the callback mechanism of the * {@link DataProducer} API. * - * @param <T> The type of data this producer returns through {@link #getData()}. + * @param <T> The type of data this producer returns through {@link DataProducer#getData}. */ public abstract class BaseDataProducer<T> implements DataProducer<T> { - private final Set<Runnable> mCallbacks = new LinkedHashSet<>(); + private final Set<Consumer<T>> mCallbacks = new LinkedHashSet<>(); @Override - public final void addDataChangedCallback(@NonNull Runnable callback) { + public final void addDataChangedCallback(@NonNull Consumer<T> callback) { mCallbacks.add(callback); + Optional<T> currentData = getCurrentData(); + currentData.ifPresent(callback); onListenersChanged(mCallbacks); } @Override - public final void removeDataChangedCallback(@NonNull Runnable callback) { + public final void removeDataChangedCallback(@NonNull Consumer<T> callback) { mCallbacks.remove(callback); onListenersChanged(mCallbacks); } - protected void onListenersChanged(Set<Runnable> callbacks) {} + protected void onListenersChanged(Set<Consumer<T>> callbacks) {} /** - * Called to notify all registered callbacks that the data provided by {@link #getData()} has - * changed. + * @return the current data if available and {@code Optional.empty()} otherwise. */ - protected void notifyDataChanged() { - for (Runnable callback : mCallbacks) { - callback.run(); + @NonNull + public abstract Optional<T> getCurrentData(); + + /** + * Called to notify all registered consumers that the data provided + * by {@link DataProducer#getData} has changed. + */ + protected void notifyDataChanged(T value) { + for (Consumer<T> callback : mCallbacks) { + callback.accept(value); } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java index d4d1a23b756b..ec301dc34aaa 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java @@ -18,26 +18,27 @@ package androidx.window.util; import android.annotation.NonNull; -import java.util.Optional; +import java.util.function.Consumer; /** - * Produces data through {@link #getData()} and provides a mechanism for receiving a callback when - * the data managed by the produces has changed. + * Produces data through {@link DataProducer#getData} and provides a mechanism for receiving + * a callback when the data managed by the produces has changed. * - * @param <T> The type of data this producer returns through {@link #getData()}. + * @param <T> The type of data this producer returns through {@link DataProducer#getData}. */ public interface DataProducer<T> { /** - * Returns the data currently stored in the provider, or {@link Optional#empty()} if the - * provider has no data. + * Emits the first available data at that point in time. + * @param dataConsumer a {@link Consumer} that will receive one value. */ - Optional<T> getData(); + void getData(@NonNull Consumer<T> dataConsumer); /** - * Adds a callback to be notified when the data returned from {@link #getData()} has changed. + * Adds a callback to be notified when the data returned + * from {@link DataProducer#getData} has changed. */ - void addDataChangedCallback(@NonNull Runnable callback); + void addDataChangedCallback(@NonNull Consumer<T> callback); - /** Removes a callback previously added with {@link #addDataChangedCallback(Runnable)}. */ - void removeDataChangedCallback(@NonNull Runnable callback); + /** Removes a callback previously added with {@link #addDataChangedCallback(Consumer)}. */ + void removeDataChangedCallback(@NonNull Consumer<T> callback); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 72519dc6da5f..e0fda58fd664 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 @@ -21,6 +21,9 @@ 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.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -29,12 +32,12 @@ import static org.mockito.Mockito.never; import android.app.Activity; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.window.extensions.embedding.SplitController.TaskContainer; import org.junit.Before; import org.junit.Test; @@ -53,6 +56,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class SplitControllerTest { private static final int TASK_ID = 10; + private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); @Mock private Activity mActivity; @@ -70,8 +74,11 @@ public class SplitControllerTest { mSplitPresenter = mSplitController.mPresenter; spyOn(mSplitController); spyOn(mSplitPresenter); + final Configuration activityConfig = new Configuration(); + activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); + activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); doReturn(mActivityResources).when(mActivity).getResources(); - doReturn(new Configuration()).when(mActivityResources).getConfiguration(); + doReturn(activityConfig).when(mActivityResources).getConfiguration(); } @Test @@ -117,4 +124,20 @@ public class SplitControllerTest { verify(mSplitController).removeContainer(tf); verify(mActivity, never()).finish(); } + + @Test + public void testNewContainer() { + // Must pass in a valid activity. + assertThrows(IllegalArgumentException.class, () -> + mSplitController.newContainer(null /* activity */, TASK_ID)); + assertThrows(IllegalArgumentException.class, () -> + mSplitController.newContainer(mActivity, null /* launchingActivity */, TASK_ID)); + + final TaskFragmentContainer tf = mSplitController.newContainer(null, mActivity, TASK_ID); + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + + assertNotNull(tf); + assertNotNull(taskContainer); + assertEquals(TASK_BOUNDS, taskContainer.getTaskBounds()); + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java new file mode 100644 index 000000000000..9fb08dffbab8 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +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.Test; +import org.junit.runner.RunWith; + +/** + * Test class for {@link TaskContainer}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:TaskContainerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TaskContainerTest { + private static final int TASK_ID = 10; + private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); + + @Test + public void testIsTaskBoundsInitialized() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + + assertFalse(taskContainer.isTaskBoundsInitialized()); + + taskContainer.setTaskBounds(TASK_BOUNDS); + + assertTrue(taskContainer.isTaskBoundsInitialized()); + } + + @Test + public void testSetTaskBounds() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + + assertFalse(taskContainer.setTaskBounds(new Rect())); + + assertTrue(taskContainer.setTaskBounds(TASK_BOUNDS)); + + assertFalse(taskContainer.setTaskBounds(TASK_BOUNDS)); + } + + @Test + public void testIsEmpty() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + + assertTrue(taskContainer.isEmpty()); + + final TaskFragmentContainer tf = new TaskFragmentContainer(null, TASK_ID); + taskContainer.mContainers.add(tf); + + assertFalse(taskContainer.isEmpty()); + + taskContainer.mFinishedContainer.add(tf.getTaskFragmentToken()); + taskContainer.mContainers.clear(); + + assertFalse(taskContainer.isEmpty()); + } +} diff --git a/libs/WindowManager/Shell/res/drawable/pip_custom_close_bg.xml b/libs/WindowManager/Shell/res/drawable/pip_custom_close_bg.xml new file mode 100644 index 000000000000..39c3fe6f6106 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_custom_close_bg.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + <solid + android:color="@color/pip_custom_close_bg" /> + <size + android:width="@dimen/pip_custom_close_bg_size" + android:height="@dimen/pip_custom_close_bg_size" /> +</shape> diff --git a/libs/WindowManager/Shell/res/layout/pip_menu_action.xml b/libs/WindowManager/Shell/res/layout/pip_menu_action.xml index a733b31d9fb0..b51dd6a00815 100644 --- a/libs/WindowManager/Shell/res/layout/pip_menu_action.xml +++ b/libs/WindowManager/Shell/res/layout/pip_menu_action.xml @@ -22,6 +22,14 @@ android:forceHasOverlappingRendering="false"> <ImageView + android:id="@+id/custom_close_bg" + android:layout_width="@dimen/pip_custom_close_bg_size" + android:layout_height="@dimen/pip_custom_close_bg_size" + android:layout_gravity="center" + android:src="@drawable/pip_custom_close_bg" + android:visibility="gone"/> + + <ImageView android:id="@+id/image" android:layout_width="@dimen/pip_action_inner_size" android:layout_height="@dimen/pip_action_inner_size" diff --git a/libs/WindowManager/Shell/res/layout/split_decor.xml b/libs/WindowManager/Shell/res/layout/split_decor.xml index 9ffa5e8aa179..dfb90affe7f6 100644 --- a/libs/WindowManager/Shell/res/layout/split_decor.xml +++ b/libs/WindowManager/Shell/res/layout/split_decor.xml @@ -20,9 +20,10 @@ android:layout_width="match_parent"> <ImageView android:id="@+id/split_resizing_icon" - android:layout_height="wrap_content" - android:layout_width="wrap_content" + android:layout_height="@*android:dimen/starting_surface_icon_size" + android:layout_width="@*android:dimen/starting_surface_icon_size" android:layout_gravity="center" + android:scaleType="fitCenter" android:padding="0dp" android:visibility="gone" android:background="@null"/> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 974960708190..6f38ecae674d 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -46,10 +46,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Superior 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Superior 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla inferior completa"</string> - <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usar Modo una mano"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usar modo Una mano"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para salir, desliza el dedo hacia arriba desde la parte inferior de la pantalla o toca cualquier zona que haya encima de la aplicación"</string> - <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar Modo una mano"</string> - <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Salir del Modo una mano"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar modo Una mano"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Salir del modo Una mano"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Ajustes de las burbujas de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menú adicional"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Volver a añadir a la pila"</string> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 4606d24d1716..6e750a3d5e34 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -30,6 +30,9 @@ <color name="bubbles_dark">@color/GM2_grey_800</color> <color name="bubbles_icon_tint">@color/GM2_grey_700</color> + <!-- PiP --> + <color name="pip_custom_close_bg">#D93025</color> + <!-- Compat controls UI --> <color name="compat_controls_background">@android:color/system_neutral1_800</color> <color name="compat_controls_text">@android:color/system_neutral1_50</color> @@ -47,4 +50,4 @@ <color name="splash_screen_bg_light">#FFFFFF</color> <color name="splash_screen_bg_dark">#000000</color> <color name="splash_window_background_default">@color/splash_screen_bg_light</color> -</resources>
\ No newline at end of file +</resources> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index d416c060c86c..8ba41ab60c87 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -46,6 +46,10 @@ <!-- Show PiP enter split icon, which allows apps to directly enter splitscreen from PiP. --> <bool name="config_pipEnableEnterSplitButton">false</bool> + <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself + if a custom action is present before closing it. --> + <integer name="config_pipForceCloseDelay">1000</integer> + <!-- Animation duration when using long press on recents to dock --> <integer name="long_press_dock_anim_duration">250</integer> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index a2f9e884b37d..c21381d1486a 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -78,6 +78,9 @@ WindowConfiguration#PINNED_WINDOWING_MODE_ELEVATION_IN_DIP --> <dimen name="pip_shadow_radius">5dp</dimen> + <!-- The width and height of the background for custom action in PiP menu. --> + <dimen name="pip_custom_close_bg_size">32dp</dimen> + <dimen name="dismiss_target_x_size">24dp</dimen> <dimen name="floating_dismiss_bottom_margin">50dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt index 4b7950e9090a..255e4d2c0d44 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.animation import android.util.ArrayMap import android.util.Log import android.view.View +import androidx.dynamicanimation.animation.AnimationHandler import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FlingAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat @@ -123,6 +124,12 @@ class PhysicsAnimator<T> private constructor (target: T) { private var defaultFling: FlingConfig = globalDefaultFling /** + * AnimationHandler to use if it need custom AnimationHandler, if this is null, it will use + * the default AnimationHandler in the DynamicAnimation. + */ + private var customAnimationHandler: AnimationHandler? = null + + /** * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add * just one permanent update and end listener to the DynamicAnimations. @@ -446,6 +453,14 @@ class PhysicsAnimator<T> private constructor (target: T) { this.defaultFling = defaultFling } + /** + * Set the custom AnimationHandler for all aniatmion in this animator. Set this with null for + * restoring to default AnimationHandler. + */ + fun setCustomAnimationHandler(handler: AnimationHandler) { + this.customAnimationHandler = handler + } + /** Starts the animations! */ fun start() { startAction() @@ -495,10 +510,13 @@ class PhysicsAnimator<T> private constructor (target: T) { // springs) on this property before flinging. cancel(animatedProperty) + // Apply the custom animation handler if it not null + val flingAnim = getFlingAnimation(animatedProperty, target) + flingAnim.animationHandler = + customAnimationHandler ?: flingAnim.animationHandler + // Apply the configuration and start the animation. - getFlingAnimation(animatedProperty, target) - .also { flingConfig.applyToAnimation(it) } - .start() + flingAnim.also { flingConfig.applyToAnimation(it) }.start() } } @@ -510,6 +528,21 @@ class PhysicsAnimator<T> private constructor (target: T) { if (flingConfig == null) { // Apply the configuration and start the animation. val springAnim = getSpringAnimation(animatedProperty, target) + + // If customAnimationHander is exist and has not been set to the animation, + // it should set here. + if (customAnimationHandler != null && + springAnim.animationHandler != customAnimationHandler) { + // Cancel the animation before set animation handler + if (springAnim.isRunning) { + cancel(animatedProperty) + } + // Apply the custom animation handler if it not null + springAnim.animationHandler = + customAnimationHandler ?: springAnim.animationHandler + } + + // Apply the configuration and start the animation. springConfig.applyToAnimation(springAnim) animationStartActions.add(springAnim::start) } else { @@ -564,10 +597,13 @@ class PhysicsAnimator<T> private constructor (target: T) { } } + // Apply the custom animation handler if it not null + val springAnim = getSpringAnimation(animatedProperty, target) + springAnim.animationHandler = + customAnimationHandler ?: springAnim.animationHandler + // Apply the configuration and start the spring animation. - getSpringAnimation(animatedProperty, target) - .also { springConfig.applyToAnimation(it) } - .start() + springAnim.also { springConfig.applyToAnimation(it) }.start() } } }) 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 5ef24133ca11..806c395bf395 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 @@ -96,6 +96,7 @@ 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.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.PinnedStackListenerForwarder; @@ -214,6 +215,8 @@ public class BubbleController { /** One handed mode controller to register transition listener. */ private Optional<OneHandedController> mOneHandedOptional; + /** Drag and drop controller to register listener for onDragStarted. */ + private DragAndDropController mDragAndDropController; /** * Creates an instance of the BubbleController. @@ -230,6 +233,7 @@ public class BubbleController { ShellTaskOrganizer organizer, DisplayController displayController, Optional<OneHandedController> oneHandedOptional, + DragAndDropController dragAndDropController, ShellExecutor mainExecutor, Handler mainHandler, TaskViewTransitions taskViewTransitions, @@ -241,8 +245,8 @@ public class BubbleController { new BubbleDataRepository(context, launcherApps, mainExecutor), statusBarService, windowManager, windowManagerShellWrapper, launcherApps, logger, taskStackListener, organizer, positioner, displayController, - oneHandedOptional, mainExecutor, mainHandler, taskViewTransitions, - syncQueue); + oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, + taskViewTransitions, syncQueue); } /** @@ -264,6 +268,7 @@ public class BubbleController { BubblePositioner positioner, DisplayController displayController, Optional<OneHandedController> oneHandedOptional, + DragAndDropController dragAndDropController, ShellExecutor mainExecutor, Handler mainHandler, TaskViewTransitions taskViewTransitions, @@ -293,6 +298,7 @@ public class BubbleController { mDisplayController = displayController; mTaskViewTransitions = taskViewTransitions; mOneHandedOptional = oneHandedOptional; + mDragAndDropController = dragAndDropController; mSyncQueue = syncQueue; } @@ -433,6 +439,7 @@ public class BubbleController { }); mOneHandedOptional.ifPresent(this::registerOneHandedState); + mDragAndDropController.addListener(this::collapseStack); } @VisibleForTesting @@ -884,7 +891,6 @@ public class BubbleController { return mBubbleData.isExpanded(); } - @VisibleForTesting public void collapseStack() { mBubbleData.setExpanded(false /* expanded */); } 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 79d795ee613f..fedb9983a65e 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,9 @@ public class DisplayLayout { } final DisplayCutout.CutoutPathParserInfo info = cutout.getCutoutPathParserInfo(); final DisplayCutout.CutoutPathParserInfo newInfo = new DisplayCutout.CutoutPathParserInfo( - info.getDisplayWidth(), info.getDisplayHeight(), info.getDensity(), - info.getCutoutSpec(), rotation, info.getScale()); + info.getDisplayWidth(), info.getDisplayHeight(), info.getStableDisplayWidth(), + info.getStableDisplayHeight(), info.getDensity(), info.getCutoutSpec(), rotation, + info.getScale(), info.getPhysicalPixelDisplaySizeRatio()); return computeSafeInsets( DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo), rotated ? displayHeight : displayWidth, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java new file mode 100644 index 000000000000..fd3aa05cfc06 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; +import android.view.View; + +import com.android.internal.jank.InteractionJankMonitor; + +/** Utils class for simplfy InteractionJank trancing call */ +public class InteractionJankMonitorUtils { + + /** + * Begin a trace session. + * + * @param cujType the specific {@link InteractionJankMonitor.CujType}. + * @param view the view to trace + * @param tag the tag to distinguish different flow of same type CUJ. + */ + public static void beginTracing(@InteractionJankMonitor.CujType int cujType, + @NonNull View view, @Nullable String tag) { + final InteractionJankMonitor.Configuration.Builder builder = + InteractionJankMonitor.Configuration.Builder.withView(cujType, view); + if (!TextUtils.isEmpty(tag)) { + builder.setTag(tag); + } + InteractionJankMonitor.getInstance().begin(builder); + } + + /** + * End a trace session. + * + * @param cujType the specific {@link InteractionJankMonitor.CujType}. + */ + public static void endTracing(@InteractionJankMonitor.CujType int cujType) { + InteractionJankMonitor.getInstance().end(cujType); + } + + /** + * Cancel the trace session. + * + * @param cujType the specific {@link InteractionJankMonitor.CujType}. + */ + public static void cancelTracing(@InteractionJankMonitor.CujType int cujType) { + InteractionJankMonitor.getInstance().cancel(cujType); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java index e270edb800bd..d5875c03ccd2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -221,7 +221,8 @@ public class SystemWindows { } final Display display = mDisplayController.getDisplay(mDisplayId); SurfaceControlViewHost viewRoot = - new SurfaceControlViewHost(view.getContext(), display, wwm); + new SurfaceControlViewHost( + view.getContext(), display, wwm, true /* useSfChoreographer */); attrs.flags |= FLAG_HARDWARE_ACCELERATED; viewRoot.setView(view, attrs); mViewRoots.put(view, viewRoot); 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 ec81d230fae7..0b8e631068fc 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 @@ -55,6 +55,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.DividerSnapAlgorithm; import com.android.internal.policy.DockedDividerUtils; import com.android.wm.shell.R; @@ -62,6 +63,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.InteractionJankMonitorUtils; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import java.io.PrintWriter; @@ -434,6 +436,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mSplitLayoutHandler.onLayoutSizeChanged(this); return; } + InteractionJankMonitorUtils.beginTracing(InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE, + mSplitWindowManager.getDividerView(), "Divider fling"); ValueAnimator animator = ValueAnimator .ofInt(from, to) .setDuration(250); @@ -446,6 +450,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (flingFinishedCallback != null) { flingFinishedCallback.run(); } + InteractionJankMonitorUtils.endTracing( + InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 833d9d50701c..864b9a7528b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -37,6 +37,7 @@ import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.SurfaceSession; +import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -170,6 +171,10 @@ public final class SplitWindowManager extends WindowlessWindowManager { mDividerView.setInteractive(interactive); } + View getDividerView() { + return mDividerView; + } + /** * Gets {@link SurfaceControl} of the surface holding divider view. @return {@code null} if not * feasible. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java index 5a94fb65f174..8f9636c0bb30 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -19,6 +19,7 @@ package com.android.wm.shell.dagger; import static android.os.Process.THREAD_PRIORITY_DISPLAY; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; +import android.animation.AnimationHandler; import android.content.Context; import android.os.Build; import android.os.Handler; @@ -28,9 +29,11 @@ import android.os.Trace; import androidx.annotation.Nullable; +import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ChoreographerSfVsync; import com.android.wm.shell.common.annotations.ExternalMainThread; import com.android.wm.shell.common.annotations.ShellAnimationThread; import com.android.wm.shell.common.annotations.ShellMainThread; @@ -168,4 +171,28 @@ public abstract class WMShellConcurrencyModule { shellSplashscreenThread.start(); return new HandlerExecutor(shellSplashscreenThread.getThreadHandler()); } + + /** + * Provide a Shell main-thread AnimationHandler. The AnimationHandler can be set on + * {@link android.animation.ValueAnimator}s and will ensure that the animation will run on + * the Shell main-thread with the SF vsync. + */ + @WMSingleton + @Provides + @ChoreographerSfVsync + public static AnimationHandler provideShellMainExecutorSfVsyncAnimationHandler( + @ShellMainThread ShellExecutor mainExecutor) { + try { + AnimationHandler handler = new AnimationHandler(); + mainExecutor.executeBlocking(() -> { + // This is called on the animation thread since it calls + // Choreographer.getSfInstance() which returns a thread-local Choreographer instance + // that uses the SF vsync + handler.setProvider(new SfVsyncFrameCallbackProvider()); + }); + return handler; + } catch (InterruptedException e) { + throw new RuntimeException("Failed to initialize SfVsync animation handler in 1s", e); + } + } } 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 4fa5a1029e28..96e5f60c952b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -16,6 +16,7 @@ package com.android.wm.shell.dagger; +import android.animation.AnimationHandler; import android.content.Context; import android.content.pm.LauncherApps; import android.os.Handler; @@ -41,7 +42,9 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ChoreographerSfVsync; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.freeform.FreeformTaskListener; import com.android.wm.shell.fullscreen.FullscreenUnfoldController; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; @@ -110,6 +113,7 @@ public class WMShellModule { ShellTaskOrganizer organizer, DisplayController displayController, @DynamicOverride Optional<OneHandedController> oneHandedOptional, + DragAndDropController dragAndDropController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, TaskViewTransitions taskViewTransitions, @@ -118,7 +122,7 @@ public class WMShellModule { floatingContentCoordinator, statusBarService, windowManager, windowManagerShellWrapper, launcherApps, taskStackListener, uiEventLogger, organizer, displayController, oneHandedOptional, - mainExecutor, mainHandler, taskViewTransitions, syncQueue); + dragAndDropController, mainExecutor, mainHandler, taskViewTransitions, syncQueue); } // @@ -182,10 +186,11 @@ public class WMShellModule { DisplayImeController displayImeController, TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, TaskStackListenerImpl taskStackListener, Transitions transitions, - @ShellMainThread ShellExecutor mainExecutor) { + @ShellMainThread ShellExecutor mainExecutor, + @ChoreographerSfVsync AnimationHandler sfVsyncAnimationHandler) { return new LegacySplitScreenController(context, displayController, systemWindows, displayImeController, transactionPool, shellTaskOrganizer, syncQueue, - taskStackListener, transitions, mainExecutor); + taskStackListener, transitions, mainExecutor, sfVsyncAnimationHandler); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 11ecc9197be7..95de2dc61a43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -35,9 +35,6 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.content.ClipDescription; import android.content.Context; import android.content.res.Configuration; @@ -52,17 +49,19 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; +import androidx.annotation.VisibleForTesting; + import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEventLogger; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; +import java.util.ArrayList; import java.util.Optional; /** @@ -80,10 +79,19 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange private SplitScreenController mSplitScreen; private ShellExecutor mMainExecutor; private DragAndDropImpl mImpl; + private ArrayList<DragAndDropListener> mListeners = new ArrayList<>(); private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + /** + * Listener called during drag events, currently just onDragStarted. + */ + public interface DragAndDropListener { + /** Called when a drag has started. */ + void onDragStarted(); + } + public DragAndDropController(Context context, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor) { mContext = context; @@ -103,6 +111,22 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange mDisplayController.addDisplayWindowListener(this); } + /** Adds a listener to be notified of drag and drop events. */ + public void addListener(DragAndDropListener listener) { + mListeners.add(listener); + } + + /** Removes a drag and drop listener. */ + public void removeListener(DragAndDropListener listener) { + mListeners.remove(listener); + } + + private void notifyListeners() { + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onDragStarted(); + } + } + @Override public void onDisplayAdded(int displayId) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display added: %d", displayId); @@ -137,13 +161,19 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); try { wm.addView(rootView, layoutParams); - mDisplayDropTargets.put(displayId, - new PerDisplay(displayId, context, wm, rootView, dragLayout)); + addDisplayDropTarget(displayId, context, wm, rootView, dragLayout); } catch (WindowManager.InvalidDisplayException e) { Slog.w(TAG, "Unable to add view for display id: " + displayId); } } + @VisibleForTesting + void addDisplayDropTarget(int displayId, Context context, WindowManager wm, + FrameLayout rootView, DragLayout dragLayout) { + mDisplayDropTargets.put(displayId, + new PerDisplay(displayId, context, wm, rootView, dragLayout)); + } + @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display changed: %d", displayId); @@ -206,6 +236,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId), event.getClipData(), loggerSessionId); setDropTargetWindowVisibility(pd, View.VISIBLE); + notifyListeners(); break; case ACTION_DRAG_ENTERED: pd.dragLayout.show(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java index 38870bcb3383..0cea36ed48c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java @@ -28,6 +28,7 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.FloatProperty; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -110,10 +111,12 @@ public class DropZoneView extends FrameLayout { mColorDrawable = new ColorDrawable(); setBackgroundDrawable(mColorDrawable); + final int iconSize = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.starting_surface_icon_size); mSplashScreenView = new ImageView(context); - mSplashScreenView.setScaleType(ImageView.ScaleType.CENTER); - addView(mSplashScreenView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); + mSplashScreenView.setScaleType(ImageView.ScaleType.FIT_CENTER); + addView(mSplashScreenView, + new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER)); mSplashScreenView.setAlpha(0f); mMarginView = new MarginView(context); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java index 9754a0369b67..73be2835d2cd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java @@ -25,6 +25,7 @@ import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR import static com.android.wm.shell.common.split.DividerView.TOUCH_ANIMATION_DURATION; import static com.android.wm.shell.common.split.DividerView.TOUCH_RELEASE_ANIMATION_DURATION; +import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -140,6 +141,7 @@ public class DividerView extends FrameLayout implements OnTouchListener, private DividerImeController mImeController; private DividerCallbacks mCallback; + private AnimationHandler mSfVsyncAnimationHandler; private ValueAnimator mCurrentAnimator; private boolean mEntranceAnimationRunning; private boolean mExitAnimationRunning; @@ -260,6 +262,10 @@ public class DividerView extends FrameLayout implements OnTouchListener, mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); } + public void setAnimationHandler(AnimationHandler sfVsyncAnimationHandler) { + mSfVsyncAnimationHandler = sfVsyncAnimationHandler; + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -651,6 +657,7 @@ public class DividerView extends FrameLayout implements OnTouchListener, } }); mCurrentAnimator = anim; + mCurrentAnimator.setAnimationHandler(mSfVsyncAnimationHandler); return anim; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java index 8b6679273bea..67e487de0993 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java @@ -25,6 +25,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.Display.DEFAULT_DISPLAY; +import android.animation.AnimationHandler; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; @@ -81,6 +82,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays private final DividerState mDividerState = new DividerState(); private final ForcedResizableInfoActivityController mForcedResizableController; private final ShellExecutor mMainExecutor; + private final AnimationHandler mSfVsyncAnimationHandler; private final LegacySplitScreenTaskListener mSplits; private final SystemWindows mSystemWindows; final TransactionPool mTransactionPool; @@ -116,12 +118,13 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays DisplayImeController imeController, TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, TaskStackListenerImpl taskStackListener, Transitions transitions, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, AnimationHandler sfVsyncAnimationHandler) { mContext = context; mDisplayController = displayController; mSystemWindows = systemWindows; mImeController = imeController; mMainExecutor = mainExecutor; + mSfVsyncAnimationHandler = sfVsyncAnimationHandler; mForcedResizableController = new ForcedResizableInfoActivityController(context, this, mainExecutor); mTransactionPool = transactionPool; @@ -311,6 +314,7 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId()); mView = (DividerView) LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null); + mView.setAnimationHandler(mSfVsyncAnimationHandler); DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId()); mView.injectDependencies(this, mWindowManager, mDividerState, mForcedResizableController, mSplits, mSplitLayout, mImePositionProcessor, mWindowManagerProxy); 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 95bb65c5873e..d357655882ff 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 @@ -30,6 +30,7 @@ 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; @@ -270,15 +271,14 @@ public class PipAnimationController { mEndValue = endValue; addListener(this); addUpdateListener(this); - mSurfaceControlTransactionFactory = - new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; mTransitionDirection = TRANSITION_DIRECTION_NONE; } @Override public void onAnimationStart(Animator animation) { mCurrentValue = mStartValue; - onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction()); + onStartTransaction(mLeash, newSurfaceControlTransaction()); if (mPipAnimationCallback != null) { mPipAnimationCallback.onPipAnimationStart(mTaskInfo, this); } @@ -286,16 +286,14 @@ public class PipAnimationController { @Override public void onAnimationUpdate(ValueAnimator animation) { - applySurfaceControlTransaction(mLeash, - mSurfaceControlTransactionFactory.getTransaction(), + applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(), animation.getAnimatedFraction()); } @Override public void onAnimationEnd(Animator animation) { mCurrentValue = mEndValue; - final SurfaceControl.Transaction tx = - mSurfaceControlTransactionFactory.getTransaction(); + final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); onEndTransaction(mLeash, tx, mTransitionDirection); if (mPipAnimationCallback != null) { mPipAnimationCallback.onPipAnimationEnd(mTaskInfo, tx, this); @@ -342,8 +340,7 @@ public class PipAnimationController { } PipTransitionAnimator<T> setUseContentOverlay(Context context) { - final SurfaceControl.Transaction tx = - mSurfaceControlTransactionFactory.getTransaction(); + final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); if (mContentOverlay != null) { // remove existing content overlay if there is any. tx.remove(mContentOverlay); @@ -418,7 +415,7 @@ public class PipAnimationController { void setDestinationBounds(Rect destinationBounds) { mDestinationBounds.set(destinationBounds); if (mAnimationType == ANIM_TYPE_ALPHA) { - onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction()); + onStartTransaction(mLeash, newSurfaceControlTransaction()); } } @@ -453,6 +450,16 @@ public class PipAnimationController { mEndValue = endValue; } + /** + * @return {@link SurfaceControl.Transaction} instance with vsync-id. + */ + protected SurfaceControl.Transaction newSurfaceControlTransaction() { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); + return tx; + } + @VisibleForTesting public void setSurfaceControlTransactionFactory( PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { 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 b349010be1fe..c6e48f53681c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -20,7 +20,6 @@ import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; -import android.view.Choreographer; import android.view.SurfaceControl; import com.android.wm.shell.R; @@ -225,18 +224,4 @@ public class PipSurfaceTransactionHelper { public interface SurfaceControlTransactionFactory { SurfaceControl.Transaction getTransaction(); } - - /** - * Implementation of {@link SurfaceControlTransactionFactory} that returns - * {@link SurfaceControl.Transaction} with VsyncId being set. - */ - public static class VsyncSurfaceControlTransactionFactory - implements SurfaceControlTransactionFactory { - @Override - public SurfaceControl.Transaction getTransaction() { - final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); - return tx; - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 664f98857cc7..d6b914138c38 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 @@ -280,8 +280,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper = surfaceTransactionHelper; mPipAnimationController = pipAnimationController; mPipUiEventLoggerLogger = pipUiEventLogger; - mSurfaceControlTransactionFactory = - new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; mSplitScreenOptional = splitScreenOptional; mTaskOrganizer = shellTaskOrganizer; mMainExecutor = mainExecutor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java index 9c23a32a7d2b..513ebba59258 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java @@ -110,7 +110,10 @@ public class PipUiEventLogger { PICTURE_IN_PICTURE_STASH_RIGHT(711), @UiEvent(doc = "User taps on the settings button in PiP menu") - PICTURE_IN_PICTURE_SHOW_SETTINGS(933); + PICTURE_IN_PICTURE_SHOW_SETTINGS(933), + + @UiEvent(doc = "Closes PiP with app-provided close action") + PICTURE_IN_PICTURE_CUSTOM_CLOSE(1058); private final int mId; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index f73b81e9d3f3..bbec4eccce3c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -121,6 +121,7 @@ public class PhonePipMenuController implements PipMenuController { private final Optional<SplitScreenController> mSplitScreenController; private final PipUiEventLogger mPipUiEventLogger; private ParceledListSlice<RemoteAction> mAppActions; + private RemoteAction mCloseAction; private ParceledListSlice<RemoteAction> mMediaActions; private SyncRtSurfaceTransactionApplier mApplier; private int mMenuState; @@ -171,7 +172,7 @@ public class PhonePipMenuController implements PipMenuController { detachPipMenuView(); } - private void attachPipMenuView() { + void attachPipMenuView() { // In case detach was not called (e.g. PIP unexpectedly closed) if (mPipMenuView != null) { detachPipMenuView(); @@ -459,6 +460,7 @@ public class PhonePipMenuController implements PipMenuController { public void setAppActions(ParceledListSlice<RemoteAction> appActions, RemoteAction closeAction) { mAppActions = appActions; + mCloseAction = closeAction; updateMenuActions(); } @@ -490,9 +492,8 @@ public class PhonePipMenuController implements PipMenuController { private void updateMenuActions() { if (mPipMenuView != null) { final ParceledListSlice<RemoteAction> menuActions = resolveMenuActions(); - if (menuActions != null) { - mPipMenuView.setActions(mPipBoundsState.getBounds(), menuActions.getList()); - } + mPipMenuView.setActions(mPipBoundsState.getBounds(), + menuActions == null ? null : menuActions.getList(), mCloseAction); } } 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 175a2445f28d..272331b7cd3f 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 @@ -521,6 +521,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb }; if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) { + mMenuController.attachPipMenuView(); // Calculate the snap fraction of the current stack along the old movement bounds final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); final Rect postChangeStackBounds = new Rect(mPipBoundsState.getBounds()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java index 11633a91e8c6..a0e22011b5d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java @@ -220,10 +220,16 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen return; } + final SurfaceControl targetViewLeash = + mTargetViewContainer.getViewRootImpl().getSurfaceControl(); + if (!targetViewLeash.isValid()) { + // The surface of mTargetViewContainer is somehow not ready, bail early + return; + } + // Put the dismiss target behind the task SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - t.setRelativeLayer(mTargetViewContainer.getViewRootImpl().getSurfaceControl(), - mTaskLeash, -1); + t.setRelativeLayer(targetViewLeash, mTaskLeash, -1); t.apply(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java index 5ddb534b6829..0f3ff36601fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java @@ -146,10 +146,11 @@ public class PipInputConsumer { "%s: Failed to create input consumer, %s", TAG, e); } mMainExecutor.execute(() -> { - // Choreographer.getInstance() must be called on the thread that the input event + // Choreographer.getSfInstance() must be called on the thread that the input event // receiver should be receiving events + // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions mInputEventReceiver = new InputEventReceiver(inputChannel, - Looper.myLooper(), Choreographer.getInstance()); + Looper.myLooper(), Choreographer.getSfInstance()); if (mRegistrationListener != null) { mRegistrationListener.onRegistrationChanged(true /* isRegistered */); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java index f11ae422e837..7f84500e8406 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip.phone; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; @@ -30,6 +31,7 @@ import com.android.wm.shell.R; */ public class PipMenuActionView extends FrameLayout { private ImageView mImageView; + private View mCustomCloseBackground; public PipMenuActionView(Context context, AttributeSet attrs) { super(context, attrs); @@ -39,10 +41,16 @@ public class PipMenuActionView extends FrameLayout { protected void onFinishInflate() { super.onFinishInflate(); mImageView = findViewById(R.id.image); + mCustomCloseBackground = findViewById(R.id.custom_close_bg); } /** pass through to internal {@link #mImageView} */ public void setImageDrawable(Drawable drawable) { mImageView.setImageDrawable(drawable); } + + /** pass through to internal {@link #mCustomCloseBackground} */ + public void setCustomCloseBackgroundVisibility(@View.Visibility int visibility) { + mCustomCloseBackground.setVisibility(visibility); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index c0fa8c0a8898..6390c8984dac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -33,8 +33,10 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.PendingIntent.CanceledException; +import android.app.PendingIntent; import android.app.RemoteAction; import android.app.WindowConfiguration; import android.content.ComponentName; @@ -72,6 +74,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -113,6 +116,7 @@ public class PipMenuView extends FrameLayout { private boolean mFocusedTaskAllowSplitScreen; private final List<RemoteAction> mActions = new ArrayList<>(); + private RemoteAction mCloseAction; private AccessibilityManager mAccessibilityManager; private Drawable mBackgroundDrawable; @@ -151,6 +155,9 @@ public class PipMenuView extends FrameLayout { protected View mTopEndContainer; protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; + // How long the shell will wait for the app to close the PiP if a custom action is set. + private final int mPipForceCloseDelay; + public PipMenuView(Context context, PhonePipMenuController controller, ShellExecutor mainExecutor, Handler mainHandler, Optional<SplitScreenController> splitScreenController, @@ -166,6 +173,9 @@ public class PipMenuView extends FrameLayout { mAccessibilityManager = context.getSystemService(AccessibilityManager.class); inflate(context, R.layout.pip_menu, this); + mPipForceCloseDelay = context.getResources().getInteger( + R.integer.config_pipForceCloseDelay); + mBackgroundDrawable = mContext.getDrawable(R.drawable.pip_menu_background); mBackgroundDrawable.setAlpha(0); mViewRoot = findViewById(R.id.background); @@ -437,9 +447,13 @@ public class PipMenuView extends FrameLayout { return new Size(width, height); } - void setActions(Rect stackBounds, List<RemoteAction> actions) { + void setActions(Rect stackBounds, @Nullable List<RemoteAction> actions, + @Nullable RemoteAction closeAction) { mActions.clear(); - mActions.addAll(actions); + if (actions != null && !actions.isEmpty()) { + mActions.addAll(actions); + } + mCloseAction = closeAction; if (mMenuState == MENU_STATE_FULL) { updateActionViews(mMenuState, stackBounds); } @@ -492,6 +506,8 @@ public class PipMenuView extends FrameLayout { final RemoteAction action = mActions.get(i); final PipMenuActionView actionView = (PipMenuActionView) mActionsGroup.getChildAt(i); + final boolean isCloseAction = mCloseAction != null && Objects.equals( + mCloseAction.getActionIntent(), action.getActionIntent()); // TODO: Check if the action drawable has changed before we reload it action.getIcon().loadDrawableAsync(mContext, d -> { @@ -500,16 +516,12 @@ public class PipMenuView extends FrameLayout { actionView.setImageDrawable(d); } }, mMainHandler); + actionView.setCustomCloseBackgroundVisibility( + isCloseAction ? View.VISIBLE : View.GONE); actionView.setContentDescription(action.getContentDescription()); if (action.isEnabled()) { - actionView.setOnClickListener(v -> { - try { - action.getActionIntent().send(); - } catch (CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send action, %s", TAG, e); - } - }); + actionView.setOnClickListener( + v -> onActionViewClicked(action.getActionIntent(), isCloseAction)); } actionView.setEnabled(action.isEnabled()); actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); @@ -559,6 +571,32 @@ public class PipMenuView extends FrameLayout { } } + /** + * Execute the {@link PendingIntent} attached to the {@link PipMenuActionView}. + * If the given {@link PendingIntent} matches {@link #mCloseAction}, we need to make sure + * the PiP is removed after a certain timeout in case the app does not respond in a + * timely manner. + */ + private void onActionViewClicked(@NonNull PendingIntent intent, boolean isCloseAction) { + try { + intent.send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + if (isCloseAction) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_CUSTOM_CLOSE); + mAllowTouches = false; + mMainExecutor.executeDelayed(() -> { + hideMenu(); + // TODO: it's unsafe to call onPipDismiss with a delay here since + // we may have a different PiP by the time this runnable is executed. + mController.onPipDismiss(); + mAllowTouches = true; + }, mPipForceCloseDelay); + } + } + private void enterSplit() { // Do not notify menu visibility when hiding the menu, the controller will do this when it // handles the message 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 7028f9a25fbe..fa0f0925a08a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -33,6 +33,11 @@ import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.os.Debug; +import android.os.Looper; +import android.view.Choreographer; + +import androidx.dynamicanimation.animation.AnimationHandler; +import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; @@ -84,6 +89,26 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** Coordinator instance for resolving conflicts with other floating content. */ private FloatingContentCoordinator mFloatingContentCoordinator; + private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = + ThreadLocal.withInitial(() -> { + final Looper initialLooper = Looper.myLooper(); + final FrameCallbackScheduler scheduler = new FrameCallbackScheduler() { + @Override + public void postFrameCallback(@androidx.annotation.NonNull Runnable runnable) { + // TODO(b/222697646): remove getSfInstance usage and use vsyncId for + // transactions + Choreographer.getSfInstance().postFrameCallback(t -> runnable.run()); + } + + @Override + public boolean isCurrentThread() { + return Looper.myLooper() == initialLooper; + } + }; + AnimationHandler handler = new AnimationHandler(scheduler); + return handler; + }); + /** * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()} * using physics animations. @@ -186,8 +211,11 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } public void init() { + // Note: Needs to get the shell main thread sf vsync animation handler mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler( + mSfAnimationHandlerThreadLocal.get()); } @NonNull diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index 89d85e4b292d..abf1a9500e6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -625,7 +625,8 @@ public class PipResizeGestureHandler { class PipResizeInputEventReceiver extends BatchedInputEventReceiver { PipResizeInputEventReceiver(InputChannel channel, Looper looper) { - super(channel, looper, Choreographer.getInstance()); + // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions + super(channel, looper, Choreographer.getSfInstance()); } public void onInputEvent(InputEvent event) { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt index 8da6224d990c..37e9344348d9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt @@ -25,6 +25,8 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -81,9 +83,19 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() /** {@inheritDoc} */ + @FlakyTest(bugId = 197726610) + @Test + override fun pipLayerExpands() { + Assume.assumeFalse(isShellTransitionsEnabled) + super.pipLayerExpands() + } + @Presubmit @Test - override fun pipLayerExpands() = super.pipLayerExpands() + fun pipLayerExpands_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + super.pipLayerExpands() + } companion object { /** diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index 9f745208d3ed..aaeebef03d0f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -16,15 +16,28 @@ package com.android.wm.shell.draganddrop; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.DragEvent.ACTION_DRAG_STARTED; + import static org.junit.Assert.assertFalse; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import android.content.ClipData; +import android.content.ClipDescription; import android.content.Context; +import android.content.Intent; import android.os.RemoteException; import android.view.Display; import android.view.DragEvent; import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -33,6 +46,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; import org.junit.Test; @@ -40,6 +54,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Optional; + /** * Tests for the drag and drop controller. */ @@ -56,6 +72,9 @@ public class DragAndDropControllerTest { @Mock private UiEventLogger mUiEventLogger; + @Mock + private DragAndDropController.DragAndDropListener mDragAndDropListener; + private DragAndDropController mController; @Before @@ -63,6 +82,7 @@ public class DragAndDropControllerTest { MockitoAnnotations.initMocks(this); mController = new DragAndDropController(mContext, mDisplayController, mUiEventLogger, mock(IconProvider.class), mock(ShellExecutor.class)); + mController.initialize(Optional.of(mock(SplitScreenController.class))); } @Test @@ -77,4 +97,45 @@ public class DragAndDropControllerTest { mController.onDisplayAdded(nonDefaultDisplayId); assertFalse(mController.onDrag(dragLayout, mock(DragEvent.class))); } + + @Test + public void testListenerOnDragStarted() { + final View dragLayout = mock(View.class); + final Display display = mock(Display.class); + doReturn(display).when(dragLayout).getDisplay(); + doReturn(DEFAULT_DISPLAY).when(display).getDisplayId(); + + final ClipData clipData = createClipData(); + final DragEvent event = mock(DragEvent.class); + doReturn(ACTION_DRAG_STARTED).when(event).getAction(); + doReturn(clipData).when(event).getClipData(); + doReturn(clipData.getDescription()).when(event).getClipDescription(); + + mController.addListener(mDragAndDropListener); + + // Ensure there's a target so that onDrag will execute + mController.addDisplayDropTarget(0, mContext, mock(WindowManager.class), + mock(FrameLayout.class), mock(DragLayout.class)); + + // Verify the listener is called on a valid drag action. + mController.onDrag(dragLayout, event); + verify(mDragAndDropListener, times(1)).onDragStarted(); + + // Verify the listener isn't called after removal. + reset(mDragAndDropListener); + mController.removeListener(mDragAndDropListener); + mController.onDrag(dragLayout, event); + verify(mDragAndDropListener, never()).onDragStarted(); + } + + private ClipData createClipData() { + ClipDescription clipDescription = new ClipDescription(MIMETYPE_APPLICATION_SHORTCUT, + new String[] { MIMETYPE_APPLICATION_SHORTCUT }); + Intent i = new Intent(); + i.putExtra(Intent.EXTRA_PACKAGE_NAME, "pkg"); + i.putExtra(Intent.EXTRA_SHORTCUT_ID, "shortcutId"); + i.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle()); + ClipData.Item item = new ClipData.Item(i); + return new ClipData(clipDescription, item); + } } diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 28f547703dde..3afb419f9b8b 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -399,10 +399,12 @@ skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) { // Skia skcms' default HLG maps encoded [0, 1] to linear [1, 12] in order to follow ARIB // but LinearEffect expects a decoded [0, 1] range instead to follow Rec 2100. std::optional<skcms_TransferFunction> GetHLGScaleTransferFunction() { - std::optional<skcms_TransferFunction> hlgFn = {}; - skcms_TransferFunction_makeScaledHLGish(&hlgFn.value(), 1.f / 12.f, 2.f, 2.f, 1.f / 0.17883277f, - 0.28466892f, 0.55991073f); - return hlgFn; + skcms_TransferFunction hlgFn; + if (skcms_TransferFunction_makeScaledHLGish(&hlgFn, 1.f / 12.f, 2.f, 2.f, 1.f / 0.17883277f, + 0.28466892f, 0.55991073f)) { + return std::make_optional<skcms_TransferFunction>(hlgFn); + } + return {}; } } // namespace uirenderer |