feat: Better At-a-Glance perceptive wallpaper colour luminance detection

This commit is contained in:
Pun Butrach
2026-01-10 20:49:46 +07:00
parent 19eb97e405
commit d36845897b
1639 changed files with 93907 additions and 146742 deletions
@@ -23,7 +23,7 @@ import android.os.Build;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.window.flags2.Flags;
import com.android.window.flags.Flags;
/**
* Constants for desktop mode feature
@@ -16,8 +16,6 @@
package com.android.wm.shell.shared;
import android.app.ActivityManager;
import com.android.wm.shell.shared.annotations.ExternalThread;
/**
@@ -33,6 +31,6 @@ public interface FocusTransitionListener {
/**
* Called when the per-app or system-wide focus state has changed for a task.
*/
default void onFocusedTaskChanged(ActivityManager.RunningTaskInfo taskInfo,
boolean isFocusedOnDisplay, boolean isFocusedGlobally) {}
default void onFocusedTaskChanged(int taskId, boolean isFocusedOnDisplay,
boolean isFocusedGlobally) {}
}
@@ -224,20 +224,19 @@ public class GroupedTaskInfo implements Parcelable {
/**
* Get primary {@link TaskInfo}.
* Nullable only if the group if TYPE_DESK, non-null for TYPE_FULLSCREEN and TYPE_SPLIT.
*
* @throws IllegalStateException if the group is TYPE_MIXED.
*/
@Nullable
@NonNull
public TaskInfo getTaskInfo1() {
if (mType == TYPE_MIXED) {
throw new IllegalStateException("No indexed tasks for a mixed task");
}
return CollectionsKt.firstOrNull(mTasks);
return mTasks.getFirst();
}
/**
* Get secondary {@link TaskInfo}, used primarily for TYPE_SPLIT, not null for TYPE_SPLIT.
* Get secondary {@link TaskInfo}, used primarily for TYPE_SPLIT.
*
* @throws IllegalStateException if the group is TYPE_MIXED.
*/
@@ -17,7 +17,6 @@
package com.android.wm.shell.shared;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.RemoteAnimationTarget.MODE_CHANGING;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -125,12 +124,6 @@ public class TransitionUtil {
return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR);
}
/** Returns `true` if `change` is a pinned Task. */
private static boolean isPipTask(TransitionInfo.Change change) {
return change.getTaskInfo() != null
&& change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED;
}
/** Returns `true` if `change` is an app's dim layer. */
public static boolean isDimLayer(TransitionInfo.Change change) {
return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER);
@@ -317,10 +310,9 @@ public class TransitionUtil {
// actual dim value).
t.setAlpha(change.getLeash(), 1.0f);
}
if (!isDividerBar(change) && !isPipTask(change)) {
// For certain components such as Divider and PiP, don't modify its inner leash
// position when creating the outer leash for the transition. In case the position
// being wrong after the transition finished.
if (!isDividerBar(change)) {
// For divider, don't modify its inner leash position when creating the outer leash
// for the transition. In case the position being wrong after the transition finished.
t.setPosition(change.getLeash(), 0, 0);
}
t.setLayer(change.getLeash(), 0);
@@ -23,12 +23,9 @@ import android.content.Context
import android.os.Handler
import android.view.Choreographer
import android.view.SurfaceControl.Transaction
import android.window.DesktopExperienceFlags
import android.window.TransitionInfo.Change
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW
import com.android.internal.jank.InteractionJankMonitor
import com.android.wm.shell.shared.animation.WindowAnimator.BoundsAnimationParams.AnimationBounds
import java.time.Duration
/** Creates minimization animation */
object MinimizeAnimator {
@@ -41,16 +38,6 @@ object MinimizeAnimator {
endOffsetYDp = 12f,
endScale = 0.97f,
interpolator = Interpolators.STANDARD_ACCELERATE,
animBounds = if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// In some cases, nav-back on the last desktop task may cause it to be reparented
// into a fullscreen TDA before being minimized back into a desk by
// [DesktopBackNavTransitionObserver]. The minimize animation would then occur when
// the task is still fullscreen, which means it should use the start bounds for the
// minimize animation.
AnimationBounds.START
} else {
AnimationBounds.END
}
)
/**
@@ -61,7 +48,6 @@ object MinimizeAnimator {
* @param animationHandler the Handler that the animation is running on.
*/
@JvmStatic
@JvmOverloads
fun create(
context: Context,
change: Change,
@@ -69,7 +55,6 @@ object MinimizeAnimator {
onAnimFinish: (Animator) -> Unit,
interactionJankMonitor: InteractionJankMonitor,
animationHandler: Handler,
startAnimDelay: Duration = Duration.ZERO,
): Animator {
val boundsAnimator = WindowAnimator.createBoundsAnimator(
context.resources.displayMetrics,
@@ -106,7 +91,6 @@ object MinimizeAnimator {
}
}
return AnimatorSet().apply {
startDelay = startAnimDelay.toMillis()
playTogether(boundsAnimator, alphaAnimator)
addListener(listener)
}
@@ -117,9 +117,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
/** End actions to run when all animations have completed. */
private val endActions = ArrayList<EndAction>()
/** End actions to run when all animations have completed or canceled. */
private val endOrCancelActions = ArrayList<EndAction>()
/** SpringConfig to use by default for properties whose springs were not provided. */
private var defaultSpring: SpringConfig = globalDefaultSpring
@@ -387,8 +384,7 @@ class PhysicsAnimator<T> private constructor (target: T) {
* Adds a listener that will be called when a property stops animating. This is useful if
* you care about a specific property ending, or want to use the end value/end velocity from a
* particular property's animation. If you just want to run an action when all property
* animations have ended, use [withEndActions]. If you want an action to run when all property
* animations have ended or canceled, use [withEndOrCancelActions].
* animations have ended, use [withEndActions].
*/
fun addEndListener(listener: EndListener<T>): PhysicsAnimator<T> {
endListeners.add(listener)
@@ -397,8 +393,8 @@ class PhysicsAnimator<T> private constructor (target: T) {
/**
* Adds end actions that will be run sequentially when animations for every property involved in
* this specific animation have ended (unless they were explicitly canceled, in which you should
* use [withEndOrCancelActions]). For example, if you call:
* this specific animation have ended (unless they were explicitly canceled). For example, if
* you call:
*
* animator
* .spring(TRANSLATION_X, ...)
@@ -428,9 +424,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
* access to the animation's end value/velocity, or you want to run these actions even if the
* animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param,
* which indicates that all relevant animations have ended.
*
* These actions run after those added via [addEndListener], and before actions added via
* [withEndOrCancelActions].
*/
fun withEndActions(vararg endActions: EndAction?): PhysicsAnimator<T> {
this.endActions.addAll(endActions.filterNotNull())
@@ -446,15 +439,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
return this
}
/**
* Like [withEndActions], but called if the animator is canceled as well. These actions are
* always run after those added via [addEndListener] and [withEndActions].
*/
fun withEndOrCancelActions(vararg endOrCancelActions: Runnable?): PhysicsAnimator<T> {
this.endOrCancelActions.addAll(endOrCancelActions.filterNotNull().map { it::run })
return this
}
fun setDefaultSpringConfig(defaultSpring: SpringConfig) {
this.defaultSpring = defaultSpring
}
@@ -597,8 +581,7 @@ class PhysicsAnimator<T> private constructor (target: T) {
getAnimatedProperties(),
ArrayList(updateListeners),
ArrayList(endListeners),
ArrayList(endActions),
ArrayList(endOrCancelActions)))
ArrayList(endActions)))
// Actually start the DynamicAnimations. This is delayed until after the InternalListener is
// constructed and added so that we don't miss the end listener firing for any animations
@@ -616,7 +599,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
updateListeners.clear()
endListeners.clear()
endActions.clear()
endOrCancelActions.clear()
}
/** Retrieves a spring animation for the given property, building one if needed. */
@@ -677,8 +659,7 @@ class PhysicsAnimator<T> private constructor (target: T) {
private var properties: Set<FloatPropertyCompat<in T>>,
private var updateListeners: List<UpdateListener<T>>,
private var endListeners: List<EndListener<T>>,
private var endActions: List<EndAction>,
private var endOrCancelActions: List<EndAction>
private var endActions: List<EndAction>
) {
/** The number of properties whose animations haven't ended. */
@@ -754,12 +735,9 @@ class PhysicsAnimator<T> private constructor (target: T) {
}
// If all of the animations that this listener cares about have ended, run the end
// actions
if (allEnded) {
if (!canceled) {
endActions.forEach { it() }
}
endOrCancelActions.forEach { it() }
// actions unless the animation was canceled.
if (allEnded && !canceled) {
endActions.forEach { it() }
}
return allEnded
@@ -807,8 +785,7 @@ class PhysicsAnimator<T> private constructor (target: T) {
* animator is under test.
*/
internal fun cancelInternal(properties: Set<FloatPropertyCompat<in T>>) {
val propertiesCopy = properties.toSet()
for (property in propertiesCopy) {
for (property in properties) {
flingAnimations[property]?.cancel()
springAnimations[property]?.cancel()
}
@@ -934,10 +911,8 @@ class PhysicsAnimator<T> private constructor (target: T) {
* to respond to specific property animations concluding (such as hiding a view when ALPHA
* ends, even if the corresponding TRANSLATION animations have not ended).
*
* If you just want to run an action when all of the property animations have ended (but not
* canceled), you can use [PhysicsAnimator.withEndActions]. If you need to run an action
* regardless of whether the animations were canceled, use
* [PhysicsAnimator.withEndOrCancelActions].
* If you just want to run an action when all of the property animations have ended, you can
* use [PhysicsAnimator.withEndActions].
*
* @param target The animated object itself.
* @param property The property whose animation has just ended.
@@ -26,8 +26,6 @@ import android.view.Choreographer
import android.view.SurfaceControl
import android.view.animation.Interpolator
import android.window.TransitionInfo
import com.android.wm.shell.shared.animation.WindowAnimator.BoundsAnimationParams.AnimationBounds.END
import com.android.wm.shell.shared.animation.WindowAnimator.BoundsAnimationParams.AnimationBounds.START
/** Creates animations that can be applied to windows/surfaces. */
object WindowAnimator {
@@ -40,10 +38,7 @@ object WindowAnimator {
val startScale: Float = 1f,
val endScale: Float = 1f,
val interpolator: Interpolator,
val animBounds: AnimationBounds = END,
) {
enum class AnimationBounds { START, END }
}
)
/**
* Creates an animator to reposition and scale the bounds of the leash of the given change.
@@ -59,14 +54,10 @@ object WindowAnimator {
change: TransitionInfo.Change,
transaction: SurfaceControl.Transaction,
): ValueAnimator {
val bounds = when (boundsAnimDef.animBounds) {
START -> change.startAbsBounds
END -> change.endAbsBounds
}
val startPos =
getPosition(
displayMetrics,
bounds,
change.endAbsBounds,
boundsAnimDef.startScale,
boundsAnimDef.startOffsetYDp,
)
@@ -74,7 +65,7 @@ object WindowAnimator {
val endPos =
getPosition(
displayMetrics,
bounds,
change.endAbsBounds,
boundsAnimDef.endScale,
boundsAnimDef.endOffsetYDp,
)
@@ -31,17 +31,6 @@ public class BubbleAnythingFlagHelper {
return enableBubbleAnything() || Flags.enableCreateAnyBubble();
}
/** Whether creating any bubble and force task excluded from recents are enabled. */
public static boolean enableCreateAnyBubbleWithForceExcludedFromRecents() {
return Flags.enableCreateAnyBubble()
&& com.android.window.flags2.Flags.excludeTaskFromRecents();
}
/** Whether creating any bubble and app compat fixes for bubbles are enabled. */
public static boolean enableCreateAnyBubbleWithAppCompatFixes() {
return Flags.enableCreateAnyBubble() && Flags.enableBubbleAppCompatFixes();
}
/**
* Whether creating any bubble and transforming to fullscreen, or the overall bubble anything
* feature is enabled.
@@ -52,26 +41,6 @@ public class BubbleAnythingFlagHelper {
&& Flags.enableCreateAnyBubble());
}
/** Whether creating a root task to manage the bubble tasks in the Core. */
public static boolean enableRootTaskForBubble() {
// This is needed to prevent tasks being hidden and re-parented to TDA when move-to-back.
if (!enableCreateAnyBubbleWithForceExcludedFromRecents()) {
return false;
}
// This is needed to allow the activity behind the root task remains in RESUMED state.
if (!com.android.window.flags2.Flags.enableSeeThroughTaskFragments()) {
return false;
}
// This is needed to allow the leaf task can be started in expected bounds.
if (!com.android.window.flags2.Flags.respectLeafTaskBounds()) {
return false;
}
return com.android.window.flags2.Flags.rootTaskForBubble();
}
/** Whether the overall bubble anything feature is enabled. */
public static boolean enableBubbleAnything() {
return Flags.enableBubbleAnything();
@@ -60,18 +60,6 @@ enum class BubbleBarLocation : Parcelable {
override fun newArray(size: Int) = arrayOfNulls<BubbleBarLocation>(size)
}
/**
* Checks whether locations are on the different sides from each other. If any of the
* locations is null returns false.
*/
fun isDifferentSides(
first: BubbleBarLocation?,
second: BubbleBarLocation?,
isRtl: Boolean
): Boolean {
return first != null && second != null && first.isOnLeft(isRtl) != second.isOnLeft(isRtl)
}
}
/** Define set of constants that allow to determine why location changed. */
@@ -19,6 +19,7 @@ package com.android.wm.shell.shared.bubbles
import android.content.Context
import android.view.View
import android.view.WindowManagerPolicyConstants
import com.android.internal.R
/** Simplifies accessing context fields. */
object ContextUtils {
@@ -26,7 +27,7 @@ object ContextUtils {
/** Gets navigation mode. */
@JvmStatic
val Context.navigationMode: Int
get() = resources.getInteger(com.android.internal.R.integer.config_navBarInteractionMode)
get() = resources.getInteger(R.integer.config_navBarInteractionMode)
/** Returns whether the navigation mode is gestures. */
@JvmStatic
@@ -55,13 +55,7 @@ class DismissView(context: Context) : FrameLayout(context) {
@DimenRes val targetSizeResId: Int,
/** dimen resource id of the icon size in the dismiss target */
@DimenRes val iconSizeResId: Int,
/**
* dimen resource id of the bottom margin for the dismiss target
*
* By default the margin is applied on top of the bottom navigation bar inset. To ignore
* the bottom navigation inset, and apply a margin relative to the bottom edge of the
* screen set [applyMarginOverNavBarInset] to `false`.
*/
/** dimen resource id of the bottom margin for the dismiss target */
@DimenRes var bottomMarginResId: Int,
/** dimen resource id of the height for dismiss area gradient */
@DimenRes val floatingGradientHeightResId: Int,
@@ -70,13 +64,7 @@ class DismissView(context: Context) : FrameLayout(context) {
/** drawable resource id of the dismiss target background */
@DrawableRes val backgroundResId: Int,
/** drawable resource id of the icon for the dismiss target */
@DrawableRes val iconResId: Int,
/**
* Whether the value provided in [bottomMarginResId] should be applied on top of the
* bottom navigation bar inset. If this is `false` the margin is relative to the bottom
* edge of the screen.
*/
val applyMarginOverNavBarInset: Boolean = true,
@DrawableRes val iconResId: Int
)
companion object {
@@ -107,9 +95,9 @@ class DismissView(context: Context) : FrameLayout(context) {
}
init {
clipToPadding = false
clipChildren = false
visibility = View.INVISIBLE
setClipToPadding(false)
setClipChildren(false)
setVisibility(View.INVISIBLE)
addView(circle)
}
@@ -147,12 +135,10 @@ class DismissView(context: Context) : FrameLayout(context) {
/**
* Animates this view in.
*
* @return `true` if the view was shown, `false` otherwise
*/
fun show(): Boolean {
if (isShowing) return false
val gradientDrawable = checkExists(gradientDrawable) ?: return false
fun show() {
if (isShowing) return
val gradientDrawable = checkExists(gradientDrawable) ?: return
isShowing = true
setVisibility(View.VISIBLE)
val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
@@ -164,7 +150,6 @@ class DismissView(context: Context) : FrameLayout(context) {
animator
.spring(DynamicAnimation.TRANSLATION_Y, 0f, spring)
.start()
return true
}
/**
@@ -224,16 +209,11 @@ class DismissView(context: Context) : FrameLayout(context) {
private fun updatePadding() {
val config = checkExists(config) ?: return
val bottomMargin = resources.getDimensionPixelSize(config.bottomMarginResId)
if (config.applyMarginOverNavBarInset) {
val insets: WindowInsets = wm.currentWindowMetrics.windowInsets
val navInset = insets.getInsetsIgnoringVisibility(
WindowInsets.Type.navigationBars()
)
setPadding(0, 0, 0, navInset.bottom + bottomMargin)
} else {
setPadding(0, 0, 0, bottomMargin)
}
val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets()
val navInset = insets.getInsetsIgnoringVisibility(
WindowInsets.Type.navigationBars())
setPadding(0, 0, 0, navInset.bottom +
resources.getDimensionPixelSize(config.bottomMarginResId))
}
/**
@@ -17,7 +17,6 @@
package com.android.wm.shell.shared.bubbles
import android.graphics.Rect
import kotlin.math.hypot
/**
* Represents an invisible area on the screen that determines what happens to a dragged object if it
@@ -31,78 +30,42 @@ import kotlin.math.hypot
sealed interface DragZone {
/** The bounds of this drag zone. */
val bounds: Bounds
val bounds: Rect
/** The bounds of the drop target associated with this drag zone. */
val dropTarget: DropTargetRect?
/** The bounds of the second drop target associated with this drag zone. */
val secondDropTarget: DropTargetRect?
val dropTarget: Rect?
fun contains(x: Int, y: Int) = bounds.contains(x, y)
sealed interface Bounds {
fun contains(x: Int, y: Int) =
when (this) {
is RectZone -> rect.contains(x, y)
is CircleZone -> hypot((x - this.x).toFloat(), (y - this.y).toFloat()) < radius
}
data class RectZone(val rect: Rect) : Bounds
data class CircleZone(val x: Int, val y: Int, val radius: Int) : Bounds
}
data class DropTargetRect(val rect: Rect, val cornerRadius: Float)
/** Represents the bubble drag area on the screen. */
sealed class Bubble(
override val bounds: Bounds.RectZone,
override val dropTarget: DropTargetRect?,
) : DragZone {
data class Left(
override val bounds: Bounds.RectZone,
override val dropTarget: DropTargetRect?,
override val secondDropTarget: DropTargetRect? = null,
) : Bubble(bounds, dropTarget)
sealed class Bubble(override val bounds: Rect, override val dropTarget: Rect) : DragZone {
data class Left(override val bounds: Rect, override val dropTarget: Rect) :
Bubble(bounds, dropTarget)
data class Right(
override val bounds: Bounds.RectZone,
override val dropTarget: DropTargetRect?,
override val secondDropTarget: DropTargetRect? = null,
) : Bubble(bounds, dropTarget)
data class Right(override val bounds: Rect, override val dropTarget: Rect) :
Bubble(bounds, dropTarget)
}
/** Represents dragging to Desktop Window. */
data class DesktopWindow(
override val bounds: Bounds.RectZone,
override val dropTarget: DropTargetRect,
override val secondDropTarget: DropTargetRect? = null,
) : DragZone
data class DesktopWindow(override val bounds: Rect, override val dropTarget: Rect) : DragZone
/** Represents dragging to Full Screen. */
data class FullScreen(
override val bounds: Bounds.RectZone,
override val dropTarget: DropTargetRect,
override val secondDropTarget: DropTargetRect? = null,
) : DragZone
data class FullScreen(override val bounds: Rect, override val dropTarget: Rect) : DragZone
/** Represents dragging to dismiss. */
data class Dismiss(override val bounds: Bounds.CircleZone) : DragZone {
override val dropTarget: DropTargetRect? = null
override val secondDropTarget: DropTargetRect? = null
data class Dismiss(override val bounds: Rect) : DragZone {
override val dropTarget: Rect? = null
}
/** Represents dragging to enter Split or replace a Split app. */
sealed class Split(override val bounds: Bounds.RectZone) : DragZone {
override val dropTarget: DropTargetRect? = null
override val secondDropTarget: DropTargetRect? = null
sealed class Split(override val bounds: Rect) : DragZone {
override val dropTarget: Rect? = null
data class Left(override val bounds: Bounds.RectZone) : Split(bounds)
data class Left(override val bounds: Rect) : Split(bounds)
data class Right(override val bounds: Bounds.RectZone) : Split(bounds)
data class Right(override val bounds: Rect) : Split(bounds)
data class Top(override val bounds: Bounds.RectZone) : Split(bounds)
data class Top(override val bounds: Rect) : Split(bounds)
data class Bottom(override val bounds: Bounds.RectZone) : Split(bounds)
data class Bottom(override val bounds: Rect) : Split(bounds)
}
}
@@ -20,9 +20,6 @@ import android.content.Context
import android.graphics.Rect
import android.util.TypedValue
import androidx.annotation.DimenRes
import com.android.wm.shell.shared.bubbles.DragZone.Bounds.CircleZone
import com.android.wm.shell.shared.bubbles.DragZone.Bounds.RectZone
import com.android.wm.shell.shared.bubbles.DragZone.DropTargetRect
import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
/** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */
@@ -31,14 +28,12 @@ class DragZoneFactory(
private val deviceConfig: DeviceConfig,
private val splitScreenModeChecker: SplitScreenModeChecker,
private val desktopWindowModeChecker: DesktopWindowModeChecker,
private val bubbleBarPropertiesProvider: BubbleBarPropertiesProvider,
) {
private val windowBounds: Rect
get() = deviceConfig.windowBounds
private var dismissDragZoneRadius = 0
private var dismissDragZoneBottomMargin = 0
private var dismissDragZoneSize = 0
private var bubbleDragZoneTabletSize = 0
private var bubbleDragZoneFoldableSize = 0
private var fullScreenDragZoneWidth = 0
@@ -47,7 +42,6 @@ class DragZoneFactory(
private var desktopWindowDragZoneHeight = 0
private var desktopWindowFromExpandedViewDragZoneWidth = 0
private var desktopWindowFromExpandedViewDragZoneHeight = 0
private var desktopWindowFromExpandedViewDragZoneYOffset = 0
private var splitFromBubbleDragZoneHeight = 0
private var splitFromBubbleDragZoneWidth = 0
private var hSplitFromExpandedViewDragZoneWidth = 0
@@ -63,98 +57,53 @@ class DragZoneFactory(
private var expandedViewDropTargetHeight = 0
private var expandedViewDropTargetPaddingBottom = 0
private var expandedViewDropTargetPaddingHorizontal = 0
private var bubbleBarDropTargetPaddingHorizontal = 0
private var dropTargetCornerRadius = 0f
private val fullScreenDropTarget: DropTargetRect
private val fullScreenDropTarget: Rect
get() =
DropTargetRect(
Rect(windowBounds).apply {
inset(fullScreenDropTargetPadding, fullScreenDropTargetPadding)
},
dropTargetCornerRadius
Rect(windowBounds).apply {
inset(fullScreenDropTargetPadding, fullScreenDropTargetPadding)
}
private val desktopWindowDropTarget: Rect
get() =
Rect(windowBounds).apply {
if (deviceConfig.isLandscape) {
inset(
/* dx= */ desktopWindowDropTargetPaddingLarge,
/* dy= */ desktopWindowDropTargetPaddingSmall
)
} else {
inset(
/* dx= */ desktopWindowDropTargetPaddingSmall,
/* dy= */ desktopWindowDropTargetPaddingLarge
)
}
}
private val expandedViewDropTargetLeft: Rect
get() =
Rect(
expandedViewDropTargetPaddingHorizontal,
windowBounds.bottom -
expandedViewDropTargetPaddingBottom -
expandedViewDropTargetHeight,
expandedViewDropTargetWidth + expandedViewDropTargetPaddingHorizontal,
windowBounds.bottom - expandedViewDropTargetPaddingBottom
)
private val desktopWindowDropTarget: DropTargetRect
private val expandedViewDropTargetRight: Rect
get() =
DropTargetRect(
Rect(windowBounds).apply {
if (deviceConfig.isLandscape) {
inset(
/* dx= */ desktopWindowDropTargetPaddingLarge,
/* dy= */ desktopWindowDropTargetPaddingSmall
)
} else {
inset(
/* dx= */ desktopWindowDropTargetPaddingSmall,
/* dy= */ desktopWindowDropTargetPaddingLarge
)
}
},
dropTargetCornerRadius
Rect(
windowBounds.right -
expandedViewDropTargetPaddingHorizontal -
expandedViewDropTargetWidth,
windowBounds.bottom -
expandedViewDropTargetPaddingBottom -
expandedViewDropTargetHeight,
windowBounds.right - expandedViewDropTargetPaddingHorizontal,
windowBounds.bottom - expandedViewDropTargetPaddingBottom
)
private val expandedViewDropTargetLeft: DropTargetRect
get() =
DropTargetRect(
Rect(
expandedViewDropTargetPaddingHorizontal,
windowBounds.bottom -
expandedViewDropTargetPaddingBottom -
expandedViewDropTargetHeight,
expandedViewDropTargetWidth + expandedViewDropTargetPaddingHorizontal,
windowBounds.bottom - expandedViewDropTargetPaddingBottom
),
dropTargetCornerRadius
)
private val expandedViewDropTargetRight: DropTargetRect
get() =
DropTargetRect(
Rect(
windowBounds.right -
expandedViewDropTargetPaddingHorizontal -
expandedViewDropTargetWidth,
windowBounds.bottom -
expandedViewDropTargetPaddingBottom -
expandedViewDropTargetHeight,
windowBounds.right - expandedViewDropTargetPaddingHorizontal,
windowBounds.bottom - expandedViewDropTargetPaddingBottom
),
dropTargetCornerRadius
)
private val bubbleBarDropTargetLeft: DropTargetRect
get() {
val rect =
Rect(
bubbleBarDropTargetPaddingHorizontal,
windowBounds.bottom -
bubbleBarPropertiesProvider.getBottomPadding() -
bubbleBarPropertiesProvider.getHeight(),
bubbleBarDropTargetPaddingHorizontal + bubbleBarPropertiesProvider.getWidth(),
windowBounds.bottom - bubbleBarPropertiesProvider.getBottomPadding()
)
return DropTargetRect(rect, rect.height() / 2f)
}
private val bubbleBarDropTargetRight: DropTargetRect
get() {
val rect =
Rect(
windowBounds.right -
bubbleBarDropTargetPaddingHorizontal -
bubbleBarPropertiesProvider.getWidth(),
windowBounds.bottom -
bubbleBarPropertiesProvider.getBottomPadding() -
bubbleBarPropertiesProvider.getHeight(),
windowBounds.right - bubbleBarDropTargetPaddingHorizontal,
windowBounds.bottom - bubbleBarPropertiesProvider.getBottomPadding()
)
return DropTargetRect(rect, rect.height() / 2f)
}
init {
onConfigurationUpdated()
}
@@ -163,8 +112,8 @@ class DragZoneFactory(
fun onConfigurationUpdated() {
// TODO b/396539130: Use the shared xml resources once we can easily access them from
// launcher
dismissDragZoneRadius = 96.dpToPx()
dismissDragZoneBottomMargin = 12.dpToPx()
dismissDragZoneSize =
if (deviceConfig.isSmallTablet) 140.dpToPx() else 200.dpToPx()
bubbleDragZoneTabletSize = 200.dpToPx()
bubbleDragZoneFoldableSize = 140.dpToPx()
fullScreenDragZoneWidth = 512.dpToPx()
@@ -173,7 +122,6 @@ class DragZoneFactory(
desktopWindowDragZoneHeight = 300.dpToPx()
desktopWindowFromExpandedViewDragZoneWidth = 200.dpToPx()
desktopWindowFromExpandedViewDragZoneHeight = 350.dpToPx()
desktopWindowFromExpandedViewDragZoneYOffset = 25.dpToPx()
splitFromBubbleDragZoneHeight = 100.dpToPx()
splitFromBubbleDragZoneWidth = 60.dpToPx()
hSplitFromExpandedViewDragZoneWidth = 60.dpToPx()
@@ -188,9 +136,6 @@ class DragZoneFactory(
expandedViewDropTargetHeight = 578.dpToPx()
expandedViewDropTargetPaddingBottom = 108.dpToPx()
expandedViewDropTargetPaddingHorizontal = 24.dpToPx()
bubbleBarDropTargetPaddingHorizontal = 24.dpToPx()
dropTargetCornerRadius = 28.dpToPx().toFloat()
}
private fun Context.resolveDimension(@DimenRes dimension: Int) =
@@ -215,7 +160,7 @@ class DragZoneFactory(
when (draggedObject) {
is DraggedObject.BubbleBar -> {
dragZones.add(createDismissDragZone())
dragZones.addAll(createBubbleHalfScreenDragZones(forBubbleBar = true))
dragZones.addAll(createBubbleHalfScreenDragZones())
}
is DraggedObject.Bubble -> {
dragZones.add(createDismissDragZone())
@@ -237,80 +182,65 @@ class DragZoneFactory(
} else {
dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet())
}
dragZones.addAll(createBubbleHalfScreenDragZones(forBubbleBar = false))
}
is DraggedObject.LauncherIcon -> {
val showDropTarget = draggedObject.showDropTarget
val showSecondDropTarget = !draggedObject.bubbleBarHasBubbles
dragZones.addAll(createBubbleCornerDragZones(showDropTarget, showSecondDropTarget))
dragZones.addAll(createBubbleHalfScreenDragZones())
}
}
return dragZones
}
fun getBubbleBarDropRect(isLeftSide: Boolean): Rect {
private fun createDismissDragZone(): DragZone {
return DragZone.Dismiss(
bounds =
Rect(
windowBounds.right / 2 - dismissDragZoneSize / 2,
windowBounds.bottom - dismissDragZoneSize,
windowBounds.right / 2 + dismissDragZoneSize / 2,
windowBounds.bottom
)
)
}
private fun createBubbleCornerDragZones(): List<DragZone> {
val dragZoneSize =
if (deviceConfig.isSmallTablet) {
bubbleDragZoneFoldableSize
} else {
bubbleDragZoneTabletSize
}
return Rect(
if (isLeftSide) 0 else windowBounds.right - dragZoneSize,
windowBounds.bottom - dragZoneSize,
if (isLeftSide) dragZoneSize else windowBounds.right,
windowBounds.bottom
)
}
private fun createDismissDragZone(): DragZone {
return DragZone.Dismiss(
bounds =
CircleZone(
x = windowBounds.right / 2,
y = windowBounds.bottom - dismissDragZoneBottomMargin - dismissDragZoneRadius,
radius = dismissDragZoneRadius
)
)
}
private fun createBubbleCornerDragZones(
showDropTarget: Boolean = true,
showSecondDropTarget: Boolean = false
): List<DragZone> {
return listOf(
DragZone.Bubble.Left(
bounds = RectZone(getBubbleBarDropRect(isLeftSide = true)),
dropTarget = if (showDropTarget) expandedViewDropTargetLeft else null,
secondDropTarget = if (showSecondDropTarget) bubbleBarDropTargetLeft else null
bounds =
Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom),
dropTarget = expandedViewDropTargetLeft,
),
DragZone.Bubble.Right(
bounds = RectZone(getBubbleBarDropRect(isLeftSide = false)),
dropTarget = if (showDropTarget) expandedViewDropTargetRight else null,
secondDropTarget = if (showSecondDropTarget) bubbleBarDropTargetRight else null
bounds =
Rect(
windowBounds.right - dragZoneSize,
windowBounds.bottom - dragZoneSize,
windowBounds.right,
windowBounds.bottom,
),
dropTarget = expandedViewDropTargetRight,
)
)
}
private fun createBubbleHalfScreenDragZones(forBubbleBar: Boolean): List<DragZone> {
private fun createBubbleHalfScreenDragZones(): List<DragZone> {
return listOf(
DragZone.Bubble.Left(
bounds = RectZone(Rect(0, 0, windowBounds.right / 2, windowBounds.bottom)),
dropTarget =
if (forBubbleBar) bubbleBarDropTargetLeft else expandedViewDropTargetLeft,
bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
dropTarget = expandedViewDropTargetLeft,
),
DragZone.Bubble.Right(
bounds =
RectZone(
Rect(
windowBounds.right / 2,
0,
windowBounds.right,
windowBounds.bottom,
),
Rect(
windowBounds.right / 2,
0,
windowBounds.right,
windowBounds.bottom,
),
dropTarget =
if (forBubbleBar) bubbleBarDropTargetRight else expandedViewDropTargetRight,
dropTarget = expandedViewDropTargetRight,
)
)
}
@@ -318,13 +248,11 @@ class DragZoneFactory(
private fun createFullScreenDragZone(): DragZone {
return DragZone.FullScreen(
bounds =
RectZone(
Rect(
windowBounds.right / 2 - fullScreenDragZoneWidth / 2,
0,
windowBounds.right / 2 + fullScreenDragZoneWidth / 2,
fullScreenDragZoneHeight
),
Rect(
windowBounds.right / 2 - fullScreenDragZoneWidth / 2,
0,
windowBounds.right / 2 + fullScreenDragZoneWidth / 2,
fullScreenDragZoneHeight
),
dropTarget = fullScreenDropTarget
)
@@ -337,22 +265,18 @@ class DragZoneFactory(
return DragZone.DesktopWindow(
bounds =
if (deviceConfig.isLandscape) {
RectZone(
Rect(
windowBounds.right / 2 - desktopWindowDragZoneWidth / 2,
windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
windowBounds.right / 2 + desktopWindowDragZoneWidth / 2,
windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
)
Rect(
windowBounds.right / 2 - desktopWindowDragZoneWidth / 2,
windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
windowBounds.right / 2 + desktopWindowDragZoneWidth / 2,
windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
)
} else {
RectZone(
Rect(
0,
windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
windowBounds.right,
windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
)
Rect(
0,
windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
windowBounds.right,
windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
)
},
dropTarget = desktopWindowDropTarget
@@ -362,15 +286,11 @@ class DragZoneFactory(
private fun createDesktopWindowDragZoneForExpandedView(): DragZone {
return DragZone.DesktopWindow(
bounds =
RectZone(
Rect(
windowBounds.right / 2 - desktopWindowFromExpandedViewDragZoneWidth / 2,
windowBounds.bottom / 2 - desktopWindowFromExpandedViewDragZoneHeight / 2 -
desktopWindowFromExpandedViewDragZoneYOffset,
windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2,
windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2 -
desktopWindowFromExpandedViewDragZoneYOffset
),
Rect(
windowBounds.right / 2 - desktopWindowFromExpandedViewDragZoneWidth / 2,
windowBounds.bottom / 2 - desktopWindowFromExpandedViewDragZoneHeight / 2,
windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2,
windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2
),
dropTarget = desktopWindowDropTarget
)
@@ -387,18 +307,15 @@ class DragZoneFactory(
SplitScreenMode.NONE ->
listOf(
DragZone.Split.Top(
bounds =
RectZone(Rect(0, 0, windowBounds.right, windowBounds.bottom / 2)),
bounds = Rect(0, 0, windowBounds.right, windowBounds.bottom / 2),
),
DragZone.Split.Bottom(
bounds =
RectZone(
Rect(
0,
windowBounds.bottom / 2,
windowBounds.right,
windowBounds.bottom
),
Rect(
0,
windowBounds.bottom / 2,
windowBounds.right,
windowBounds.bottom
),
)
)
@@ -406,24 +323,20 @@ class DragZoneFactory(
listOf(
DragZone.Split.Top(
bounds =
RectZone(
Rect(
0,
0,
windowBounds.right,
windowBounds.bottom - splitFromBubbleDragZoneHeight
),
Rect(
0,
0,
windowBounds.right,
windowBounds.bottom - splitFromBubbleDragZoneHeight
),
),
DragZone.Split.Bottom(
bounds =
RectZone(
Rect(
0,
windowBounds.bottom - splitFromBubbleDragZoneHeight,
windowBounds.right,
windowBounds.bottom
),
Rect(
0,
windowBounds.bottom - splitFromBubbleDragZoneHeight,
windowBounds.right,
windowBounds.bottom
),
)
)
@@ -431,20 +344,15 @@ class DragZoneFactory(
SplitScreenMode.SPLIT_10_90 -> {
listOf(
DragZone.Split.Top(
bounds =
RectZone(
Rect(0, 0, windowBounds.right, splitFromBubbleDragZoneHeight)
),
bounds = Rect(0, 0, windowBounds.right, splitFromBubbleDragZoneHeight),
),
DragZone.Split.Bottom(
bounds =
RectZone(
Rect(
0,
splitFromBubbleDragZoneHeight,
windowBounds.right,
windowBounds.bottom
),
Rect(
0,
splitFromBubbleDragZoneHeight,
windowBounds.right,
windowBounds.bottom
),
)
)
@@ -457,18 +365,15 @@ class DragZoneFactory(
SplitScreenMode.NONE ->
listOf(
DragZone.Split.Left(
bounds =
RectZone(Rect(0, 0, windowBounds.right / 2, windowBounds.bottom)),
bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
),
DragZone.Split.Right(
bounds =
RectZone(
Rect(
windowBounds.right / 2,
0,
windowBounds.right,
windowBounds.bottom
),
Rect(
windowBounds.right / 2,
0,
windowBounds.right,
windowBounds.bottom
),
)
)
@@ -476,44 +381,35 @@ class DragZoneFactory(
listOf(
DragZone.Split.Left(
bounds =
RectZone(
Rect(
0,
0,
windowBounds.right - splitFromBubbleDragZoneWidth,
windowBounds.bottom
),
Rect(
0,
0,
windowBounds.right - splitFromBubbleDragZoneWidth,
windowBounds.bottom
),
),
DragZone.Split.Right(
bounds =
RectZone(
Rect(
windowBounds.right - splitFromBubbleDragZoneWidth,
0,
windowBounds.right,
windowBounds.bottom
),
Rect(
windowBounds.right - splitFromBubbleDragZoneWidth,
0,
windowBounds.right,
windowBounds.bottom
),
)
)
SplitScreenMode.SPLIT_10_90 ->
listOf(
DragZone.Split.Left(
bounds =
RectZone(
Rect(0, 0, splitFromBubbleDragZoneWidth, windowBounds.bottom)
),
bounds = Rect(0, 0, splitFromBubbleDragZoneWidth, windowBounds.bottom),
),
DragZone.Split.Right(
bounds =
RectZone(
Rect(
splitFromBubbleDragZoneWidth,
0,
windowBounds.right,
windowBounds.bottom
),
Rect(
splitFromBubbleDragZoneWidth,
0,
windowBounds.right,
windowBounds.bottom
),
)
)
@@ -526,35 +422,27 @@ class DragZoneFactory(
createHorizontalSplitDragZonesForExpandedView()
} else {
// for tablets in portrait mode, split drag zones appear below the full screen drag zone
// for the top split zone. the bottom edge of the bottom split zone starts at the
// dismiss zone upper half circle to cover the area outside of the dismiss circle but
// within the split zone width. Both are horizontally centered.
// for the top split zone, and above the dismiss zone. Both are horizontally centered.
val splitZoneLeft = windowBounds.right / 2 - vSplitFromExpandedViewDragZoneWidth / 2
val splitZoneRight = splitZoneLeft + vSplitFromExpandedViewDragZoneWidth
val bottomSplitZoneBottom =
windowBounds.bottom - dismissDragZoneBottomMargin - dismissDragZoneRadius * 2
val bottomSplitZoneBottom = windowBounds.bottom - dismissDragZoneSize
listOf(
DragZone.Split.Top(
bounds =
RectZone(
Rect(
splitZoneLeft,
fullScreenDragZoneHeight,
splitZoneRight,
fullScreenDragZoneHeight +
vSplitFromExpandedViewDragZoneHeightTablet
),
Rect(
splitZoneLeft,
fullScreenDragZoneHeight,
splitZoneRight,
fullScreenDragZoneHeight + vSplitFromExpandedViewDragZoneHeightTablet
),
),
DragZone.Split.Bottom(
bounds =
RectZone(
Rect(
splitZoneLeft,
bottomSplitZoneBottom - vSplitFromExpandedViewDragZoneHeightTablet,
splitZoneRight,
bottomSplitZoneBottom
),
Rect(
splitZoneLeft,
bottomSplitZoneBottom - vSplitFromExpandedViewDragZoneHeightTablet,
splitZoneRight,
bottomSplitZoneBottom
),
)
)
@@ -572,26 +460,22 @@ class DragZoneFactory(
listOf(
DragZone.Split.Top(
bounds =
RectZone(
Rect(
splitZoneLeft,
fullScreenDragZoneHeight,
splitZoneLeft + fullScreenDragZoneWidth,
fullScreenDragZoneHeight +
vSplitFromExpandedViewDragZoneHeightFoldTall
),
Rect(
splitZoneLeft,
fullScreenDragZoneHeight,
splitZoneLeft + fullScreenDragZoneWidth,
fullScreenDragZoneHeight +
vSplitFromExpandedViewDragZoneHeightFoldTall
),
),
DragZone.Split.Bottom(
bounds =
RectZone(
Rect(
splitZoneLeft,
windowBounds.bottom / 2,
splitZoneLeft + fullScreenDragZoneWidth,
windowBounds.bottom / 2 +
vSplitFromExpandedViewDragZoneHeightFoldTall
),
Rect(
splitZoneLeft,
windowBounds.bottom / 2,
splitZoneLeft + fullScreenDragZoneWidth,
windowBounds.bottom / 2 +
vSplitFromExpandedViewDragZoneHeightFoldTall
),
)
)
@@ -599,25 +483,21 @@ class DragZoneFactory(
listOf(
DragZone.Split.Top(
bounds =
RectZone(
Rect(
0,
0,
windowBounds.right,
vSplitFromExpandedViewDragZoneHeightFoldShort
),
Rect(
0,
0,
windowBounds.right,
vSplitFromExpandedViewDragZoneHeightFoldShort
),
),
DragZone.Split.Bottom(
bounds =
RectZone(
Rect(
splitZoneLeft,
vSplitFromExpandedViewDragZoneHeightFoldShort,
splitZoneLeft + fullScreenDragZoneWidth,
vSplitFromExpandedViewDragZoneHeightFoldShort +
vSplitFromExpandedViewDragZoneHeightFoldTall
),
Rect(
splitZoneLeft,
vSplitFromExpandedViewDragZoneHeightFoldShort,
splitZoneLeft + fullScreenDragZoneWidth,
vSplitFromExpandedViewDragZoneHeightFoldShort +
vSplitFromExpandedViewDragZoneHeightFoldTall
),
)
)
@@ -625,26 +505,22 @@ class DragZoneFactory(
listOf(
DragZone.Split.Top(
bounds =
RectZone(
Rect(
splitZoneLeft,
fullScreenDragZoneHeight,
splitZoneLeft + fullScreenDragZoneWidth,
fullScreenDragZoneHeight +
vSplitFromExpandedViewDragZoneHeightFoldTall
),
Rect(
splitZoneLeft,
fullScreenDragZoneHeight,
splitZoneLeft + fullScreenDragZoneWidth,
fullScreenDragZoneHeight +
vSplitFromExpandedViewDragZoneHeightFoldTall
),
),
DragZone.Split.Bottom(
bounds =
RectZone(
Rect(
0,
windowBounds.bottom -
vSplitFromExpandedViewDragZoneHeightFoldShort,
windowBounds.right,
windowBounds.bottom
),
Rect(
0,
windowBounds.bottom -
vSplitFromExpandedViewDragZoneHeightFoldShort,
windowBounds.right,
windowBounds.bottom
),
)
)
@@ -658,20 +534,23 @@ class DragZoneFactory(
private fun createHorizontalSplitDragZonesForExpandedView(): List<DragZone> {
// horizontal split drag zones for expanded view appear on the edges of the screen from the
// top down until the dismiss drag zone height
val bottomY = windowBounds.bottom - dismissDragZoneBottomMargin - dismissDragZoneRadius * 2
return listOf(
DragZone.Split.Left(
bounds = RectZone(Rect(0, 0, hSplitFromExpandedViewDragZoneWidth, bottomY))
bounds =
Rect(
0,
0,
hSplitFromExpandedViewDragZoneWidth,
windowBounds.bottom - dismissDragZoneSize
),
),
DragZone.Split.Right(
bounds =
RectZone(
Rect(
windowBounds.right - hSplitFromExpandedViewDragZoneWidth,
0,
windowBounds.right,
bottomY
),
Rect(
windowBounds.right - hSplitFromExpandedViewDragZoneWidth,
0,
windowBounds.right,
windowBounds.bottom - dismissDragZoneSize
),
)
)
@@ -694,13 +573,4 @@ class DragZoneFactory(
fun interface DesktopWindowModeChecker {
fun isSupported(): Boolean
}
/** Bubble bar properties for generating a drop target. */
interface BubbleBarPropertiesProvider {
fun getHeight(): Int = 0
fun getWidth(): Int = 0
fun getBottomPadding(): Int = 0
}
}
@@ -18,17 +18,10 @@ package com.android.wm.shell.shared.bubbles
/** A Bubble object being dragged. */
sealed interface DraggedObject {
/** The initial location of the object at the start of the drag gesture. */
val initialLocation: BubbleBarLocation
data class Bubble(val initialLocation: BubbleBarLocation) : DraggedObject
data class BubbleBar(val initialLocation: BubbleBarLocation) : DraggedObject
data class ExpandedView(val initialLocation: BubbleBarLocation) : DraggedObject
// TODO(b/411505605) Remove onDropAction and move showDropTarget up
data class LauncherIcon(
val bubbleBarHasBubbles: Boolean,
val showDropTarget: Boolean = true,
val onDropAction: Runnable
) : DraggedObject
data class Bubble(override val initialLocation: BubbleBarLocation) : DraggedObject
data class BubbleBar(override val initialLocation: BubbleBarLocation) : DraggedObject
data class ExpandedView(override val initialLocation: BubbleBarLocation) : DraggedObject
}
@@ -17,20 +17,14 @@
package com.android.wm.shell.shared.bubbles
import android.content.Context
import android.graphics.Rect
import android.graphics.RectF
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import androidx.core.animation.Animator
import androidx.core.animation.AnimatorListenerAdapter
import androidx.core.animation.ValueAnimator
import com.android.wm.shell.shared.bubbles.DragZone.DropTargetRect
import com.android.wm.shell.shared.bubbles.DraggedObject.Bubble
import com.android.wm.shell.shared.bubbles.DraggedObject.BubbleBar
import com.android.wm.shell.shared.bubbles.DraggedObject.ExpandedView
import com.android.wm.shell.shared.bubbles.DraggedObject.LauncherIcon
import com.android.wm.shell.R
/**
* Manages animating drop targets in response to dragging bubble icons or bubble expanded views
@@ -43,19 +37,15 @@ class DropTargetManager(
) {
private var state: DragState? = null
@VisibleForTesting val dropTargetView = DropTargetView(context)
@VisibleForTesting var secondDropTargetView: DropTargetView? = null
private val dropTargetView = DropTargetView(context)
private var animator: ValueAnimator? = null
private var morphRect: RectF = RectF(0f, 0f, 0f, 0f)
private val isLayoutRtl = container.isLayoutRtl
private val viewAnimatorsMap = mutableMapOf<View, ValueAnimator>()
private var onDropTargetsRemovedAction: Runnable? = null
private companion object {
const val MORPH_ANIM_DURATION = 250L
const val DROP_TARGET_ALPHA_IN_DURATION = 150L
const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
const val DROP_TARGET_ELEVATION_DP = 2f
}
/** Must be called when a drag gesture is starting. */
@@ -63,38 +53,22 @@ class DropTargetManager(
val state = DragState(dragZones, draggedObject)
dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone)
this.state = state
viewAnimatorsMap.values.forEach { it.cancel() }
setupDropTarget(dropTargetView)
if (dragZones.any { it.secondDropTarget != null }) {
secondDropTargetView = secondDropTargetView ?: DropTargetView(context)
setupDropTarget(secondDropTargetView)
} else {
secondDropTargetView?.let { container.removeView(it) }
secondDropTargetView = null
}
animator?.cancel()
setupDropTarget()
}
private fun setupDropTarget(view: View?) {
if (view == null) return
if (view.parent != null) container.removeView(view)
container.addView(view, 0)
view.alpha = 0f
view.elevation = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
DROP_TARGET_ELEVATION_DP, context.resources.displayMetrics
)
private fun setupDropTarget() {
if (dropTargetView.parent != null) container.removeView(dropTargetView)
container.addView(dropTargetView, 0)
dropTargetView.alpha = 0f
dropTargetView.elevation = context.resources.getDimension(R.dimen.drop_target_elevation)
// Match parent and the target is drawn within the view
view.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
dropTargetView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
/**
* Called when the user drags to a new location.
*
* @return DragZone that matches provided x and y coordinates.
*/
fun onDragUpdated(x: Int, y: Int): DragZone? {
val state = state ?: return null
/** Called when the user drags to a new location. */
fun onDragUpdated(x: Int, y: Int) {
val state = state ?: return
val oldDragZone = state.currentDragZone
val newDragZone = state.getMatchingDragZone(x = x, y = y)
state.currentDragZone = newDragZone
@@ -102,156 +76,95 @@ class DropTargetManager(
dragZoneChangedListener.onDragZoneChanged(
draggedObject = state.draggedObject,
from = oldDragZone,
to = newDragZone,
to = newDragZone
)
updateDropTarget()
}
return newDragZone
}
/** Called when the drag ended. */
fun onDragEnded() {
val dropState = state ?: return
startFadeAnimation(dropTargetView, to = 0f) {
startFadeAnimation(from = dropTargetView.alpha, to = 0f) {
container.removeView(dropTargetView)
onDropTargetRemoved()
}
startFadeAnimation(secondDropTargetView, to = 0f) {
container.removeView(secondDropTargetView)
secondDropTargetView = null
onDropTargetRemoved()
}
dragZoneChangedListener.onDragEnded(dropState.currentDragZone)
state = null
}
/**
* Runs the provided action once all drop target views are removed from the container.
* If there are no drop target views currently present or being animated, the action will be
* executed immediately.
*/
fun onDropTargetRemoved(action: Runnable) {
onDropTargetsRemovedAction = action
onDropTargetRemoved()
}
private fun updateDropTarget() {
val dropState = state ?: return
val currentDragZone = dropState.currentDragZone
if (currentDragZone == null) {
startFadeAnimation(dropTargetView, to = 0f)
startFadeAnimation(secondDropTargetView, to = 0f)
return
}
val dropTargetRect = currentDragZone.dropTarget
val currentDragZone = state?.currentDragZone ?: return
val dropTargetBounds = currentDragZone.dropTarget
when {
dropTargetRect == null -> startFadeAnimation(dropTargetView, to = 0f)
dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f)
dropTargetView.alpha == 0f -> {
dropTargetView.update(RectF(dropTargetRect.rect), dropTargetRect.cornerRadius)
startFadeAnimation(dropTargetView, to = 1f)
}
else -> startMorphAnimation(dropTargetRect)
}
val secondDropTargetRect = currentDragZone.secondDropTarget
when {
secondDropTargetRect == null -> startFadeAnimation(secondDropTargetView, to = 0f)
else -> {
val secondDropTargetView = secondDropTargetView ?: return
secondDropTargetView.update(
RectF(secondDropTargetRect.rect),
secondDropTargetRect.cornerRadius,
)
startFadeAnimation(secondDropTargetView, to = 1f)
dropTargetView.update(RectF(dropTargetBounds))
startFadeAnimation(from = 0f, to = 1f)
}
else -> startMorphAnimation(dropTargetBounds)
}
}
private fun startFadeAnimation(view: View?, to: Float, onEnd: (() -> Unit)? = null) {
if (view == null) return
val from = view.alpha
viewAnimatorsMap[view]?.cancel()
private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) {
animator?.cancel()
val duration =
if (from < to) DROP_TARGET_ALPHA_IN_DURATION else DROP_TARGET_ALPHA_OUT_DURATION
val animator = ValueAnimator.ofFloat(from, to).setDuration(duration)
animator.addUpdateListener { _ -> view.alpha = animator.animatedValue as Float }
animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float }
if (onEnd != null) {
animator.doOnEnd(onEnd)
}
viewAnimatorsMap[view] = animator
this.animator = animator
animator.start()
}
private fun startMorphAnimation(dropTargetRect: DropTargetRect) {
viewAnimatorsMap[dropTargetView]?.cancel()
private fun startMorphAnimation(endBounds: Rect) {
animator?.cancel()
val startAlpha = dropTargetView.alpha
val startRect = dropTargetView.getRect()
val endRect = dropTargetRect.rect
val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(MORPH_ANIM_DURATION)
animator.addUpdateListener { _ ->
val fraction = animator.animatedValue as Float
dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction
morphRect.left = (startRect.left + (endRect.left - startRect.left) * fraction)
morphRect.top = (startRect.top + (endRect.top - startRect.top) * fraction)
morphRect.right = (startRect.right + (endRect.right - startRect.right) * fraction)
morphRect.bottom = (startRect.bottom + (endRect.bottom - startRect.bottom) * fraction)
dropTargetView.update(morphRect, dropTargetRect.cornerRadius)
morphRect.left = (startRect.left + (endBounds.left - startRect.left) * fraction)
morphRect.top = (startRect.top + (endBounds.top - startRect.top) * fraction)
morphRect.right = (startRect.right + (endBounds.right - startRect.right) * fraction)
morphRect.bottom = (startRect.bottom + (endBounds.bottom - startRect.bottom) * fraction)
dropTargetView.update(morphRect)
}
viewAnimatorsMap[dropTargetView] = animator
this.animator = animator
animator.start()
}
private fun onDropTargetRemoved() {
val action = onDropTargetsRemovedAction ?: return
if ((0 until container.childCount).any { container.getChildAt(it) is DropTargetView }) {
return
}
onDropTargetsRemovedAction = null
action.run()
}
/** Stores the current drag state. */
private inner class DragState(
private val dragZones: List<DragZone>,
val draggedObject: DraggedObject,
val draggedObject: DraggedObject
) {
val initialDragZone =
draggedObject.initialLocation?.let {
if (it.isOnLeft(isLayoutRtl)) {
dragZones.filterIsInstance<DragZone.Bubble.Left>().first()
} else {
dragZones.filterIsInstance<DragZone.Bubble.Right>().first()
}
if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) {
dragZones.filterIsInstance<DragZone.Bubble.Left>().first()
} else {
dragZones.filterIsInstance<DragZone.Bubble.Right>().first()
}
var currentDragZone: DragZone? = initialDragZone
var currentDragZone: DragZone = initialDragZone
fun getMatchingDragZone(x: Int, y: Int): DragZone? {
return dragZones.firstOrNull { it.contains(x, y) }
fun getMatchingDragZone(x: Int, y: Int): DragZone {
return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone
}
}
private val DraggedObject.initialLocation: BubbleBarLocation?
get() =
when (this) {
is Bubble -> initialLocation
is BubbleBar -> initialLocation
is ExpandedView -> initialLocation
is LauncherIcon -> null
}
/** An interface to be notified when drag zones change. */
interface DragZoneChangedListener {
/** An initial drag zone was set. Called when a drag starts. */
fun onInitialDragZoneSet(dragZone: DragZone?)
fun onInitialDragZoneSet(dragZone: DragZone)
/** Called when the object was dragged to a different drag zone. */
fun onDragZoneChanged(draggedObject: DraggedObject, from: DragZone?, to: DragZone?)
fun onDragZoneChanged(draggedObject: DraggedObject, from: DragZone, to: DragZone)
/** Called when the drag has ended with the zone it ended in. */
fun onDragEnded(zone: DragZone?)
fun onDragEnded(zone: DragZone)
}
private fun Animator.doOnEnd(onEnd: () -> Unit) {
@@ -23,34 +23,34 @@ import android.graphics.RectF
import android.util.TypedValue
import android.view.View
/** Shows a drop target within this view. */
/**
* Shows a drop target within this view.
*/
class DropTargetView(context: Context) : View(context) {
private val rectPaint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(com.android.internal.R.color.materialColorPrimaryFixed)
style = Paint.Style.FILL
alpha = (0.35f * 255).toInt()
}
private val rectPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(com.android.internal.R.color.materialColorPrimaryFixed)
style = Paint.Style.FILL
alpha = (0.35f * 255).toInt()
}
private val strokePaint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(com.android.internal.R.color.materialColorPrimaryFixed)
style = Paint.Style.STROKE
strokeWidth = 2.dpToPx()
}
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(com.android.internal.R.color.materialColorPrimaryFixed)
style = Paint.Style.STROKE
strokeWidth = 2.dpToPx()
}
private var cornerRadius = 0f
private val cornerRadius = 28.dpToPx()
private val rect = RectF(0f, 0f, 0f, 0f)
// TODO b/396539130: Use shared xml resources once we can access them in launcher
private fun Int.dpToPx() =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
context.resources.displayMetrics
)
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
context.resources.displayMetrics
)
override fun onDraw(canvas: Canvas) {
canvas.save()
@@ -59,8 +59,7 @@ class DropTargetView(context: Context) : View(context) {
canvas.restore()
}
fun update(positionRect: RectF, cornerRadius: Float) {
this.cornerRadius = cornerRadius
fun update(positionRect: RectF) {
rect.set(positionRect)
invalidate()
}
@@ -2,4 +2,5 @@
madym@google.com
atsjenk@google.com
liranb@google.com
sukeshram@google.com
mpodolian@google.com
@@ -20,7 +20,6 @@ import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.app.TaskInfo
import android.content.Context
import android.content.pm.PackageManager
import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
import com.android.internal.R
import com.android.internal.policy.DesktopModeCompatUtils
@@ -44,56 +43,28 @@ class DesktopModeCompatPolicy(private val context: Context) {
/**
* If the top activity should be exempt from desktop windowing and forced back to fullscreen.
* Currently includes all system ui, default home and transparent stack activities with the
* relevant permission or signature. However if the top activity is not being displayed,
* regardless of its configuration, we will not exempt it as to remain in the desktop windowing
* environment.
* Currently includes all system ui, default home and transparent stack activities. However if
* the top activity is not being displayed, regardless of its configuration, we will not exempt
* it as to remain in the desktop windowing environment.
*/
fun isTopActivityExemptFromDesktopWindowing(task: TaskInfo): Boolean {
val packageName = task.baseActivity?.packageName ?: return false
fun isTopActivityExemptFromDesktopWindowing(task: TaskInfo) =
isTopActivityExemptFromDesktopWindowing(task.baseActivity?.packageName,
task.numActivities, task.isTopActivityNoDisplay, task.isActivityStackTransparent,
task.userId)
return when {
!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue -> false
// If activity is not being displayed, window mode change has no visual affect so leave
// unchanged.
task.isTopActivityNoDisplay -> false
// If activity belongs to system ui package, safe to force out of desktop.
isSystemUiTask(packageName) -> true
// If activity belongs to default home package, safe to force out of desktop.
isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) -> true
// If all activities in task stack are transparent AND package has the relevant
// fullscreen transparent permission OR is signed with platform key, safe to force out
// of desktop.
isTransparentTask(task.isActivityStackTransparent, task.numActivities) &&
(hasFullscreenTransparentPermission(packageName, task.userId) ||
hasPlatformSignature(task)) -> true
else -> false
}
}
fun shouldDisableDesktopEntryPoints(task: TaskInfo) = shouldDisableDesktopEntryPoints(
task.baseActivity?.packageName, task.numActivities, task.isTopActivityNoDisplay,
task.isActivityStackTransparent)
fun shouldDisableDesktopEntryPoints(
fun isTopActivityExemptFromDesktopWindowing(
packageName: String?,
numActivities: Int,
isTopActivityNoDisplay: Boolean,
isActivityStackTransparent: Boolean,
) = when {
// Activity will not be displayed, no need to show desktop entry point.
isTopActivityNoDisplay -> true
// If activity belongs to system ui package, hide desktop entry point.
isSystemUiTask(packageName) -> true
// If activity belongs to default home package, safe to force out of desktop.
isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) -> true
// If all activities in task stack are transparent AND package has the relevant fullscreen
// transparent permission, safe to force out of desktop.
isTransparentTask(isActivityStackTransparent, numActivities) -> true
else -> false
}
userId: Int
) =
DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue &&
((isSystemUiTask(packageName) ||
isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) ||
(isTransparentTask(isActivityStackTransparent, numActivities) &&
hasFullscreenTransparentPermission(packageName, userId))) &&
!isTopActivityNoDisplay)
/** @see DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds */
fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean =
@@ -115,8 +86,11 @@ class DesktopModeCompatPolicy(private val context: Context) {
private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage
// Checks if the app for the given package has the SYSTEM_ALERT_WINDOW permission.
private fun hasFullscreenTransparentPermission(packageName: String, userId: Int): Boolean {
private fun hasFullscreenTransparentPermission(packageName: String?, userId: Int): Boolean {
if (DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue) {
if (packageName == null) {
return false
}
return packageInfoCache.getOrPut("$userId@$packageName") {
try {
val packageInfo = pkgManager.getPackageInfoAsUser(
@@ -130,19 +104,8 @@ class DesktopModeCompatPolicy(private val context: Context) {
}
}
}
// If the ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS flag is disabled, make neutral condition
// dependant on the ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE flag.
return !DesktopExperienceFlags.ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE.isTrue
}
// Checks if the app is signed with the platform signature.
private fun hasPlatformSignature(task: TaskInfo): Boolean {
if (DesktopExperienceFlags.ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE.isTrue) {
return task.topActivityInfo?.applicationInfo?.isSignedWithPlatformKey ?: false
}
// If the ENABLE_MODALS_FULLSCREEN_WITH_PLATFORM_SIGNATURE flag is disabled, make neutral
// condition dependant on the ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS flag.
return !DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue
// If the flag is disabled we make this condition neutral.
return true
}
/**
@@ -17,11 +17,14 @@
package com.android.wm.shell.shared.desktopmode;
import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE;
import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement;
import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.app.TaskInfo;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.SystemProperties;
@@ -32,21 +35,48 @@ import android.window.DesktopModeFlags;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.window.flags2.Flags;
import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.util.Arrays;
/**
* Constants for desktop mode feature
*
* @deprecated Use {@link DesktopState} or {@link DesktopConfig} instead.
*/
@Deprecated(forRemoval = true)
// TODO(b/237575897): Move this file to the `com.android.wm.shell.shared.desktopmode` package
public class DesktopModeStatus {
private static final String TAG = "DesktopModeStatus";
@Nullable
private static Boolean sIsLargeScreenDevice = null;
/**
* Flag to indicate whether task resizing is veiled.
*/
private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean(
"persist.wm.debug.desktop_veiled_resizing", true);
/**
* Flag to indicate is moving task to another display is enabled.
*/
public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean(
"persist.wm.debug.desktop_change_display", false);
/**
* Flag to indicate whether to apply shadows to windows in desktop mode.
*/
private static final boolean USE_WINDOW_SHADOWS = SystemProperties.getBoolean(
"persist.wm.debug.desktop_use_window_shadows", true);
/**
* Flag to indicate whether to apply shadows to the focused window in desktop mode.
*
* Note: this flag is only relevant if USE_WINDOW_SHADOWS is false.
*/
private static final boolean USE_WINDOW_SHADOWS_FOCUSED_WINDOW = SystemProperties.getBoolean(
"persist.wm.debug.desktop_use_window_shadows_focused_window", false);
/**
* Flag to indicate whether to use rounded corners for windows in desktop mode.
*/
@@ -59,6 +89,27 @@ public class DesktopModeStatus {
private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
"persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
private static final boolean USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS =
SystemProperties.getBoolean(
"persist.wm.debug.use_app_to_web_build_time_generic_links", true);
/** Whether the desktop density override is enabled. */
public static final boolean DESKTOP_DENSITY_OVERRIDE_ENABLED =
SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false);
/** Override density for tasks when they're inside the desktop. */
public static final int DESKTOP_DENSITY_OVERRIDE =
SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284);
/** The minimum override density allowed for tasks inside the desktop. */
private static final int DESKTOP_DENSITY_MIN = 100;
/** The maximum override density allowed for tasks inside the desktop. */
private static final int DESKTOP_DENSITY_MAX = 1000;
/** The number of [WindowDecorViewHost] instances to warm up on system start. */
private static final int WINDOW_DECOR_PRE_WARM_SIZE = 2;
/**
* Sysprop declaring whether to enters desktop mode by default when the windowing mode of the
* display's root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM.
@@ -69,6 +120,51 @@ public class DesktopModeStatus {
public static final String ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP =
"persist.wm.debug.enter_desktop_by_default_on_freeform_display";
/**
* Sysprop declaring whether to enable drag-to-maximize for desktop windows.
*
* <p>If it is not defined, then {@code R.integer.config_dragToMaximizeInDesktopMode}
* is used.
*/
public static final String ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP =
"persist.wm.debug.enable_drag_to_maximize";
/**
* Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time.
*
* <p>If it is not defined, then {@code R.integer.config_maxDesktopWindowingActiveTasks} is
* used.
*
* <p>The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen
* recording window, or Bluetooth pairing window).
*/
private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit";
/**
* Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system start.
*
* <p>If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used.
*/
private static final String WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP =
"persist.wm.debug.desktop_window_decor_pre_warm_size";
/**
* Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
*/
public static boolean isVeiledResizeEnabled() {
return IS_VEILED_RESIZE_ENABLED;
}
/**
* Return whether to use window shadows.
*
* @param isFocusedWindow whether the window to apply shadows to is focused
*/
public static boolean useWindowShadow(boolean isFocusedWindow) {
return USE_WINDOW_SHADOWS
|| (USE_WINDOW_SHADOWS_FOCUSED_WINDOW && isFocusedWindow);
}
/**
* Return whether to use rounded corners for windows.
*/
@@ -84,6 +180,35 @@ public class DesktopModeStatus {
return ENFORCE_DEVICE_RESTRICTIONS;
}
/**
* Return the maximum limit on the number of Tasks to show in Desktop Mode at any one time.
*/
public static int getMaxTaskLimit(@NonNull Context context) {
return SystemProperties.getInt(MAX_TASK_LIMIT_SYS_PROP,
context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks));
}
/**
* Return the maximum size of the window decoration surface control view host pool, or zero if
* there should be no pooling.
*/
public static int getWindowDecorScvhPoolSize(@NonNull Context context) {
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SCVH_CACHE.isTrue()) return 0;
final int maxTaskLimit = getMaxTaskLimit(context);
if (maxTaskLimit > 0) {
return maxTaskLimit;
}
// TODO: b/368032552 - task limit equal to 0 means unlimited. Figure out what the pool
// size should be in that case.
return 0;
}
/** The number of [WindowDecorViewHost] instances to warm up on system start. */
public static int getWindowDecorPreWarmSize() {
return SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP,
WINDOW_DECOR_PRE_WARM_SIZE);
}
/**
* Return {@code true} if the current device supports desktop mode.
*/
@@ -119,7 +244,7 @@ public class DesktopModeStatus {
*/
public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) {
return Flags.showDesktopExperienceDevOption()
&& isDeviceEligibleForDesktopExperienceDevOption(context);
&& isDeviceEligibleForDesktopMode(context);
}
/** Returns if desktop mode dev option should be enabled if there is no user override. */
@@ -132,20 +257,28 @@ public class DesktopModeStatus {
* Return {@code true} if desktop mode is enabled and can be entered on the current device.
*/
public static boolean canEnterDesktopMode(@NonNull Context context) {
boolean isEligibleForDesktopMode = isDeviceEligibleForDesktopMode(context) && (
DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue()
|| canInternalDisplayHostDesktops(context));
boolean desktopModeEnabled =
isEligibleForDesktopMode && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue();
return desktopModeEnabled || isDesktopModeEnabledByDevOption(context);
try {
return (isDeviceEligibleForDesktopMode(context)
&& DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue())
|| isDesktopModeEnabledByDevOption(context);
} catch (Throwable e) {
// Lawnchair-TODO-Postmerge: All of the LC-Ignored MAY be only accessible to newer APIs.
// LC-Ignored
return false;
}
}
/**
* Check if Desktop mode should be enabled because the dev option is shown and enabled.
*/
private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) {
return DesktopModeFlags.isDesktopModeForcedEnabled()
try {
return DesktopModeFlags.isDesktopModeForcedEnabled()
&& canShowDesktopModeDevOption(context);
} catch (Throwable e) {
// LC-Ignored
return false;
}
}
/**
@@ -164,21 +297,14 @@ public class DesktopModeStatus {
}
// TODO (b/395014779): Change this to use WM API
if (!DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) {
return false;
if ((display.getType() == Display.TYPE_EXTERNAL
|| display.getType() == Display.TYPE_OVERLAY)
&& enableDisplayContentModeManagement()) {
final WindowManager wm = context.getSystemService(WindowManager.class);
return wm != null && wm.shouldShowSystemDecors(display.getDisplayId());
}
final WindowManager wm = context.getSystemService(WindowManager.class);
return wm != null && wm.isEligibleForDesktopMode(display.getDisplayId());
}
/**
* Returns true if the multi-desks frontend should be enabled on the display.
*/
public static boolean isMultipleDesktopFrontendEnabledOnDisplay(@NonNull Context context,
Display display) {
return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_FRONTEND.isTrue()
&& DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()
&& isDesktopModeSupportedOnDisplay(context, display);
return false;
}
/**
@@ -186,9 +312,14 @@ public class DesktopModeStatus {
* frontend implementations).
*/
public static boolean enableMultipleDesktops(@NonNull Context context) {
return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()
&& DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_FRONTEND.isTrue()
try {
return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()
&& Flags.enableMultipleDesktopsFrontend()
&& canEnterDesktopMode(context);
} catch (Throwable e) {
// LC-Ignored
return false;
}
}
/**
@@ -196,8 +327,27 @@ public class DesktopModeStatus {
* necessarily enabling desktop mode
*/
public static boolean overridesShowAppHandle(@NonNull Context context) {
return (Flags.showAppHandleLargeScreens() || enableBubbleToFullscreen())
&& deviceHasLargeScreen(context);
try {
return (Flags.showAppHandleLargeScreens() || enableBubbleToFullscreen())
&& deviceHasLargeScreen(context);
} catch (Throwable t) {
return false;
}
}
/**
* @return If {@code true} we set opaque background for all freeform tasks to prevent freeform
* tasks below from being visible if freeform task window above is translucent.
* Otherwise if fluid resize is enabled, add a background to freeform tasks.
*/
public static boolean shouldSetBackground(@NonNull TaskInfo taskInfo) {
try {
return taskInfo.isFreeform() && (!DesktopModeStatus.isVeiledResizeEnabled()
|| DesktopModeFlags.ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS.isTrue());
} catch (Throwable e) {
// LC-Ignored
return false;
}
}
/**
@@ -208,30 +358,64 @@ public class DesktopModeStatus {
return canEnterDesktopMode(context) || overridesShowAppHandle(context);
}
/**
/**
* Return {@code true} if the override desktop density is enabled and valid.
*/
public static boolean useDesktopOverrideDensity() {
return isDesktopDensityOverrideEnabled() && isValidDesktopDensityOverrideSet();
}
/**
* Returns {@code true} if the app-to-web feature is using the build-time generic links list.
*/
public static boolean useAppToWebBuildTimeGenericLinks() {
return USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS;
}
/**
* Return {@code true} if the override desktop density is enabled.
*/
private static boolean isDesktopDensityOverrideEnabled() {
return DESKTOP_DENSITY_OVERRIDE_ENABLED;
}
/**
* Return {@code true} if the override desktop density is set and within a valid range.
*/
private static boolean isValidDesktopDensityOverrideSet() {
return DESKTOP_DENSITY_OVERRIDE >= DESKTOP_DENSITY_MIN
&& DESKTOP_DENSITY_OVERRIDE <= DESKTOP_DENSITY_MAX;
}
/**
* Return {@code true} if desktop mode is unrestricted and is supported on the device.
*/
public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
if (!enforceDeviceRestrictions()) {
return true;
}
final boolean enableDesktopModeThroughDevOption;
if (false) {
// LC-Ignored: Lawnchair-TODO: Intentional unless we can find a way to detect QPR1 build or skip to Android 17
enableDesktopModeThroughDevOption = Flags.enableDesktopModeThroughDevOption();
} else {
enableDesktopModeThroughDevOption = false;
}
final boolean desktopModeSupportedByDevOptions =
enableDesktopModeThroughDevOption
try {
// If projected display is enabled, #canInternalDisplayHostDesktops is no longer a
// requirement.
final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue()
? isDesktopModeSupported(context) : (isDesktopModeSupported(context)
&& canInternalDisplayHostDesktops(context));
final boolean desktopModeSupportedByDevOptions =
Flags.enableDesktopModeThroughDevOption()
&& isDesktopModeDevOptionSupported(context);
return isDesktopModeSupported(context) || desktopModeSupportedByDevOptions;
return desktopModeSupported || desktopModeSupportedByDevOptions;
} catch (Throwable e) {
// LC-Ignored
return false;
}
}
/**
* Return {@code true} if the developer option for desktop mode is supported on this device.
* Return {@code true} if the developer option for desktop mode is unrestricted and is supported
* in the device.
*
* <p> This method doesn't check if the developer option flag is enabled or not.
* Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then
* {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true.
*/
private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) {
if (!enforceDeviceRestrictions()) {
@@ -242,19 +426,6 @@ public class DesktopModeStatus {
return desktopModeSupported || isDesktopModeDevOptionSupported(context);
}
/**
* Return {@code true} if the developer option for desktop experience is supported on this
* device.
*
* <p> This method doesn't check if the developer option flag is enabled or not.
*/
private static boolean isDeviceEligibleForDesktopExperienceDevOption(@NonNull Context context) {
if (!enforceDeviceRestrictions()) {
return true;
}
return isDesktopModeSupported(context) || isDesktopModeDevOptionSupported(context);
}
/**
* @return {@code true} if this device has an internal large screen
*/
@@ -275,14 +446,52 @@ public class DesktopModeStatus {
* of the display's root [TaskDisplayArea] is set to WINDOWING_MODE_FREEFORM.
*/
public static boolean enterDesktopByDefaultOnFreeformDisplay(@NonNull Context context) {
if (DesktopExperienceFlags.ENABLE_DESKTOP_FIRST_BASED_DEFAULT_TO_DESKTOP_BUGFIX.isTrue()) {
return true;
}
if (!DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue()) {
try {
if (!DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue()) {
return false;
}
} catch (Throwable e) {
// LC-Ignored
return false;
}
return SystemProperties.getBoolean(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP,
context.getResources().getBoolean(
R.bool.config_enterDesktopByDefaultOnFreeformDisplay));
}
/**
* Return {@code true} if a window should be maximized when it's dragged to the top edge of the
* screen.
*/
public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) {
try {
if (!DesktopExperienceFlags.ENABLE_DRAG_TO_MAXIMIZE.isTrue()) {
return false;
}
} catch (Throwable e) {
// LC-Ignored
return false;
}
return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP,
context.getResources().getBoolean(R.bool.config_dragToMaximizeInDesktopMode));
}
/** Dumps DesktopModeStatus flags and configs. */
public static void dump(PrintWriter pw, String prefix, Context context) {
String innerPrefix = prefix + " ";
pw.print(prefix); pw.println(TAG);
pw.print(innerPrefix); pw.print("maxTaskLimit="); pw.println(getMaxTaskLimit(context));
pw.print(innerPrefix); pw.print("maxTaskLimit config override=");
pw.println(context.getResources().getInteger(
R.integer.config_maxDesktopWindowingActiveTasks));
SystemProperties.Handle maxTaskLimitHandle = SystemProperties.find(MAX_TASK_LIMIT_SYS_PROP);
pw.print(innerPrefix); pw.print("maxTaskLimit sysprop=");
pw.println(maxTaskLimitHandle == null ? "null" : maxTaskLimitHandle.getInt(/* def= */ -1));
pw.print(innerPrefix); pw.print("showAppHandle config override=");
pw.println(overridesShowAppHandle(context));
}
}
@@ -21,8 +21,6 @@ import android.os.Parcelable
/** Transition source types for Desktop Mode. */
enum class DesktopModeTransitionSource : Parcelable {
/** Transitions that originated from an adb command. */
ADB_COMMAND,
/** Transitions that originated as a consequence of task dragging. */
TASK_DRAG,
/** Transitions that originated from an app from Overview. */
@@ -35,7 +35,6 @@ import android.view.ViewGroup.MarginLayoutParams
import android.widget.LinearLayout
import android.window.TaskSnapshot
import com.android.wm.shell.R
//import com.android.wm.shell.shared.R
/**
* View for the All Windows menu option, used by both Desktop Windowing and Taskbar.
@@ -15,18 +15,12 @@
*/
package com.android.wm.shell.shared.split;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import android.annotation.NonNull;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
@@ -53,42 +47,13 @@ public class SplitBounds implements Parcelable {
* the bounds were originally in
*/
public final boolean appsStackedVertically;
/**
* If {@code true}, that means at the time of creation of this object, the phone was in
* seascape orientation. This is important on devices with insets, because they do not split
* evenly -- one of the insets must be slightly larger to account for the inset.
* From landscape, it is the leftTop task that expands slightly.
* From seascape, it is the rightBottom task that expands slightly.
*/
public final boolean initiatedFromSeascape;
/** @deprecated Use {@link #leftTopTaskIds} instead. */
@Deprecated
public final int leftTopTaskId;
/** @deprecated Use {@link #rightBottomTaskIds} instead. */
@Deprecated
public final int rightBottomTaskId;
@NonNull
public final List<Integer> leftTopTaskIds;
@NonNull
public final List<Integer> rightBottomTaskIds;
public SplitBounds(Rect leftTopBounds, Rect rightBottomBounds,
int leftTopTaskId, int rightBottomTaskId,
@NonNull List<Integer> leftTopTaskIds, @NonNull List<Integer> rightBottomTaskIds,
@PersistentSnapPosition int snapPosition) {
if (leftTopTaskId == INVALID_TASK_ID || rightBottomTaskId == INVALID_TASK_ID
|| leftTopTaskId == rightBottomTaskId
|| leftTopTaskIds.isEmpty() || rightBottomTaskIds.isEmpty()) {
throw new IllegalArgumentException("The Split task ids are invalid:"
+ " leftTopTaskId: " + leftTopTaskId
+ " rightBottomTaskId: " + rightBottomTaskId
+ " leftTopTaskId size: " + leftTopTaskIds.size()
+ " rightBottomTaskId size: " + rightBottomTaskIds.size());
}
public SplitBounds(Rect leftTopBounds, Rect rightBottomBounds, int leftTopTaskId,
int rightBottomTaskId, @PersistentSnapPosition int snapPosition) {
this.leftTopBounds = leftTopBounds;
this.rightBottomBounds = rightBottomBounds;
this.leftTopTaskIds = List.copyOf(leftTopTaskIds);
this.rightBottomTaskIds = List.copyOf(rightBottomTaskIds);
this.leftTopTaskId = leftTopTaskId;
this.rightBottomTaskId = rightBottomTaskId;
this.snapPosition = snapPosition;
@@ -98,18 +63,11 @@ public class SplitBounds implements Parcelable {
this.visualDividerBounds = new Rect(leftTopBounds.left, leftTopBounds.bottom,
leftTopBounds.right, rightBottomBounds.top);
appsStackedVertically = true;
initiatedFromSeascape = false;
} else {
// horizontal apps, vertical divider
this.visualDividerBounds = new Rect(leftTopBounds.right, leftTopBounds.top,
rightBottomBounds.left, leftTopBounds.bottom);
appsStackedVertically = false;
// The following check is unreliable on devices without insets
// (initiatedFromSeascape will always be set to false.) This happens to be OK for
// all our current uses, but should be refactored.
// TODO: Create a more reliable check, or refactor how splitting works on devices
// with insets.
initiatedFromSeascape = rightBottomBounds.width() > leftTopBounds.width();
}
float totalWidth = rightBottomBounds.right - leftTopBounds.left;
@@ -120,46 +78,6 @@ public class SplitBounds implements Parcelable {
dividerHeightPercent = visualDividerBounds.height() / totalHeight;
}
public SplitBounds(Rect leftTopBounds, Rect rightBottomBounds, int leftTopTaskId,
int rightBottomTaskId, @PersistentSnapPosition int snapPosition) {
this(leftTopBounds, rightBottomBounds, leftTopTaskId, rightBottomTaskId,
Collections.singletonList(leftTopTaskId),
Collections.singletonList(rightBottomTaskId), snapPosition);
}
/**
* Returns the percentage size of the left/top task (compared to the full width/height of
* the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and the
* right task is 4 units, this method will return 0.4f.
*/
public float getLeftTopTaskPercent() {
// topTaskPercent and leftTaskPercent are defined at creation time, and are not updated
// on device rotate, so we have to check appsStackedVertically to return the right
// creation-time measurements.
return appsStackedVertically ? topTaskPercent : leftTaskPercent;
}
/**
* Returns the percentage size of the divider's thickness (compared to the full width/height
* of the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and
* the right task is 4 units, this method will return 0.2f.
*/
public float getDividerPercent() {
// dividerHeightPercent and dividerWidthPercent are defined at creation time, and are
// not updated on device rotate, so we have to check appsStackedVertically to return
// the right creation-time measurements.
return appsStackedVertically ? dividerHeightPercent : dividerWidthPercent;
}
/**
* Returns the percentage size of the right/bottom task (compared to the full width/height
* of the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and
* the right task is 4 units, this method will return 0.4f.
*/
public float getRightBottomTaskPercent() {
return 1 - (getLeftTopTaskPercent() + getDividerPercent());
}
public SplitBounds(Parcel parcel) {
leftTopBounds = parcel.readTypedObject(Rect.CREATOR);
rightBottomBounds = parcel.readTypedObject(Rect.CREATOR);
@@ -167,22 +85,11 @@ public class SplitBounds implements Parcelable {
topTaskPercent = parcel.readFloat();
leftTaskPercent = parcel.readFloat();
appsStackedVertically = parcel.readBoolean();
initiatedFromSeascape = parcel.readBoolean();
leftTopTaskId = parcel.readInt();
rightBottomTaskId = parcel.readInt();
dividerWidthPercent = parcel.readFloat();
dividerHeightPercent = parcel.readFloat();
snapPosition = parcel.readInt();
leftTopTaskId = parcel.readInt();
rightBottomTaskId = parcel.readInt();
int size = parcel.readInt();
leftTopTaskIds = new ArrayList<>();
for (int i = 0; i < size; i++) {
leftTopTaskIds.add(parcel.readInt());
}
size = parcel.readInt();
rightBottomTaskIds = new ArrayList<>();
for (int i = 0; i < size; i++) {
rightBottomTaskIds.add(parcel.readInt());
}
}
@Override
@@ -193,20 +100,11 @@ public class SplitBounds implements Parcelable {
parcel.writeFloat(topTaskPercent);
parcel.writeFloat(leftTaskPercent);
parcel.writeBoolean(appsStackedVertically);
parcel.writeBoolean(initiatedFromSeascape);
parcel.writeInt(leftTopTaskId);
parcel.writeInt(rightBottomTaskId);
parcel.writeFloat(dividerWidthPercent);
parcel.writeFloat(dividerHeightPercent);
parcel.writeInt(snapPosition);
parcel.writeInt(leftTopTaskId);
parcel.writeInt(rightBottomTaskId);
parcel.writeInt(leftTopTaskIds.size());
for (Integer id : leftTopTaskIds) {
parcel.writeInt(id); // Write each Integer in the List
}
parcel.writeInt(rightBottomTaskIds.size());
for (Integer id : rightBottomTaskIds) {
parcel.writeInt(id); // Write each Integer in the List
}
}
@Override
@@ -223,23 +121,19 @@ public class SplitBounds implements Parcelable {
final SplitBounds other = (SplitBounds) obj;
return Objects.equals(leftTopBounds, other.leftTopBounds)
&& Objects.equals(rightBottomBounds, other.rightBottomBounds)
&& leftTopTaskIds.equals(other.leftTopTaskIds)
&& rightBottomTaskIds.equals(other.rightBottomTaskIds)
&& snapPosition == other.snapPosition;
&& leftTopTaskId == other.leftTopTaskId
&& rightBottomTaskId == other.rightBottomTaskId;
}
@Override
public int hashCode() {
return Objects.hash(leftTopBounds, rightBottomBounds,
leftTopTaskId, rightBottomTaskId, leftTopTaskIds, rightBottomTaskIds);
return Objects.hash(leftTopBounds, rightBottomBounds, leftTopTaskId, rightBottomTaskId);
}
@Override
public String toString() {
return "LeftTop: " + leftTopBounds + " taskId: " + leftTopTaskId
+ ", taskIds: " + leftTopTaskIds + "\n"
+ "RightBottom: " + rightBottomBounds + " taskId: " + rightBottomTaskId
+ ", taskIds: " + rightBottomTaskIds + "\n"
return "LeftTop: " + leftTopBounds + ", taskId: " + leftTopTaskId + "\n"
+ "RightBottom: " + rightBottomBounds + ", taskId: " + rightBottomTaskId + "\n"
+ "Divider: " + visualDividerBounds + "\n"
+ "AppsVertical? " + appsStackedVertically + "\n"
+ "snapPosition: " + snapPosition;
@@ -157,12 +157,6 @@ public class SplitScreenConstants {
*/
public static final int SNAP_TO_3_10_45_45 = 7;
/**
* A transitional state where the user has tapped an offscreen app, and the offscreen app is
* currently animating back onscreen.
*/
public static final int ANIMATING_OFFSCREEN_TAP = 100;
/**
* These snap targets are used for split pairs in a stable, non-transient state. They may be
* persisted in Launcher when the user saves an app pair. They are a subset of
@@ -182,7 +176,7 @@ public class SplitScreenConstants {
/**
* These are all the valid "states" that split screen can be in. It's the set of
* {@link PersistentSnapPosition} + {@link #NOT_IN_SPLIT} + other mid-animation states.
* {@link PersistentSnapPosition} + {@link #NOT_IN_SPLIT}.
*/
@IntDef(value = {
NOT_IN_SPLIT, // user is not in split screen
@@ -195,11 +189,10 @@ public class SplitScreenConstants {
SNAP_TO_3_33_33_33,
SNAP_TO_3_45_45_10,
SNAP_TO_3_10_45_45,
ANIMATING_OFFSCREEN_TAP // user tapped offscreen app to retrieve it
})
public @interface SplitScreenState {}
/** Converts a {@link SplitScreenState} to a human-readable string, for debug use. */
/** Converts a {@link SplitScreenState} to a human-readable string. */
public static String stateToString(@SplitScreenState int state) {
return switch (state) {
case NOT_IN_SPLIT -> "NOT_IN_SPLIT";
@@ -212,38 +205,10 @@ public class SplitScreenConstants {
case SNAP_TO_3_33_33_33 -> "SNAP_TO_3_33_33_33";
case SNAP_TO_3_45_45_10 -> "SNAP_TO_3_45_45_10";
case SNAP_TO_3_10_45_45 -> "SNAP_TO_3_10_45_45";
case ANIMATING_OFFSCREEN_TAP -> "ANIMATING_OFFSCREEN_TAP";
default -> "UNKNOWN";
};
}
/** Converts a {@link SnapPosition} to a string, for UI use. */
public static String snapPositionToUIString(@SnapPosition int snapPosition) {
return switch (snapPosition) {
case SNAP_TO_START_AND_DISMISS -> "\u2715";
case SNAP_TO_END_AND_DISMISS -> "\u2715";
case SNAP_TO_2_33_66 -> "30:70";
case SNAP_TO_2_50_50 -> "50:50";
case SNAP_TO_2_66_33 -> "70:30";
case SNAP_TO_2_90_10 -> "90:10";
case SNAP_TO_2_10_90 -> "10:90";
default -> "Split";
};
}
/**
* Convenience method to convert between the IntDef's to avoid some errors
* @return {@code -1} if splitScreenState does not have a valid/corresponding
* PersistentSnapPosition
*/
@PersistentSnapPosition
public static int splitStateToSnapPosition(@SplitScreenState int splitScreenState) {
return switch (splitScreenState) {
case NOT_IN_SPLIT, SNAP_TO_NONE, ANIMATING_OFFSCREEN_TAP -> -1;
default -> splitScreenState;
};
}
/**
* Checks if the snapPosition in question is a {@link PersistentSnapPosition}.
*/