diff --git a/res/color-night-v31/popup_color_first.xml b/res/color-night-v31/popup_shade_first.xml similarity index 100% rename from res/color-night-v31/popup_color_first.xml rename to res/color-night-v31/popup_shade_first.xml diff --git a/res/color-night-v31/popup_color_second.xml b/res/color-night-v31/popup_shade_second.xml similarity index 100% rename from res/color-night-v31/popup_color_second.xml rename to res/color-night-v31/popup_shade_second.xml diff --git a/res/color-night-v31/popup_color_third.xml b/res/color-night-v31/popup_shade_third.xml similarity index 100% rename from res/color-night-v31/popup_color_third.xml rename to res/color-night-v31/popup_shade_third.xml diff --git a/res/color-v31/popup_color_first.xml b/res/color-v31/popup_shade_first.xml similarity index 100% rename from res/color-v31/popup_color_first.xml rename to res/color-v31/popup_shade_first.xml diff --git a/res/color-v31/popup_color_second.xml b/res/color-v31/popup_shade_second.xml similarity index 100% rename from res/color-v31/popup_color_second.xml rename to res/color-v31/popup_shade_second.xml diff --git a/res/color-v31/popup_color_third.xml b/res/color-v31/popup_shade_third.xml similarity index 100% rename from res/color-v31/popup_color_third.xml rename to res/color-v31/popup_shade_third.xml diff --git a/res/color/popup_color_first.xml b/res/color/popup_shade_first.xml similarity index 93% rename from res/color/popup_color_first.xml rename to res/color/popup_shade_first.xml index d9999a20c8..151190b6d4 100644 --- a/res/color/popup_color_first.xml +++ b/res/color/popup_shade_first.xml @@ -14,5 +14,5 @@ limitations under the License. --> - + diff --git a/res/color/popup_color_second.xml b/res/color/popup_shade_second.xml similarity index 93% rename from res/color/popup_color_second.xml rename to res/color/popup_shade_second.xml index d9999a20c8..86608501f0 100644 --- a/res/color/popup_color_second.xml +++ b/res/color/popup_shade_second.xml @@ -14,5 +14,5 @@ limitations under the License. --> - + diff --git a/res/color/popup_color_third.xml b/res/color/popup_shade_third.xml similarity index 92% rename from res/color/popup_color_third.xml rename to res/color/popup_shade_third.xml index d7e9e794e2..95447288fd 100644 --- a/res/color/popup_color_third.xml +++ b/res/color/popup_shade_third.xml @@ -14,5 +14,5 @@ limitations under the License. --> - + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index ed1db30155..00cf31c65c 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -25,6 +25,9 @@ + + + diff --git a/res/values/colors.xml b/res/values/colors.xml index 76e821d72a..76fd1d7753 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -53,6 +53,13 @@ #202124 #757575 + #F9F9F9 + #F1F1F1 + #E2E2E2 + #303030 + #262626 + #1B1B1B + #FFF #757575 @@ -62,8 +69,8 @@ #FFF #FF000000 - #FFFFFF - #FF3C4043 + #F9F9F9 + #464746 ?attr/colorPrimary diff --git a/res/values/styles.xml b/res/values/styles.xml index dc99893bea..416c711edc 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -37,6 +37,9 @@ @color/popup_color_primary_light @color/popup_color_secondary_light @color/popup_color_tertiary_light + @color/popup_shade_first_light + @color/popup_shade_second_light + @color/popup_shade_third_light @color/popup_notification_dot_light false false @@ -100,6 +103,9 @@ @color/popup_color_secondary_dark @color/popup_color_tertiary_dark @color/popup_notification_dot_dark + @color/popup_shade_first_dark + @color/popup_shade_second_dark + @color/popup_shade_third_dark @style/WidgetContainerTheme.Dark @color/folder_dot_color @color/folder_background_dark diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java index c946c7218f..b9f183c713 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java @@ -58,7 +58,7 @@ import java.util.List; @RunWith(RobolectricTestRunner.class) public final class WidgetsDiffReporterTest { - private static final String TEST_PACKAGE_PREFIX = "com.google.test"; + private static final String TEST_PACKAGE_PREFIX = "com.android.test"; private static final WidgetListBaseRowEntryComparator COMPARATOR = new WidgetListBaseRowEntryComparator(); @@ -241,6 +241,30 @@ public final class WidgetsDiffReporterTest { assertThat(currentList).containsExactlyElementsIn(newList); } + @Test + public void headersContentsMix_contentMaxSpanSizeModified_shouldInvokeCorrectCallbacks() { + // GIVEN the current list has app headers [A, B, E content]. + ArrayList currentList = new ArrayList<>( + List.of(mHeaderA, mHeaderB, mContentE)); + // GIVEN the new list has max span size in "E content" modified. + List newList = List.of( + mHeaderA, + mHeaderB, + new WidgetsListContentEntry( + mContentE.mPkgItem, + mContentE.mTitleSectionName, + mContentE.mWidgets, + mContentE.getMaxSpanSizeInCells() + 1)); + + // WHEN computing the list difference. + mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); + + // THEN notify "E content" has been changed. + verify(mAdapter).notifyItemChanged(/* position= */ 2); + // THEN the current list contains all elements from the new list. + assertThat(currentList).containsExactlyElementsIn(newList); + } + private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName, int numOfWidgets) { diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java index 2d22c45e2c..106cac0fdf 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java @@ -49,8 +49,10 @@ import java.util.Map; @RunWith(RobolectricTestRunner.class) public final class WidgetsListContentEntryTest { - private static final String PACKAGE_NAME = "com.google.test"; - private final PackageItemInfo mPackageItemInfo = new PackageItemInfo(PACKAGE_NAME); + private static final String PACKAGE_NAME = "com.android.test"; + private static final String PACKAGE_NAME_2 = "com.android.test2"; + private final PackageItemInfo mPackageItemInfo1 = new PackageItemInfo(PACKAGE_NAME); + private final PackageItemInfo mPackageItemInfo2 = new PackageItemInfo(PACKAGE_NAME_2); private final ComponentName mWidget1 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget1"); private final ComponentName mWidget2 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget2"); private final ComponentName mWidget3 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget3"); @@ -91,7 +93,7 @@ public final class WidgetsListContentEntryTest { WidgetItem widgetItem3 = createWidgetItem(mWidget3, /* spanX= */ 2, /* spanY= */ 3); // WHEN creates a WidgetsListRowEntry with the unsorted widgets. - WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo, + WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo1, /* titleSectionName= */ "T", List.of(widgetItem1, widgetItem2, widgetItem3)); @@ -100,7 +102,7 @@ public final class WidgetsListContentEntryTest { .containsExactly(widgetItem3, widgetItem1, widgetItem2) .inOrder(); assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T"); - assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo); + assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo1); } @Test @@ -114,7 +116,7 @@ public final class WidgetsListContentEntryTest { WidgetItem widgetItem3 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 2); // WHEN creates a WidgetsListRowEntry with the unsorted widgets. - WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo, + WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo1, /* titleSectionName= */ "T", List.of(widgetItem1, widgetItem2, widgetItem3)); @@ -124,7 +126,7 @@ public final class WidgetsListContentEntryTest { .containsExactly(widgetItem2, widgetItem3, widgetItem1) .inOrder(); assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T"); - assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo); + assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo1); } @Test @@ -140,7 +142,7 @@ public final class WidgetsListContentEntryTest { WidgetItem widgetItem4 = createWidgetItem(mWidget3, /* spanX= */ 2, /* spanY= */ 2); // WHEN creates a WidgetsListRowEntry with the unsorted widgets. - WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo, + WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo1, /* titleSectionName= */ "T", List.of(widgetItem1, widgetItem2, widgetItem3, widgetItem4)); @@ -151,9 +153,96 @@ public final class WidgetsListContentEntryTest { .containsExactly(widgetItem4, widgetItem2, widgetItem1, widgetItem3) .inOrder(); assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T"); - assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo); + assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo1); } + @Test + public void equals_entriesWithDifferentPackageItemInfo_returnFalse() { + WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3); + WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "T", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 3); + WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry( + mPackageItemInfo2, + /* titleSectionName= */ "T", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 3); + + assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isFalse(); + } + + @Test + public void equals_entriesWithDifferentTitleSectionName_returnFalse() { + WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3); + WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "T", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 3); + WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "S", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 3); + + assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isFalse(); + } + + @Test + public void equals_entriesWithDifferentWidgetsList_returnFalse() { + WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3); + WidgetItem widgetItem2 = createWidgetItem(mWidget2, /* spanX= */ 2, /* spanY= */ 3); + WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "T", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 3); + WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "T", + List.of(widgetItem2), + /* maxSpanSizeInCells= */ 3); + + assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isFalse(); + } + + @Test + public void equals_entriesWithDifferentMaxSpanSize_returnFalse() { + WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3); + WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "T", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 3); + WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "T", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 2); + + assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isFalse(); + } + + @Test + public void equals_entriesWithSameContents_returnTrue() { + WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3); + WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "T", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 3); + WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry( + mPackageItemInfo1, + /* titleSectionName= */ "T", + List.of(widgetItem1), + /* maxSpanSizeInCells= */ 3); + + assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isTrue(); + } + + private WidgetItem createWidgetItem(ComponentName componentName, int spanX, int spanY) { String label = mWidgetsToLabels.get(componentName); ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager()); diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java index a89fb3b618..b7fe3480fa 100644 --- a/src/com/android/launcher3/popup/ArrowPopup.java +++ b/src/com/android/launcher3/popup/ArrowPopup.java @@ -183,12 +183,12 @@ public abstract class ArrowPopup> if (isAboveAnotherSurface) { mColors = new int[] { - getColorStateList(context, R.color.popup_color_first).getDefaultColor()}; + getColorStateList(context, R.color.popup_shade_first).getDefaultColor()}; } else { mColors = new int[] { - getColorStateList(context, R.color.popup_color_first).getDefaultColor(), - getColorStateList(context, R.color.popup_color_second).getDefaultColor(), - getColorStateList(context, R.color.popup_color_third).getDefaultColor()}; + getColorStateList(context, R.color.popup_shade_first).getDefaultColor(), + getColorStateList(context, R.color.popup_shade_second).getDefaultColor(), + getColorStateList(context, R.color.popup_shade_third).getDefaultColor()}; } } diff --git a/src/com/android/launcher3/views/TopRoundedCornerView.java b/src/com/android/launcher3/views/TopRoundedCornerView.java index 5519df1f49..92cce92ae7 100644 --- a/src/com/android/launcher3/views/TopRoundedCornerView.java +++ b/src/com/android/launcher3/views/TopRoundedCornerView.java @@ -17,12 +17,10 @@ package com.android.launcher3.views; import android.content.Context; import android.graphics.Canvas; -import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.util.AttributeSet; -import com.android.launcher3.R; import com.android.launcher3.util.Themes; /** @@ -34,41 +32,23 @@ public class TopRoundedCornerView extends SpringRelativeLayout { private final Path mClipPath = new Path(); private float[] mRadii; - private final Paint mNavBarScrimPaint; - private int mNavBarScrimHeight = 0; - public TopRoundedCornerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); float radius = Themes.getDialogCornerRadius(context); mRadii = new float[] {radius, radius, radius, radius, 0, 0, 0, 0}; - - mNavBarScrimPaint = new Paint(); - mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor)); } public TopRoundedCornerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public void setNavBarScrimHeight(int height) { - if (mNavBarScrimHeight != height) { - mNavBarScrimHeight = height; - invalidate(); - } - } - @Override public void draw(Canvas canvas) { canvas.save(); canvas.clipPath(mClipPath); super.draw(canvas); canvas.restore(); - - if (mNavBarScrimHeight > 0) { - canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), - mNavBarScrimPaint); - } } @Override diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java index 0328cf68ba..73b17f1d4f 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java @@ -26,14 +26,39 @@ import java.util.List; */ public final class WidgetsListContentEntry extends WidgetsListBaseEntry { + private final int mMaxSpanSizeInCells; + + /** + * Constructor for {@link WidgetsListContentEntry}. + * + * @param pkgItem package info associated with the entry + * @param titleSectionName title section name associated with the entry. + * @param items list of widgets for the package. + */ public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName, List items) { + this(pkgItem, titleSectionName, items, /* maxSpanSizeInCells= */ 0); + } + + /** + * Constructor for {@link WidgetsListContentEntry}. + * + * @param pkgItem package info associated with the entry + * @param titleSectionName title section name associated with the entry. + * @param items list of widgets for the package. + * @param maxSpanSizeInCells the max horizontal span in cells that is allowed for grouping more + * than one widgets in a table row. + */ + public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName, + List items, int maxSpanSizeInCells) { super(pkgItem, titleSectionName, items); + mMaxSpanSizeInCells = maxSpanSizeInCells; } @Override public String toString() { - return "Content:" + mPkgItem.packageName + ":" + mWidgets.size(); + return "Content:" + mPkgItem.packageName + ":" + mWidgets.size() + " maxSpanSizeInCells: " + + mMaxSpanSizeInCells; } @Override @@ -42,11 +67,36 @@ public final class WidgetsListContentEntry extends WidgetsListBaseEntry { return RANK_WIDGETS_LIST_CONTENT; } + /** + * Returns a copy of this {@link WidgetsListContentEntry} with updated + * {@param maxSpanSizeInCells}. + * + * @param maxSpanSizeInCells the maximum horizontal span in cells that is allowed for grouping + * more than one widgets in a table row. + */ + public WidgetsListContentEntry withMaxSpanSize(int maxSpanSizeInCells) { + if (mMaxSpanSizeInCells == maxSpanSizeInCells) return this; + return new WidgetsListContentEntry( + mPkgItem, + mTitleSectionName, + mWidgets, + /* maxSpanSizeInCells= */ maxSpanSizeInCells); + } + + /** + * Returns the max horizontal span size in cells that is allowed for grouping more than one + * widget in a table row. + */ + public int getMaxSpanSizeInCells() { + return mMaxSpanSizeInCells; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof WidgetsListContentEntry)) return false; WidgetsListContentEntry otherEntry = (WidgetsListContentEntry) obj; return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem) - && mTitleSectionName.equals(otherEntry.mTitleSectionName); + && mTitleSectionName.equals(otherEntry.mTitleSectionName) + && mMaxSpanSizeInCells == otherEntry.mMaxSpanSizeInCells; } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java index dfe447a38e..99374f5eef 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java @@ -115,7 +115,7 @@ public class WidgetsDiffReporter { // or did the widget size and desc, span, etc change? if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem) || hasHeaderUpdated(orgRowEntry, newRowEntry) - || hasWidgetsListChanged(orgRowEntry, newRowEntry)) { + || hasWidgetsListContentChanged(orgRowEntry, newRowEntry)) { index = currentEntries.indexOf(orgRowEntry); currentEntries.set(index, newRowEntry); mListener.notifyItemChanged(index); @@ -158,17 +158,15 @@ public class WidgetsDiffReporter { /** * Returns {@code true} if both {@code curRow} & {@code newRow} are - * {@link WidgetsListContentEntry}s with a different list of widgets. + * {@link WidgetsListContentEntry}s with a different list or arrangement of widgets. */ - private boolean hasWidgetsListChanged(WidgetsListBaseEntry curRow, + private boolean hasWidgetsListContentChanged(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) { if (!(curRow instanceof WidgetsListContentEntry) || !(newRow instanceof WidgetsListContentEntry)) { return false; } - WidgetsListContentEntry orgRowEntry = (WidgetsListContentEntry) curRow; - WidgetsListContentEntry newRowEntry = (WidgetsListContentEntry) newRow; - return !orgRowEntry.mWidgets.equals(newRowEntry.mWidgets); + return !curRow.equals(newRow); } /** diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 1a58bb066d..0106ef5d5f 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -323,7 +323,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet clearNavBarColor(); } - ((TopRoundedCornerView) mContent).setNavBarScrimHeight(mInsets.bottom); requestLayout(); } diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java index b668c90031..08a2263eff 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java @@ -111,6 +111,7 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC @Nullable private PackageUserKey mPendingClickHeader; private final int mShortcutPreviewPadding; private final int mSpacingBetweenEntries; + private int mMaxSpanSize = 4; private final WidgetPreviewLoadedCallback mPreviewLoadedCallback = ignored -> updateVisibleEntries(); @@ -249,10 +250,14 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC .filter(entry -> (mFilter == null || mFilter.test(entry)) && mHeaderAndSelectedContentFilter.test(entry)) .map(entry -> { - // Adjust the original entries to expand headers for the selected content. if (entry instanceof WidgetsListBaseEntry.Header && matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) { + // Adjust the original entries to expand headers for the selected content. return ((WidgetsListBaseEntry.Header) entry).withWidgetListShown(); + } else if (entry instanceof WidgetsListContentEntry) { + // Adjust the original content entries to accommodate for the current + // maxSpanSize. + return ((WidgetsListContentEntry) entry).withMaxSpanSize(mMaxSpanSize); } return entry; }) @@ -491,20 +496,12 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC } /** - * Sets the max horizontal spans that are allowed for grouping more than one widgets in a table - * row. - * - *

