Merge changes from topic "mm_remove_floating_tasks" into tm-qpr-dev
* changes: Add a flag for the bubble bar Remove FloatingTaskController & replace usages with Bubbles
This commit is contained in:
@@ -24,6 +24,7 @@ import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||
|
||||
import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
|
||||
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
|
||||
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
|
||||
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
|
||||
@@ -37,7 +38,6 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE;
|
||||
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
|
||||
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
|
||||
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
|
||||
import static com.android.wm.shell.floating.FloatingTasksController.SHOW_FLOATING_TASKS_AS_BUBBLES;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.UserIdInt;
|
||||
@@ -150,6 +150,9 @@ public class BubbleController implements ConfigurationChangeListener {
|
||||
|
||||
private final ShellExecutor mBackgroundExecutor;
|
||||
|
||||
// Whether or not we should show bubbles pinned at the bottom of the screen.
|
||||
private boolean mIsBubbleBarEnabled;
|
||||
|
||||
private BubbleLogger mLogger;
|
||||
private BubbleData mBubbleData;
|
||||
@Nullable private BubbleStackView mStackView;
|
||||
@@ -210,7 +213,6 @@ public class BubbleController implements ConfigurationChangeListener {
|
||||
/** Drag and drop controller to register listener for onDragStarted. */
|
||||
private DragAndDropController mDragAndDropController;
|
||||
|
||||
|
||||
public BubbleController(Context context,
|
||||
ShellInit shellInit,
|
||||
ShellCommandHandler shellCommandHandler,
|
||||
@@ -526,6 +528,12 @@ public class BubbleController implements ConfigurationChangeListener {
|
||||
mDataRepository.removeBubblesForUser(removedUserId, parentUserId);
|
||||
}
|
||||
|
||||
// TODO(b/256873975): Should pass this into the constructor once flags are available to shell.
|
||||
/** Sets whether the bubble bar is enabled (i.e. bubbles pinned to bottom on large screens). */
|
||||
public void setBubbleBarEnabled(boolean enabled) {
|
||||
mIsBubbleBarEnabled = enabled;
|
||||
}
|
||||
|
||||
/** Whether this userId belongs to the current user. */
|
||||
private boolean isCurrentProfile(int userId) {
|
||||
return userId == UserHandle.USER_ALL
|
||||
@@ -591,7 +599,8 @@ public class BubbleController implements ConfigurationChangeListener {
|
||||
}
|
||||
mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
|
||||
}
|
||||
if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubblePositioner.isLargeScreen()) {
|
||||
|
||||
if (mIsBubbleBarEnabled && mBubblePositioner.isLargeScreen()) {
|
||||
mBubblePositioner.setUsePinnedLocation(true);
|
||||
} else {
|
||||
mBubblePositioner.setUsePinnedLocation(false);
|
||||
@@ -959,14 +968,18 @@ public class BubbleController implements ConfigurationChangeListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a bubble for a specific intent. These bubbles are <b>not</b> backed by a notification
|
||||
* and remain until the user dismisses the bubble or bubble stack. Only one intent bubble
|
||||
* is supported at a time.
|
||||
* Adds and expands bubble for a specific intent. These bubbles are <b>not</b> backed by a n
|
||||
* otification and remain until the user dismisses the bubble or bubble stack. Only one intent
|
||||
* bubble is supported at a time.
|
||||
*
|
||||
* @param intent the intent to display in the bubble expanded view.
|
||||
*/
|
||||
public void addAppBubble(Intent intent) {
|
||||
public void showAppBubble(Intent intent) {
|
||||
if (intent == null || intent.getPackage() == null) return;
|
||||
|
||||
PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId);
|
||||
if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return;
|
||||
|
||||
Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
|
||||
b.setShouldAutoExpand(true);
|
||||
inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
|
||||
@@ -1489,18 +1502,23 @@ public class BubbleController implements ConfigurationChangeListener {
|
||||
}
|
||||
PackageManager packageManager = getPackageManagerForUser(
|
||||
context, entry.getStatusBarNotification().getUser().getIdentifier());
|
||||
ActivityInfo info =
|
||||
intent.getIntent().resolveActivityInfo(packageManager, 0);
|
||||
return isResizableActivity(intent.getIntent(), packageManager, entry.getKey());
|
||||
}
|
||||
|
||||
static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) {
|
||||
if (intent == null) {
|
||||
Log.w(TAG, "Unable to send as bubble: " + key + " null intent");
|
||||
return false;
|
||||
}
|
||||
ActivityInfo info = intent.resolveActivityInfo(packageManager, 0);
|
||||
if (info == null) {
|
||||
Log.w(TAG, "Unable to send as bubble, "
|
||||
+ entry.getKey() + " couldn't find activity info for intent: "
|
||||
+ intent);
|
||||
Log.w(TAG, "Unable to send as bubble: " + key
|
||||
+ " couldn't find activity info for intent: " + intent);
|
||||
return false;
|
||||
}
|
||||
if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
|
||||
Log.w(TAG, "Unable to send as bubble, "
|
||||
+ entry.getKey() + " activity is not resizable for intent: "
|
||||
+ intent);
|
||||
Log.w(TAG, "Unable to send as bubble: " + key
|
||||
+ " activity is not resizable for intent: " + intent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1673,6 +1691,13 @@ public class BubbleController implements ConfigurationChangeListener {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showAppBubble(Intent intent) {
|
||||
mMainExecutor.execute(() -> {
|
||||
BubbleController.this.showAppBubble(intent);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleDismissalInterception(BubbleEntry entry,
|
||||
@Nullable List<BubbleEntry> children, IntConsumer removeCallback,
|
||||
@@ -1783,6 +1808,13 @@ public class BubbleController implements ConfigurationChangeListener {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBubbleBarEnabled(boolean enabled) {
|
||||
mMainExecutor.execute(() -> {
|
||||
BubbleController.this.setBubbleBarEnabled(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationPanelExpandedChanged(boolean expanded) {
|
||||
mMainExecutor.execute(
|
||||
|
||||
@@ -22,6 +22,7 @@ import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.os.UserHandle;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
@@ -107,6 +108,15 @@ public interface Bubbles {
|
||||
*/
|
||||
void expandStackAndSelectBubble(Bubble bubble);
|
||||
|
||||
/**
|
||||
* Adds and expands bubble that is not notification based, but instead based on an intent from
|
||||
* the app. The intent must be explicit (i.e. include a package name or fully qualified
|
||||
* component class name) and the activity for it should be resizable.
|
||||
*
|
||||
* @param intent the intent to populate the bubble.
|
||||
*/
|
||||
void showAppBubble(Intent intent);
|
||||
|
||||
/**
|
||||
* @return a bubble that matches the provided shortcutId, if one exists.
|
||||
*/
|
||||
@@ -232,6 +242,11 @@ public interface Bubbles {
|
||||
*/
|
||||
void onUserRemoved(int removedUserId);
|
||||
|
||||
/**
|
||||
* Sets whether bubble bar should be enabled or not.
|
||||
*/
|
||||
void setBubbleBarEnabled(boolean enabled);
|
||||
|
||||
/** Listener to find out about stack expansion / collapse events. */
|
||||
interface BubbleExpandListener {
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,6 @@ 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;
|
||||
@@ -65,8 +64,6 @@ 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;
|
||||
@@ -574,47 +571,6 @@ 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,
|
||||
Optional<BubbleController> bubbleController,
|
||||
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,
|
||||
bubbleController,
|
||||
windowManager,
|
||||
organizer,
|
||||
taskViewTransitions,
|
||||
mainExecutor,
|
||||
bgExecutor,
|
||||
syncQueue));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Starting window
|
||||
//
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.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);
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.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 static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS;
|
||||
|
||||
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.bubbles.BubbleController;
|
||||
import com.android.wm.shell.common.ExternalInterfaceBinder;
|
||||
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;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
public static final boolean SHOW_FLOATING_TASKS_AS_BUBBLES =
|
||||
SystemProperties.getBoolean("persist.wm.debug.floating_tasks_as_bubbles", 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 @Nullable BubbleController mBubbleController;
|
||||
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,
|
||||
Optional<BubbleController> bubbleController,
|
||||
WindowManager windowManager,
|
||||
ShellTaskOrganizer organizer,
|
||||
TaskViewTransitions transitions,
|
||||
@ShellMainThread ShellExecutor mainExecutor,
|
||||
@ShellBackgroundThread ShellExecutor bgExceutor,
|
||||
SyncTransactionQueue syncTransactionQueue) {
|
||||
mContext = context;
|
||||
mShellController = shellController;
|
||||
mShellCommandHandler = shellCommandHandler;
|
||||
mBubbleController = bubbleController.get();
|
||||
mWindowManager = windowManager;
|
||||
mTaskOrganizer = organizer;
|
||||
mTaskViewTransitions = transitions;
|
||||
mMainExecutor = mainExecutor;
|
||||
mBackgroundThread = bgExceutor;
|
||||
mSyncQueue = syncTransactionQueue;
|
||||
if (isFloatingTasksEnabled()) {
|
||||
shellInit.addInitCallback(this::onInit, this);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onInit() {
|
||||
mShellController.addConfigurationChangeListener(this);
|
||||
mShellController.addExternalInterface(KEY_EXTRA_SHELL_FLOATING_TASKS,
|
||||
this::createExternalInterface, this);
|
||||
mShellCommandHandler.addDumpCallback(this::dump, 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;
|
||||
}
|
||||
|
||||
private ExternalInterfaceBinder createExternalInterface() {
|
||||
return new IFloatingTasksImpl(this);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/** Returns true if the task was or should be shown as a bubble. */
|
||||
private boolean maybeShowTaskAsBubble(Intent intent) {
|
||||
if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubbleController != null) {
|
||||
removeFloatingLayer();
|
||||
if (intent.getPackage() != null) {
|
||||
mBubbleController.addAppBubble(intent);
|
||||
ProtoLog.d(WM_SHELL_FLOATING_APPS, "showing floating task as bubble: %s", intent);
|
||||
} else {
|
||||
ProtoLog.d(WM_SHELL_FLOATING_APPS,
|
||||
"failed to show floating task as bubble: %s; unknown package", intent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
if (maybeShowTaskAsBubble(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;
|
||||
if (maybeShowTaskAsBubble(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 {
|
||||
@Override
|
||||
public void showOrSetStashed(Intent intent) {
|
||||
mMainExecutor.execute(() -> FloatingTasksController.this.showOrSetStashed(intent));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for calls from outside the host process.
|
||||
*/
|
||||
@BinderThread
|
||||
private static class IFloatingTasksImpl extends IFloatingTasks.Stub
|
||||
implements ExternalInterfaceBinder {
|
||||
private FloatingTasksController mController;
|
||||
|
||||
IFloatingTasksImpl(FloatingTasksController controller) {
|
||||
mController = controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates this instance, preventing future calls from updating the controller.
|
||||
*/
|
||||
@Override
|
||||
public void invalidate() {
|
||||
mController = null;
|
||||
}
|
||||
|
||||
public void showTask(Intent intent) {
|
||||
executeRemoteCallWithTaskPermission(mController, "showTask",
|
||||
(controller) -> controller.showTask(intent));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.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;
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.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);
|
||||
}
|
||||
}
|
||||
@@ -1,687 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.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());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.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 com.android.wm.shell.sysui.ShellSharedConstants;
|
||||
|
||||
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;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 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),
|
||||
Optional.empty(),
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onInit_addExternalInterface() {
|
||||
if (FLOATING_TASKS_ACTUALLY_ENABLED) {
|
||||
createController();
|
||||
setUpTabletConfig();
|
||||
mController.onInit();
|
||||
|
||||
verify(mShellController, times(1)).addExternalInterface(
|
||||
ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS, any(), 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);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,6 @@ public abstract class SystemUIInitializer {
|
||||
.setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
|
||||
.setRecentTasks(mWMComponent.getRecentTasks())
|
||||
.setBackAnimation(mWMComponent.getBackAnimation())
|
||||
.setFloatingTasks(mWMComponent.getFloatingTasks())
|
||||
.setDesktopMode(mWMComponent.getDesktopMode());
|
||||
|
||||
// Only initialize when not starting from tests since this currently initializes some
|
||||
@@ -118,7 +117,6 @@ public abstract class SystemUIInitializer {
|
||||
.setStartingSurface(Optional.ofNullable(null))
|
||||
.setRecentTasks(Optional.ofNullable(null))
|
||||
.setBackAnimation(Optional.ofNullable(null))
|
||||
.setFloatingTasks(Optional.ofNullable(null))
|
||||
.setDesktopMode(Optional.ofNullable(null));
|
||||
}
|
||||
mSysUIComponent = builder.build();
|
||||
|
||||
@@ -40,7 +40,6 @@ import com.android.wm.shell.back.BackAnimation;
|
||||
import com.android.wm.shell.bubbles.Bubbles;
|
||||
import com.android.wm.shell.desktopmode.DesktopMode;
|
||||
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,9 +109,6 @@ public interface SysUIComponent {
|
||||
@BindsInstance
|
||||
Builder setBackAnimation(Optional<BackAnimation> b);
|
||||
|
||||
@BindsInstance
|
||||
Builder setFloatingTasks(Optional<FloatingTasks> f);
|
||||
|
||||
@BindsInstance
|
||||
Builder setDesktopMode(Optional<DesktopMode> d);
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.android.systemui.demomode.dagger.DemoModeModule;
|
||||
import com.android.systemui.doze.dagger.DozeComponent;
|
||||
import com.android.systemui.dreams.dagger.DreamModule;
|
||||
import com.android.systemui.dump.DumpManager;
|
||||
import com.android.systemui.flags.FeatureFlags;
|
||||
import com.android.systemui.flags.FlagsModule;
|
||||
import com.android.systemui.fragments.FragmentService;
|
||||
import com.android.systemui.keyguard.data.BouncerViewModule;
|
||||
@@ -242,6 +243,7 @@ public abstract class SystemUIModule {
|
||||
CommonNotifCollection notifCollection,
|
||||
NotifPipeline notifPipeline,
|
||||
SysUiState sysUiState,
|
||||
FeatureFlags featureFlags,
|
||||
@Main Executor sysuiMainExecutor) {
|
||||
return Optional.ofNullable(BubblesManager.create(context,
|
||||
bubblesOptional,
|
||||
@@ -258,6 +260,7 @@ public abstract class SystemUIModule {
|
||||
notifCollection,
|
||||
notifPipeline,
|
||||
sysUiState,
|
||||
featureFlags,
|
||||
sysuiMainExecutor));
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import com.android.wm.shell.dagger.WMShellModule;
|
||||
import com.android.wm.shell.dagger.WMSingleton;
|
||||
import com.android.wm.shell.desktopmode.DesktopMode;
|
||||
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;
|
||||
@@ -111,9 +110,6 @@ public interface WMComponent {
|
||||
@WMSingleton
|
||||
Optional<BackAnimation> getBackAnimation();
|
||||
|
||||
@WMSingleton
|
||||
Optional<FloatingTasks> getFloatingTasks();
|
||||
|
||||
/**
|
||||
* Optional {@link DesktopMode} component for interacting with desktop mode.
|
||||
*/
|
||||
|
||||
@@ -284,15 +284,6 @@ object Flags {
|
||||
@Keep
|
||||
val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false)
|
||||
|
||||
@JvmField
|
||||
@Keep
|
||||
val FLOATING_TASKS_ENABLED = SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false)
|
||||
|
||||
@JvmField
|
||||
@Keep
|
||||
val SHOW_FLOATING_TASKS_AS_BUBBLES =
|
||||
SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false)
|
||||
|
||||
@JvmField
|
||||
@Keep
|
||||
val ENABLE_FLING_TO_DISMISS_BUBBLE =
|
||||
@@ -308,6 +299,9 @@ object Flags {
|
||||
val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
|
||||
SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false)
|
||||
|
||||
// TODO(b/256873975): Tracking Bug
|
||||
@JvmField @Keep val WM_BUBBLE_BAR = UnreleasedFlag(1111)
|
||||
|
||||
// 1200 - predictive back
|
||||
@JvmField
|
||||
@Keep
|
||||
|
||||
@@ -22,7 +22,7 @@ import android.os.UserManager
|
||||
import android.view.KeyEvent
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.util.kotlin.getOrNull
|
||||
import com.android.wm.shell.floating.FloatingTasks
|
||||
import com.android.wm.shell.bubbles.Bubbles
|
||||
import java.util.Optional
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -39,7 +39,7 @@ internal class NoteTaskController
|
||||
constructor(
|
||||
private val context: Context,
|
||||
private val intentResolver: NoteTaskIntentResolver,
|
||||
private val optionalFloatingTasks: Optional<FloatingTasks>,
|
||||
private val optionalBubbles: Optional<Bubbles>,
|
||||
private val optionalKeyguardManager: Optional<KeyguardManager>,
|
||||
private val optionalUserManager: Optional<UserManager>,
|
||||
@NoteTaskEnabledKey private val isEnabled: Boolean,
|
||||
@@ -54,7 +54,7 @@ constructor(
|
||||
}
|
||||
|
||||
private fun showNoteTask() {
|
||||
val floatingTasks = optionalFloatingTasks.getOrNull() ?: return
|
||||
val bubbles = optionalBubbles.getOrNull() ?: return
|
||||
val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
|
||||
val userManager = optionalUserManager.getOrNull() ?: return
|
||||
val intent = intentResolver.resolveIntent() ?: return
|
||||
@@ -66,7 +66,7 @@ constructor(
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
// TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
|
||||
floatingTasks.showOrSetStashed(intent)
|
||||
bubbles.showAppBubble(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package com.android.systemui.notetask
|
||||
|
||||
import com.android.systemui.statusbar.CommandQueue
|
||||
import com.android.wm.shell.floating.FloatingTasks
|
||||
import com.android.wm.shell.bubbles.Bubbles
|
||||
import dagger.Lazy
|
||||
import java.util.Optional
|
||||
import javax.inject.Inject
|
||||
@@ -26,7 +26,7 @@ import javax.inject.Inject
|
||||
internal class NoteTaskInitializer
|
||||
@Inject
|
||||
constructor(
|
||||
private val optionalFloatingTasks: Optional<FloatingTasks>,
|
||||
private val optionalBubbles: Optional<Bubbles>,
|
||||
private val lazyNoteTaskController: Lazy<NoteTaskController>,
|
||||
private val commandQueue: CommandQueue,
|
||||
@NoteTaskEnabledKey private val isEnabled: Boolean,
|
||||
@@ -40,7 +40,7 @@ constructor(
|
||||
}
|
||||
|
||||
fun initialize() {
|
||||
if (isEnabled && optionalFloatingTasks.isPresent) {
|
||||
if (isEnabled && optionalBubbles.isPresent) {
|
||||
commandQueue.addCallback(callbacks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import static android.service.notification.NotificationListenerService.REASON_GR
|
||||
import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE;
|
||||
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
|
||||
|
||||
import static com.android.systemui.flags.Flags.WM_BUBBLE_BAR;
|
||||
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
|
||||
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
|
||||
|
||||
@@ -51,6 +52,7 @@ import androidx.annotation.Nullable;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.statusbar.IStatusBarService;
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
import com.android.systemui.flags.FeatureFlags;
|
||||
import com.android.systemui.model.SysUiState;
|
||||
import com.android.systemui.shade.ShadeController;
|
||||
import com.android.systemui.shared.system.QuickStepContract;
|
||||
@@ -129,6 +131,7 @@ public class BubblesManager {
|
||||
CommonNotifCollection notifCollection,
|
||||
NotifPipeline notifPipeline,
|
||||
SysUiState sysUiState,
|
||||
FeatureFlags featureFlags,
|
||||
Executor sysuiMainExecutor) {
|
||||
if (bubblesOptional.isPresent()) {
|
||||
return new BubblesManager(context,
|
||||
@@ -146,6 +149,7 @@ public class BubblesManager {
|
||||
notifCollection,
|
||||
notifPipeline,
|
||||
sysUiState,
|
||||
featureFlags,
|
||||
sysuiMainExecutor);
|
||||
} else {
|
||||
return null;
|
||||
@@ -168,6 +172,7 @@ public class BubblesManager {
|
||||
CommonNotifCollection notifCollection,
|
||||
NotifPipeline notifPipeline,
|
||||
SysUiState sysUiState,
|
||||
FeatureFlags featureFlags,
|
||||
Executor sysuiMainExecutor) {
|
||||
mContext = context;
|
||||
mBubbles = bubbles;
|
||||
@@ -352,6 +357,7 @@ public class BubblesManager {
|
||||
});
|
||||
}
|
||||
};
|
||||
mBubbles.setBubbleBarEnabled(featureFlags.isEnabled(WM_BUBBLE_BAR));
|
||||
mBubbles.setSysuiProxy(mSysuiProxy);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ import androidx.test.runner.AndroidJUnit4
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.android.wm.shell.floating.FloatingTasks
|
||||
import java.util.*
|
||||
import com.android.wm.shell.bubbles.Bubbles
|
||||
import java.util.Optional
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -49,8 +49,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
|
||||
@Mock lateinit var context: Context
|
||||
@Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
|
||||
@Mock lateinit var floatingTasks: FloatingTasks
|
||||
@Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks>
|
||||
@Mock lateinit var bubbles: Bubbles
|
||||
@Mock lateinit var optionalBubbles: Optional<Bubbles>
|
||||
@Mock lateinit var keyguardManager: KeyguardManager
|
||||
@Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
|
||||
@Mock lateinit var optionalUserManager: Optional<UserManager>
|
||||
@@ -61,7 +61,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
|
||||
whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks)
|
||||
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
|
||||
whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
|
||||
whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
|
||||
whenever(userManager.isUserUnlocked).thenReturn(true)
|
||||
@@ -71,7 +71,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
return NoteTaskController(
|
||||
context = context,
|
||||
intentResolver = noteTaskIntentResolver,
|
||||
optionalFloatingTasks = optionalFloatingTasks,
|
||||
optionalBubbles = optionalBubbles,
|
||||
optionalKeyguardManager = optionalKeyguardManager,
|
||||
optionalUserManager = optionalUserManager,
|
||||
isEnabled = isEnabled,
|
||||
@@ -85,16 +85,16 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
|
||||
|
||||
verify(context).startActivity(notesIntent)
|
||||
verify(floatingTasks, never()).showOrSetStashed(notesIntent)
|
||||
verify(bubbles, never()).showAppBubble(notesIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSystemKey_keyguardIsUnlocked_shouldStartFloatingTask() {
|
||||
fun handleSystemKey_keyguardIsUnlocked_shouldStartBubbles() {
|
||||
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
|
||||
|
||||
createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
|
||||
|
||||
verify(floatingTasks).showOrSetStashed(notesIntent)
|
||||
verify(bubbles).showAppBubble(notesIntent)
|
||||
verify(context, never()).startActivity(notesIntent)
|
||||
}
|
||||
|
||||
@@ -103,17 +103,17 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
|
||||
|
||||
verify(context, never()).startActivity(notesIntent)
|
||||
verify(floatingTasks, never()).showOrSetStashed(notesIntent)
|
||||
verify(bubbles, never()).showAppBubble(notesIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSystemKey_floatingTasksIsNull_shouldDoNothing() {
|
||||
whenever(optionalFloatingTasks.orElse(null)).thenReturn(null)
|
||||
fun handleSystemKey_bubblesIsNull_shouldDoNothing() {
|
||||
whenever(optionalBubbles.orElse(null)).thenReturn(null)
|
||||
|
||||
createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
|
||||
|
||||
verify(context, never()).startActivity(notesIntent)
|
||||
verify(floatingTasks, never()).showOrSetStashed(notesIntent)
|
||||
verify(bubbles, never()).showAppBubble(notesIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,7 +123,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
|
||||
|
||||
verify(context, never()).startActivity(notesIntent)
|
||||
verify(floatingTasks, never()).showOrSetStashed(notesIntent)
|
||||
verify(bubbles, never()).showAppBubble(notesIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -133,7 +133,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
|
||||
|
||||
verify(context, never()).startActivity(notesIntent)
|
||||
verify(floatingTasks, never()).showOrSetStashed(notesIntent)
|
||||
verify(bubbles, never()).showAppBubble(notesIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -143,7 +143,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
|
||||
|
||||
verify(context, never()).startActivity(notesIntent)
|
||||
verify(floatingTasks, never()).showOrSetStashed(notesIntent)
|
||||
verify(bubbles, never()).showAppBubble(notesIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -151,7 +151,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
|
||||
|
||||
verify(context, never()).startActivity(notesIntent)
|
||||
verify(floatingTasks, never()).showOrSetStashed(notesIntent)
|
||||
verify(bubbles, never()).showAppBubble(notesIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -161,6 +161,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
|
||||
|
||||
verify(context, never()).startActivity(notesIntent)
|
||||
verify(floatingTasks, never()).showOrSetStashed(notesIntent)
|
||||
verify(bubbles, never()).showAppBubble(notesIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.statusbar.CommandQueue
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.android.wm.shell.floating.FloatingTasks
|
||||
import java.util.*
|
||||
import com.android.wm.shell.bubbles.Bubbles
|
||||
import java.util.Optional
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -43,20 +43,20 @@ import org.mockito.MockitoAnnotations
|
||||
internal class NoteTaskInitializerTest : SysuiTestCase() {
|
||||
|
||||
@Mock lateinit var commandQueue: CommandQueue
|
||||
@Mock lateinit var floatingTasks: FloatingTasks
|
||||
@Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks>
|
||||
@Mock lateinit var bubbles: Bubbles
|
||||
@Mock lateinit var optionalBubbles: Optional<Bubbles>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
whenever(optionalFloatingTasks.isPresent).thenReturn(true)
|
||||
whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks)
|
||||
whenever(optionalBubbles.isPresent).thenReturn(true)
|
||||
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
|
||||
}
|
||||
|
||||
private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
|
||||
return NoteTaskInitializer(
|
||||
optionalFloatingTasks = optionalFloatingTasks,
|
||||
optionalBubbles = optionalBubbles,
|
||||
lazyNoteTaskController = mock(),
|
||||
commandQueue = commandQueue,
|
||||
isEnabled = isEnabled,
|
||||
@@ -78,8 +78,8 @@ internal class NoteTaskInitializerTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialize_floatingTasksNotPresent_shouldDoNothing() {
|
||||
whenever(optionalFloatingTasks.isPresent).thenReturn(false)
|
||||
fun initialize_bubblesNotPresent_shouldDoNothing() {
|
||||
whenever(optionalBubbles.isPresent).thenReturn(false)
|
||||
|
||||
createNoteTaskInitializer().initialize()
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.biometrics.AuthController;
|
||||
import com.android.systemui.colorextraction.SysuiColorExtractor;
|
||||
import com.android.systemui.dump.DumpManager;
|
||||
import com.android.systemui.flags.FeatureFlags;
|
||||
import com.android.systemui.keyguard.KeyguardViewMediator;
|
||||
import com.android.systemui.model.SysUiState;
|
||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||
@@ -394,6 +395,7 @@ public class BubblesTest extends SysuiTestCase {
|
||||
mCommonNotifCollection,
|
||||
mNotifPipeline,
|
||||
mSysUiState,
|
||||
mock(FeatureFlags.class),
|
||||
syncExecutor);
|
||||
mBubblesManager.addNotifCallback(mNotifCallback);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user