Merge "Remove TaplIsTabletTest" into main
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index a11974c..b433a59 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -29,9 +29,9 @@
android:id="@+id/split_instructions_text"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
- android:maxWidth="@dimen/split_instructions_view_max_width"
android:textColor="?androidprv:attr/textColorOnAccent"
- android:text="@string/toast_split_select_app" />
+ android:text="@string/toast_split_select_app"
+ android:layout_weight="1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/split_instructions_text_cancel"
diff --git a/quickstep/res/values-sw600dp/dimens.xml b/quickstep/res/values-sw600dp/dimens.xml
index e24d8fe..37a90a1 100644
--- a/quickstep/res/values-sw600dp/dimens.xml
+++ b/quickstep/res/values-sw600dp/dimens.xml
@@ -44,9 +44,4 @@
<dimen name="allset_page_margin_horizontal">120dp</dimen>
<dimen name="allset_page_allset_text_size">38sp</dimen>
<dimen name="allset_page_swipe_up_text_size">15sp</dimen>
-
- <!-- Splitscreen -->
- <!-- Max width of the split instructions view -->
- <dimen name="split_instructions_view_max_width">300dp</dimen>
-
</resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index b404bb5..de0b2c7 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -522,9 +522,4 @@
<!-- Digital Wellbeing -->
<dimen name="digital_wellbeing_toast_height">48dp</dimen>
-
- <!-- Splitscreen -->
- <!-- Max width of the split instructions view -->
- <dimen name="split_instructions_view_max_width">220dp</dimen>
-
</resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index df949c3..f126568 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -49,9 +49,6 @@
<!-- Accessibility title for the list of recent apps [CHAR_LIMIT=none] -->
<string name="accessibility_recent_apps">Recent apps</string>
- <!-- Accessibility confirmation for task closed -->
- <string name="task_view_closed">Task Closed</string>
-
<!-- Accessibility title for an app card in Recents for apps that have time limit set
[CHAR_LIMIT=none] -->
<string name="task_contents_description_with_remaining_time"><xliff:g id="task_description" example="GMail">%1$s</xliff:g>, <xliff:g id="remaining_time" example="7 minutes left today">%2$s</xliff:g></string>
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 3dcf2b4..7d39bf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -342,10 +342,10 @@
mBubbleBarViewController.showOverflow(update.showOverflow);
}
- BubbleBarBubble bubbleToSelect = null;
if (update.addedBubble != null) {
mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
}
+ BubbleBarBubble bubbleToSelect = null;
if (update.selectedBubbleKey != null) {
if (mSelectedBubble == null
|| !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
@@ -363,7 +363,7 @@
&& update.removedBubbles.isEmpty()
&& !mBubbles.isEmpty()) {
// A bubble was added from the overflow (& now it's empty / not showing)
- mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble);
+ mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble, bubbleToSelect);
} else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
// we're adding and removing a bubble at the same time. handle this as a single update.
RemovedBubble removedBubble = update.removedBubbles.get(0);
@@ -371,7 +371,8 @@
boolean showOverflow = update.showOverflowChanged && update.showOverflow;
if (bubbleToRemove != null) {
mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble,
- bubbleToRemove, isExpanding, suppressAnimation, showOverflow);
+ bubbleToRemove, bubbleToSelect, isExpanding, suppressAnimation,
+ showOverflow);
} else {
mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
suppressAnimation, bubbleToSelect);
@@ -387,7 +388,7 @@
if (bubble != null && overflowNeedsToBeAdded) {
// First removal, show the overflow
overflowNeedsToBeAdded = false;
- mBubbleBarViewController.addOverflowAndRemoveBubble(bubble);
+ mBubbleBarViewController.addOverflowAndRemoveBubble(bubble, bubbleToSelect);
} else if (bubble != null) {
mBubbleBarViewController.removeBubble(bubble);
} else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index aa6ad25..2d4d279 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -741,35 +741,32 @@
/** Add a new bubble and remove an old bubble from the bubble bar. */
public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble,
- Runnable onEndRunnable) {
+ @Nullable BubbleView bubbleToSelect, Runnable onEndRunnable) {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
Gravity.LEFT);
- boolean isOverflowSelected =
- mSelectedBubbleView != null && mSelectedBubbleView.isOverflow();
- boolean removingOverflow = removedBubble.isOverflow();
- boolean addingOverflow = addedBubble.isOverflow();
-
+ int addedIndex = addedBubble.isOverflow() ? getChildCount() : 0;
if (!isExpanded()) {
removeView(removedBubble);
- int index = addingOverflow ? getChildCount() : 0;
- addView(addedBubble, index, lp);
+ addView(addedBubble, addedIndex, lp);
if (onEndRunnable != null) {
onEndRunnable.run();
}
return;
}
- int index = addingOverflow ? getChildCount() : 0;
addedBubble.setScaleX(0f);
addedBubble.setScaleY(0f);
- addView(addedBubble, index, lp);
-
- if (isOverflowSelected && removingOverflow) {
- // The added bubble will be selected
- mSelectedBubbleView = addedBubble;
- }
- int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
+ addView(addedBubble, addedIndex, lp);
+ int indexOfCurrentSelectedBubble = indexOfChild(mSelectedBubbleView);
int indexOfBubbleToRemove = indexOfChild(removedBubble);
-
+ int indexOfNewlySelectedBubble = bubbleToSelect == null
+ ? indexOfCurrentSelectedBubble : indexOfChild(bubbleToSelect);
+ // Since removed bubble is kept till the end of the animation we should check if there are
+ // more than one bubble. In such a case the bar will remain open without the selected bubble
+ if (mSelectedBubbleView == removedBubble
+ && bubbleToSelect == null
+ && getBubbleChildCount() > 1) {
+ Log.w(TAG, "Remove the currently selected bubble without selecting a new one.");
+ }
mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
@@ -802,8 +799,8 @@
invalidate();
}
};
- mBubbleAnimator.animateNewAndRemoveOld(indexOfSelectedBubble, indexOfBubbleToRemove,
- listener);
+ mBubbleAnimator.animateNewAndRemoveOld(indexOfCurrentSelectedBubble,
+ indexOfNewlySelectedBubble, indexOfBubbleToRemove, addedIndex, listener);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 5685093..0b627d2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -481,7 +481,7 @@
/** Whether the bubble bar has bubbles. */
public boolean hasBubbles() {
- return mBubbleBarController.getSelectedBubbleKey() != null;
+ return mBarView.getBubbleChildCount() > 0;
}
/**
@@ -843,11 +843,12 @@
}
/** Adds a new bubble and removes an old bubble at the same time. */
- public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble,
- BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation,
- boolean addOverflowToo) {
+ public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble, BubbleBarBubble removedBubble,
+ @Nullable BubbleBarBubble bubbleToSelect, boolean isExpanding,
+ boolean suppressAnimation, boolean addOverflowToo) {
+ BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView(),
- addOverflowToo ? () -> showOverflow(true) : null);
+ bubbleToSelectView, addOverflowToo ? () -> showOverflow(true) : null);
addedBubble.getView().setOnClickListener(mBubbleClickListener);
addedBubble.getView().setController(mBubbleViewController);
removedBubble.getView().setController(null);
@@ -878,22 +879,26 @@
}
/** Adds the overflow view to the bubble bar while animating a view away. */
- public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) {
+ public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble,
+ @Nullable BubbleBarBubble bubbleToSelect) {
if (mOverflowAdded) return;
mOverflowAdded = true;
+ BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView(),
- null /* onEndRunnable */);
+ bubbleToSelectView, null /* onEndRunnable */);
mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
mOverflowBubble.getView().setController(mBubbleViewController);
removedBubble.getView().setController(null);
}
/** Removes the overflow view to the bubble bar while animating a view in. */
- public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) {
+ public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble,
+ @Nullable BubbleBarBubble bubbleToSelect) {
if (!mOverflowAdded) return;
mOverflowAdded = false;
+ BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView(),
- null /* onEndRunnable */);
+ bubbleToSelectView, null /* onEndRunnable */);
addedBubble.getView().setOnClickListener(mBubbleClickListener);
addedBubble.getView().setController(mBubbleViewController);
mOverflowBubble.getView().setController(null);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
index 944e806..26d6ccc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
@@ -18,6 +18,8 @@
import androidx.core.animation.Animator
import androidx.core.animation.ValueAnimator
+import kotlin.math.max
+import kotlin.math.min
/**
* Animates individual bubbles within the bubble bar while the bubble bar is expanded.
@@ -70,14 +72,18 @@
fun animateNewAndRemoveOld(
selectedBubbleIndex: Int,
+ newlySelectedBubbleIndex: Int,
removedBubbleIndex: Int,
+ addedBubbleIndex: Int,
listener: Listener,
) {
animator = createAnimator(listener)
state =
State.AddingAndRemoving(
selectedBubbleIndex = selectedBubbleIndex,
+ newlySelectedBubbleIndex = newlySelectedBubbleIndex,
removedBubbleIndex = removedBubbleIndex,
+ addedBubbleIndex = addedBubbleIndex,
)
animator.start()
}
@@ -137,6 +143,7 @@
getBubbleTranslationXWhileAddingBubbleAtLimit(
bubbleIndex = bubbleIndex,
removedBubbleIndex = state.removedBubbleIndex,
+ addedBubbleIndex = state.addedBubbleIndex,
addedBubbleScale = animator.animatedFraction,
removedBubbleScale = 1 - animator.animatedFraction,
)
@@ -187,34 +194,25 @@
State.Idle -> 0f
is State.AddingBubble -> getArrowPositionWhenAddingBubble(state)
is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state)
- is State.AddingAndRemoving -> {
- // we never remove the selected bubble, so the arrow stays pointing to its center
- val tx =
- getBubbleTranslationXWhileAddingBubbleAtLimit(
- bubbleIndex = state.selectedBubbleIndex,
- removedBubbleIndex = state.removedBubbleIndex,
- addedBubbleScale = animator.animatedFraction,
- removedBubbleScale = 1 - animator.animatedFraction,
- )
- tx + iconSize / 2f
- }
+ is State.AddingAndRemoving -> getArrowPositionWhenAddingAndRemovingBubble(state)
}
}
private fun getArrowPositionWhenAddingBubble(state: State.AddingBubble): Float {
val scale = animator.animatedFraction
- var tx = getBubbleTranslationXWhileScalingBubble(
- bubbleIndex = state.selectedBubbleIndex,
- scalingBubbleIndex = 0,
- bubbleScale = scale
- ) + iconSize / 2f
+ var tx =
+ getBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = state.selectedBubbleIndex,
+ scalingBubbleIndex = 0,
+ bubbleScale = scale,
+ ) + iconSize / 2f
if (state.newlySelectedBubbleIndex != null) {
val selectedBubbleScale = if (state.newlySelectedBubbleIndex == 0) scale else 1f
val finalTx =
getBubbleTranslationXWhileScalingBubble(
bubbleIndex = state.newlySelectedBubbleIndex,
scalingBubbleIndex = 0,
- bubbleScale = 1f,
+ bubbleScale = scale,
) + iconSize * selectedBubbleScale / 2f
tx += (finalTx - tx) * animator.animatedFraction
}
@@ -266,6 +264,46 @@
}
}
+ private fun getArrowPositionWhenAddingAndRemovingBubble(state: State.AddingAndRemoving): Float {
+ // The bubble bar keeps constant width while adding and removing bubble. So we just need to
+ // find selected bubble arrow position on the animation start and newly selected bubble
+ // arrow position on the animation end interpolating the arrow between these positions
+ // during the animation.
+ // The indexes in the state are provided for the bubble bar containing all bubbles. So for
+ // certain circumstances indexes should be adjusted.
+ // When animation is started added bubble has zero scale as well as removed bubble when the
+ // animation is ended, so for both cases we should compute translation as it is one less
+ // bubble.
+ val bubbleCountOnEnd = bubbleCount - 1
+ var selectedIndex = state.selectedBubbleIndex
+ // We only need to adjust the selected index if added bubble was added before the selected.
+ if (selectedIndex > state.addedBubbleIndex) {
+ // If the selectedIndex is higher index than the added bubble index, we need to reduce
+ // selectedIndex by one because the added bubble has zero scale when animation is
+ // started.
+ selectedIndex--
+ }
+ var newlySelectedIndex = state.newlySelectedBubbleIndex
+ // We only need to adjust newlySelectedIndex if removed bubble was removed before the newly
+ // selected bubble.
+ if (newlySelectedIndex > state.removedBubbleIndex) {
+ // If the newlySelectedIndex is higher index than the removed bubble index, we need to
+ // reduce newlySelectedIndex by one because the removed bubble has zero scale when
+ // animation is ended.
+ newlySelectedIndex--
+ }
+ val iconAndSpacing: Float = iconSize + expandedBarIconSpacing
+ val startTx = getBubblesToTheLeft(selectedIndex, bubbleCountOnEnd) * iconAndSpacing
+ val endTx = getBubblesToTheLeft(newlySelectedIndex, bubbleCountOnEnd) * iconAndSpacing
+ val tx = startTx + (endTx - startTx) * animator.animatedFraction
+ return tx + iconSize / 2f
+ }
+
+ private fun getBubblesToTheLeft(bubbleIndex: Int, bubbleCount: Int = this.bubbleCount): Int =
+ // when bar is on left the index - 0 corresponds to the right - most bubble and when the
+ // bubble bar is on the right - 0 corresponds to the left - most bubble.
+ if (onLeft) bubbleCount - bubbleIndex - 1 else bubbleIndex
+
/**
* Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
* expanded and a bubble is animating in or out.
@@ -290,6 +328,7 @@
// the bar is on the left and the current bubble is to the right of the scaling
// bubble so account for its scale
(bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing
+
bubbleIndex == scalingBubbleIndex -> {
// the bar is on the left and this is the scaling bubble
val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize
@@ -299,6 +338,7 @@
val scaledSpace = bubbleScale * expandedBarIconSpacing
totalIconSize + totalSpacing + scaledSpace + pivotAdjustment
}
+
else ->
// the bar is on the left and the scaling bubble is on the right. the current
// bubble is unaffected by the scaling bubble
@@ -310,10 +350,12 @@
// the bar is on the right and the scaling bubble is on the right. the current
// bubble is unaffected by the scaling bubble
iconAndSpacing * bubbleIndex
+
bubbleIndex == scalingBubbleIndex ->
// the bar is on the right, and this is the animating bubble. it only needs to
// be adjusted for the scaling pivot.
iconAndSpacing * bubbleIndex + pivotAdjustment
+
else ->
// the bar is on the right and the scaling bubble is on the left so account for
// its scale
@@ -325,61 +367,67 @@
private fun getBubbleTranslationXWhileAddingBubbleAtLimit(
bubbleIndex: Int,
removedBubbleIndex: Int,
+ addedBubbleIndex: Int,
addedBubbleScale: Float,
removedBubbleScale: Float,
): Float {
val iconAndSpacing = iconSize + expandedBarIconSpacing
// the bubbles are scaling from the center, so we need to adjust their translation so
// that the distance to the adjacent bubble scales at the same rate.
- val addedBubblePivotAdjustment = -(1 - addedBubbleScale) * iconSize / 2f
- val removedBubblePivotAdjustment = -(1 - removedBubbleScale) * iconSize / 2f
+ val addedBubblePivotAdjustment = (addedBubbleScale - 1) * iconSize / 2f
+ val removedBubblePivotAdjustment = (removedBubbleScale - 1) * iconSize / 2f
- return if (onLeft) {
- // this is how many bubbles there are to the left of the current bubble.
- // when the bubble bar is on the right the added bubble is the right-most bubble so it
- // doesn't affect the translation of any other bubble.
- // when the removed bubble is to the left of the current bubble, we need to subtract it
- // from bubblesToLeft and use removedBubbleScale instead when calculating the
- // translation.
- val bubblesToLeft = bubbleCount - bubbleIndex - 1
- when {
- bubbleIndex == 0 ->
- // this is the added bubble and it's the right-most bubble. account for all the
- // other bubbles -- including the removed bubble -- and adjust for the added
- // bubble pivot.
- (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing +
- addedBubblePivotAdjustment
- bubbleIndex < removedBubbleIndex ->
+ val minAddedRemovedIndex = min(addedBubbleIndex, removedBubbleIndex)
+ val maxAddedRemovedIndex = max(addedBubbleIndex, removedBubbleIndex)
+ val isBetweenAddedAndRemoved =
+ bubbleIndex in (minAddedRemovedIndex + 1)..<maxAddedRemovedIndex
+ val isRemovedBubbleToLeftOfAddedBubble = onLeft == addedBubbleIndex < removedBubbleIndex
+ val bubblesToLeft = getBubblesToTheLeft(bubbleIndex)
+ return when {
+ isBetweenAddedAndRemoved -> {
+ if (isRemovedBubbleToLeftOfAddedBubble) {
// the removed bubble is to the left so account for it
(bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing
- bubbleIndex == removedBubbleIndex -> {
- // this is the removed bubble. all the bubbles to the left are at full scale
- // but we need to scale the spacing between the removed bubble and the bubble to
- // its left because the removed bubble disappears towards the left side
+ } else {
+ // the added bubble is to the left so account for it
+ (bubblesToLeft - 1 + addedBubbleScale) * iconAndSpacing
+ }
+ }
+
+ bubbleIndex == addedBubbleIndex -> {
+ if (isRemovedBubbleToLeftOfAddedBubble) {
+ // the removed bubble is to the left so account for it
+ (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing
+ } else {
+ // it's the left-most scaling bubble, all bubbles on the left are at full scale
+ bubblesToLeft * iconAndSpacing
+ } + addedBubblePivotAdjustment
+ }
+
+ bubbleIndex == removedBubbleIndex -> {
+ if (isRemovedBubbleToLeftOfAddedBubble) {
+ // All the bubbles to the left are at full scale, but we need to scale the
+ // spacing between the removed bubble and the bubble next to it
val totalIconSize = bubblesToLeft * iconSize
val totalSpacing =
(bubblesToLeft - 1 + removedBubbleScale) * expandedBarIconSpacing
- totalIconSize + totalSpacing + removedBubblePivotAdjustment
- }
- else ->
- // both added and removed bubbles are to the right so they don't affect the tx
- bubblesToLeft * iconAndSpacing
+ totalIconSize + totalSpacing
+ } else {
+ // The added bubble is to the left, so account for it
+ (bubblesToLeft - 1 + addedBubbleScale) * iconAndSpacing
+ } + removedBubblePivotAdjustment
}
- } else {
- when {
- bubbleIndex == 0 -> addedBubblePivotAdjustment // we always add bubbles at index 0
- bubbleIndex < removedBubbleIndex ->
- // the bar is on the right and the removed bubble is on the right. the current
- // bubble is unaffected by the removed bubble. only need to factor in the added
- // bubble's scale.
- iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale)
- bubbleIndex == removedBubbleIndex ->
- // the bar is on the right, and this is the animating bubble.
- iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale) +
- removedBubblePivotAdjustment
- else ->
- // both the added and the removed bubbles are to the left of the current bubble
- iconAndSpacing * (bubbleIndex - 2 + addedBubbleScale + removedBubbleScale)
+
+ else -> {
+ // if bubble index is on the right side of the animated bubbles, we need to deduct
+ // one, since both the added and the removed bubbles takes a single place
+ val onTheRightOfAnimatedBubbles =
+ if (onLeft) {
+ bubbleIndex < minAddedRemovedIndex
+ } else {
+ bubbleIndex > maxAddedRemovedIndex
+ }
+ (bubblesToLeft - if (onTheRightOfAnimatedBubbles) 1 else 0) * iconAndSpacing
}
}
}
@@ -413,10 +461,17 @@
val removingLastRemainingBubble: Boolean,
) : State
- // TODO add index where bubble is being added, and index for newly selected bubble
/** A new bubble is being added and an old bubble is being removed from the bubble bar. */
- data class AddingAndRemoving(val selectedBubbleIndex: Int, val removedBubbleIndex: Int) :
- State
+ data class AddingAndRemoving(
+ /** The index of the selected bubble. */
+ val selectedBubbleIndex: Int,
+ /** The index of the newly selected bubble. */
+ val newlySelectedBubbleIndex: Int,
+ /** The index of the bubble being removed. */
+ val removedBubbleIndex: Int,
+ /** The index of the added bubble. */
+ val addedBubbleIndex: Int,
+ ) : State
}
/** Callbacks for the animation. */
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 3469e2f..d3ac411 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -562,6 +562,11 @@
mSplitSelectStateController.onDestroy();
}
+ RecentsView recentsView = getOverviewPanel();
+ if (recentsView != null) {
+ recentsView.destroy();
+ }
+
super.onDestroy();
mHotseatPredictionController.destroy();
if (mViewCapture != null) mViewCapture.close();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 417bb74..7c09e9a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -17,8 +17,6 @@
package com.android.launcher3.uioverrides.flags
import android.app.PendingIntent
-import android.app.blob.BlobHandle.createWithSha256
-import android.app.blob.BlobStoreManager
import android.content.Context
import android.content.IIntentReceiver
import android.content.IIntentSender.Stub
@@ -29,10 +27,8 @@
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
-import android.os.ParcelFileDescriptor.AutoCloseOutputStream
import android.provider.DeviceConfig
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
-import android.provider.Settings.Secure
import android.text.Html
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
@@ -44,33 +40,16 @@
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreference
-import com.android.launcher3.AutoInstallsLayout
import com.android.launcher3.ExtendedEditText
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherPrefs
-import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
-import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
-import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
import com.android.launcher3.R
-import com.android.launcher3.model.data.FolderInfo
-import com.android.launcher3.model.data.ItemInfo
-import com.android.launcher3.model.data.LauncherAppWidgetInfo
-import com.android.launcher3.pm.UserCache
import com.android.launcher3.proxy.ProxyActivityStarter
import com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher
-import com.android.launcher3.shortcuts.ShortcutKey
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
-import com.android.launcher3.util.Executors.MODEL_EXECUTOR
import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
-import com.android.launcher3.util.LauncherLayoutBuilder
+import com.android.launcher3.util.LayoutImportExportHelper
import com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT
import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_COUNT
import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN
@@ -80,14 +59,12 @@
import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
import com.android.launcher3.util.PluginManagerWrapper
import com.android.launcher3.util.StartActivityParams
-import com.android.launcher3.util.UserIconInfo
import com.android.quickstep.util.DeviceConfigHelper
import com.android.quickstep.util.DeviceConfigHelper.Companion.NAMESPACE_LAUNCHER
import com.android.quickstep.util.DeviceConfigHelper.DebugInfo
import com.android.systemui.shared.plugins.PluginEnabler
import com.android.systemui.shared.plugins.PluginPrefs
-import java.io.OutputStreamWriter
-import java.security.MessageDigest
+import java.nio.charset.StandardCharsets
import java.util.Locale
import java.util.concurrent.Executor
@@ -421,26 +398,12 @@
title = "Export"
intent =
createUriPickerIntent(ACTION_CREATE_DOCUMENT, MAIN_EXECUTOR) { uri ->
- model.enqueueModelUpdateTask { _, dataModel, _ ->
- val builder = LauncherLayoutBuilder()
- dataModel.workspaceItems.forEach { info ->
- val loc =
- when (info.container) {
- CONTAINER_DESKTOP ->
- builder.atWorkspace(info.cellX, info.cellY, info.screenId)
- CONTAINER_HOTSEAT -> builder.atHotseat(info.screenId)
- else -> return@forEach
- }
- loc.addItem(info)
- }
- dataModel.appWidgets.forEach { info ->
- builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(info)
- }
-
+ LayoutImportExportHelper.exportModelDbAsXml(context) { layoutXml ->
context.contentResolver.openOutputStream(uri).use { os ->
- builder.build(OutputStreamWriter(os))
+ val bytes: ByteArray =
+ layoutXml.toByteArray(StandardCharsets.UTF_8) // Encode to UTF-8
+ os?.write(bytes)
}
-
MAIN_EXECUTOR.execute {
Toast.makeText(context, "File saved", Toast.LENGTH_LONG).show()
}
@@ -458,66 +421,12 @@
resolver.openInputStream(uri).use { stream ->
stream?.readAllBytes() ?: return@createUriPickerIntent
}
-
- val digest = MessageDigest.getInstance("SHA-256").digest(data)
- val handle = createWithSha256(digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)
- val blobManager = context.getSystemService(BlobStoreManager::class.java)!!
-
- blobManager.openSession(blobManager.createSession(handle)).use { session ->
- AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) }
- session.allowPublicAccess()
-
- session.commit(ORDERED_BG_EXECUTOR) {
- Secure.putString(
- resolver,
- LAYOUT_PROVIDER_KEY,
- createBlobProviderKey(digest),
- )
-
- MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
- MAIN_EXECUTOR.submit { model.forceReload() }.get()
- MODEL_EXECUTOR.submit {}.get()
- Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
- }
- }
+ LayoutImportExportHelper.importModelFromXml(context, data)
}
category.addPreference(this)
}
}
- private fun LauncherLayoutBuilder.ItemTarget.addItem(info: ItemInfo) {
- val userType: String? =
- when (UserCache.INSTANCE.get(context).getUserInfo(info.user).type) {
- UserIconInfo.TYPE_WORK -> AutoInstallsLayout.USER_TYPE_WORK
- UserIconInfo.TYPE_CLONED -> AutoInstallsLayout.USER_TYPE_CLONED
- else -> null
- }
- when (info.itemType) {
- ITEM_TYPE_APPLICATION ->
- info.targetComponent?.let { c -> putApp(c.packageName, c.className, userType) }
- ITEM_TYPE_DEEP_SHORTCUT ->
- ShortcutKey.fromItemInfo(info).let { key ->
- putShortcut(key.packageName, key.id, userType)
- }
- ITEM_TYPE_FOLDER ->
- (info as FolderInfo).let { folderInfo ->
- putFolder(folderInfo.title?.toString() ?: "").also { folderBuilder ->
- folderInfo.getContents().forEach { folderContent ->
- folderBuilder.addItem(folderContent)
- }
- }
- }
- ITEM_TYPE_APPWIDGET ->
- putWidget(
- (info as LauncherAppWidgetInfo).providerName.packageName,
- info.providerName.className,
- info.spanX,
- info.spanY,
- userType,
- )
- }
- }
-
private fun createUriPickerIntent(
action: String,
executor: Executor,
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 64c1129..1f95c41 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -268,7 +268,7 @@
*
* @return the overview intent
*/
- Intent getOverviewIntentIgnoreSysUiState() {
+ public Intent getOverviewIntentIgnoreSysUiState() {
return mIsDefaultHome ? mMyHomeIntent : mOverviewIntent;
}
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 9c4d9bf..5e8ea37 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -452,6 +452,10 @@
@Override
protected void onDestroy() {
+ RecentsView recentsView = getOverviewPanel();
+ if (recentsView != null) {
+ recentsView.destroy();
+ }
super.onDestroy();
ACTIVITY_TRACKER.onContextDestroyed(this);
mActivityLaunchAnimationRunner = null;
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index f5593b0..910963d 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -503,6 +503,11 @@
}
}
+ if (Float.isNaN(scale)) {
+ Log.e(TAG, "Scale is NaN: starting dimensions=[" + startWidth + ", " + startHeight
+ + "], current dimensions=[" + currentWidth + ", " + currentHeight + "]");
+ }
+
mTargetTaskView.setScaleX(scale);
mTargetTaskView.setScaleY(scale);
mTargetTaskView.setTranslationX(
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index e0d4ddd..731c256 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -20,7 +20,6 @@
import static com.android.launcher3.Flags.enableHandleDelayedGestureCallbacks;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
@@ -53,7 +52,6 @@
import com.android.quickstep.util.SystemUiFlagUtils;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -110,16 +108,6 @@
return SystemUiProxy.INSTANCE.get(mCtx);
}
- /**
- * Preloads the recents animation.
- */
- public void preloadRecentsAnimation(Intent intent) {
- // Pass null animation handler to indicate this start is for preloading
- UI_HELPER_EXECUTOR.execute(() -> {
- ActivityManagerWrapper.getInstance().preloadRecentsActivity(intent);
- });
- }
-
boolean shouldIgnoreMotionEvents() {
return mShouldIgnoreMotionEvents;
}
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 210065a..bfd6107 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -37,13 +37,16 @@
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitStageInfo;
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
import com.android.launcher3.util.SplitConfigurationOptions.StageType;
import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -60,15 +63,17 @@
import java.util.LinkedList;
import java.util.List;
+import javax.inject.Inject;
+
/**
* This class tracked the top-most task and some 'approximate' task history to allow faster
* system state estimation during touch interaction
*/
-public class TopTaskTracker extends ISplitScreenListener.Stub
- implements TaskStackChangeListener, SafeCloseable {
+@LauncherAppSingleton
+public class TopTaskTracker extends ISplitScreenListener.Stub implements TaskStackChangeListener {
private static final String TAG = "TopTaskTracker";
- public static MainThreadInitializedObject<TopTaskTracker> INSTANCE =
- new MainThreadInitializedObject<>(TopTaskTracker::new);
+ public static DaggerSingletonObject<TopTaskTracker> INSTANCE =
+ new DaggerSingletonObject<>(QuickstepBaseAppComponent::getTopTaskTracker);
private static final int HISTORY_SIZE = 5;
@@ -86,7 +91,9 @@
// bottom most.
private ArrayMap<Integer, ArrayList<GroupedTaskInfo>> mVisibleTasks = new ArrayMap<>();
- private TopTaskTracker(Context context) {
+ @Inject
+ public TopTaskTracker(@ApplicationContext Context context, DaggerSingletonTracker tracker,
+ SystemUiProxy systemUiProxy) {
mContext = context;
if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
@@ -98,18 +105,17 @@
mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE;
TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
- SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this);
- }
- }
-
- @Override
- public void close() {
- if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
- return;
+ systemUiProxy.registerSplitScreenListener(this);
}
- TaskStackChangeListeners.getInstance().unregisterTaskStackListener(this);
- SystemUiProxy.INSTANCE.get(mContext).unregisterSplitScreenListener(this);
+ tracker.addCloseable(() -> {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(this);
+ systemUiProxy.unregisterSplitScreenListener(this);
+ });
}
@Override
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a06029b..efd9a56 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -49,7 +49,6 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.SystemClock;
-import android.os.Trace;
import android.util.Log;
import android.view.Choreographer;
import android.view.InputDevice;
@@ -68,7 +67,6 @@
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.desktop.DesktopAppLaunchTransitionManager;
-import com.android.launcher3.provider.RestoreDbTask;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -96,6 +94,7 @@
import com.android.quickstep.util.ActiveGestureLog.CompoundString;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.util.ActiveTrackpadList;
+import com.android.quickstep.util.ActivityPreloadUtil;
import com.android.quickstep.util.ContextualSearchInvoker;
import com.android.quickstep.util.ContextualSearchStateManager;
import com.android.quickstep.views.RecentsViewContainer;
@@ -183,7 +182,7 @@
recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode,
unfoldTransition, dragAndDrop);
tis.initInputMonitor("TISBinder#onInitialize()");
- tis.preloadOverview(true /* fromInit */);
+ ActivityPreloadUtil.preloadOverviewForTIS(tis, true /* fromInit */);
}));
}
@@ -350,16 +349,6 @@
));
}
- /**
- * Preloads the Overview activity.
- * <p>
- * This method should only be used when the All Set page of the SUW is reached to safely
- * preload the Launcher for the SUW first reveal.
- */
- public void preloadOverviewForSUWAllSet() {
- executeForTouchInteractionService(tis -> tis.preloadOverview(false, true));
- }
-
@Override
public void onRotationProposal(int rotation, boolean isValid) {
executeForTaskbarManager(taskbarManager ->
@@ -1035,47 +1024,6 @@
}
}
- private void preloadOverview(boolean fromInit) {
- Trace.beginSection("preloadOverview(fromInit=" + fromInit + ")");
- preloadOverview(fromInit, false);
- Trace.endSection();
- }
-
- private void preloadOverview(boolean fromInit, boolean forSUWAllSet) {
- if (!LockedUserState.get(this).isUserUnlocked()) {
- return;
- }
-
- if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
- // Prevent the overview from being started before the real home on first boot.
- return;
- }
-
- if ((RestoreDbTask.isPending(this) && !forSUWAllSet)
- || !mDeviceState.isUserSetupComplete()) {
- // Preloading while a restore is pending may cause launcher to start the restore
- // too early.
- return;
- }
-
- final BaseContainerInterface containerInterface =
- mOverviewComponentObserver.getContainerInterface();
- final Intent overviewIntent = new Intent(
- mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
- if (containerInterface.getCreatedContainer() != null && fromInit) {
- // The activity has been created before the initialization of overview service. It is
- // usually happens when booting or launcher is the top activity, so we should already
- // have the latest state.
- return;
- }
-
- // TODO(b/258022658): Remove temporary logging.
- Log.i(TAG, "preloadOverview: forSUWAllSet=" + forSUWAllSet
- + ", isHomeAndOverviewSame=" + mOverviewComponentObserver.isHomeAndOverviewSame());
- ActiveGestureProtoLogProxy.logPreloadRecentsAnimation();
- mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
- }
-
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (!LockedUserState.get(this).isUserUnlocked()) {
@@ -1103,7 +1051,7 @@
return;
}
- preloadOverview(false /* fromInit */);
+ ActivityPreloadUtil.preloadOverviewForTIS(this, false /* fromInit */);
}
private static boolean isTablet(Configuration config) {
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
index 549c15b..1d40d76 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -22,6 +22,7 @@
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.quickstep.OverviewComponentObserver;
import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.util.AsyncClockEventDelegate;
@@ -46,4 +47,6 @@
OverviewComponentObserver getOverviewComponentObserver();
DesktopVisibilityController getDesktopVisibilityController();
+
+ TopTaskTracker getTopTaskTracker();
}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index a9259d9..505f2cb 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -24,6 +24,8 @@
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.util.DaggerSingletonObject
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.quickstep.DisplayModel
import com.android.quickstep.FallbackWindowInterface
import com.android.quickstep.dagger.QuickstepBaseAppComponent
@@ -31,7 +33,9 @@
import javax.inject.Inject
@LauncherAppSingleton
-class RecentsDisplayModel @Inject constructor(@ApplicationContext context: Context) :
+class RecentsDisplayModel
+@Inject
+constructor(@ApplicationContext context: Context, tracker: DaggerSingletonTracker) :
DisplayModel<RecentsDisplayResource>(context) {
companion object {
@@ -47,17 +51,38 @@
init {
if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
- displayManager.registerDisplayListener(displayListener, Handler.getMain())
- createDisplayResource(Display.DEFAULT_DISPLAY)
+ MAIN_EXECUTOR.execute {
+ displayManager.registerDisplayListener(displayListener, Handler.getMain())
+ // In the scenario where displays were added before this display listener was
+ // registered, we should store the RecentsDisplayResources for those displays
+ // directly.
+ displayManager.displays
+ .filter { getDisplayResource(it.displayId) == null }
+ .forEach { storeRecentsDisplayResource(it.displayId, it) }
+ }
+ tracker.addCloseable { destroy() }
}
}
override fun createDisplayResource(displayId: Int) {
- if (DEBUG) Log.d(TAG, "create: displayId=$displayId")
+ if (DEBUG) Log.d(TAG, "createDisplayResource: displayId=$displayId")
getDisplayResource(displayId)?.let {
return
}
val display = displayManager.getDisplay(displayId)
+ if (display == null) {
+ if (DEBUG)
+ Log.w(
+ TAG,
+ "createDisplayResource: could not create display for displayId=$displayId",
+ Exception(),
+ )
+ return
+ }
+ storeRecentsDisplayResource(displayId, display)
+ }
+
+ private fun storeRecentsDisplayResource(displayId: Int, display: Display) {
displayResourceArray[displayId] =
RecentsDisplayResource(displayId, context.createDisplayContext(display))
}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index e22736c..5d99aec 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -163,6 +163,7 @@
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
callbacks?.removeListener(recentsAnimationListener)
recentsWindowTracker.onContextDestroyed(this)
+ recentsView?.destroy()
}
override fun startHome() {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index 390d097..b2e7015 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -57,6 +57,7 @@
private final int mTouchSlop;
private final PointF mDownPos = new PointF();
private final PointF mLastPos = new PointF();
+ private long mDownTime;
private final long mTimeForLongPress;
private int mActivePointerId = INVALID_POINTER_ID;
@@ -81,6 +82,7 @@
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
+ mDownTime = System.currentTimeMillis();
mActivePointerId = ev.getPointerId(0);
mDownPos.set(ev.getX(), ev.getY());
mLastPos.set(mDownPos);
@@ -120,13 +122,15 @@
}
break;
case MotionEvent.ACTION_UP:
+ long tapTime = System.currentTimeMillis() - mDownTime;
boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null
&& mBubbleBarSwipeController.isSwipeGesture();
// Anything less than a long-press is a tap
- boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForLongPress;
+ boolean isWithinTapTime = tapTime <= mTimeForLongPress;
Log.d(TAG, "ACTION_UP swipeUp=" + swipeUpOnBubbleHandle + " isInTapTime="
- + isWithinTapTime + " passedTouchSlop=" + mPassedTouchSlop
- + " stashedOrCollapsedOnDown=" + mStashedOrCollapsedOnDown);
+ + isWithinTapTime + " tapTime=" + tapTime + " passedTouchSlop="
+ + mPassedTouchSlop + " stashedOrCollapsedOnDown="
+ + mStashedOrCollapsedOnDown);
if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
&& mStashedOrCollapsedOnDown) {
Log.d(TAG, "ACTION_UP showing bubble bar");
@@ -153,6 +157,7 @@
}
mPassedTouchSlop = false;
mPilfered = false;
+ mDownTime = 0;
}
private boolean isCollapsed() {
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 4995e77..c986b88 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -73,6 +73,7 @@
import com.android.quickstep.OverviewComponentObserver;
import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.util.ActivityPreloadUtil;
import com.android.quickstep.util.LottieAnimationColorUtils;
import com.android.quickstep.util.TISBindHelper;
@@ -202,6 +203,7 @@
OverviewComponentObserver.INSTANCE.get(this)
.addOverviewChangeListener(mOverviewChangeListener);
+ ActivityPreloadUtil.preloadOverviewForSUWAllSet(this);
}
private InvariantDeviceProfile getIDP() {
@@ -291,7 +293,6 @@
private void onTISConnected(TISBinder binder) {
setSetupUIVisible(isResumed());
binder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null);
- binder.preloadOverviewForSUWAllSet();
TaskbarManager taskbarManager = binder.getTaskbarManager();
if (taskbarManager != null) {
mLauncherStartAnim = taskbarManager.createLauncherStartFromSuwAnim(MAX_SWIPE_DURATION);
@@ -299,10 +300,7 @@
}
private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
- TISBinder binder = mTISBindHelper.getBinder();
- if (binder != null) {
- binder.preloadOverviewForSUWAllSet();
- }
+ ActivityPreloadUtil.preloadOverviewForSUWAllSet(this);
}
@Override
diff --git a/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt b/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt
new file mode 100644
index 0000000..47b39db
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2025 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.quickstep.util
+
+import android.content.Context
+import android.content.Intent
+import android.os.Trace
+import com.android.launcher3.provider.RestoreDbTask
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.LockedUserState
+import com.android.quickstep.OverviewComponentObserver
+import com.android.quickstep.RecentsAnimationDeviceState
+import com.android.systemui.shared.system.ActivityManagerWrapper
+
+/** Utility class for preloading overview */
+object ActivityPreloadUtil {
+
+ @JvmStatic
+ fun preloadOverviewForSUWAllSet(ctx: Context) {
+ preloadOverview(ctx, fromInit = false, forSUWAllSet = true)
+ }
+
+ @JvmStatic
+ fun preloadOverviewForTIS(ctx: Context, fromInit: Boolean) {
+ preloadOverview(ctx, fromInit = fromInit, forSUWAllSet = false)
+ }
+
+ private fun preloadOverview(ctx: Context, fromInit: Boolean, forSUWAllSet: Boolean) {
+ Trace.beginSection("preloadOverview(fromInit=$fromInit, forSUWAllSet=$forSUWAllSet)")
+
+ try {
+ if (!LockedUserState.get(ctx).isUserUnlocked) return
+
+ val deviceState = RecentsAnimationDeviceState.INSTANCE[ctx]
+ val overviewCompObserver = OverviewComponentObserver.INSTANCE[ctx]
+
+ // Prevent the overview from being started before the real home on first boot
+ if (deviceState.isButtonNavMode && !overviewCompObserver.isHomeAndOverviewSame) return
+
+ // Preloading while a restore is pending may cause launcher to start the restore too
+ // early
+ if ((RestoreDbTask.isPending(ctx) && !forSUWAllSet) || !deviceState.isUserSetupComplete)
+ return
+
+ // The activity has been created before the initialization of overview service. It is
+ // usually happens when booting or launcher is the top activity, so we should already
+ // have the latest state.
+ if (fromInit && overviewCompObserver.containerInterface.createdContainer != null) return
+
+ ActiveGestureProtoLogProxy.logPreloadRecentsAnimation()
+ val overviewIntent = Intent(overviewCompObserver.overviewIntentIgnoreSysUiState)
+ Executors.UI_HELPER_EXECUTOR.execute {
+ ActivityManagerWrapper.getInstance().preloadRecentsActivity(overviewIntent)
+ }
+ } finally {
+ Trace.endSection()
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index a382a05..045b823 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -927,8 +927,11 @@
mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
mEmptyMessagePaint.setTextSize(getResources()
.getDimension(R.dimen.recents_empty_message_text_size));
- mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
- Typeface.NORMAL));
+ Typeface typeface = Typeface.create(
+ Typeface.create(Themes.getDefaultBodyFont(context), Typeface.NORMAL),
+ getFontWeight(),
+ false);
+ mEmptyMessagePaint.setTypeface(typeface);
mEmptyMessagePaint.setAntiAlias(true);
mEmptyMessagePadding = getResources()
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
@@ -1240,8 +1243,15 @@
mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
}
reset();
+ }
+
+ /**
+ * Execute clean-up logic needed when the view is destroyed.
+ */
+ public void destroy() {
+ Log.d(TAG, "destroy");
if (enableRefactorTaskThumbnail()) {
- mHelper.onDetachedFromWindow();
+ mHelper.onDestroy();
RecentsDependencies.destroy();
}
}
@@ -4047,8 +4057,6 @@
} else {
removeTaskInternal(dismissedTaskView);
}
- announceForAccessibility(
- getResources().getString(R.string.task_view_closed));
mContainer.getStatsLogManager().logger()
.withItemInfo(dismissedTaskView.getFirstItemInfo())
.log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
@@ -6852,6 +6860,14 @@
}
}
+ private int getFontWeight() {
+ int fontWeightAdjustment = getResources().getConfiguration().fontWeightAdjustment;
+ if (fontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
+ return Typeface.Builder.NORMAL_WEIGHT + fontWeightAdjustment;
+ }
+ return Typeface.Builder.NORMAL_WEIGHT;
+ }
+
public interface TaskLaunchListener {
void onTaskLaunched();
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index d92c4d0..ff711da 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -32,8 +32,8 @@
private val recentsCoroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
) {
- fun onDetachedFromWindow() {
- recentsCoroutineScope.cancel("RecentsView detaching from window")
+ fun onDestroy() {
+ recentsCoroutineScope.cancel("RecentsView is being destroyed")
}
fun switchToScreenshot(
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 0dbad70..5b99286 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -633,7 +633,11 @@
with(info) {
// Only make actions available if the app icon menu is visible to the user.
// When modalness is >0, the user is in select mode and the icon menu is hidden.
- if (modalness == 0f) {
+ // When split selection is active, they should only be able to select the app and not
+ // take any other action.
+ val shouldPopulateAccessibilityMenu =
+ modalness == 0f && recentsView?.isSplitSelectionActive == false
+ if (shouldPopulateAccessibilityMenu) {
addAction(
AccessibilityAction(
R.id.action_close,
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
index 3ca36ec..da362bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
@@ -94,7 +94,9 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync {
bubbleAnimator.animateNewAndRemoveOld(
selectedBubbleIndex = 3,
- removedBubbleIndex = 2,
+ newlySelectedBubbleIndex = 2,
+ removedBubbleIndex = 1,
+ addedBubbleIndex = 3,
listener,
)
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
index ffb1f23..0738336 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -25,7 +25,6 @@
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.util.LauncherModelHelper
import com.android.launcher3.util.MSDLPlayerWrapper
-import com.android.quickstep.fallback.window.RecentsDisplayModel
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.shared.system.InputConsumerController
import dagger.BindsInstance
@@ -67,9 +66,7 @@
@Before
fun setup() {
sandboxContext.initDaggerComponent(
- DaggerTestComponent.builder()
- .bindSystemUiProxy(systemUiProxy)
- .bindRecentsDisplayModel(RecentsDisplayModel(sandboxContext))
+ DaggerTestComponent.builder().bindSystemUiProxy(systemUiProxy)
)
sandboxContext.putObject(
RotationTouchHelper.INSTANCE,
@@ -122,8 +119,6 @@
interface Builder : LauncherAppComponent.Builder {
@BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
- @BindsInstance fun bindRecentsDisplayModel(model: RecentsDisplayModel): Builder
-
override fun build(): TestComponent
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 98a3607..8879a01 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -45,6 +45,9 @@
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppModule;
+import com.android.launcher3.dagger.LauncherAppSingleton;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
import com.android.quickstep.DeviceConfigWrapper;
@@ -56,6 +59,9 @@
import com.android.quickstep.util.TestExtensions;
import com.android.systemui.shared.system.InputMonitorCompat;
+import dagger.BindsInstance;
+import dagger.Component;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -423,7 +429,10 @@
mContext.onDestroy();
}
mContext = new SandboxContext(getApplicationContext());
- mContext.putObject(TopTaskTracker.INSTANCE, mTopTaskTracker);
+ mContext.initDaggerComponent(
+ DaggerNavHandleLongPressInputConsumerTest_TopTaskTrackerComponent
+ .builder()
+ .bindTopTaskTracker(mTopTaskTracker));
mScreenWidth = DisplayController.INSTANCE.get(mContext).getInfo().currentSize.x;
mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor,
mDeviceState, mNavHandle, mGestureState);
@@ -450,4 +459,17 @@
value,
() -> DeviceConfigWrapper.get().getEnableLpnhTwoStages());
}
+
+ @LauncherAppSingleton
+ @Component(modules = LauncherAppModule.class)
+ public interface TopTaskTrackerComponent extends LauncherAppComponent {
+ @Component.Builder
+ interface Builder extends LauncherAppComponent.Builder {
+ @BindsInstance
+ Builder bindTopTaskTracker(TopTaskTracker topTaskTracker);
+
+ @Override
+ TopTaskTrackerComponent build();
+ }
+ }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
index d2aa6ac..44ea73e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
@@ -28,14 +28,9 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW
import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW
-import com.android.launcher3.dagger.LauncherAppComponent
-import com.android.launcher3.dagger.LauncherAppModule
-import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.util.LauncherModelHelper
import com.android.launcher3.util.window.CachedDisplayInfo
import com.android.quickstep.fallback.window.RecentsDisplayModel
-import dagger.BindsInstance
-import dagger.Component
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
@@ -75,10 +70,6 @@
whenever(displayManager.getDisplay(anyInt())).thenReturn(display)
runOnMainSync { recentsDisplayModel = RecentsDisplayModel.INSTANCE.get(context) }
- context.initDaggerComponent(
- DaggerRecentsDisplayModelComponent.builder()
- .bindRecentsDisplayModel(recentsDisplayModel)
- )
}
@Test
@@ -125,14 +116,3 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync { f.run() }
}
}
-
-@LauncherAppSingleton
-@Component(modules = [LauncherAppModule::class])
-interface RecentsDisplayModelComponent : LauncherAppComponent {
- @Component.Builder
- interface Builder : LauncherAppComponent.Builder {
- @BindsInstance fun bindRecentsDisplayModel(model: RecentsDisplayModel): Builder
-
- override fun build(): RecentsDisplayModelComponent
- }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index e065dba..88be752 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
@@ -165,7 +165,6 @@
}
@Test
- @ScreenRecordRule.ScreenRecord // b/355466672
public void testPrivateSpaceLockingBehaviour() throws IOException {
assumeFalse(mLauncher.isTablet()); // b/367258373
// Scroll to the bottom of All Apps
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
index f58c84e..b744039 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
@@ -57,16 +57,25 @@
.switchToOverview()
.apply { flingForward() }
.also { moveTaskToDesktop(TEST_ACTIVITY_1) }
-
TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
- // Launch static DesktopTaskView
- val desktop =
+ // Launch static DesktopTaskView without live tile in Overview
+ val desktopTask =
mLauncher.goHome().switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
// Launch live-tile DesktopTaskView
- desktop.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
+ desktopTask.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
+ TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+
+ // Launch static DesktopTaskView with live tile in Overview
+ mLauncher.goHome()
+ startTestActivity(TEST_ACTIVITY_EXTRA)
+ mLauncher.launchedAppState
+ .switchToOverview()
+ .apply { flingBackward() }
+ .getTestActivityTask(TEST_ACTIVITIES)
+ .open()
TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 501e650..a02516a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -408,9 +408,6 @@
<!-- Accessibility action to move item to the current location. [CHAR_LIMIT=30] -->
<string name="action_move_here">Move item here</string>
- <!-- Accessibility confirmation for item added to workspace. -->
- <string name="item_added_to_workspace">Item added to home screen</string>
-
<!-- Accessibility confirmation for item removed. [CHAR_LIMIT=50]-->
<string name="item_removed">Item removed</string>
diff --git a/res/xml/folder_shapes.xml b/res/xml/folder_shapes.xml
deleted file mode 100644
index e60d333..0000000
--- a/res/xml/folder_shapes.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2019 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.
--->
-<shapes xmlns:launcher="http://schemas.android.com/apk/res-auto" >
-
- <Circle launcher:folderIconRadius="1" />
-
- <!-- Default icon for AOSP -->
- <RoundedSquare launcher:folderIconRadius="0.16" />
-
- <!-- Rounded icon from RRO -->
- <RoundedSquare launcher:folderIconRadius="0.6" />
-
- <!-- Square icon -->
- <RoundedSquare launcher:folderIconRadius="0" />
-
- <TearDrop launcher:folderIconRadius="0.3" />
- <Squircle launcher:folderIconRadius="0.2" />
-
-</shapes>
\ No newline at end of file
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 6e2d357..a526b89 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -24,25 +24,38 @@
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
+import android.os.Bundle;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.model.ModelDbController;
+import com.android.launcher3.util.LayoutImportExportHelper;
import com.android.launcher3.widget.LauncherWidgetHolder;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.function.ToIntFunction;
public class LauncherProvider extends ContentProvider {
private static final String TAG = "LauncherProvider";
+ // Method API For Provider#call method.
+ private static final String METHOD_EXPORT_LAYOUT_XML = "EXPORT_LAYOUT_XML";
+ private static final String METHOD_IMPORT_LAYOUT_XML = "IMPORT_LAYOUT_XML";
+ private static final String KEY_RESULT = "KEY_RESULT";
+ private static final String KEY_LAYOUT = "KEY_LAYOUT";
+ private static final String SUCCESS = "success";
+ private static final String FAILURE = "failure";
+
/**
* $ adb shell dumpsys activity provider com.android.launcher3
*/
@@ -142,6 +155,43 @@
return executeControllerTask(c -> c.update(args.table, values, args.where, args.args));
}
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ Bundle b = new Bundle();
+
+ // The caller must have the read or write permission for this content provider to
+ // access the "call" method at all. We also enforce the appropriate per-method permissions.
+ switch(method) {
+ case METHOD_EXPORT_LAYOUT_XML:
+ if (getContext().checkCallingOrSelfPermission(getReadPermission())
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller doesn't have read permission");
+ }
+
+ CompletableFuture<String> resultFuture = LayoutImportExportHelper.INSTANCE
+ .exportModelDbAsXmlFuture(getContext());
+ try {
+ b.putString(KEY_LAYOUT, resultFuture.get());
+ b.putString(KEY_RESULT, SUCCESS);
+ } catch (ExecutionException | InterruptedException e) {
+ b.putString(KEY_RESULT, FAILURE);
+ }
+ return b;
+
+ case METHOD_IMPORT_LAYOUT_XML:
+ if (getContext().checkCallingOrSelfPermission(getWritePermission())
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller doesn't have write permission");
+ }
+
+ LayoutImportExportHelper.INSTANCE.importModelFromXml(getContext(), arg);
+ b.putString(KEY_RESULT, SUCCESS);
+ return b;
+ default:
+ return null;
+ }
+ }
+
private int executeControllerTask(ToIntFunction<ModelDbController> task) {
if (Binder.getCallingPid() == Process.myPid()) {
throw new IllegalArgumentException("Same process should call model directly");
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index b05a46d..5072e37 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -718,12 +718,14 @@
}
/**
- * Queues the given callback to be run once {@code mPageScrolls} has been initialized.
+ * Run the given `callback` immediately once {@code mPageScrolls} has been initialized,
+ * otherwise queue the callback to `mOnPageScrollsInitializedCallbacks`.
*/
public void runOnPageScrollsInitialized(Runnable callback) {
- mOnPageScrollsInitializedCallbacks.add(callback);
if (isPageScrollsInitialized()) {
- onPageScrollsInitialized();
+ callback.run();
+ } else {
+ mOnPageScrollsInitializedCallbacks.add(callback);
}
}
@@ -903,14 +905,12 @@
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
- mPageScrolls = null;
dispatchPageCountChanged();
}
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
- mPageScrolls = null;
runOnPageScrollsInitialized(() -> {
mCurrentPage = validateNewPage(mCurrentPage);
mCurrentScrollOverPage = mCurrentPage;
diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
index d115f9f..c91e783 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -24,7 +24,6 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.R;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.shortcuts.DeepShortcutView;
@@ -67,7 +66,6 @@
screenId, coordinates[0], coordinates[1]);
mContext.bindItems(Collections.singletonList(info), true);
AbstractFloatingView.closeAllOpenViews(mContext);
- announceConfirmation(R.string.item_added_to_workspace);
}));
return true;
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 42556ca..b76e098 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -697,15 +697,23 @@
}
}
+ Log.d("b/383526431", "animateOpen: content child count before: "
+ + mContent.getTotalChildCount());
+
mContent.completePendingPageChanges();
mContent.setCurrentPage(pageNo);
+ Log.d("b/383526431", "animateOpen: content child count after pending page"
+ + " changes: " + mContent.getTotalChildCount());
+
// This is set to true in close(), but isn't reset to false until onDropCompleted(). This
// leads to an inconsistent state if you drag out of the folder and drag back in without
// dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
mDeleteFolderOnDropCompleted = false;
cancelRunningAnimations();
+ Log.d("b/383526431", "animateOpen: content child count after cancelling"
+ + " animation: " + mContent.getTotalChildCount());
FolderAnimationManager fam = new FolderAnimationManager(this, true /* isOpening */);
AnimatorSet anim = fam.getAnimator();
anim.addListener(new AnimatorListenerAdapter() {
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
index a7ab7b9..06286d6 100644
--- a/src/com/android/launcher3/folder/FolderGridOrganizer.java
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
import android.graphics.Point;
+import android.util.Log;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.model.data.FolderInfo;
@@ -178,6 +179,14 @@
break;
}
}
+
+ if (result.isEmpty()) {
+ // Log specifics since we are getting empty result
+ Log.d("b/383526431", "previewItemsForPage: "
+ + "mCountX = " + mCountX
+ + ", mCountY = " + mCountY
+ + ", content size = " + contents.size());
+ }
return result;
}
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 22f1164..bebe1a4 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -58,6 +58,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
@@ -531,6 +532,16 @@
verifyVisibleHighResIcons(getCurrentPage() + 1);
}
+ int getTotalChildCount() {
+ AtomicInteger count = new AtomicInteger();
+ iterateOverItems((i, v) -> {
+ count.getAndIncrement();
+ return false;
+ });
+
+ return count.get();
+ }
+
/**
* Ensures that all the icons on the given page are of high-res
*/
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 5461485..f144d14 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -136,7 +136,7 @@
switch (path) {
case KEY_SHAPE_OPTIONS: {
- if (Flags.newCustomizationPickerUi() && Flags.enableLauncherIconShapes()) {
+ if (Flags.newCustomizationPickerUi()) {
MatrixCursor cursor = new MatrixCursor(new String[]{
KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
List<AppShape> shapes = AppShapesProvider.INSTANCE.getShapes();
diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/IconShape.kt
index c64d4da..1377610 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/IconShape.kt
@@ -17,23 +17,29 @@
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
-import android.animation.FloatArrayEvaluator
import android.animation.ValueAnimator
-import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Matrix.ScaleToFit.FILL
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
+import android.graphics.RectF
import android.graphics.Region
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable
-import android.util.Xml
+import android.util.Log
import android.view.View
import android.view.ViewOutlineProvider
-import com.android.launcher3.R
+import androidx.annotation.VisibleForTesting
+import androidx.graphics.shapes.CornerRounding
+import androidx.graphics.shapes.Morph
+import androidx.graphics.shapes.RoundedPolygon
+import androidx.graphics.shapes.SvgPathParser
+import androidx.graphics.shapes.toPath
+import androidx.graphics.shapes.transformed
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
-import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppComponent
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
@@ -42,74 +48,38 @@
import com.android.launcher3.util.DaggerSingletonObject
import com.android.launcher3.util.DaggerSingletonTracker
import com.android.launcher3.views.ClipPathView
-import java.io.IOException
import javax.inject.Inject
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
/** Abstract representation of the shape of an icon shape */
@LauncherAppSingleton
-class IconShape
-@Inject
-constructor(
- @ApplicationContext context: Context,
- themeManager: ThemeManager,
- lifeCycle: DaggerSingletonTracker,
-) {
- var shape: ShapeDelegate = Circle()
- private set
+class IconShape @Inject constructor(themeManager: ThemeManager, lifeCycle: DaggerSingletonTracker) {
var normalizationScale: Float = IconNormalizer.ICON_VISIBLE_AREA_FACTOR
private set
- init {
- pickBestShape(context)
+ var shape: ShapeDelegate = pickBestShape(themeManager)
+ private set
- val changeListener = ThemeChangeListener { pickBestShape(context) }
+ init {
+ val changeListener = ThemeChangeListener { shape = pickBestShape(themeManager) }
+
themeManager.addChangeListener(changeListener)
lifeCycle.addCloseable { themeManager.removeChangeListener(changeListener) }
}
/** Initializes the shape which is closest to the [AdaptiveIconDrawable] */
- fun pickBestShape(context: Context) {
- // Pick any large size
- val size = 200
- val full = Region(0, 0, size, size)
- val shapePath = Path()
- val shapeR = Region()
- val iconR = Region()
- val drawable = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), ColorDrawable(Color.BLACK))
- drawable.setBounds(0, 0, size, size)
- iconR.setPath(drawable.iconMask, full)
-
- // Find the shape with minimum area of divergent region.
- var minArea = Int.MAX_VALUE
- var closestShape: ShapeDelegate? = null
- for (shape in getAllShapes(context)) {
- shapePath.reset()
- shape.addToPath(shapePath, 0f, 0f, size / 2f)
- shapeR.setPath(shapePath, full)
- shapeR.op(iconR, Region.Op.XOR)
-
- val area = GraphicsUtils.getArea(shapeR)
- if (area < minArea) {
- minArea = area
- closestShape = shape
+ private fun pickBestShape(themeManager: ThemeManager): ShapeDelegate {
+ val drawable =
+ AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)).apply {
+ setBounds(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)
}
- }
- if (closestShape != null) {
- shape = closestShape
- }
-
- // Initialize shape properties
- normalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null)
+ normalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, AREA_CALC_SIZE, null)
+ return pickBestShape(drawable.iconMask, themeManager.iconState.iconMask)
}
interface ShapeDelegate {
- fun enableShapeDetection(): Boolean {
- return false
- }
+ fun enableShapeDetection() = false
fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
@@ -121,38 +91,11 @@
endRect: Rect,
endRadius: Float,
isReversed: Boolean,
- ): ValueAnimator where T : View?, T : ClipPathView?
+ ): ValueAnimator where T : View, T : ClipPathView
}
- /** Abstract shape where the reveal animation is a derivative of a round rect animation */
- private abstract class SimpleRectShape : ShapeDelegate {
- override fun <T> createRevealAnimator(
- target: T,
- startRect: Rect,
- endRect: Rect,
- endRadius: Float,
- isReversed: Boolean,
- ): ValueAnimator where T : View?, T : ClipPathView? {
- return object :
- RoundedRectRevealOutlineProvider(
- getStartRadius(startRect),
- endRadius,
- startRect,
- endRect,
- ) {
- override fun shouldRemoveElevationDuringAnimation(): Boolean {
- return true
- }
- }
- .createRevealAnimator(target, isReversed)
- }
-
- protected abstract fun getStartRadius(startRect: Rect): Float
- }
-
- /** Abstract shape which draws using [Path] */
- abstract class PathShape : ShapeDelegate {
- private val mTmpPath = Path()
+ @VisibleForTesting
+ class Circle : RoundedSquare(1f) {
override fun drawShape(
canvas: Canvas,
@@ -160,138 +103,18 @@
offsetY: Float,
radius: Float,
paint: Paint,
- ) {
- mTmpPath.reset()
- addToPath(mTmpPath, offsetX, offsetY, radius)
- canvas.drawPath(mTmpPath, paint)
- }
+ ) = canvas.drawCircle(radius + offsetX, radius + offsetY, radius, paint)
- protected abstract fun newUpdateListener(
- startRect: Rect,
- endRect: Rect,
- endRadius: Float,
- outPath: Path,
- ): ValueAnimator.AnimatorUpdateListener
-
- override fun <T> createRevealAnimator(
- target: T,
- startRect: Rect,
- endRect: Rect,
- endRadius: Float,
- isReversed: Boolean,
- ): ValueAnimator where T : View?, T : ClipPathView? {
- val path = Path()
- val listener = newUpdateListener(startRect, endRect, endRadius, path)
-
- val va =
- if (isReversed) ValueAnimator.ofFloat(1f, 0f) else ValueAnimator.ofFloat(0f, 1f)
- va.addListener(
- object : AnimatorListenerAdapter() {
- private var mOldOutlineProvider: ViewOutlineProvider? = null
-
- override fun onAnimationStart(animation: Animator) {
- target?.apply {
- mOldOutlineProvider = outlineProvider
- outlineProvider = null
- translationZ = -target.elevation
- }
- }
-
- override fun onAnimationEnd(animation: Animator) {
- target?.apply {
- translationZ = 0f
- setClipPath(null)
- outlineProvider = mOldOutlineProvider
- }
- }
- }
- )
-
- va.addUpdateListener { anim: ValueAnimator ->
- path.reset()
- listener.onAnimationUpdate(anim)
- target?.setClipPath(path)
- }
-
- return va
- }
- }
-
- open class Circle : PathShape() {
- private val mTempRadii = FloatArray(8)
-
- override fun newUpdateListener(
- startRect: Rect,
- endRect: Rect,
- endRadius: Float,
- outPath: Path,
- ): ValueAnimator.AnimatorUpdateListener {
- val r1 = getStartRadius(startRect)
-
- val startValues =
- floatArrayOf(
- startRect.left.toFloat(),
- startRect.top.toFloat(),
- startRect.right.toFloat(),
- startRect.bottom.toFloat(),
- r1,
- r1,
- )
- val endValues =
- floatArrayOf(
- endRect.left.toFloat(),
- endRect.top.toFloat(),
- endRect.right.toFloat(),
- endRect.bottom.toFloat(),
- endRadius,
- endRadius,
- )
-
- val evaluator = FloatArrayEvaluator(FloatArray(6))
-
- return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
- val progress = anim.animatedValue as Float
- val values = evaluator.evaluate(progress, startValues, endValues)
- outPath.addRoundRect(
- values[0],
- values[1],
- values[2],
- values[3],
- getRadiiArray(values[4], values[5]),
- Path.Direction.CW,
- )
- }
- }
-
- private fun getRadiiArray(r1: Float, r2: Float): FloatArray {
- mTempRadii[7] = r1
- mTempRadii[6] = mTempRadii[7]
- mTempRadii[3] = mTempRadii[6]
- mTempRadii[2] = mTempRadii[3]
- mTempRadii[1] = mTempRadii[2]
- mTempRadii[0] = mTempRadii[1]
- mTempRadii[5] = r2
- mTempRadii[4] = mTempRadii[5]
- return mTempRadii
- }
-
- override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
+ override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) =
path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW)
- }
- private fun getStartRadius(startRect: Rect): Float {
- return startRect.width() / 2f
- }
-
- override fun enableShapeDetection(): Boolean {
- return true
- }
+ override fun enableShapeDetection() = true
}
- private class RoundedSquare(
- /** Ratio of corner radius to half size. */
- private val mRadiusRatio: Float
- ) : SimpleRectShape() {
+ /** Rounded square with [radiusRatio] as a ratio of its half edge size */
+ @VisibleForTesting
+ open class RoundedSquare(val radiusRatio: Float) : ShapeDelegate {
+
override fun drawShape(
canvas: Canvas,
offsetX: Float,
@@ -301,14 +124,14 @@
) {
val cx = radius + offsetX
val cy = radius + offsetY
- val cr = radius * mRadiusRatio
+ val cr = radius * radiusRatio
canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, paint)
}
override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
val cx = radius + offsetX
val cy = radius + offsetY
- val cr = radius * mRadiusRatio
+ val cr = radius * radiusRatio
path.addRoundRect(
cx - radius,
cy - radius,
@@ -320,213 +143,205 @@
)
}
- override fun getStartRadius(startRect: Rect): Float {
- return (startRect.width() / 2f) * mRadiusRatio
+ override fun <T> createRevealAnimator(
+ target: T,
+ startRect: Rect,
+ endRect: Rect,
+ endRadius: Float,
+ isReversed: Boolean,
+ ): ValueAnimator where T : View, T : ClipPathView {
+ return object :
+ RoundedRectRevealOutlineProvider(
+ (startRect.width() / 2f) * radiusRatio,
+ endRadius,
+ startRect,
+ endRect,
+ ) {
+ override fun shouldRemoveElevationDuringAnimation() = true
+ }
+ .createRevealAnimator(target, isReversed)
}
}
- private class TearDrop(
- /**
- * Radio of short radius to large radius, based on the shape options defined in the config.
- */
- private val mRadiusRatio: Float
- ) : PathShape() {
- private val mTempRadii = FloatArray(8)
-
- override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
- val r2 = radius * mRadiusRatio
- val cx = radius + offsetX
- val cy = radius + offsetY
-
- path.addRoundRect(
- cx - radius,
- cy - radius,
- cx + radius,
- cy + radius,
- getRadiiArray(radius, r2),
- Path.Direction.CW,
+ /** Generic shape delegate with pathString in bounds [0, 0, 100, 100] */
+ class GenericPathShape(pathString: String) : ShapeDelegate {
+ private val poly =
+ RoundedPolygon(
+ features = SvgPathParser.parseFeatures(pathString),
+ centerX = 50f,
+ centerY = 50f,
)
- }
-
- fun getRadiiArray(r1: Float, r2: Float): FloatArray {
- mTempRadii[7] = r1
- mTempRadii[6] = mTempRadii[7]
- mTempRadii[3] = mTempRadii[6]
- mTempRadii[2] = mTempRadii[3]
- mTempRadii[1] = mTempRadii[2]
- mTempRadii[0] = mTempRadii[1]
- mTempRadii[5] = r2
- mTempRadii[4] = mTempRadii[5]
- return mTempRadii
- }
-
- override fun newUpdateListener(
- startRect: Rect,
- endRect: Rect,
- endRadius: Float,
- outPath: Path,
- ): ValueAnimator.AnimatorUpdateListener {
- val r1 = startRect.width() / 2f
- val r2 = r1 * mRadiusRatio
-
- val startValues =
- floatArrayOf(
- startRect.left.toFloat(),
- startRect.top.toFloat(),
- startRect.right.toFloat(),
- startRect.bottom.toFloat(),
- r1,
- r2,
- )
- val endValues =
- floatArrayOf(
- endRect.left.toFloat(),
- endRect.top.toFloat(),
- endRect.right.toFloat(),
- endRect.bottom.toFloat(),
- endRadius,
- endRadius,
- )
-
- val evaluator = FloatArrayEvaluator(FloatArray(6))
-
- return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
- val progress = anim.animatedValue as Float
- val values = evaluator.evaluate(progress, startValues, endValues)
- outPath.addRoundRect(
- values[0],
- values[1],
- values[2],
- values[3],
- getRadiiArray(values[4], values[5]),
- Path.Direction.CW,
- )
+ // This ensures that a valid morph is possible from the provided path
+ private val basePath =
+ Path().apply {
+ Morph(poly, createRoundedRect(0f, 0f, 100f, 100f, 25f)).toPath(0f, this)
}
- }
- }
+ private val tmpPath = Path()
+ private val tmpMatrix = Matrix()
- private class Squircle(
- /** Radio of radius to circle radius, based on the shape options defined in the config. */
- private val mRadiusRatio: Float
- ) : PathShape() {
+ override fun drawShape(
+ canvas: Canvas,
+ offsetX: Float,
+ offsetY: Float,
+ radius: Float,
+ paint: Paint,
+ ) {
+ tmpPath.reset()
+ addToPath(tmpPath, offsetX, offsetY, radius)
+ canvas.drawPath(tmpPath, paint)
+ }
+
override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
- val cx = radius + offsetX
- val cy = radius + offsetY
- val control = radius - radius * mRadiusRatio
-
- path.moveTo(cx, cy - radius)
- addLeftCurve(cx, cy, radius, control, path)
- addRightCurve(cx, cy, radius, control, path)
- addLeftCurve(cx, cy, -radius, -control, path)
- addRightCurve(cx, cy, -radius, -control, path)
- path.close()
+ tmpMatrix.setScale(radius / 50, radius / 50)
+ tmpMatrix.postTranslate(offsetX, offsetY)
+ basePath.transform(tmpMatrix, path)
}
- fun addLeftCurve(cx: Float, cy: Float, r: Float, control: Float, path: Path) {
- path.cubicTo(cx - control, cy - r, cx - r, cy - control, cx - r, cy)
- }
-
- fun addRightCurve(cx: Float, cy: Float, r: Float, control: Float, path: Path) {
- path.cubicTo(cx - r, cy + control, cx - control, cy + r, cx, cy + r)
- }
-
- override fun newUpdateListener(
+ override fun <T> createRevealAnimator(
+ target: T,
startRect: Rect,
endRect: Rect,
endRadius: Float,
- outPath: Path,
- ): ValueAnimator.AnimatorUpdateListener {
- val startCX = startRect.exactCenterX()
- val startCY = startRect.exactCenterY()
- val startR = startRect.width() / 2f
- val startControl = startR - startR * mRadiusRatio
- val startHShift = 0f
- val startVShift = 0f
+ isReversed: Boolean,
+ ): ValueAnimator where T : View, T : ClipPathView {
+ // End poly is defined as a rectangle starting at top/center so that the
+ // transformation has minimum motion
+ val morph =
+ Morph(
+ start =
+ poly.transformed(
+ Matrix().apply {
+ setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(startRect), FILL)
+ }
+ ),
+ end =
+ createRoundedRect(
+ left = endRect.left.toFloat(),
+ top = endRect.top.toFloat(),
+ right = endRect.right.toFloat(),
+ bottom = endRect.bottom.toFloat(),
+ cornerR = endRadius,
+ ),
+ )
- val endCX = endRect.exactCenterX()
- val endCY = endRect.exactCenterY()
- // Approximate corner circle using bezier curves
- // http://spencermortensen.com/articles/bezier-circle/
- val endControl = endRadius * 0.551915024494f
- val endHShift = endRect.width() / 2f - endRadius
- val endVShift = endRect.height() / 2f - endRadius
+ val va =
+ if (isReversed) ValueAnimator.ofFloat(1f, 0f) else ValueAnimator.ofFloat(0f, 1f)
+ va.addListener(
+ object : AnimatorListenerAdapter() {
+ private var oldOutlineProvider: ViewOutlineProvider? = null
- return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
- val progress = anim.animatedValue as Float
- val cx = (1 - progress) * startCX + progress * endCX
- val cy = (1 - progress) * startCY + progress * endCY
- val r = (1 - progress) * startR + progress * endRadius
- val control = (1 - progress) * startControl + progress * endControl
- val hShift = (1 - progress) * startHShift + progress * endHShift
- val vShift = (1 - progress) * startVShift + progress * endVShift
+ override fun onAnimationStart(animation: Animator) {
+ target?.apply {
+ oldOutlineProvider = outlineProvider
+ outlineProvider = null
+ translationZ = -target.elevation
+ }
+ }
- outPath.moveTo(cx, cy - vShift - r)
- outPath.rLineTo(-hShift, 0f)
+ override fun onAnimationEnd(animation: Animator) {
+ target.apply {
+ translationZ = 0f
+ setClipPath(null)
+ outlineProvider = oldOutlineProvider
+ }
+ }
+ }
+ )
- addLeftCurve(cx - hShift, cy - vShift, r, control, outPath)
- outPath.rLineTo(0f, vShift + vShift)
-
- addRightCurve(cx - hShift, cy + vShift, r, control, outPath)
- outPath.rLineTo(hShift + hShift, 0f)
-
- addLeftCurve(cx + hShift, cy + vShift, -r, -control, outPath)
- outPath.rLineTo(0f, -vShift - vShift)
-
- addRightCurve(cx + hShift, cy - vShift, -r, -control, outPath)
- outPath.close()
+ val path = Path()
+ va.addUpdateListener { anim: ValueAnimator ->
+ path.reset()
+ morph.toPath(anim.animatedValue as Float, path)
+ target.setClipPath(path)
}
+ return va
}
}
companion object {
@JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape)
- private fun getShapeDefinition(type: String, radius: Float): ShapeDelegate {
- return when (type) {
- "Circle" -> Circle()
- "RoundedSquare" -> RoundedSquare(radius)
- "TearDrop" -> TearDrop(radius)
- "Squircle" -> Squircle(radius)
- else -> throw IllegalArgumentException("Invalid shape type: $type")
+ const val TAG = "IconShape"
+
+ const val AREA_CALC_SIZE = 1000
+ // .1% error margin
+ const val AREA_DIFF_THRESHOLD = AREA_CALC_SIZE * AREA_CALC_SIZE / 1000
+
+ /** Returns a function to calculate area diff from [base] */
+ @VisibleForTesting
+ fun areaDiffCalculator(base: Path): (ShapeDelegate) -> Int {
+ val fullRegion = Region(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)
+ val iconRegion = Region().apply { setPath(base, fullRegion) }
+
+ val shapePath = Path()
+ val shapeRegion = Region()
+ return fun(shape: ShapeDelegate): Int {
+ shapePath.reset()
+ shape.addToPath(shapePath, 0f, 0f, AREA_CALC_SIZE / 2f)
+ shapeRegion.setPath(shapePath, fullRegion)
+ shapeRegion.op(iconRegion, Region.Op.XOR)
+ return GraphicsUtils.getArea(shapeRegion)
}
}
- private fun getAllShapes(context: Context): List<ShapeDelegate> {
- val result = ArrayList<ShapeDelegate>()
- try {
- context.resources.getXml(R.xml.folder_shapes).use { parser ->
- // Find the root tag
- var type: Int = parser.next()
- while (
- type != XmlPullParser.END_TAG &&
- type != XmlPullParser.END_DOCUMENT &&
- "shapes" != parser.name
- ) {
- type = parser.next()
- }
- val depth = parser.depth
- val radiusAttr = intArrayOf(R.attr.folderIconRadius)
- type = parser.next()
- while (
- (type != XmlPullParser.END_TAG || parser.depth > depth) &&
- type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG) {
- val attrs = Xml.asAttributeSet(parser)
- val arr = context.obtainStyledAttributes(attrs, radiusAttr)
- val shape = getShapeDefinition(parser.name, arr.getFloat(0, 1f))
- arr.recycle()
- result.add(shape)
- }
- type = parser.next()
- }
+ @VisibleForTesting
+ fun pickBestShape(baseShape: Path, shapeStr: String): ShapeDelegate {
+ val calcAreaDiff = areaDiffCalculator(baseShape)
+
+ // Find the shape with minimum area of divergent region.
+ var closestShape: ShapeDelegate = Circle()
+ var minAreaDiff = calcAreaDiff(closestShape)
+
+ // Try some common rounded rect edges
+ for (f in 0..20) {
+ val rectShape = RoundedSquare(f.toFloat() / 20)
+ val rectArea = calcAreaDiff(rectShape)
+ if (rectArea < minAreaDiff) {
+ minAreaDiff = rectArea
+ closestShape = rectShape
}
- } catch (e: IOException) {
- throw RuntimeException(e)
- } catch (e: XmlPullParserException) {
- throw RuntimeException(e)
}
- return result
+
+ // Use the generic shape only if we have more than .1% error
+ if (shapeStr.isNotEmpty() && minAreaDiff > AREA_DIFF_THRESHOLD) {
+ try {
+ val generic = GenericPathShape(shapeStr)
+ closestShape = generic
+ } catch (e: Exception) {
+ Log.e(TAG, "Error converting mask to generic shape", e)
+ }
+ }
+ return closestShape
}
+
+ /**
+ * Creates a rounded rect with the start point at the center of the top edge. This ensures a
+ * better animation since our shape paths also start at top-center of the bounding box.
+ */
+ fun createRoundedRect(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ cornerR: Float,
+ ) =
+ RoundedPolygon(
+ vertices =
+ floatArrayOf(
+ (left + right) / 2,
+ top,
+ right,
+ top,
+ right,
+ bottom,
+ left,
+ bottom,
+ left,
+ top,
+ ),
+ centerX = (left + right) / 2,
+ centerY = (top + bottom) / 2,
+ rounding = CornerRounding(cornerR),
+ )
}
}
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index bfa00bd..211c351 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -126,33 +126,43 @@
return true;
}
- if (isDestNewDb
+ boolean shouldMigrateToStrictlyTallerGrid = isDestNewDb
&& srcDeviceState.getColumns().equals(destDeviceState.getColumns())
- && srcDeviceState.getRows() < destDeviceState.getRows()) {
- // Only use this strategy when comparing the previous grid to the new grid and the
- // columns are the same and the destination has more rows
+ && srcDeviceState.getRows() < destDeviceState.getRows();
+ if (shouldMigrateToStrictlyTallerGrid) {
copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
-
- if (oneGridSpecs()) {
- DbReader destReader = new DbReader(
- target.getWritableDatabase(), TABLE_NAME, context);
- boolean shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.getRows());
- if (shouldShiftCells) {
- shiftTableByXCells(
- target.getWritableDatabase(),
- (destDeviceState.getRows() - srcDeviceState.getRows()),
- TABLE_NAME);
- }
- }
-
- // Save current configuration, so that the migration does not run again.
- destDeviceState.writeToPrefs(context);
- return true;
+ } else {
+ copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
}
- copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
long migrationStartTime = System.currentTimeMillis();
try (SQLiteTransaction t = new SQLiteTransaction(target.getWritableDatabase())) {
+
+ if (shouldMigrateToStrictlyTallerGrid) {
+ // This is a special case where if the grid is the same amount of columns but a
+ // larger amount of rows we simply copy over the source grid to the destination
+ // grid, rather than undergoing the general grid migration. If there are more icons
+ // on the bottom of the first page then we shift the icons down to the bottom of the
+ // grid so that the icons remain bottom-anchored.
+ if (oneGridSpecs()) {
+ DbReader destReader = new DbReader(
+ target.getWritableDatabase(), TABLE_NAME, context);
+ boolean shouldShiftCells =
+ shouldShiftCells(destReader, srcDeviceState.getRows());
+ if (shouldShiftCells) {
+ shiftTableByXCells(
+ target.getWritableDatabase(),
+ (destDeviceState.getRows() - srcDeviceState.getRows()),
+ TABLE_NAME);
+ }
+ }
+
+ // Save current configuration, so that the migration does not run again.
+ destDeviceState.writeToPrefs(context);
+ t.commit();
+ return true;
+ }
+
DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context);
DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context);
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index 6f86ae0..0b12af8 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -65,33 +65,42 @@
"$srcDeviceState\ndestDeviceState: $destDeviceState\nisDestNewDb: $isDestNewDb",
)
- // This is a special case where if the grid is the same amount of columns but a larger
- // amount of rows we simply copy over the source grid to the destination grid, rather
- // than undergoing the general grid migration.
- if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
- Log.d(TAG, "Migrating to strictly taller grid")
+ val shouldMigrateToStrtictlyTallerGrid =
+ shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)
+ if (shouldMigrateToStrtictlyTallerGrid) {
copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context)
- if (oneGridSpecs()) {
- val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
- val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows)
- if (shouldShiftCells) {
- shiftTableByXCells(
- target.writableDatabase,
- (destDeviceState.rows - srcDeviceState.rows),
- TABLE_NAME,
- )
- }
- }
- // Save current configuration, so that the migration does not run again.
- destDeviceState.writeToPrefs(context)
- return
+ } else {
+ copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)
}
- copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)
-
val migrationStartTime = System.currentTimeMillis()
try {
SQLiteTransaction(target.writableDatabase).use { t ->
+ // This is a special case where if the grid is the same amount of columns but a
+ // larger amount of rows we simply copy over the source grid to the destination
+ // grid, rather than undergoing the general grid migration. If there are more icons
+ // on the bottom of the first page then we shift the icons down to the bottom of the
+ // grid so that the icons remain bottom-anchored.
+ if (shouldMigrateToStrtictlyTallerGrid) {
+ Log.d(TAG, "Migrating to strictly taller grid")
+ if (oneGridSpecs()) {
+ val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
+ val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows)
+ if (shouldShiftCells) {
+ Log.i("TAGTAG", "should shift cells")
+ shiftTableByXCells(
+ target.writableDatabase,
+ (destDeviceState.rows - srcDeviceState.rows),
+ TABLE_NAME,
+ )
+ }
+ }
+ // Save current configuration, so that the migration does not run again.
+ destDeviceState.writeToPrefs(context)
+ t.commit()
+ return
+ }
+
val srcReader = DbReader(t.db, TMP_TABLE, context)
val destReader = DbReader(t.db, TABLE_NAME, context)
diff --git a/src/com/android/launcher3/shapes/AppShapesProvider.kt b/src/com/android/launcher3/shapes/AppShapesProvider.kt
index 8c2f181..3f4549a 100644
--- a/src/com/android/launcher3/shapes/AppShapesProvider.kt
+++ b/src/com/android/launcher3/shapes/AppShapesProvider.kt
@@ -21,27 +21,27 @@
object AppShapesProvider {
val shapes =
- if (Flags.newCustomizationPickerUi())
+ if (Flags.newCustomizationPickerUi() && Flags.enableLauncherIconShapes())
listOf(
AppShape(
"arch",
"arch",
- "M100 83.46C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116.884 93.916.1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0 77.614 0 100 22.386 100 50V83.46Z",
+ "M100 83.46C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0 77.614 0 100 22.386 100 50V83.46Z",
),
AppShape(
"4_sided_cookie",
"4 sided cookie",
- "M63.605 3C84.733-6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176-6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C-6.176 15.268 15.267-6.176 36.395 3L39.888 4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3Z",
+ "M63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3L39.888 4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3Z",
),
AppShape(
"seven_sided_cookie",
"7 sided cookie",
- "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82-2.742 55.18-2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24.273 66.266-2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
+ "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
),
AppShape(
"sunny",
"sunny",
- "M42.846 4.873C46.084-.531 53.916-.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C-.531 53.916-.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
+ "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C-.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
),
AppShape(
"circle",
@@ -51,8 +51,16 @@
AppShape(
"square",
"square",
- "M99.18 53.689C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18H46.311C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758.82 74.306.82 67.434.82 53.689L.82 46.311C.82 32.566.82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694.82 32.566.82 46.311.82L53.689.82C67.434.82 74.306.82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311V53.689Z\n",
+ "M99.18 53.689C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18H46.311C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689L.82 46.311C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82L53.689 .82C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311V53.689Z",
),
)
+ else if (Flags.newCustomizationPickerUi() && !Flags.enableLauncherIconShapes())
+ listOf(
+ AppShape(
+ "circle",
+ "circle",
+ "M99.18 50C99.18 77.162 77.162 99.18 50 99.18 22.838 99.18.82 77.162.82 50 .82 22.839 22.838.82 50 .82 77.162.82 99.18 22.839 99.18 50Z",
+ )
+ )
else emptyList()
}
diff --git a/src/com/android/launcher3/util/LayoutImportExportHelper.kt b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
new file mode 100644
index 0000000..4033f60
--- /dev/null
+++ b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 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.launcher3.util
+
+import android.app.blob.BlobHandle.createWithSha256
+import android.app.blob.BlobStoreManager
+import android.content.Context
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream
+import android.provider.Settings.Secure
+import com.android.launcher3.AutoInstallsLayout
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
+import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+import java.util.concurrent.CompletableFuture
+
+object LayoutImportExportHelper {
+ fun exportModelDbAsXmlFuture(context: Context): CompletableFuture<String> {
+ val future = CompletableFuture<String>()
+ exportModelDbAsXml(context) { xmlString -> future.complete(xmlString) }
+ return future
+ }
+
+ fun exportModelDbAsXml(context: Context, callback: (String) -> Unit) {
+ val model = LauncherAppState.getInstance(context).model
+
+ model.enqueueModelUpdateTask { _, dataModel, _ ->
+ val builder = LauncherLayoutBuilder()
+ dataModel.workspaceItems.forEach { info ->
+ val loc =
+ when (info.container) {
+ CONTAINER_DESKTOP ->
+ builder.atWorkspace(info.cellX, info.cellY, info.screenId)
+
+ CONTAINER_HOTSEAT -> builder.atHotseat(info.screenId)
+ else -> return@forEach
+ }
+ loc.addItem(context, info)
+ }
+ dataModel.appWidgets.forEach { info ->
+ builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(context, info)
+ }
+
+ val layoutXml = builder.build()
+ callback(layoutXml)
+ }
+ }
+
+ fun importModelFromXml(context: Context, xmlString: String) {
+ importModelFromXml(context, xmlString.toByteArray(StandardCharsets.UTF_8))
+ }
+
+ fun importModelFromXml(context: Context, data: ByteArray) {
+ val model = LauncherAppState.getInstance(context).model
+
+ val digest = MessageDigest.getInstance("SHA-256").digest(data)
+ val handle = createWithSha256(digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)
+ val blobManager = context.getSystemService(BlobStoreManager::class.java)!!
+
+ val resolver = context.contentResolver
+
+ blobManager.openSession(blobManager.createSession(handle)).use { session ->
+ AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) }
+ session.allowPublicAccess()
+
+ session.commit(ORDERED_BG_EXECUTOR) {
+ Secure.putString(resolver, LAYOUT_PROVIDER_KEY, createBlobProviderKey(digest))
+
+ MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
+ MAIN_EXECUTOR.submit { model.forceReload() }.get()
+ MODEL_EXECUTOR.submit {}.get()
+ Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
+ }
+ }
+ }
+
+ private fun LauncherLayoutBuilder.ItemTarget.addItem(context: Context, info: ItemInfo) {
+ val userType: String? =
+ when (UserCache.INSTANCE.get(context).getUserInfo(info.user).type) {
+ UserIconInfo.TYPE_WORK -> AutoInstallsLayout.USER_TYPE_WORK
+ UserIconInfo.TYPE_CLONED -> AutoInstallsLayout.USER_TYPE_CLONED
+ else -> null
+ }
+ when (info.itemType) {
+ ITEM_TYPE_APPLICATION ->
+ info.targetComponent?.let { c -> putApp(c.packageName, c.className, userType) }
+ ITEM_TYPE_DEEP_SHORTCUT ->
+ ShortcutKey.fromItemInfo(info).let { key ->
+ putShortcut(key.packageName, key.id, userType)
+ }
+ ITEM_TYPE_FOLDER ->
+ (info as FolderInfo).let { folderInfo ->
+ putFolder(folderInfo.title?.toString() ?: "").also { folderBuilder ->
+ folderInfo.getContents().forEach { folderContent ->
+ folderBuilder.addItem(context, folderContent)
+ }
+ }
+ }
+ ITEM_TYPE_APPWIDGET ->
+ putWidget(
+ (info as LauncherAppWidgetInfo).providerName.packageName,
+ info.providerName.className,
+ info.spanX,
+ info.spanY,
+ userType,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Android.bp b/tests/Android.bp
index f666bba..4bc654c 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -226,6 +226,5 @@
],
instrumentation_for: "Launcher3",
plugins: ["dagger2-compiler"],
- upstream: true,
strict_mode: false,
}
diff --git a/tests/Launcher3Tests.xml b/tests/Launcher3Tests.xml
index a876860..56dd6a4 100644
--- a/tests/Launcher3Tests.xml
+++ b/tests/Launcher3Tests.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="settings put system show_touches 1" />
<option name="run-command" value="setprop pixel_legal_joint_permission_v2 true" />
+
+ <option name="run-command" value="settings put global verifier_verify_adb_installs 0" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
index a62258c..bf8e8b1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
@@ -17,9 +17,6 @@
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -41,7 +38,6 @@
import com.android.launcher3.celllayout.testgenerator.RandomMultiBoardGenerator;
import com.android.launcher3.util.ActivityContextWrapper;
import com.android.launcher3.util.rule.TestStabilityRule;
-import com.android.launcher3.util.rule.TestStabilityRule.Stability;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
import org.junit.Rule;
@@ -82,7 +78,6 @@
* This test reads existing test cases and makes sure the CellLayout produces the same
* output for each of them for a given input.
*/
- @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
@Test
public void testAllCases() throws IOException {
List<ReorderAlgorithmUnitTestCase> testCases = getTestCases(
@@ -125,7 +120,6 @@
/**
* Same as above but testing the Multipage CellLayout.
*/
- @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
@Test
public void generateValidTests_Multi() {
Random generator = new Random(SEED);
diff --git a/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt b/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
new file mode 100644
index 0000000..311676a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2025 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.launcher3.graphics
+
+import android.graphics.Matrix
+import android.graphics.Matrix.ScaleToFit.FILL
+import android.graphics.Path
+import android.graphics.Path.Direction
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.Region
+import android.platform.uiautomatorhelpers.DeviceHelpers.context
+import android.view.View
+import androidx.core.graphics.PathParser
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.graphics.IconShape.Circle
+import com.android.launcher3.graphics.IconShape.Companion.AREA_CALC_SIZE
+import com.android.launcher3.graphics.IconShape.Companion.AREA_DIFF_THRESHOLD
+import com.android.launcher3.graphics.IconShape.Companion.areaDiffCalculator
+import com.android.launcher3.graphics.IconShape.Companion.pickBestShape
+import com.android.launcher3.graphics.IconShape.GenericPathShape
+import com.android.launcher3.graphics.IconShape.RoundedSquare
+import com.android.launcher3.icons.GraphicsUtils
+import com.android.launcher3.views.ClipPathView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IconShapeTest {
+
+ @Test
+ fun `areaDiffCalculator increases with outwards shape`() {
+ val diffCalculator =
+ areaDiffCalculator(
+ Path().apply {
+ addCircle(
+ AREA_CALC_SIZE / 2f,
+ AREA_CALC_SIZE / 2f,
+ AREA_CALC_SIZE / 2f,
+ Direction.CW,
+ )
+ }
+ )
+ assertThat(diffCalculator(Circle())).isLessThan(AREA_DIFF_THRESHOLD)
+ assertThat(diffCalculator(Circle())).isLessThan(diffCalculator(RoundedSquare(.9f)))
+ assertThat(diffCalculator(RoundedSquare(.9f)))
+ .isLessThan(diffCalculator(RoundedSquare(.8f)))
+ assertThat(diffCalculator(RoundedSquare(.8f)))
+ .isLessThan(diffCalculator(RoundedSquare(.7f)))
+ assertThat(diffCalculator(RoundedSquare(.7f)))
+ .isLessThan(diffCalculator(RoundedSquare(.6f)))
+ assertThat(diffCalculator(RoundedSquare(.6f)))
+ .isLessThan(diffCalculator(RoundedSquare(.5f)))
+ }
+
+ @Test
+ fun `areaDiffCalculator increases with inwards shape`() {
+ val diffCalculator = areaDiffCalculator(roundedRectPath(0.5f))
+ assertThat(diffCalculator(RoundedSquare(.5f))).isLessThan(AREA_DIFF_THRESHOLD)
+ assertThat(diffCalculator(RoundedSquare(.5f)))
+ .isLessThan(diffCalculator(RoundedSquare(.6f)))
+ assertThat(diffCalculator(RoundedSquare(.5f)))
+ .isLessThan(diffCalculator(RoundedSquare(.4f)))
+ }
+
+ @Test
+ fun `pickBestShape picks circle`() {
+ val r = AREA_CALC_SIZE / 2
+ val pathStr = "M 50 0 a 50 50 0 0 1 0 100 a 50 50 0 0 1 0 -100"
+ val path = Path().apply { addCircle(r.toFloat(), r.toFloat(), r.toFloat(), Direction.CW) }
+ assertThat(pickBestShape(path, pathStr)).isInstanceOf(Circle::class.java)
+ }
+
+ @Test
+ fun `pickBestShape picks rounded rect`() {
+ val factor = 0.5f
+ var shape = pickBestShape(roundedRectPath(factor), roundedRectString(factor))
+ assertThat(shape).isInstanceOf(RoundedSquare::class.java)
+ assertThat((shape as RoundedSquare).radiusRatio).isEqualTo(factor)
+
+ val factor2 = 0.2f
+ shape = pickBestShape(roundedRectPath(factor2), roundedRectString(factor2))
+ assertThat(shape).isInstanceOf(RoundedSquare::class.java)
+ assertThat((shape as RoundedSquare).radiusRatio).isEqualTo(factor2)
+ }
+
+ @Test
+ fun `pickBestShape picks generic shape`() {
+ val path = cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE))
+ val pathStr = FOUR_SIDED_COOKIE
+ val shape = pickBestShape(path, pathStr)
+ assertThat(shape).isInstanceOf(GenericPathShape::class.java)
+
+ val diffCalculator = areaDiffCalculator(path)
+ assertThat(diffCalculator(shape)).isLessThan(AREA_DIFF_THRESHOLD)
+ }
+
+ @Test
+ fun `generic shape creates smooth animation`() {
+ val shape = GenericPathShape(FOUR_SIDED_COOKIE)
+ val target = TestClipView()
+ val anim =
+ shape.createRevealAnimator(
+ target,
+ Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE),
+ Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE),
+ AREA_CALC_SIZE * .25f,
+ false,
+ )
+
+ // Verify that the start rect is similar to initial path
+ anim.setCurrentFraction(0f)
+ assertThat(
+ getAreaDiff(
+ target.currentClip!!,
+ cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)),
+ )
+ )
+ .isLessThan(AREA_CALC_SIZE)
+
+ // Verify that end rect is similar to end path
+ anim.setCurrentFraction(1f)
+ assertThat(getAreaDiff(target.currentClip!!, roundedRectPath(0.5f)))
+ .isLessThan(AREA_CALC_SIZE)
+
+ // Ensure that when running animation, area increases smoothly. We run the animation over
+ // [steps] and verify increase of max 5 times the linear diff increase
+ val steps = 1000
+ val incrementalDiff =
+ getAreaDiff(
+ cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)),
+ roundedRectPath(0.5f),
+ ) * 5 / steps
+ var lastPath = cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE))
+ for (progress in 1..steps) {
+ anim.setCurrentFraction(progress / 1000f)
+ val currentPath = Path(target.currentClip!!)
+ assertThat(getAreaDiff(lastPath, currentPath)).isLessThan(incrementalDiff)
+ lastPath = currentPath
+ }
+ assertThat(getAreaDiff(lastPath, roundedRectPath(0.5f))).isLessThan(AREA_CALC_SIZE)
+ }
+
+ private fun roundedRectPath(factor: Float) =
+ Path().apply {
+ val r = factor * AREA_CALC_SIZE / 2
+ addRoundRect(
+ 0f,
+ 0f,
+ AREA_CALC_SIZE.toFloat(),
+ AREA_CALC_SIZE.toFloat(),
+ r,
+ r,
+ Direction.CW,
+ )
+ }
+
+ private fun roundedRectString(factor: Float): String {
+ val s = 100f
+ val r = (factor * s / 2)
+ val t = s - r
+ return "M $r 0 " +
+ "L $t 0 " +
+ "A $r $r 0 0 1 $s $r " +
+ "L $s $t " +
+ "A $r $r 0 0 1 $t $s " +
+ "L $r $s " +
+ "A $r $r 0 0 1 0 $t " +
+ "L 0 $r " +
+ "A $r $r 0 0 1 $r 0 Z"
+ }
+
+ private fun getAreaDiff(p1: Path, p2: Path): Int {
+ val fullRegion = Region(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)
+ val iconRegion = Region().apply { setPath(p1, fullRegion) }
+ val shapeRegion = Region().apply { setPath(p2, fullRegion) }
+ shapeRegion.op(iconRegion, Region.Op.XOR)
+ return GraphicsUtils.getArea(shapeRegion)
+ }
+
+ class TestClipView : View(context), ClipPathView {
+
+ var currentClip: Path? = null
+
+ override fun setClipPath(clipPath: Path?) {
+ currentClip = clipPath
+ }
+ }
+
+ companion object {
+ const val FOUR_SIDED_COOKIE =
+ "M63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3L39.888 4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3Z"
+
+ private fun cookiePath(bounds: Rect) =
+ PathParser.createPathFromPathData(FOUR_SIDED_COOKIE).apply {
+ transform(
+ Matrix().apply { setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(bounds), FILL) }
+ )
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index e2f9feb9a..2e02eb0 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -64,6 +64,7 @@
@Test
@PortraitLandscape
@PlatinumTest(focusArea = "launcher")
+ @ScreenRecordRule.ScreenRecord // b/383917141
public void testDragToFolder() {
// TODO: add the use case to drag an icon to an existing folder. Currently it either fails
// on tablets or phones due to difference in resolution.
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 1816030..20684b3 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -110,6 +110,9 @@
@PortraitLandscape
@PlatinumTest(focusArea = "launcher")
public void testUninstallFromAllApps() throws Exception {
+ // Ensure no existing app icons on the workspace cause scroll to all apps interruptions
+ mLauncher.clearLauncherData();
+
installDummyAppAndWaitForUIUpdate();
try {
Workspace workspace = mLauncher.getWorkspace();
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index d49168f..124c18f 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -76,8 +76,8 @@
mTest.mDevice.setOrientationNatural();
mTest.executeOnLauncher(launcher ->
{
- LauncherPrefs.get(launcher).put(FIXED_LANDSCAPE_MODE, false);
if (launcher != null) {
+ LauncherPrefs.get(launcher).put(FIXED_LANDSCAPE_MODE, false);
launcher.getRotationHelper().forceAllowRotationForTesting(false);
}
});
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 2fb7987..7e8d759 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -51,6 +51,7 @@
import com.android.launcher3.util.BlockingBroadcastReceiver;
import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.ShellCommandRule;
import org.junit.Before;
@@ -74,6 +75,9 @@
@Rule
public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher();
+ @Rule
+ public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
+
private String mCallbackAction;
private String mShortcutId;
private int mAppWidgetId;
@@ -87,6 +91,7 @@
@Test
public void testEmpty() throws Throwable { /* needed while the broken tests are being fixed */ }
+ @ScreenRecordRule.ScreenRecord // b/386243192
@Test
public void testPinWidgetNoConfig() throws Throwable {
runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo
@@ -95,6 +100,7 @@
.equals(AppWidgetNoConfig.class.getName()));
}
+ @ScreenRecordRule.ScreenRecord // b/386243192
@Test
public void testPinWidgetNoConfig_customPreview() throws Throwable {
// Command to set custom preview
@@ -108,6 +114,7 @@
.equals(AppWidgetNoConfig.class.getName()), command);
}
+ @ScreenRecordRule.ScreenRecord // b/386243192
@Test
public void testPinWidgetWithConfig() throws Throwable {
runTest("pinWidgetWithConfig", true,
@@ -117,6 +124,7 @@
.equals(AppWidgetWithConfig.class.getName()));
}
+ @ScreenRecordRule.ScreenRecord // b/386243192
@Test
public void testPinShortcut() throws Throwable {
// Command to set the shortcut id
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
index c852729..a123170 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -61,8 +61,7 @@
setThemeEnabled(false);
new FavoriteItemsTransaction(targetContext()).commit();
loadLauncherSync();
- goToState(LauncherState.ALL_APPS);
- freezeAllApps();
+ switchToAllApps();
scrollToAppIcon(APP_NAME);
BubbleTextView btv = getFromLauncher(
@@ -76,8 +75,7 @@
setThemeEnabled(false);
new FavoriteItemsTransaction(targetContext()).commit();
loadLauncherSync();
- goToState(LauncherState.ALL_APPS);
- freezeAllApps();
+ switchToAllApps();
scrollToAppIcon(TEST_APP_NAME);
BubbleTextView btv = getFromLauncher(l -> findBtv(TEST_APP_NAME, l.getAppsView()));
@@ -95,8 +93,7 @@
setThemeEnabled(true);
new FavoriteItemsTransaction(targetContext()).commit();
loadLauncherSync();
- goToState(LauncherState.ALL_APPS);
- freezeAllApps();
+ switchToAllApps();
scrollToAppIcon(APP_NAME);
BubbleTextView btv = getFromLauncher(l ->
@@ -109,8 +106,7 @@
public void testShortcutIconWithTheme() throws Exception {
setThemeEnabled(true);
loadLauncherSync();
- goToState(LauncherState.ALL_APPS);
- freezeAllApps();
+ switchToAllApps();
scrollToAppIcon(TEST_APP_NAME);
BubbleTextView btv = getFromLauncher(l -> findBtv(TEST_APP_NAME, l.getAppsView()));
@@ -158,6 +154,13 @@
}
}
+ private void switchToAllApps() {
+ goToState(LauncherState.ALL_APPS);
+ waitForState("Launcher internal state didn't switch to All Apps",
+ () -> LauncherState.ALL_APPS);
+ freezeAllApps();
+ }
+
private void scrollToAppIcon(String appName) {
executeOnLauncher(l -> {
l.hideKeyboard();