If there is only one widget in a row, that widget horizontal span is allowed to exceed - * {@code maxHorizontalSpans}. - *

Let's say the max horizontal spans is set to 5. Widgets can be grouped in the same row if - * their total horizontal spans added don't exceed 5. - * Example 1: Row 1: 2x2, 2x3, 1x1. Total horizontal spans is 5. This is okay. - * Example 2: Row 1: 2x2, 4x3, 1x1. the total horizontal spans is 7. This is wrong. - * 4x3 and 1x1 should be moved to a new row. - * Example 3: Row 1: 6x4. This is okay because this is the only item in the row. + * Sets the max horizontal span in cells that is allowed for grouping more than one widget in a + * table row. */ public void setMaxHorizontalSpansPerRow(int maxHorizontalSpans) { - mWidgetsListTableViewHolderBinder.setMaxSpansPerRow(maxHorizontalSpans); + mMaxSpanSize = maxHorizontalSpans; + updateVisibleEntries(); } /** diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java index 7b526639f5..57dec1401e 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java @@ -49,7 +49,6 @@ public final class WidgetsListTableViewHolderBinder private static final boolean DEBUG = false; private static final String TAG = "WidgetsListRowViewHolderBinder"; - private int mMaxSpansPerRow = 4; private final LayoutInflater mLayoutInflater; private final OnClickListener mIconClickListener; private final OnLongClickListener mIconLongClickListener; @@ -82,10 +81,6 @@ public final class WidgetsListTableViewHolderBinder mApplyBitmapDeferred = applyBitmapDeferred; } - public void setMaxSpansPerRow(int maxSpansPerRow) { - mMaxSpansPerRow = maxSpansPerRow; - } - @Override public WidgetsRowViewHolder newViewHolder(ViewGroup parent) { if (DEBUG) { @@ -113,7 +108,8 @@ public final class WidgetsListTableViewHolderBinder position == mWidgetsListAdapter.getItemCount() - 1 ? LAST : MIDDLE); List> widgetItemsTable = - WidgetsTableUtils.groupWidgetItemsIntoTable(entry.mWidgets, mMaxSpansPerRow); + WidgetsTableUtils.groupWidgetItemsIntoTable( + entry.mWidgets, entry.getMaxSpanSizeInCells()); recycleTableBeforeBinding(table, widgetItemsTable); // Bind the widget items. for (int i = 0; i < widgetItemsTable.size(); i++) { diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java index 7294a3ab31..54aaf93ac0 100644 --- a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java +++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java @@ -56,6 +56,13 @@ public final class WidgetsTableUtils { * 3. The order shortcuts are grouped together in the same row until their total horizontal * spans exceed the {@code maxSpansPerRow} - 1. * 4. If there is only one widget in a row, its width may exceed the {@code maxSpansPerRow}. + * + *

Let's say the {@code maxSpansPerRow} is set to 6. Widgets can be grouped in the same row + * if their total horizontal spans added don't exceed 5. + * Example 1: Row 1: 2x2, 2x3, 1x1. Total horizontal spans is 5. This is okay. + * Example 2: Row 1: 2x2, 4x3, 1x1. the total horizontal spans is 7. This is wrong. 4x3 and 1x1 + * should be moved to a new row. + * Example 3: Row 1: 6x4. This is okay because this is the only item in the row. */ public static List> groupWidgetItemsIntoTable( List widgetItems, final int maxSpansPerRow) { diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index 2712bc02ce..bf4eba0f64 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -65,6 +65,7 @@ import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.FailureWatcher; import com.android.launcher3.util.rule.LauncherActivityRule; +import com.android.launcher3.util.rule.ScreenRecordRule; import com.android.launcher3.util.rule.ShellCommandRule; import com.android.launcher3.util.rule.TestStabilityRule; @@ -204,6 +205,9 @@ public abstract class AbstractLauncherUiTest { public ShellCommandRule mDisableHeadsUpNotification = ShellCommandRule.disableHeadsUpNotification(); + @Rule + public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule(); + protected void clearPackageData(String pkg) throws IOException, InterruptedException { final CountDownLatch count = new CountDownLatch(2); final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 06bc26a2a2..4dd44f4193 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -36,6 +36,7 @@ import com.android.launcher3.tapl.AppIconMenu; import com.android.launcher3.tapl.AppIconMenuItem; import com.android.launcher3.tapl.Widgets; import com.android.launcher3.tapl.Workspace; +import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; import com.android.launcher3.views.OptionsPopupView; import com.android.launcher3.widget.picker.WidgetsFullSheet; import com.android.launcher3.widget.picker.WidgetsRecyclerView; @@ -92,6 +93,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { } @Test + @ScreenRecord //b/187080582 public void testDevicePressMenu() throws Exception { mDevice.pressMenu(); mDevice.waitForIdle(); diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java index 822fefc208..745dc220ba 100644 --- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java +++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java @@ -42,6 +42,7 @@ import com.android.launcher3.testcomponent.RequestPinItemActivity; import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.util.Wait; import com.android.launcher3.util.Wait.Condition; +import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; import com.android.launcher3.util.rule.ShellCommandRule; import org.junit.Before; @@ -77,6 +78,7 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { public void testEmpty() throws Throwable { /* needed while the broken tests are being fixed */ } @Test + @ScreenRecord //b/192010616 public void testPinWidgetNoConfig() throws Throwable { runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId && @@ -85,6 +87,7 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { } @Test + @ScreenRecord //b/192005114 public void testPinWidgetNoConfig_customPreview() throws Throwable { // Command to set custom preview Intent command = RequestPinItemActivity.getCommandIntent( diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java index 4c4794789e..dc59bddabd 100644 --- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java +++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java @@ -2,6 +2,8 @@ package com.android.launcher3.util.rule; import static androidx.test.InstrumentationRegistry.getInstrumentation; +import android.os.FileUtils; +import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.util.Log; import androidx.test.uiautomator.UiDevice; @@ -12,9 +14,12 @@ import com.android.launcher3.ui.AbstractLauncherUiTest; import org.junit.rules.TestWatcher; import org.junit.runner.Description; -import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; public class FailureWatcher extends TestWatcher { private static final String TAG = "FailureWatcher"; @@ -26,20 +31,6 @@ public class FailureWatcher extends TestWatcher { mLauncher = launcher; } - private static void dumpViewHierarchy(UiDevice device) { - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - try { - device.dumpWindowHierarchy(stream); - stream.flush(); - stream.close(); - for (String line : stream.toString().split("\\r?\\n")) { - Log.e(TAG, line.trim()); - } - } catch (IOException e) { - Log.e(TAG, "error dumping XML to logcat", e); - } - } - @Override protected void succeeded(Description description) { super.succeeded(description); @@ -53,22 +44,41 @@ public class FailureWatcher extends TestWatcher { public static void onError(UiDevice device, Description description, Throwable e) { if (device == null) return; - final String pathname = getInstrumentation().getTargetContext(). - getFilesDir().getPath() + "/TestScreenshot-" + description.getMethodName() - + ".png"; - Log.e(TAG, "Failed test " + description.getMethodName() + - ", screenshot will be saved to " + pathname + - ", track trace is below, UI object dump is further below:\n" + - Log.getStackTraceString(e)); - dumpViewHierarchy(device); + final File parentFile = getInstrumentation().getTargetContext().getFilesDir(); + final File sceenshot = new File(parentFile, + "TestScreenshot-" + description.getMethodName() + ".png"); + final File hierarchy = new File(parentFile, + "Hierarchy-" + description.getMethodName() + ".zip"); - try { - final String dumpsysResult = device.executeShellCommand( - "dumpsys activity service TouchInteractionService"); - Log.d(TAG, "TouchInteractionService: " + dumpsysResult); - } catch (IOException ex) { + // Dump window hierarchy + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(hierarchy))) { + out.putNextEntry(new ZipEntry("bugreport.txt")); + dumpStringCommand("dumpsys window windows", out); + dumpStringCommand("dumpsys package", out); + dumpStringCommand("dumpsys activity service TouchInteractionService", out); + out.closeEntry(); + + out.putNextEntry(new ZipEntry("visible_windows.zip")); + dumpCommand("cmd window dump-visible-window-views", out); + out.closeEntry(); + } catch (IOException ex) { } + + Log.e(TAG, "Failed test " + description.getMethodName() + + ",\nscreenshot will be saved to " + sceenshot + + ",\nUI dump at: " + hierarchy + + " (use go/web-hv to open the dump file)", e); + device.takeScreenshot(sceenshot); + } + + private static void dumpStringCommand(String cmd, OutputStream out) throws IOException { + out.write(("\n\n" + cmd + "\n").getBytes()); + dumpCommand(cmd, out); + } + + private static void dumpCommand(String cmd, OutputStream out) throws IOException { + try (AutoCloseInputStream in = new AutoCloseInputStream(getInstrumentation() + .getUiAutomation().executeShellCommand(cmd))) { + FileUtils.copy(in, out); } - - device.takeScreenshot(new File(pathname)); } } diff --git a/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java new file mode 100644 index 0000000000..00b1cdd35e --- /dev/null +++ b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 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.rule; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import androidx.test.uiautomator.UiDevice; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.File; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Rule which captures a screen record for a test. + * After adding this rule to the test class, apply the annotation @ScreenRecord to individual tests + */ +public class ScreenRecordRule implements TestRule { + + private static final String TAG = "ScreenRecordRule"; + + @Override + public Statement apply(Statement base, Description description) { + if (description.getAnnotation(ScreenRecord.class) == null) { + return base; + } + + return new Statement() { + @Override + public void evaluate() throws Throwable { + Instrumentation inst = getInstrumentation(); + UiAutomation automation = inst.getUiAutomation(); + UiDevice device = UiDevice.getInstance(inst); + + File outputFile = new File(inst.getTargetContext().getFilesDir(), + "screenrecord-" + description.getMethodName() + ".mp4"); + device.executeShellCommand("killall screenrecord"); + ParcelFileDescriptor output = + automation.executeShellCommand("screenrecord " + outputFile); + String screenRecordPid = device.executeShellCommand("pidof screenrecord"); + try { + base.evaluate(); + } finally { + device.executeShellCommand("kill -INT " + screenRecordPid); + Log.e(TAG, "Screenrecord captured at: " + outputFile); + output.close(); + } + } + }; + } + + /** + * Interface to indicate that the test should capture screenrecord + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface ScreenRecord { } +}