Merge "Support for a floating task window (behind a sysui flag)" into tm-qpr-dev

This commit is contained in:
Mady Mellor
2022-09-22 14:58:12 +00:00
committed by Android (Google) Code Review
20 changed files with 2254 additions and 10 deletions

View File

@@ -297,4 +297,28 @@
when the pinned stack size is overridden by app via minWidth/minHeight.
-->
<dimen name="overridable_minimal_size_pip_resizable_task">48dp</dimen>
<!-- The size of the drag handle / menu shown along with a floating task. -->
<dimen name="floating_task_menu_size">32dp</dimen>
<!-- The size of menu items in the floating task menu. -->
<dimen name="floating_task_menu_item_size">24dp</dimen>
<!-- The horizontal margin of menu items in the floating task menu. -->
<dimen name="floating_task_menu_item_padding">5dp</dimen>
<!-- The width of visible floating view region when stashed. -->
<dimen name="floating_task_stash_offset">32dp</dimen>
<!-- The amount of elevation for a floating task. -->
<dimen name="floating_task_elevation">8dp</dimen>
<!-- The amount of padding around the bottom and top of the task. -->
<dimen name="floating_task_vertical_padding">8dp</dimen>
<!-- The normal size of the dismiss target. -->
<dimen name="floating_task_dismiss_circle_size">150dp</dimen>
<!-- The smaller size of the dismiss target (shrinks when something is in the target). -->
<dimen name="floating_dismiss_circle_small">120dp</dimen>
</resources>

View File

@@ -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();
}

View File

@@ -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<FloatingTasks> provideFloatingTasks(
Optional<FloatingTasksController> floatingTaskController) {
return floatingTaskController.map((controller) -> controller.asFloatingTasks());
}
@WMSingleton
@Provides
static Optional<FloatingTasksController> 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
//

View File

@@ -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<View> 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<View> getMagnetizedView(View v) {
if (mMagnetizedObject != null
&& Objects.equals(mMagnetizedObject.getUnderlyingObject(), v)) {
// Same view being dragged, we can reuse the magnetic object.
return mMagnetizedObject;
}
MagnetizedObject<View> magnetizedView = new MagnetizedObject<View>(
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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<FloatingTasksController>,
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));
}
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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<Rect> 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<View> 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<FlingAnimation> 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<FlingAnimation> getAnimationForUpEvent(FloatingTaskView v,
boolean shouldStash, float startVelX, float minValue, float maxValue,
float destinationRelativeX) {
final float ty = v.getTranslationY();
final ArrayList<FlingAnimation> 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;
}
}
}

View File

@@ -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());
}
}
};
}

View File

@@ -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;

View File

@@ -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<FloatingTaskLayer> 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);
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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<BackAnimation> b);
@BindsInstance
Builder setFloatingTasks(Optional<FloatingTasks> f);
SysUIComponent build();
}

View File

@@ -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<BackAnimation> getBackAnimation();
@WMSingleton
Optional<FloatingTasks> getFloatingTasks();
}

View File

@@ -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(

View File

@@ -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<Pip> mPipOptional;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<FloatingTasks> mFloatingTasksOptional;
private SysUiState mSysUiState;
private final Handler mHandler;
private final Lazy<NavigationBarController> 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<Pip> pipOptional,
Optional<SplitScreen> splitScreenOptional,
Optional<FloatingTasks> floatingTasksOptional,
Optional<OneHanded> oneHandedOptional,
Optional<RecentTasks> recentTasks,
Optional<BackAnimation> backAnimation,
@@ -631,6 +637,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
mCommandQueue = commandQueue;
mSplitScreenOptional = splitScreenOptional;
mFloatingTasksOptional = floatingTasksOptional;
// Listen for user setup
startTracking();

View File

@@ -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<Pip> mPipOptional;
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<OneHanded> mOneHandedOptional;
private final Optional<FloatingTasks> mFloatingTasksOptional;
private final CommandQueue mCommandQueue;
private final ConfigurationController mConfigurationController;
@@ -166,6 +168,7 @@ public final class WMShell extends CoreStartable
Optional<Pip> pipOptional,
Optional<SplitScreen> splitScreenOptional,
Optional<OneHanded> oneHandedOptional,
Optional<FloatingTasks> 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;
}

View File

@@ -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