diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 5696b8dfa8e38..0bc70857a1137 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -297,4 +297,28 @@
when the pinned stack size is overridden by app via minWidth/minHeight.
-->
48dp
+
+
+ 32dp
+
+
+ 24dp
+
+
+ 5dp
+
+
+ 32dp
+
+
+ 8dp
+
+
+ 8dp
+
+
+ 150dp
+
+
+ 120dp
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
index 976fba52b9e22..e0c782d1675bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
@@ -49,6 +49,8 @@ public class DismissCircleView extends FrameLayout {
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
+ final Resources res = getResources();
+ setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
setViewSizes();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 7c3c14e2210c3..c9d523c779ea9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -24,6 +24,7 @@ import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.SystemProperties;
import android.view.IWindowManager;
+import android.view.WindowManager;
import com.android.internal.logging.UiEventLogger;
import com.android.launcher3.icons.IconProvider;
@@ -60,6 +61,8 @@ import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.floating.FloatingTasks;
+import com.android.wm.shell.floating.FloatingTasksController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
@@ -571,6 +574,45 @@ public abstract class WMShellBaseModule {
return Optional.empty();
}
+ //
+ // Floating tasks
+ //
+
+ @WMSingleton
+ @Provides
+ static Optional provideFloatingTasks(
+ Optional floatingTaskController) {
+ return floatingTaskController.map((controller) -> controller.asFloatingTasks());
+ }
+
+ @WMSingleton
+ @Provides
+ static Optional provideFloatingTasksController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ ShellCommandHandler shellCommandHandler,
+ WindowManager windowManager,
+ ShellTaskOrganizer organizer,
+ TaskViewTransitions taskViewTransitions,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
+ SyncTransactionQueue syncQueue) {
+ if (FloatingTasksController.FLOATING_TASKS_ENABLED) {
+ return Optional.of(new FloatingTasksController(context,
+ shellInit,
+ shellController,
+ shellCommandHandler,
+ windowManager,
+ organizer,
+ taskViewTransitions,
+ mainExecutor,
+ bgExecutor,
+ syncQueue));
+ } else {
+ return Optional.empty();
+ }
+ }
+
//
// Starting window
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java
new file mode 100644
index 0000000000000..83a1734dc71ab
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.floating;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.floating.views.FloatingTaskLayer;
+import com.android.wm.shell.floating.views.FloatingTaskView;
+
+import java.util.Objects;
+
+/**
+ * Controls a floating dismiss circle that has a 'magnetic' field around it, causing views moved
+ * close to the target to be stuck to it unless moved out again.
+ */
+public class FloatingDismissController {
+
+ /** Velocity required to dismiss the view without dragging it into the dismiss target. */
+ private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f;
+ /**
+ * Max velocity that the view can be moving through the target with to stick (i.e. if it's
+ * more than this velocity, it will pass through the target.
+ */
+ private static final float STICK_TO_TARGET_MAX_X_VELOCITY = 2000f;
+ /**
+ * Percentage of the target width to use to determine if an object flung towards the target
+ * should dismiss (e.g. if target is 100px and this is set ot 2f, anything flung within a
+ * 200px-wide area around the target will be considered 'near' enough get dismissed).
+ */
+ private static final float FLING_TO_TARGET_WIDTH_PERCENT = 2f;
+ /** Minimum alpha to apply to the view being dismissed when it is in the target. */
+ private static final float DISMISS_VIEW_MIN_ALPHA = 0.6f;
+ /** Amount to scale down the view being dismissed when it is in the target. */
+ private static final float DISMISS_VIEW_SCALE_DOWN_PERCENT = 0.15f;
+
+ private Context mContext;
+ private FloatingTasksController mController;
+ private FloatingTaskLayer mParent;
+
+ private DismissView mDismissView;
+ private ValueAnimator mDismissAnimator;
+ private View mViewBeingDismissed;
+ private float mDismissSizePercent;
+ private float mDismissSize;
+
+ /**
+ * The currently magnetized object, which is being dragged and will be attracted to the magnetic
+ * dismiss target.
+ */
+ private MagnetizedObject mMagnetizedObject;
+ /**
+ * The MagneticTarget instance for our circular dismiss view. This is added to the
+ * MagnetizedObject instances for the view being dragged.
+ */
+ private MagnetizedObject.MagneticTarget mMagneticTarget;
+ /** Magnet listener that handles animating and dismissing the view. */
+ private MagnetizedObject.MagnetListener mFloatingViewMagnetListener;
+
+ public FloatingDismissController(Context context, FloatingTasksController controller,
+ FloatingTaskLayer parent) {
+ mContext = context;
+ mController = controller;
+ mParent = parent;
+ updateSizes();
+ createAndAddDismissView();
+
+ mDismissAnimator = ValueAnimator.ofFloat(1f, 0f);
+ mDismissAnimator.addUpdateListener(animation -> {
+ final float value = (float) animation.getAnimatedValue();
+ if (mDismissView != null) {
+ mDismissView.setPivotX((mDismissView.getRight() - mDismissView.getLeft()) / 2f);
+ mDismissView.setPivotY((mDismissView.getBottom() - mDismissView.getTop()) / 2f);
+ final float scaleValue = Math.max(value, mDismissSizePercent);
+ mDismissView.getCircle().setScaleX(scaleValue);
+ mDismissView.getCircle().setScaleY(scaleValue);
+ }
+ if (mViewBeingDismissed != null) {
+ // TODO: alpha doesn't actually apply to taskView currently.
+ mViewBeingDismissed.setAlpha(Math.max(value, DISMISS_VIEW_MIN_ALPHA));
+ mViewBeingDismissed.setScaleX(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT));
+ mViewBeingDismissed.setScaleY(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT));
+ }
+ });
+
+ mFloatingViewMagnetListener = new MagnetizedObject.MagnetListener() {
+ @Override
+ public void onStuckToTarget(
+ @NonNull MagnetizedObject.MagneticTarget target) {
+ animateDismissing(/* dismissing= */ true);
+ }
+
+ @Override
+ public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ float velX, float velY, boolean wasFlungOut) {
+ animateDismissing(/* dismissing= */ false);
+ mParent.onUnstuckFromTarget((FloatingTaskView) mViewBeingDismissed, velX, velY,
+ wasFlungOut);
+ }
+
+ @Override
+ public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ doDismiss();
+ }
+ };
+ }
+
+ /** Updates all the sizes used and applies them to the {@link DismissView}. */
+ public void updateSizes() {
+ Resources res = mContext.getResources();
+ mDismissSize = res.getDimensionPixelSize(
+ R.dimen.floating_task_dismiss_circle_size);
+ final float minDismissSize = res.getDimensionPixelSize(
+ R.dimen.floating_dismiss_circle_small);
+ mDismissSizePercent = minDismissSize / mDismissSize;
+
+ if (mDismissView != null) {
+ mDismissView.updateResources();
+ }
+ }
+
+ /** Prepares the view being dragged to be magnetic. */
+ public void setUpMagneticObject(View viewBeingDragged) {
+ mViewBeingDismissed = viewBeingDragged;
+ mMagnetizedObject = getMagnetizedView(viewBeingDragged);
+ mMagnetizedObject.clearAllTargets();
+ mMagnetizedObject.addTarget(mMagneticTarget);
+ mMagnetizedObject.setMagnetListener(mFloatingViewMagnetListener);
+ }
+
+ /** Shows or hides the dismiss target. */
+ public void showDismiss(boolean show) {
+ if (show) {
+ mDismissView.show();
+ } else {
+ mDismissView.hide();
+ }
+ }
+
+ /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
+ public boolean passEventToMagnetizedObject(MotionEvent event) {
+ return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
+ }
+
+ private void createAndAddDismissView() {
+ if (mDismissView != null) {
+ mParent.removeView(mDismissView);
+ }
+ mDismissView = new DismissView(mContext);
+ mDismissView.setTargetSizeResId(R.dimen.floating_task_dismiss_circle_size);
+ mDismissView.updateResources();
+ mParent.addView(mDismissView);
+
+ final float dismissRadius = mDismissSize;
+ // Save the MagneticTarget instance for the newly set up view - we'll add this to the
+ // MagnetizedObjects when the dismiss view gets shown.
+ mMagneticTarget = new MagnetizedObject.MagneticTarget(
+ mDismissView.getCircle(), (int) dismissRadius);
+ }
+
+ private MagnetizedObject getMagnetizedView(View v) {
+ if (mMagnetizedObject != null
+ && Objects.equals(mMagnetizedObject.getUnderlyingObject(), v)) {
+ // Same view being dragged, we can reuse the magnetic object.
+ return mMagnetizedObject;
+ }
+ MagnetizedObject magnetizedView = new MagnetizedObject(
+ mContext,
+ v,
+ DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y
+ ) {
+ @Override
+ public float getWidth(@NonNull View underlyingObject) {
+ return underlyingObject.getWidth();
+ }
+
+ @Override
+ public float getHeight(@NonNull View underlyingObject) {
+ return underlyingObject.getHeight();
+ }
+
+ @Override
+ public void getLocationOnScreen(@NonNull View underlyingObject,
+ @NonNull int[] loc) {
+ loc[0] = (int) underlyingObject.getTranslationX();
+ loc[1] = (int) underlyingObject.getTranslationY();
+ }
+ };
+ magnetizedView.setHapticsEnabled(true);
+ magnetizedView.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ magnetizedView.setStickToTargetMaxXVelocity(STICK_TO_TARGET_MAX_X_VELOCITY);
+ magnetizedView.setFlingToTargetWidthPercent(FLING_TO_TARGET_WIDTH_PERCENT);
+ return magnetizedView;
+ }
+
+ /** Animates the dismiss treatment on the view being dismissed. */
+ private void animateDismissing(boolean shouldDismiss) {
+ if (mViewBeingDismissed == null) {
+ return;
+ }
+ if (shouldDismiss) {
+ mDismissAnimator.removeAllListeners();
+ mDismissAnimator.start();
+ } else {
+ mDismissAnimator.removeAllListeners();
+ mDismissAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ resetDismissAnimator();
+ }
+ });
+ mDismissAnimator.reverse();
+ }
+ }
+
+ /** Actually dismisses the view. */
+ private void doDismiss() {
+ mDismissView.hide();
+ mController.removeTask();
+ resetDismissAnimator();
+ mViewBeingDismissed = null;
+ }
+
+ private void resetDismissAnimator() {
+ mDismissAnimator.removeAllListeners();
+ mDismissAnimator.cancel();
+ if (mDismissView != null) {
+ mDismissView.cancelAnimators();
+ mDismissView.getCircle().setScaleX(1f);
+ mDismissView.getCircle().setScaleY(1f);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
new file mode 100644
index 0000000000000..935666026bf42
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.floating;
+
+import android.content.Intent;
+
+import com.android.wm.shell.common.annotations.ExternalThread;
+
+/**
+ * Interface to interact with floating tasks.
+ */
+@ExternalThread
+public interface FloatingTasks {
+
+ /**
+ * Shows, stashes, or un-stashes the floating task depending on state:
+ * - If there is no floating task for this intent, it shows the task for the provided intent.
+ * - If there is a floating task for this intent, but it's stashed, this un-stashes it.
+ * - If there is a floating task for this intent, and it's not stashed, this stashes it.
+ */
+ void showOrSetStashed(Intent intent);
+
+ /** Returns a binder that can be passed to an external process to manipulate FloatingTasks. */
+ default IFloatingTasks createExternalInterface() {
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
new file mode 100644
index 0000000000000..c79b9b8cc443a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.floating;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.os.SystemProperties;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.TaskViewTransitions;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.floating.views.FloatingTaskLayer;
+import com.android.wm.shell.floating.views.FloatingTaskView;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+
+/**
+ * Entry point for creating and managing floating tasks.
+ *
+ * A single window layer is added and the task(s) are displayed using a {@link FloatingTaskView}
+ * within that window.
+ *
+ * Currently optimized for a single task. Multiple tasks are not supported.
+ */
+public class FloatingTasksController implements RemoteCallable,
+ ConfigurationChangeListener {
+
+ private static final String TAG = FloatingTasksController.class.getSimpleName();
+
+ public static final boolean FLOATING_TASKS_ENABLED =
+ SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
+
+ @VisibleForTesting
+ static final int SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET = 600;
+
+ // Only used for testing
+ private Configuration mConfig;
+ private boolean mFloatingTasksEnabledForTests;
+
+ private FloatingTaskImpl mImpl = new FloatingTaskImpl();
+ private Context mContext;
+ private ShellController mShellController;
+ private ShellCommandHandler mShellCommandHandler;
+ private WindowManager mWindowManager;
+ private ShellTaskOrganizer mTaskOrganizer;
+ private TaskViewTransitions mTaskViewTransitions;
+ private @ShellMainThread ShellExecutor mMainExecutor;
+ // TODO: mBackgroundThread is not used but we'll probs need it eventually?
+ private @ShellBackgroundThread ShellExecutor mBackgroundThread;
+ private SyncTransactionQueue mSyncQueue;
+
+ private boolean mIsFloatingLayerAdded;
+ private FloatingTaskLayer mFloatingTaskLayer;
+ private final Point mLastPosition = new Point(-1, -1);
+
+ private Task mTask;
+
+ // Simple class to hold onto info for intent or shortcut based tasks.
+ public static class Task {
+ public int taskId = INVALID_TASK_ID;
+ @Nullable
+ public Intent intent;
+ @Nullable
+ public ShortcutInfo info;
+ @Nullable
+ public FloatingTaskView floatingView;
+ }
+
+ public FloatingTasksController(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ ShellCommandHandler shellCommandHandler,
+ WindowManager windowManager,
+ ShellTaskOrganizer organizer,
+ TaskViewTransitions transitions,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellBackgroundThread ShellExecutor bgExceutor,
+ SyncTransactionQueue syncTransactionQueue) {
+ mContext = context;
+ mShellController = shellController;
+ mShellCommandHandler = shellCommandHandler;
+ mWindowManager = windowManager;
+ mTaskOrganizer = organizer;
+ mTaskViewTransitions = transitions;
+ mMainExecutor = mainExecutor;
+ mBackgroundThread = bgExceutor;
+ mSyncQueue = syncTransactionQueue;
+ if (isFloatingTasksEnabled()) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ }
+
+ protected void onInit() {
+ mShellController.addConfigurationChangeListener(this);
+ }
+
+ /** Only used for testing. */
+ @VisibleForTesting
+ void setConfig(Configuration config) {
+ mConfig = config;
+ }
+
+ /** Only used for testing. */
+ @VisibleForTesting
+ void setFloatingTasksEnabled(boolean enabled) {
+ mFloatingTasksEnabledForTests = enabled;
+ }
+
+ /** Whether the floating layer is available. */
+ boolean isFloatingLayerAvailable() {
+ Configuration config = mConfig == null
+ ? mContext.getResources().getConfiguration()
+ : mConfig;
+ return config.smallestScreenWidthDp >= SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
+ }
+
+ /** Whether floating tasks are enabled. */
+ boolean isFloatingTasksEnabled() {
+ return FLOATING_TASKS_ENABLED || mFloatingTasksEnabledForTests;
+ }
+
+ @Override
+ public void onThemeChanged() {
+ if (mIsFloatingLayerAdded) {
+ mFloatingTaskLayer.updateSizes();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // TODO: probably other stuff here to do (e.g. handle rotation)
+ if (mIsFloatingLayerAdded) {
+ mFloatingTaskLayer.updateSizes();
+ }
+ }
+
+ /** Returns false if the task shouldn't be shown. */
+ private boolean canShowTask(Intent intent) {
+ ProtoLog.d(WM_SHELL_FLOATING_APPS, "canShowTask -- %s", intent);
+ if (!isFloatingTasksEnabled() || !isFloatingLayerAvailable()) return false;
+ if (intent == null) {
+ ProtoLog.e(WM_SHELL_FLOATING_APPS, "canShowTask given null intent, doing nothing");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Shows, stashes, or un-stashes the floating task depending on state:
+ * - If there is no floating task for this intent, it shows this the provided task.
+ * - If there is a floating task for this intent, but it's stashed, this un-stashes it.
+ * - If there is a floating task for this intent, and it's not stashed, this stashes it.
+ */
+ public void showOrSetStashed(Intent intent) {
+ if (!canShowTask(intent)) return;
+
+ addFloatingLayer();
+
+ if (isTaskAttached(mTask) && intent.filterEquals(mTask.intent)) {
+ // The task is already added, toggle the stash state.
+ mFloatingTaskLayer.setStashed(mTask, !mTask.floatingView.isStashed());
+ return;
+ }
+
+ // If we're here it's either a new or different task
+ showNewTask(intent);
+ }
+
+ /**
+ * Shows a floating task with the provided intent.
+ * If the same task is present it will un-stash it or do nothing if it is already un-stashed.
+ * Removes any other floating tasks that might exist.
+ */
+ public void showTask(Intent intent) {
+ if (!canShowTask(intent)) return;
+
+ addFloatingLayer();
+
+ if (isTaskAttached(mTask) && intent.filterEquals(mTask.intent)) {
+ // The task is already added, show it if it's stashed.
+ if (mTask.floatingView.isStashed()) {
+ mFloatingTaskLayer.setStashed(mTask, false);
+ }
+ return;
+ }
+ showNewTask(intent);
+ }
+
+ private void showNewTask(Intent intent) {
+ if (mTask != null && !intent.filterEquals(mTask.intent)) {
+ mFloatingTaskLayer.removeAllTaskViews();
+ mTask.floatingView.cleanUpTaskView();
+ mTask = null;
+ }
+
+ FloatingTaskView ftv = new FloatingTaskView(mContext, this);
+ ftv.createTaskView(mContext, mTaskOrganizer, mTaskViewTransitions, mSyncQueue);
+
+ mTask = new Task();
+ mTask.floatingView = ftv;
+ mTask.intent = intent;
+
+ // Add & start the task.
+ mFloatingTaskLayer.addTask(mTask);
+ ProtoLog.d(WM_SHELL_FLOATING_APPS, "showNewTask, startingIntent: %s", intent);
+ mTask.floatingView.startTask(mMainExecutor, mTask);
+ }
+
+ /**
+ * Removes the task and cleans up the view.
+ */
+ public void removeTask() {
+ if (mTask != null) {
+ ProtoLog.d(WM_SHELL_FLOATING_APPS, "Removing task with id=%d", mTask.taskId);
+
+ if (mTask.floatingView != null) {
+ // TODO: animate it
+ mFloatingTaskLayer.removeView(mTask.floatingView);
+ mTask.floatingView.cleanUpTaskView();
+ }
+ removeFloatingLayer();
+ }
+ }
+
+ /**
+ * Whether there is a floating task and if it is stashed.
+ */
+ public boolean isStashed() {
+ return isTaskAttached(mTask) && mTask.floatingView.isStashed();
+ }
+
+ /**
+ * If a floating task exists, this sets whether it is stashed and animates if needed.
+ */
+ public void setStashed(boolean shouldStash) {
+ if (mTask != null && mTask.floatingView != null && mIsFloatingLayerAdded) {
+ mFloatingTaskLayer.setStashed(mTask, shouldStash);
+ }
+ }
+
+ /**
+ * Saves the last position the floating task was in so that it can be put there again.
+ */
+ public void setLastPosition(int x, int y) {
+ mLastPosition.set(x, y);
+ }
+
+ /**
+ * Returns the last position the floating task was in.
+ */
+ public Point getLastPosition() {
+ return mLastPosition;
+ }
+
+ /**
+ * Whether the provided task has a view that's attached to the floating layer.
+ */
+ private boolean isTaskAttached(Task t) {
+ return t != null && t.floatingView != null
+ && mIsFloatingLayerAdded
+ && mFloatingTaskLayer.getTaskViewCount() > 0
+ && Objects.equals(mFloatingTaskLayer.getFirstTaskView(), t.floatingView);
+ }
+
+ // TODO: when this is added, if there are bubbles, they get hidden? Is only one layer of this
+ // type allowed? Bubbles & floating tasks should probably be in the same layer to reduce
+ // # of windows.
+ private void addFloatingLayer() {
+ if (mIsFloatingLayerAdded) {
+ return;
+ }
+
+ mFloatingTaskLayer = new FloatingTaskLayer(mContext, this, mWindowManager);
+
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ PixelFormat.TRANSLUCENT
+ );
+ params.setTrustedOverlay();
+ params.setFitInsetsTypes(0);
+ params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+ params.setTitle("FloatingTaskLayer");
+ params.packageName = mContext.getPackageName();
+ params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+
+ try {
+ mIsFloatingLayerAdded = true;
+ mWindowManager.addView(mFloatingTaskLayer, params);
+ } catch (IllegalStateException e) {
+ // This means the floating layer has already been added which shouldn't happen.
+ e.printStackTrace();
+ }
+ }
+
+ private void removeFloatingLayer() {
+ if (!mIsFloatingLayerAdded) {
+ return;
+ }
+ try {
+ mIsFloatingLayerAdded = false;
+ if (mFloatingTaskLayer != null) {
+ mWindowManager.removeView(mFloatingTaskLayer);
+ }
+ } catch (IllegalArgumentException e) {
+ // This means the floating layer has already been removed which shouldn't happen.
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Description of current floating task state.
+ */
+ private void dump(PrintWriter pw, String prefix) {
+ pw.println("FloatingTaskController state:");
+ pw.print(" isFloatingLayerAvailable= "); pw.println(isFloatingLayerAvailable());
+ pw.print(" isFloatingTasksEnabled= "); pw.println(isFloatingTasksEnabled());
+ pw.print(" mIsFloatingLayerAdded= "); pw.println(mIsFloatingLayerAdded);
+ pw.print(" mLastPosition= "); pw.println(mLastPosition);
+ pw.println();
+ }
+
+ /** Returns the {@link FloatingTasks} implementation. */
+ public FloatingTasks asFloatingTasks() {
+ return mImpl;
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
+ }
+
+ /**
+ * The interface for calls from outside the shell, within the host process.
+ */
+ @ExternalThread
+ private class FloatingTaskImpl implements FloatingTasks {
+ private IFloatingTasksImpl mIFloatingTasks;
+
+ @Override
+ public void showOrSetStashed(Intent intent) {
+ mMainExecutor.execute(() -> FloatingTasksController.this.showOrSetStashed(intent));
+ }
+
+ @Override
+ public IFloatingTasks createExternalInterface() {
+ if (mIFloatingTasks != null) {
+ mIFloatingTasks.invalidate();
+ }
+ mIFloatingTasks = new IFloatingTasksImpl(FloatingTasksController.this);
+ return mIFloatingTasks;
+ }
+ }
+
+ /**
+ * The interface for calls from outside the host process.
+ */
+ @BinderThread
+ private static class IFloatingTasksImpl extends IFloatingTasks.Stub {
+ private FloatingTasksController mController;
+
+ IFloatingTasksImpl(FloatingTasksController controller) {
+ mController = controller;
+ }
+
+ /**
+ * Invalidates this instance, preventing future calls from updating the controller.
+ */
+ void invalidate() {
+ mController = null;
+ }
+
+ public void showTask(Intent intent) {
+ executeRemoteCallWithTaskPermission(mController, "showTask",
+ (controller) -> controller.showTask(intent));
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl
new file mode 100644
index 0000000000000..f79ca1039865d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.floating;
+
+import android.content.Intent;
+
+/**
+ * Interface that is exposed to remote callers to manipulate floating task features.
+ */
+interface IFloatingTasks {
+
+ void showTask(in Intent intent) = 1;
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java
new file mode 100644
index 0000000000000..c922109751ba6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.floating.views;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.android.wm.shell.R;
+
+/**
+ * Displays the menu items for a floating task view (e.g. close).
+ */
+public class FloatingMenuView extends LinearLayout {
+
+ private int mItemSize;
+ private int mItemMargin;
+
+ public FloatingMenuView(Context context) {
+ super(context);
+ setOrientation(LinearLayout.HORIZONTAL);
+ setGravity(Gravity.CENTER);
+
+ mItemSize = context.getResources().getDimensionPixelSize(
+ R.dimen.floating_task_menu_item_size);
+ mItemMargin = context.getResources().getDimensionPixelSize(
+ R.dimen.floating_task_menu_item_padding);
+ }
+
+ /** Adds a clickable item to the menu bar. Items are ordered as added. */
+ public void addMenuItem(@Nullable Drawable drawable, View.OnClickListener listener) {
+ ImageView itemView = new ImageView(getContext());
+ itemView.setScaleType(ImageView.ScaleType.CENTER);
+ if (drawable != null) {
+ itemView.setImageDrawable(drawable);
+ }
+ LinearLayout.LayoutParams lp = new LayoutParams(mItemSize,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ lp.setMarginStart(mItemMargin);
+ lp.setMarginEnd(mItemMargin);
+ addView(itemView, lp);
+
+ itemView.setOnClickListener(listener);
+ }
+
+ /**
+ * The menu extends past the top of the TaskView because of the rounded corners. This means
+ * to center content in the menu we must subtract the radius (i.e. the amount of space covered
+ * by TaskView).
+ */
+ public void setCornerRadius(float radius) {
+ setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java
new file mode 100644
index 0000000000000..16dab2415bf2c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java
@@ -0,0 +1,687 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.floating.views;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FlingAnimation;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.floating.FloatingDismissController;
+import com.android.wm.shell.floating.FloatingTasksController;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This is the layout that {@link FloatingTaskView}s are contained in. It handles input and
+ * movement of the task views.
+ */
+public class FloatingTaskLayer extends FrameLayout
+ implements ViewTreeObserver.OnComputeInternalInsetsListener {
+
+ private static final String TAG = FloatingTaskLayer.class.getSimpleName();
+
+ /** How big to make the task view based on screen width of the largest size. */
+ private static final float START_SIZE_WIDTH_PERCENT = 0.33f;
+ /** Min fling velocity required to move the view from one side of the screen to the other. */
+ private static final float ESCAPE_VELOCITY = 750f;
+ /** Amount of friction to apply to fling animations. */
+ private static final float FLING_FRICTION = 1.9f;
+
+ private final FloatingTasksController mController;
+ private final FloatingDismissController mDismissController;
+ private final WindowManager mWindowManager;
+ private final TouchHandlerImpl mTouchHandler;
+
+ private final Region mTouchableRegion = new Region();
+ private final Rect mPositionRect = new Rect();
+ private final Point mDefaultStartPosition = new Point();
+ private final Point mTaskViewSize = new Point();
+ private WindowInsets mWindowInsets;
+ private int mVerticalPadding;
+ private int mOverhangWhenStashed;
+
+ private final List mSystemGestureExclusionRects = Collections.singletonList(new Rect());
+ private ViewTreeObserver.OnDrawListener mSystemGestureExclusionListener =
+ this::updateSystemGestureExclusion;
+
+ /** Interface allowing something to handle the touch events going to a task. */
+ interface FloatingTaskTouchHandler {
+ void onDown(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float viewInitialX, float viewInitialY);
+
+ void onMove(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float dx, float dy);
+
+ void onUp(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float dx, float dy, float velX, float velY);
+
+ void onClick(@NonNull FloatingTaskView v);
+ }
+
+ public FloatingTaskLayer(Context context,
+ FloatingTasksController controller,
+ WindowManager windowManager) {
+ super(context);
+ // TODO: Why is this necessary? Without it FloatingTaskView does not render correctly.
+ setBackgroundColor(Color.argb(0, 0, 0, 0));
+
+ mController = controller;
+ mWindowManager = windowManager;
+ updateSizes();
+
+ // TODO: Might make sense to put dismiss controller in the touch handler since that's the
+ // main user of dismiss controller.
+ mDismissController = new FloatingDismissController(context, mController, this);
+ mTouchHandler = new TouchHandlerImpl();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ getViewTreeObserver().addOnDrawListener(mSystemGestureExclusionListener);
+ setOnApplyWindowInsetsListener((view, windowInsets) -> {
+ if (!windowInsets.equals(mWindowInsets)) {
+ mWindowInsets = windowInsets;
+ updateSizes();
+ }
+ return windowInsets;
+ });
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+ getViewTreeObserver().removeOnDrawListener(mSystemGestureExclusionListener);
+ }
+
+ @Override
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ mTouchableRegion.setEmpty();
+ getTouchableRegion(mTouchableRegion);
+ inoutInfo.touchableRegion.set(mTouchableRegion);
+ }
+
+ /** Adds a floating task to the layout. */
+ public void addTask(FloatingTasksController.Task task) {
+ if (task.floatingView == null) return;
+
+ task.floatingView.setTouchHandler(mTouchHandler);
+ addView(task.floatingView, new LayoutParams(mTaskViewSize.x, mTaskViewSize.y));
+ updateTaskViewPosition(task.floatingView);
+ }
+
+ /** Animates the stashed state of the provided task, if it's part of the floating layer. */
+ public void setStashed(FloatingTasksController.Task task, boolean shouldStash) {
+ if (task.floatingView != null && task.floatingView.getParent() == this) {
+ mTouchHandler.stashTaskView(task.floatingView, shouldStash);
+ }
+ }
+
+ /** Removes all {@link FloatingTaskView} from the layout. */
+ public void removeAllTaskViews() {
+ int childCount = getChildCount();
+ ArrayList viewsToRemove = new ArrayList<>();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i) instanceof FloatingTaskView) {
+ viewsToRemove.add(getChildAt(i));
+ }
+ }
+ for (View v : viewsToRemove) {
+ removeView(v);
+ }
+ }
+
+ /** Returns the number of task views in the layout. */
+ public int getTaskViewCount() {
+ int taskViewCount = 0;
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i) instanceof FloatingTaskView) {
+ taskViewCount++;
+ }
+ }
+ return taskViewCount;
+ }
+
+ /**
+ * Called when the task view is un-stuck from the dismiss target.
+ * @param v the task view being moved.
+ * @param velX the x velocity of the motion event.
+ * @param velY the y velocity of the motion event.
+ * @param wasFlungOut true if the user flung the task view out of the dismiss target (i.e. there
+ * was an 'up' event), otherwise the user is still dragging.
+ */
+ public void onUnstuckFromTarget(FloatingTaskView v, float velX, float velY,
+ boolean wasFlungOut) {
+ mTouchHandler.onUnstuckFromTarget(v, velX, velY, wasFlungOut);
+ }
+
+ /**
+ * Updates dimensions and applies them to any task views.
+ */
+ public void updateSizes() {
+ if (mDismissController != null) {
+ mDismissController.updateSizes();
+ }
+
+ mOverhangWhenStashed = getResources().getDimensionPixelSize(
+ R.dimen.floating_task_stash_offset);
+ mVerticalPadding = getResources().getDimensionPixelSize(
+ R.dimen.floating_task_vertical_padding);
+
+ WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+ WindowInsets windowInsets = windowMetrics.getWindowInsets();
+ Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
+ | WindowInsets.Type.statusBars()
+ | WindowInsets.Type.displayCutout());
+ Rect bounds = windowMetrics.getBounds();
+ mPositionRect.set(bounds.left + insets.left,
+ bounds.top + insets.top + mVerticalPadding,
+ bounds.right - insets.right,
+ bounds.bottom - insets.bottom - mVerticalPadding);
+
+ int taskViewWidth = Math.max(bounds.height(), bounds.width());
+ int taskViewHeight = Math.min(bounds.height(), bounds.width());
+ taskViewHeight = taskViewHeight - (insets.top + insets.bottom + (mVerticalPadding * 2));
+ mTaskViewSize.set((int) (taskViewWidth * START_SIZE_WIDTH_PERCENT), taskViewHeight);
+ mDefaultStartPosition.set(mPositionRect.left, mPositionRect.top);
+
+ // Update existing views
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i) instanceof FloatingTaskView) {
+ FloatingTaskView child = (FloatingTaskView) getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.width = mTaskViewSize.x;
+ lp.height = mTaskViewSize.y;
+ child.setLayoutParams(lp);
+ updateTaskViewPosition(child);
+ }
+ }
+ }
+
+ /** Returns the first floating task view in the layout. (Currently only ever 1 view). */
+ @Nullable
+ public FloatingTaskView getFirstTaskView() {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child instanceof FloatingTaskView) {
+ return (FloatingTaskView) child;
+ }
+ }
+ return null;
+ }
+
+ private void updateTaskViewPosition(FloatingTaskView floatingView) {
+ Point lastPosition = mController.getLastPosition();
+ if (lastPosition.x == -1 && lastPosition.y == -1) {
+ floatingView.setX(mDefaultStartPosition.x);
+ floatingView.setY(mDefaultStartPosition.y);
+ } else {
+ floatingView.setX(lastPosition.x);
+ floatingView.setY(lastPosition.y);
+ }
+ if (mTouchHandler.isStashedPosition(floatingView)) {
+ floatingView.setStashed(true);
+ }
+ floatingView.updateLocation();
+ }
+
+ /**
+ * Updates the area of the screen that shouldn't allow the back gesture due to the placement
+ * of task view (i.e. when task view is stashed on an edge, tapping or swiping that edge would
+ * un-stash the task view instead of performing the back gesture).
+ */
+ private void updateSystemGestureExclusion() {
+ Rect excludeZone = mSystemGestureExclusionRects.get(0);
+ FloatingTaskView floatingTaskView = getFirstTaskView();
+ if (floatingTaskView != null && floatingTaskView.isStashed()) {
+ excludeZone.set(floatingTaskView.getLeft(),
+ floatingTaskView.getTop(),
+ floatingTaskView.getRight(),
+ floatingTaskView.getBottom());
+ excludeZone.offset((int) (floatingTaskView.getTranslationX()),
+ (int) (floatingTaskView.getTranslationY()));
+ setSystemGestureExclusionRects(mSystemGestureExclusionRects);
+ } else {
+ excludeZone.setEmpty();
+ setSystemGestureExclusionRects(Collections.emptyList());
+ }
+ }
+
+ /**
+ * Fills in the touchable region for floating windows. This is used by WindowManager to
+ * decide which touch events go to the floating windows.
+ */
+ private void getTouchableRegion(Region outRegion) {
+ int childCount = getChildCount();
+ Rect temp = new Rect();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child instanceof FloatingTaskView) {
+ child.getBoundsOnScreen(temp);
+ outRegion.op(temp, Region.Op.UNION);
+ }
+ }
+ }
+
+ /**
+ * Implementation of the touch handler. Animates the task view based on touch events.
+ */
+ private class TouchHandlerImpl implements FloatingTaskTouchHandler {
+ /**
+ * The view can be stashed by swiping it towards the current edge or moving it there. If
+ * the view gets moved in a way that is not one of these gestures, this is flipped to false.
+ */
+ private boolean mCanStash = true;
+ /**
+ * This is used to indicate that the view has been un-stuck from the dismiss target and
+ * needs to spring to the current touch location.
+ */
+ // TODO: implement this behavior
+ private boolean mSpringToTouchOnNextMotionEvent = false;
+
+ private ArrayList mFlingAnimations;
+ private ViewPropertyAnimator mViewPropertyAnimation;
+
+ private float mViewInitialX;
+ private float mViewInitialY;
+
+ private float[] mMinMax = new float[2];
+
+ @Override
+ public void onDown(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY) {
+ mCanStash = true;
+ mViewInitialX = viewInitialX;
+ mViewInitialY = viewInitialY;
+ mDismissController.setUpMagneticObject(v);
+ mDismissController.passEventToMagnetizedObject(ev);
+ }
+
+ @Override
+ public void onMove(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float dx, float dy) {
+ // Shows the magnetic dismiss target if needed.
+ mDismissController.showDismiss(/* show= */ true);
+
+ // Send it to magnetic target first.
+ if (mDismissController.passEventToMagnetizedObject(ev)) {
+ v.setStashed(false);
+ mCanStash = true;
+
+ return;
+ }
+
+ // If we're here magnetic target didn't want it so move as per normal.
+
+ v.setTranslationX(capX(v, mViewInitialX + dx, /* isMoving= */ true));
+ v.setTranslationY(capY(v, mViewInitialY + dy));
+ if (v.isStashed()) {
+ // Check if we've moved far enough to be not stashed.
+ final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
+ final boolean viewInitiallyOnLeftSide = mViewInitialX < centerX;
+ if (viewInitiallyOnLeftSide) {
+ if (v.getTranslationX() > mPositionRect.left) {
+ v.setStashed(false);
+ mCanStash = true;
+ }
+ } else if (v.getTranslationX() + v.getWidth() < mPositionRect.right) {
+ v.setStashed(false);
+ mCanStash = true;
+ }
+ }
+ }
+
+ // Reference for math / values: StackAnimationController#flingStackThenSpringToEdge.
+ // TODO clean up the code here, pretty hard to comprehend
+ // TODO code here doesn't work the best when in portrait (e.g. can't fling up/down on edges)
+ @Override
+ public void onUp(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
+ float dx, float dy, float velX, float velY) {
+
+ // Send it to magnetic target first.
+ if (mDismissController.passEventToMagnetizedObject(ev)) {
+ v.setStashed(false);
+ return;
+ }
+ mDismissController.showDismiss(/* show= */ false);
+
+ // If we're here magnetic target didn't want it so handle up as per normal.
+
+ final float x = capX(v, mViewInitialX + dx, /* isMoving= */ false);
+ final float centerX = mPositionRect.centerX();
+ final boolean viewInitiallyOnLeftSide = mViewInitialX + v.getWidth() < centerX;
+ final boolean viewOnLeftSide = x + v.getWidth() < centerX;
+ final boolean isFling = Math.abs(velX) > ESCAPE_VELOCITY;
+ final boolean isFlingLeft = isFling && velX < ESCAPE_VELOCITY;
+ // TODO: check velX here sometimes it doesn't stash on move when I think it should
+ final boolean shouldStashFromMove =
+ (velX < 0 && v.getTranslationX() < mPositionRect.left)
+ || (velX > 0
+ && v.getTranslationX() + v.getWidth() > mPositionRect.right);
+ final boolean shouldStashFromFling = viewInitiallyOnLeftSide == viewOnLeftSide
+ && isFling
+ && ((viewOnLeftSide && velX < ESCAPE_VELOCITY)
+ || (!viewOnLeftSide && velX > ESCAPE_VELOCITY));
+ final boolean shouldStash = mCanStash && (shouldStashFromFling || shouldStashFromMove);
+
+ ProtoLog.d(WM_SHELL_FLOATING_APPS,
+ "shouldStash=%s shouldStashFromFling=%s shouldStashFromMove=%s"
+ + " viewInitiallyOnLeftSide=%s viewOnLeftSide=%s isFling=%s velX=%f"
+ + " isStashed=%s", shouldStash, shouldStashFromFling, shouldStashFromMove,
+ viewInitiallyOnLeftSide, viewOnLeftSide, isFling, velX, v.isStashed());
+
+ if (v.isStashed()) {
+ mMinMax[0] = viewOnLeftSide
+ ? mPositionRect.left - v.getWidth() + mOverhangWhenStashed
+ : mPositionRect.right - v.getWidth();
+ mMinMax[1] = viewOnLeftSide
+ ? mPositionRect.left
+ : mPositionRect.right - mOverhangWhenStashed;
+ } else {
+ populateMinMax(v, viewOnLeftSide, shouldStash, mMinMax);
+ }
+
+ boolean movingLeft = isFling ? isFlingLeft : viewOnLeftSide;
+ float destinationRelativeX = movingLeft
+ ? mMinMax[0]
+ : mMinMax[1];
+
+ // TODO: why is this necessary / when does this happen?
+ if (mMinMax[1] < v.getTranslationX()) {
+ mMinMax[1] = v.getTranslationX();
+ }
+ if (v.getTranslationX() < mMinMax[0]) {
+ mMinMax[0] = v.getTranslationX();
+ }
+
+ // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity
+ // so that it'll make it all the way to the side of the screen.
+ final float minimumVelocityToReachEdge =
+ getMinimumVelocityToReachEdge(v, destinationRelativeX);
+ final float startXVelocity = movingLeft
+ ? Math.min(minimumVelocityToReachEdge, velX)
+ : Math.max(minimumVelocityToReachEdge, velX);
+
+ cancelAnyAnimations(v);
+
+ mFlingAnimations = getAnimationForUpEvent(v, shouldStash,
+ startXVelocity, mMinMax[0], mMinMax[1], destinationRelativeX);
+ for (int i = 0; i < mFlingAnimations.size(); i++) {
+ mFlingAnimations.get(i).start();
+ }
+ }
+
+ @Override
+ public void onClick(@NonNull FloatingTaskView v) {
+ if (v.isStashed()) {
+ final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
+ final boolean viewOnLeftSide = v.getTranslationX() < centerX;
+ final float destinationRelativeX = viewOnLeftSide
+ ? mPositionRect.left
+ : mPositionRect.right - v.getWidth();
+ final float minimumVelocityToReachEdge =
+ getMinimumVelocityToReachEdge(v, destinationRelativeX);
+ populateMinMax(v, viewOnLeftSide, /* stashed= */ true, mMinMax);
+
+ cancelAnyAnimations(v);
+
+ FlingAnimation flingAnimation = new FlingAnimation(v,
+ DynamicAnimation.TRANSLATION_X);
+ flingAnimation.setFriction(FLING_FRICTION)
+ .setStartVelocity(minimumVelocityToReachEdge)
+ .setMinValue(mMinMax[0])
+ .setMaxValue(mMinMax[1])
+ .addEndListener((animation, canceled, value, velocity) -> {
+ if (canceled) return;
+ mController.setLastPosition((int) v.getTranslationX(),
+ (int) v.getTranslationY());
+ v.setStashed(false);
+ v.updateLocation();
+ });
+ mFlingAnimations = new ArrayList<>();
+ mFlingAnimations.add(flingAnimation);
+ flingAnimation.start();
+ }
+ }
+
+ public void onUnstuckFromTarget(FloatingTaskView v, float velX, float velY,
+ boolean wasFlungOut) {
+ if (wasFlungOut) {
+ snapTaskViewToEdge(v, velX, /* shouldStash= */ false);
+ } else {
+ // TODO: use this for something / to spring the view to the touch location
+ mSpringToTouchOnNextMotionEvent = true;
+ }
+ }
+
+ public void stashTaskView(FloatingTaskView v, boolean shouldStash) {
+ if (v.isStashed() == shouldStash) {
+ return;
+ }
+ final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
+ final boolean viewOnLeftSide = v.getTranslationX() < centerX;
+ snapTaskViewToEdge(v, viewOnLeftSide ? -ESCAPE_VELOCITY : ESCAPE_VELOCITY, shouldStash);
+ }
+
+ public boolean isStashedPosition(View v) {
+ return v.getTranslationX() < mPositionRect.left
+ || v.getTranslationX() + v.getWidth() > mPositionRect.right;
+ }
+
+ // TODO: a lot of this is duplicated in onUp -- can it be unified?
+ private void snapTaskViewToEdge(FloatingTaskView v, float velX, boolean shouldStash) {
+ final boolean movingLeft = velX < ESCAPE_VELOCITY;
+ populateMinMax(v, movingLeft, shouldStash, mMinMax);
+ float destinationRelativeX = movingLeft
+ ? mMinMax[0]
+ : mMinMax[1];
+
+ // TODO: why is this necessary / when does this happen?
+ if (mMinMax[1] < v.getTranslationX()) {
+ mMinMax[1] = v.getTranslationX();
+ }
+ if (v.getTranslationX() < mMinMax[0]) {
+ mMinMax[0] = v.getTranslationX();
+ }
+
+ // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity
+ // so that it'll make it all the way to the side of the screen.
+ final float minimumVelocityToReachEdge =
+ getMinimumVelocityToReachEdge(v, destinationRelativeX);
+ final float startXVelocity = movingLeft
+ ? Math.min(minimumVelocityToReachEdge, velX)
+ : Math.max(minimumVelocityToReachEdge, velX);
+
+ cancelAnyAnimations(v);
+
+ mFlingAnimations = getAnimationForUpEvent(v,
+ shouldStash, startXVelocity, mMinMax[0], mMinMax[1],
+ destinationRelativeX);
+ for (int i = 0; i < mFlingAnimations.size(); i++) {
+ mFlingAnimations.get(i).start();
+ }
+ }
+
+ private void cancelAnyAnimations(FloatingTaskView v) {
+ if (mFlingAnimations != null) {
+ for (int i = 0; i < mFlingAnimations.size(); i++) {
+ if (mFlingAnimations.get(i).isRunning()) {
+ mFlingAnimations.get(i).cancel();
+ }
+ }
+ }
+ if (mViewPropertyAnimation != null) {
+ mViewPropertyAnimation.cancel();
+ mViewPropertyAnimation = null;
+ }
+ }
+
+ private ArrayList getAnimationForUpEvent(FloatingTaskView v,
+ boolean shouldStash, float startVelX, float minValue, float maxValue,
+ float destinationRelativeX) {
+ final float ty = v.getTranslationY();
+ final ArrayList animations = new ArrayList<>();
+ if (ty != capY(v, ty)) {
+ // The view was being dismissed so the Y is out of bounds, need to animate that.
+ FlingAnimation yFlingAnimation = new FlingAnimation(v,
+ DynamicAnimation.TRANSLATION_Y);
+ yFlingAnimation.setFriction(FLING_FRICTION)
+ .setStartVelocity(startVelX)
+ .setMinValue(mPositionRect.top)
+ .setMaxValue(mPositionRect.bottom - mTaskViewSize.y);
+ animations.add(yFlingAnimation);
+ }
+ FlingAnimation flingAnimation = new FlingAnimation(v, DynamicAnimation.TRANSLATION_X);
+ flingAnimation.setFriction(FLING_FRICTION)
+ .setStartVelocity(startVelX)
+ .setMinValue(minValue)
+ .setMaxValue(maxValue)
+ .addEndListener((animation, canceled, value, velocity) -> {
+ if (canceled) return;
+ Runnable endAction = () -> {
+ v.setStashed(shouldStash);
+ v.updateLocation();
+ if (!v.isStashed()) {
+ mController.setLastPosition((int) v.getTranslationX(),
+ (int) v.getTranslationY());
+ }
+ };
+ if (!shouldStash) {
+ final int xTranslation = (int) v.getTranslationX();
+ if (xTranslation != destinationRelativeX) {
+ // TODO: this animation doesn't feel great, should figure out
+ // a better way to do this or remove the need for it all together.
+ mViewPropertyAnimation = v.animate()
+ .translationX(destinationRelativeX)
+ .setListener(getAnimationListener(endAction));
+ mViewPropertyAnimation.start();
+ } else {
+ endAction.run();
+ }
+ } else {
+ endAction.run();
+ }
+ });
+ animations.add(flingAnimation);
+ return animations;
+ }
+
+ private AnimatorListenerAdapter getAnimationListener(Runnable endAction) {
+ return new AnimatorListenerAdapter() {
+ boolean translationCanceled = false;
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ translationCanceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (!translationCanceled) {
+ endAction.run();
+ }
+ }
+ };
+ }
+
+ private void populateMinMax(FloatingTaskView v, boolean onLeft, boolean shouldStash,
+ float[] out) {
+ if (shouldStash) {
+ out[0] = onLeft
+ ? mPositionRect.left - v.getWidth() + mOverhangWhenStashed
+ : mPositionRect.right - v.getWidth();
+ out[1] = onLeft
+ ? mPositionRect.left
+ : mPositionRect.right - mOverhangWhenStashed;
+ } else {
+ out[0] = mPositionRect.left;
+ out[1] = mPositionRect.right - mTaskViewSize.x;
+ }
+ }
+
+ private float getMinimumVelocityToReachEdge(FloatingTaskView v,
+ float destinationRelativeX) {
+ // Minimum velocity required for the view to make it to the targeted side of the screen,
+ // taking friction into account (4.2f is the number that friction scalars are multiplied
+ // by in DynamicAnimation.DragForce). This is an estimate and could be slightly off, the
+ // animation at the end will ensure that it reaches the destination X regardless.
+ return (destinationRelativeX - v.getTranslationX()) * (FLING_FRICTION * 4.2f);
+ }
+
+ private float capX(FloatingTaskView v, float x, boolean isMoving) {
+ final int width = v.getWidth();
+ if (v.isStashed() || isMoving) {
+ if (x < mPositionRect.left - v.getWidth() + mOverhangWhenStashed) {
+ return mPositionRect.left - v.getWidth() + mOverhangWhenStashed;
+ }
+ if (x > mPositionRect.right - mOverhangWhenStashed) {
+ return mPositionRect.right - mOverhangWhenStashed;
+ }
+ } else {
+ if (x < mPositionRect.left) {
+ return mPositionRect.left;
+ }
+ if (x > mPositionRect.right - width) {
+ return mPositionRect.right - width;
+ }
+ }
+ return x;
+ }
+
+ private float capY(FloatingTaskView v, float y) {
+ final int height = v.getHeight();
+ if (y < mPositionRect.top) {
+ return mPositionRect.top;
+ }
+ if (y > mPositionRect.bottom - height) {
+ return mPositionRect.bottom - height;
+ }
+ return y;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java
new file mode 100644
index 0000000000000..581204a82ec7e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.floating.views;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
+
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.TaskView;
+import com.android.wm.shell.TaskViewTransitions;
+import com.android.wm.shell.bubbles.RelativeTouchListener;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.floating.FloatingTasksController;
+
+/**
+ * A view that holds a floating task using {@link TaskView} along with additional UI to manage
+ * the task.
+ */
+public class FloatingTaskView extends FrameLayout {
+
+ private static final String TAG = FloatingTaskView.class.getSimpleName();
+
+ private FloatingTasksController mController;
+
+ private FloatingMenuView mMenuView;
+ private int mMenuHeight;
+ private TaskView mTaskView;
+
+ private float mCornerRadius = 0f;
+ private int mBackgroundColor;
+
+ private FloatingTasksController.Task mTask;
+
+ private boolean mIsStashed;
+
+ /**
+ * Creates a floating task view.
+ *
+ * @param context the context to use.
+ * @param controller the controller to notify about changes in the floating task (e.g. removal).
+ */
+ public FloatingTaskView(Context context, FloatingTasksController controller) {
+ super(context);
+ mController = controller;
+ setElevation(getResources().getDimensionPixelSize(R.dimen.floating_task_elevation));
+ mMenuHeight = context.getResources().getDimensionPixelSize(R.dimen.floating_task_menu_size);
+ mMenuView = new FloatingMenuView(context);
+ addView(mMenuView);
+
+ applyThemeAttrs();
+
+ setClipToOutline(true);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
+ }
+ });
+ }
+
+ // TODO: call this when theme/config changes
+ void applyThemeAttrs() {
+ boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
+ mContext.getResources());
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.dialogCornerRadius,
+ android.R.attr.colorBackgroundFloating});
+ mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
+ mCornerRadius = mCornerRadius / 2f;
+ mBackgroundColor = ta.getColor(1, Color.WHITE);
+
+ ta.recycle();
+
+ mMenuView.setCornerRadius(mCornerRadius);
+ mMenuHeight = getResources().getDimensionPixelSize(
+ R.dimen.floating_task_menu_size);
+
+ if (mTaskView != null) {
+ mTaskView.setCornerRadius(mCornerRadius);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ // Add corner radius here so that the menu extends behind the rounded corners of TaskView.
+ int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height);
+ measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
+ MeasureSpec.getMode(heightMeasureSpec)));
+
+ if (mTaskView != null) {
+ int taskViewHeight = height - menuViewHeight;
+ measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(taskViewHeight,
+ MeasureSpec.getMode(heightMeasureSpec)));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Drag handle above
+ final int dragHandleBottom = t + mMenuView.getMeasuredHeight();
+ mMenuView.layout(l, t, r, dragHandleBottom);
+ if (mTaskView != null) {
+ // Subtract radius so that the menu extends behind the rounded corners of TaskView.
+ mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r,
+ dragHandleBottom + mTaskView.getMeasuredHeight());
+ }
+ }
+
+ /**
+ * Constructs the TaskView to display the task. Must be called for {@link #startTask} to work.
+ */
+ public void createTaskView(Context context, ShellTaskOrganizer organizer,
+ TaskViewTransitions transitions, SyncTransactionQueue syncQueue) {
+ mTaskView = new TaskView(context, organizer, transitions, syncQueue);
+ addView(mTaskView);
+ mTaskView.setEnableSurfaceClipping(true);
+ mTaskView.setCornerRadius(mCornerRadius);
+ }
+
+ /**
+ * Starts the provided task in the TaskView, if the TaskView exists. This should be called after
+ * {@link #createTaskView}.
+ */
+ public void startTask(@ShellMainThread ShellExecutor executor,
+ FloatingTasksController.Task task) {
+ if (mTaskView == null) {
+ Log.e(TAG, "starting task before creating the view!");
+ return;
+ }
+ mTask = task;
+ mTaskView.setListener(executor, mTaskViewListener);
+ }
+
+ /**
+ * Sets the touch handler for the view.
+ *
+ * @param handler the touch handler for the view.
+ */
+ public void setTouchHandler(FloatingTaskLayer.FloatingTaskTouchHandler handler) {
+ setOnTouchListener(new RelativeTouchListener() {
+ @Override
+ public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
+ handler.onDown(FloatingTaskView.this, ev, v.getTranslationX(), v.getTranslationY());
+ return true;
+ }
+
+ @Override
+ public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY, float dx, float dy) {
+ handler.onMove(FloatingTaskView.this, ev, dx, dy);
+ }
+
+ @Override
+ public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY, float dx, float dy, float velX, float velY) {
+ handler.onUp(FloatingTaskView.this, ev, dx, dy, velX, velY);
+ }
+ });
+ setOnClickListener(view -> {
+ handler.onClick(FloatingTaskView.this);
+ });
+
+ mMenuView.addMenuItem(null, view -> {
+ if (mIsStashed) {
+ // If we're stashed all clicks un-stash.
+ handler.onClick(FloatingTaskView.this);
+ }
+ });
+ }
+
+ private void setContentVisibility(boolean visible) {
+ if (mTaskView == null) return;
+ mTaskView.setAlpha(visible ? 1f : 0f);
+ }
+
+ /**
+ * Sets the alpha of both this view and the TaskView.
+ */
+ public void setTaskViewAlpha(float alpha) {
+ if (mTaskView != null) {
+ mTaskView.setAlpha(alpha);
+ }
+ setAlpha(alpha);
+ }
+
+ /**
+ * Call when the location or size of the view has changed to update TaskView.
+ */
+ public void updateLocation() {
+ if (mTaskView == null) return;
+ mTaskView.onLocationChanged();
+ }
+
+ private void updateMenuColor() {
+ ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo();
+ int color = info != null ? info.taskDescription.getBackgroundColor() : -1;
+ if (color != -1) {
+ mMenuView.setBackgroundColor(color);
+ } else {
+ mMenuView.setBackgroundColor(mBackgroundColor);
+ }
+ }
+
+ /**
+ * Sets whether the view is stashed or not.
+ *
+ * Also updates the touchable area based on this. If the view is stashed we don't direct taps
+ * on the activity to the activity, instead a tap will un-stash the view.
+ */
+ public void setStashed(boolean isStashed) {
+ if (mIsStashed != isStashed) {
+ mIsStashed = isStashed;
+ if (mTaskView == null) {
+ return;
+ }
+ updateObscuredTouchRect();
+ }
+ }
+
+ /** Whether the view is stashed at the edge of the screen or not. **/
+ public boolean isStashed() {
+ return mIsStashed;
+ }
+
+ private void updateObscuredTouchRect() {
+ if (mIsStashed) {
+ Rect tmpRect = new Rect();
+ getBoundsOnScreen(tmpRect);
+ mTaskView.setObscuredTouchRect(tmpRect);
+ } else {
+ mTaskView.setObscuredTouchRect(null);
+ }
+ }
+
+ /**
+ * Whether the task needs to be restarted, this can happen when {@link #cleanUpTaskView()} has
+ * been called on this view or if
+ * {@link #startTask(ShellExecutor, FloatingTasksController.Task)} was never called.
+ */
+ public boolean needsTaskStarted() {
+ // If the task needs to be restarted then TaskView would have been cleaned up.
+ return mTaskView == null;
+ }
+
+ /** Call this when the floating task activity is no longer in use. */
+ public void cleanUpTaskView() {
+ if (mTask != null && mTask.taskId != INVALID_TASK_ID) {
+ try {
+ ActivityTaskManager.getService().removeTask(mTask.taskId);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage());
+ }
+ }
+ if (mTaskView != null) {
+ mTaskView.release();
+ removeView(mTaskView);
+ mTaskView = null;
+ }
+ }
+
+ // TODO: use task background colour / how to get the taskInfo ?
+ private static int getDragBarColor(ActivityManager.RunningTaskInfo taskInfo) {
+ final int taskBgColor = taskInfo.taskDescription.getStatusBarColor();
+ return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
+ }
+
+ private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
+ private boolean mInitialized = false;
+ private boolean mDestroyed = false;
+
+ @Override
+ public void onInitialized() {
+ if (mDestroyed || mInitialized) {
+ return;
+ }
+ // Custom options so there is no activity transition animation
+ ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
+ /* enterResId= */ 0, /* exitResId= */ 0);
+
+ Rect launchBounds = new Rect();
+ mTaskView.getBoundsOnScreen(launchBounds);
+
+ try {
+ options.setTaskAlwaysOnTop(true);
+ if (mTask.intent != null) {
+ Intent fillInIntent = new Intent();
+ // Apply flags to make behaviour match documentLaunchMode=always.
+ fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+
+ PendingIntent pi = PendingIntent.getActivity(mContext, 0, mTask.intent,
+ PendingIntent.FLAG_MUTABLE,
+ null);
+ mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+ } else {
+ ProtoLog.e(WM_SHELL_FLOATING_APPS, "Tried to start a task with null intent");
+ }
+ } catch (RuntimeException e) {
+ ProtoLog.e(WM_SHELL_FLOATING_APPS, "Exception while starting task: %s",
+ e.getMessage());
+ mController.removeTask();
+ }
+ mInitialized = true;
+ }
+
+ @Override
+ public void onReleased() {
+ mDestroyed = true;
+ }
+
+ @Override
+ public void onTaskCreated(int taskId, ComponentName name) {
+ mTask.taskId = taskId;
+ updateMenuColor();
+ setContentVisibility(true);
+ }
+
+ @Override
+ public void onTaskVisibilityChanged(int taskId, boolean visible) {
+ setContentVisibility(visible);
+ }
+
+ @Override
+ public void onTaskRemovalStarted(int taskId) {
+ // Must post because this is called from a binder thread.
+ post(() -> {
+ mController.removeTask();
+ cleanUpTaskView();
+ });
+ }
+
+ @Override
+ public void onBackPressedOnTaskRoot(int taskId) {
+ if (mTask.taskId == taskId && !mIsStashed) {
+ // TODO: is removing the window the desired behavior?
+ post(() -> mController.removeTask());
+ }
+ }
+ };
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index 3fef823527285..c52ed249c2ca7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -48,6 +48,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
Consts.TAG_WM_SHELL),
WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
+ WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+ Consts.TAG_WM_SHELL),
TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
private final boolean mEnabled;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
new file mode 100644
index 0000000000000..f1b4311e5aa56
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.floating;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.wm.shell.floating.FloatingTasksController.SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TaskViewTransitions;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.floating.views.FloatingTaskLayer;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the floating tasks controller.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class FloatingTasksControllerTest extends ShellTestCase {
+ // Some behavior in the controller constructor is dependent on this so we can only
+ // validate if it's working for the real value for those things.
+ private static final boolean FLOATING_TASKS_ACTUALLY_ENABLED =
+ SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
+
+ @Mock private ShellInit mShellInit;
+ @Mock private ShellController mShellController;
+ @Mock private WindowManager mWindowManager;
+ @Mock private ShellTaskOrganizer mTaskOrganizer;
+ @Captor private ArgumentCaptor mFloatingTaskLayerCaptor;
+
+ private FloatingTasksController mController;
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+
+ WindowMetrics windowMetrics = mock(WindowMetrics.class);
+ WindowInsets windowInsets = mock(WindowInsets.class);
+ Insets insets = Insets.of(0, 0, 0, 0);
+ when(mWindowManager.getCurrentWindowMetrics()).thenReturn(windowMetrics);
+ when(windowMetrics.getWindowInsets()).thenReturn(windowInsets);
+ when(windowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1000, 1000));
+ when(windowInsets.getInsetsIgnoringVisibility(anyInt())).thenReturn(insets);
+
+ // For the purposes of this test, just run everything synchronously
+ ShellExecutor shellExecutor = new TestShellExecutor();
+ when(mTaskOrganizer.getExecutor()).thenReturn(shellExecutor);
+ }
+
+ @After
+ public void tearDown() {
+ if (mController != null) {
+ mController.removeTask();
+ mController = null;
+ }
+ }
+
+ private void setUpTabletConfig() {
+ Configuration config = mock(Configuration.class);
+ config.smallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
+ mController.setConfig(config);
+ }
+
+ private void setUpPhoneConfig() {
+ Configuration config = mock(Configuration.class);
+ config.smallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET - 1;
+ mController.setConfig(config);
+ }
+
+ private void createController() {
+ mController = new FloatingTasksController(mContext,
+ mShellInit,
+ mShellController,
+ mock(ShellCommandHandler.class),
+ mWindowManager,
+ mTaskOrganizer,
+ mock(TaskViewTransitions.class),
+ mock(ShellExecutor.class),
+ mock(ShellExecutor.class),
+ mock(SyncTransactionQueue.class));
+ spyOn(mController);
+ }
+
+ //
+ // Shell specific
+ //
+ @Test
+ public void instantiateController_addInitCallback() {
+ if (FLOATING_TASKS_ACTUALLY_ENABLED) {
+ createController();
+ setUpTabletConfig();
+
+ verify(mShellInit, times(1)).addInitCallback(any(), any());
+ }
+ }
+
+ @Test
+ public void instantiateController_doesntAddInitCallback() {
+ if (!FLOATING_TASKS_ACTUALLY_ENABLED) {
+ createController();
+
+ verify(mShellInit, never()).addInitCallback(any(), any());
+ }
+ }
+
+ @Test
+ public void onInit_registerConfigChangeListener() {
+ if (FLOATING_TASKS_ACTUALLY_ENABLED) {
+ createController();
+ setUpTabletConfig();
+ mController.onInit();
+
+ verify(mShellController, times(1)).addConfigurationChangeListener(any());
+ }
+ }
+
+ //
+ // Tests for floating layer, which is only available for tablets.
+ //
+
+ @Test
+ public void testIsFloatingLayerAvailable_true() {
+ createController();
+ setUpTabletConfig();
+ assertThat(mController.isFloatingLayerAvailable()).isTrue();
+ }
+
+ @Test
+ public void testIsFloatingLayerAvailable_false() {
+ createController();
+ setUpPhoneConfig();
+ assertThat(mController.isFloatingLayerAvailable()).isFalse();
+ }
+
+ //
+ // Tests for floating tasks being enabled, guarded by sysprop flag.
+ //
+
+ @Test
+ public void testIsFloatingTasksEnabled_true() {
+ createController();
+ mController.setFloatingTasksEnabled(true);
+ setUpTabletConfig();
+ assertThat(mController.isFloatingTasksEnabled()).isTrue();
+ }
+
+ @Test
+ public void testIsFloatingTasksEnabled_false() {
+ createController();
+ mController.setFloatingTasksEnabled(false);
+ setUpTabletConfig();
+ assertThat(mController.isFloatingTasksEnabled()).isFalse();
+ }
+
+ //
+ // Tests for behavior depending on flags
+ //
+
+ @Test
+ public void testShowTaskIntent_enabled() {
+ createController();
+ mController.setFloatingTasksEnabled(true);
+ setUpTabletConfig();
+
+ mController.showTask(mock(Intent.class));
+ verify(mWindowManager).addView(mFloatingTaskLayerCaptor.capture(), any());
+ assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testShowTaskIntent_notEnabled() {
+ createController();
+ mController.setFloatingTasksEnabled(false);
+ setUpTabletConfig();
+
+ mController.showTask(mock(Intent.class));
+ verify(mWindowManager, never()).addView(any(), any());
+ }
+
+ @Test
+ public void testRemoveTask() {
+ createController();
+ mController.setFloatingTasksEnabled(true);
+ setUpTabletConfig();
+
+ mController.showTask(mock(Intent.class));
+ verify(mWindowManager).addView(mFloatingTaskLayerCaptor.capture(), any());
+ assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(1);
+
+ mController.removeTask();
+ verify(mWindowManager).removeView(mFloatingTaskLayerCaptor.capture());
+ assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(0);
+ }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 97e0242387789..6d12485fd6b5f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -47,6 +47,8 @@ public class QuickStepContract {
public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip";
// See ISplitScreen.aidl
public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen";
+ // See IFloatingTasks.aidl
+ public static final String KEY_EXTRA_SHELL_FLOATING_TASKS = "extra_shell_floating_tasks";
// See IOneHanded.aidl
public static final String KEY_EXTRA_SHELL_ONE_HANDED = "extra_shell_one_handed";
// See IShellTransitions.aidl
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 5f586c927ef74..50c38e54b4d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -96,7 +96,8 @@ public abstract class SystemUIInitializer {
.setStartingSurface(mWMComponent.getStartingSurface())
.setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
.setRecentTasks(mWMComponent.getRecentTasks())
- .setBackAnimation(mWMComponent.getBackAnimation());
+ .setBackAnimation(mWMComponent.getBackAnimation())
+ .setFloatingTasks(mWMComponent.getFloatingTasks());
// Only initialize when not starting from tests since this currently initializes some
// components that shouldn't be run in the test environment
@@ -115,7 +116,8 @@ public abstract class SystemUIInitializer {
.setDisplayAreaHelper(Optional.ofNullable(null))
.setStartingSurface(Optional.ofNullable(null))
.setRecentTasks(Optional.ofNullable(null))
- .setBackAnimation(Optional.ofNullable(null));
+ .setBackAnimation(Optional.ofNullable(null))
+ .setFloatingTasks(Optional.ofNullable(null));
}
mSysUIComponent = builder.build();
if (initializeComponents) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 029cabb0bc0bb..7e30431a30c83 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -41,6 +41,7 @@ import com.android.wm.shell.TaskViewFactory;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
+import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.recents.RecentTasks;
@@ -109,6 +110,9 @@ public interface SysUIComponent {
@BindsInstance
Builder setBackAnimation(Optional b);
+ @BindsInstance
+ Builder setFloatingTasks(Optional f);
+
SysUIComponent build();
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
index b6923a867507c..dd115490abfa3 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
@@ -23,8 +23,6 @@ import androidx.annotation.Nullable;
import com.android.systemui.SystemUIInitializerFactory;
import com.android.systemui.tv.TvWMComponent;
-import com.android.wm.shell.sysui.ShellCommandHandler;
-import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.TaskViewFactory;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.bubbles.Bubbles;
@@ -33,6 +31,7 @@ import com.android.wm.shell.dagger.TvWMShellModule;
import com.android.wm.shell.dagger.WMShellModule;
import com.android.wm.shell.dagger.WMSingleton;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
+import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.recents.RecentTasks;
@@ -110,4 +109,7 @@ public interface WMComponent {
@WMSingleton
Optional getBackAnimation();
+
+ @WMSingleton
+ Optional getFloatingTasks();
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index cd6c57aea76b7..e712ea413a7f8 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -234,6 +234,10 @@ public class Flags {
public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL =
new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false);
+ @Keep
+ public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED =
+ new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false);
+
// 1200 - predictive back
@Keep
public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 3788ad98092f6..95edb359c53cf 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -27,6 +27,7 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_RECENT_TASKS;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_BACK_ANIMATION;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_FLOATING_TASKS;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
@@ -109,6 +110,7 @@ import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
import com.android.systemui.statusbar.policy.CallbackController;
import com.android.wm.shell.back.BackAnimation;
+import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
@@ -150,6 +152,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
private final Optional mPipOptional;
private final Lazy> mCentralSurfacesOptionalLazy;
private final Optional mSplitScreenOptional;
+ private final Optional mFloatingTasksOptional;
private SysUiState mSysUiState;
private final Handler mHandler;
private final Lazy mNavBarControllerLazy;
@@ -382,7 +385,6 @@ public class OverviewProxyService extends CurrentUserTracker implements
mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::togglePanel));
}
-
private boolean verifyCaller(String reason) {
final int callerId = Binder.getCallingUserHandle().getIdentifier();
if (callerId != mCurrentBoundedUserId) {
@@ -466,6 +468,9 @@ public class OverviewProxyService extends CurrentUserTracker implements
mSplitScreenOptional.ifPresent((splitscreen) -> params.putBinder(
KEY_EXTRA_SHELL_SPLIT_SCREEN,
splitscreen.createExternalInterface().asBinder()));
+ mFloatingTasksOptional.ifPresent(floatingTasks -> params.putBinder(
+ KEY_EXTRA_SHELL_FLOATING_TASKS,
+ floatingTasks.createExternalInterface().asBinder()));
mOneHandedOptional.ifPresent((onehanded) -> params.putBinder(
KEY_EXTRA_SHELL_ONE_HANDED,
onehanded.createExternalInterface().asBinder()));
@@ -563,6 +568,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
Optional pipOptional,
Optional splitScreenOptional,
+ Optional floatingTasksOptional,
Optional oneHandedOptional,
Optional recentTasks,
Optional backAnimation,
@@ -631,6 +637,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
mCommandQueue = commandQueue;
mSplitScreenOptional = splitScreenOptional;
+ mFloatingTasksOptional = floatingTasksOptional;
// Listen for user setup
startTracking();
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 3961a8bcc9805..3472cb1c2a7db 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -55,6 +55,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.systemui.tracing.nano.SystemUiTraceProto;
+import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.nano.WmShellTraceProto;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
@@ -106,6 +107,7 @@ public final class WMShell extends CoreStartable
private final Optional mPipOptional;
private final Optional mSplitScreenOptional;
private final Optional mOneHandedOptional;
+ private final Optional mFloatingTasksOptional;
private final CommandQueue mCommandQueue;
private final ConfigurationController mConfigurationController;
@@ -166,6 +168,7 @@ public final class WMShell extends CoreStartable
Optional pipOptional,
Optional splitScreenOptional,
Optional oneHandedOptional,
+ Optional floatingTasksOptional,
CommandQueue commandQueue,
ConfigurationController configurationController,
KeyguardStateController keyguardStateController,
@@ -190,6 +193,7 @@ public final class WMShell extends CoreStartable
mWakefulnessLifecycle = wakefulnessLifecycle;
mProtoTracer = protoTracer;
mUserTracker = userTracker;
+ mFloatingTasksOptional = floatingTasksOptional;
mSysUiMainExecutor = sysUiMainExecutor;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index da33fa62a9ab3..cebe946a459c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -34,6 +34,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -74,16 +75,16 @@ public class WMShellTest extends SysuiTestCase {
@Mock ProtoTracer mProtoTracer;
@Mock UserTracker mUserTracker;
@Mock ShellExecutor mSysUiMainExecutor;
+ @Mock FloatingTasks mFloatingTasks;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
-
mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip),
- Optional.of(mSplitScreen), Optional.of(mOneHanded), mCommandQueue,
- mConfigurationController, mKeyguardStateController, mKeyguardUpdateMonitor,
- mScreenLifecycle, mSysUiState, mProtoTracer, mWakefulnessLifecycle,
- mUserTracker, mSysUiMainExecutor);
+ Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks),
+ mCommandQueue, mConfigurationController, mKeyguardStateController,
+ mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer,
+ mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor);
}
@Test