Merge "Support for a floating task window (behind a sysui flag)" into tm-qpr-dev
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user