diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index a15501c7d4..b5d19f392a 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -1532,7 +1532,7 @@ private void doSave() { DBHelper.setLoyaltyCardGroups(mDatabase, viewModel.getLoyaltyCardId(), selectedGroups); - ShortcutHelper.updateShortcuts(this, DBHelper.getLoyaltyCard(this, mDatabase, viewModel.getLoyaltyCardId())); + ShortcutHelper.updateShortcuts(this); if (viewModel.getDuplicateFromLoyaltyCardId()) { Intent intent = new Intent(getApplicationContext(), MainActivity.class); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 3ede421190..466ddeaeab 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -794,7 +794,7 @@ protected void onResume() { invalidateOptionsMenu(); - ShortcutHelper.updateShortcuts(this, loyaltyCard); + ShortcutHelper.updateShortcuts(this); } private void setStateBasedOnImageTypes() { @@ -896,7 +896,6 @@ public boolean onOptionsItemSelected(MenuItem item) { DBHelper.updateLoyaltyCardArchiveStatus(database, loyaltyCardId, 1); Toast.makeText(LoyaltyCardViewActivity.this, R.string.archived, Toast.LENGTH_LONG).show(); - ShortcutHelper.removeShortcut(LoyaltyCardViewActivity.this, loyaltyCardId); new ListWidget().updateAll(LoyaltyCardViewActivity.this); // Re-init loyaltyCard with new data from DB @@ -922,7 +921,6 @@ public boolean onOptionsItemSelected(MenuItem item) { DBHelper.deleteLoyaltyCard(database, LoyaltyCardViewActivity.this, loyaltyCardId); - ShortcutHelper.removeShortcut(LoyaltyCardViewActivity.this, loyaltyCardId); new ListWidget().updateAll(LoyaltyCardViewActivity.this); finish(); diff --git a/app/src/main/java/protect/card_locker/MainActivity.kt b/app/src/main/java/protect/card_locker/MainActivity.kt index e4083ef787..2cf536fba3 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.kt +++ b/app/src/main/java/protect/card_locker/MainActivity.kt @@ -156,8 +156,6 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener { Log.d(TAG, "Deleting card: " + loyaltyCard.id) DBHelper.deleteLoyaltyCard(mDatabase, this@MainActivity, loyaltyCard.id) - - ShortcutHelper.removeShortcut(this@MainActivity, loyaltyCard.id) } val tab = groupsTabLayout.getTabAt(selectedTab) mGroup = tab?.tag @@ -177,7 +175,6 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener { for (loyaltyCard in mAdapter.getSelectedItems()) { Log.d(TAG, "Archiving card: " + loyaltyCard.id) DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1) - ShortcutHelper.removeShortcut(this@MainActivity, loyaltyCard.id) updateLoyaltyCardList(false) inputMode.finish() invalidateOptionsMenu() @@ -466,6 +463,7 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener { } ListWidget().updateAll(mAdapter.mContext) + ShortcutHelper.updateShortcuts(mAdapter.mContext) } private fun processParseResultList( diff --git a/app/src/main/java/protect/card_locker/ShortcutHelper.java b/app/src/main/java/protect/card_locker/ShortcutHelper.java index f57ae7c52c..f82c1a797c 100644 --- a/app/src/main/java/protect/card_locker/ShortcutHelper.java +++ b/app/src/main/java/protect/card_locker/ShortcutHelper.java @@ -2,31 +2,35 @@ import android.content.Context; import android.content.Intent; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.os.Bundle; +import androidx.annotation.VisibleForTesting; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; -import androidx.core.graphics.ColorUtils; import androidx.core.graphics.drawable.IconCompat; import org.jetbrains.annotations.NotNull; import java.util.Collections; -import java.util.Comparator; import java.util.LinkedList; -import java.util.List; class ShortcutHelper { - // Android documentation says that no more than 5 shortcuts - // are supported. However, that may be too many, as not all - // launcher will show all 5. Instead, the number is limited - // to 3 here, so that the most recent shortcut has a good - // chance of being shown. - private static final int MAX_SHORTCUTS = 3; + /** + * This variable controls the maximum number of shortcuts available. + * It is made public only to make testing easier and should not be + * manually modified. We use -1 here as a default value to check if + * the value has been set either manually by the test scenario or + * automatically in the `updateShortcuts` function. + * Its actual value will be set based on the maximum amount of shortcuts + * declared by the launcher via `getMaxShortcutCountPerActivity`. + */ + @VisibleForTesting + public static int maxShortcuts = -1; // https://developer.android.com/reference/android/graphics/drawable/AdaptiveIconDrawable.html private static final int ADAPTIVE_BITMAP_SCALE = 1; @@ -35,86 +39,42 @@ class ShortcutHelper { private static final int ADAPTIVE_BITMAP_IMAGE_SIZE = ADAPTIVE_BITMAP_VISIBLE_SIZE + 5 * ADAPTIVE_BITMAP_SCALE; /** - * Add a card to the app shortcuts, and maintain a list of the most - * recently used cards. If there is already a shortcut for the card, - * the card is marked as the most recently used card. If adding this - * card exceeds the max number of shortcuts, then the least recently - * used card shortcut is discarded. + * Update the dynamic shortcut list with the most recently viewed cards + * based on the lastUsed field. Archived cards are excluded from the shortcuts + * list. The list keeps at most maxShortcuts number of elements. */ - static void updateShortcuts(Context context, LoyaltyCard card) { - if (card.archiveStatus == 1) { - // Don't add archived card to menu - return; + static void updateShortcuts(Context context) { + if (maxShortcuts == -1) { + maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context); } - - LinkedList list = new LinkedList<>(ShortcutManagerCompat.getDynamicShortcuts(context)); - + LinkedList finalList = new LinkedList<>(); SQLiteDatabase database = new DBHelper(context).getReadableDatabase(); + Cursor loyaltyCardCursor = DBHelper.getLoyaltyCardCursor( + database, + "", + null, + DBHelper.LoyaltyCardOrder.LastUsed, + DBHelper.LoyaltyCardOrderDirection.Ascending, + DBHelper.LoyaltyCardArchiveFilter.Unarchived + ); - String shortcutId = Integer.toString(card.id); - - // Sort the shortcuts by rank, so working with the relative order will be easier. - // This sorts so that the lowest rank is first. - Collections.sort(list, Comparator.comparingInt(ShortcutInfoCompat::getRank)); - - Integer foundIndex = null; - - for (int index = 0; index < list.size(); index++) { - if (list.get(index).getId().equals(shortcutId)) { - // Found the item already - foundIndex = index; - break; - } - } - - if (foundIndex != null) { - // If the item is already found, then the list needs to be - // reordered, so that the selected item now has the lowest - // rank, thus letting it survive longer. - ShortcutInfoCompat found = list.remove(foundIndex.intValue()); - list.addFirst(found); - } else { - // The item is new to the list. We add it and trim the list later. - ShortcutInfoCompat shortcut = createShortcutBuilder(context, card).build(); - list.addFirst(shortcut); - } - - LinkedList finalList = new LinkedList<>(); int rank = 0; - // The ranks are now updated; the order in the list is the rank. - for (int index = 0; index < list.size(); index++) { - ShortcutInfoCompat prevShortcut = list.get(index); - - LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, Integer.parseInt(prevShortcut.getId())); + while (rank < maxShortcuts && loyaltyCardCursor.moveToNext()) { + int id = loyaltyCardCursor.getInt(loyaltyCardCursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID)); + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, id); - // skip outdated cards that no longer exist - if (loyaltyCard != null) { - ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard) - .setRank(rank) - .build(); + ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard) + .setRank(rank) + .build(); - finalList.addLast(updatedShortcut); - rank++; - - // trim the list - if (rank >= MAX_SHORTCUTS) { - break; - } - } + finalList.addLast(updatedShortcut); + rank++; } ShortcutManagerCompat.setDynamicShortcuts(context, finalList); } - /** - * Remove the given card id from the app shortcuts, if such a - * shortcut exists. - */ - static void removeShortcut(Context context, int cardId) { - ShortcutManagerCompat.removeDynamicShortcuts(context, Collections.singletonList(Integer.toString(cardId))); - } - static @NotNull Bitmap createAdaptiveBitmap(@NotNull Bitmap in, int paddingColor) { Bitmap ret = Bitmap.createBitmap(ADAPTIVE_BITMAP_SIZE, ADAPTIVE_BITMAP_SIZE, Bitmap.Config.ARGB_8888); diff --git a/app/src/test/java/protect/card_locker/ShortcutHelperTest.java b/app/src/test/java/protect/card_locker/ShortcutHelperTest.java new file mode 100644 index 0000000000..14cbdc7e34 --- /dev/null +++ b/app/src/test/java/protect/card_locker/ShortcutHelperTest.java @@ -0,0 +1,124 @@ +package protect.card_locker; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.app.Activity; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; + +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; + +import com.google.zxing.BarcodeFormat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ActivityController; + +import java.math.BigDecimal; +import java.util.Comparator; + +@RunWith(RobolectricTestRunner.class) +public class ShortcutHelperTest { + private Activity mActivity; + private SQLiteDatabase mDatabase; + private int id1; + private int id2; + private int id3; + private int id4; + + @Before + public void setUp() { + mActivity = Robolectric.setupActivity(MainActivity.class); + mDatabase = TestHelpers.getEmptyDb(mActivity).getWritableDatabase(); + + long now = System.currentTimeMillis(); + id1 = (int) DBHelper.insertLoyaltyCard(mDatabase, "store1", "note1", null, null, new BigDecimal("0"), null, "cardId1", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), null, Color.BLACK, 0, now,0); + id2 = (int) DBHelper.insertLoyaltyCard(mDatabase, "store2", "note2", null, null, new BigDecimal("0"), null, "cardId2", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), null, Color.BLACK, 0, now + 10,0); + id3 = (int) DBHelper.insertLoyaltyCard(mDatabase, "store3", "note3", null, null, new BigDecimal("0"), null, "cardId3", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), null, Color.BLACK, 0, now + 20,0); + id4 = (int) DBHelper.insertLoyaltyCard(mDatabase, "store4", "note4", null, null, new BigDecimal("0"), null, "cardId4", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), null, Color.BLACK, 0, now + 30,0); + + ShortcutHelper.maxShortcuts = 3; + } + + private Integer[] getShortcutIds(Context context) { + return ShortcutManagerCompat.getDynamicShortcuts(context) + .stream() + .sorted(Comparator.comparingInt(ShortcutInfoCompat::getRank)) + .map(shortcut -> Integer.parseInt(shortcut.getId())) + .toArray(Integer[]::new); + } + + @Test + public void onArchiveUnarchive() { + ActivityController activityController = Robolectric.buildActivity(MainActivity.class).create(); + + Activity mainActivity = (Activity) activityController.get(); + + activityController.pause(); + activityController.resume(); + + assertEquals(3, ShortcutManagerCompat.getDynamicShortcuts(mainActivity).stream().count()); + + Integer[] ids = getShortcutIds(mainActivity); + + assertArrayEquals(new Integer[] {id4, id3, id2}, ids); + + DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, id4, 1); + + activityController.pause(); + activityController.resume(); + + Integer[] idsAfterArchive = getShortcutIds(mainActivity); + + assertArrayEquals(new Integer[] {id3, id2, id1}, idsAfterArchive); + + DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, id4, 0); + + activityController.pause(); + activityController.resume(); + + Integer[] idsAfterUnarchive = getShortcutIds(mainActivity); + + assertArrayEquals(new Integer[] {id4, id3, id2}, idsAfterUnarchive); + } + + @Test + public void onAddRemoveFavorite() { + ActivityController activityController = Robolectric.buildActivity(MainActivity.class).create(); + + Activity mainActivity = (Activity) activityController.get(); + + activityController.pause(); + activityController.resume(); + + assertEquals(3, ShortcutManagerCompat.getDynamicShortcuts(mainActivity).stream().count()); + + Integer[] ids = getShortcutIds(mainActivity); + + assertArrayEquals(new Integer[] {id4, id3, id2}, ids); + + DBHelper.updateLoyaltyCardStarStatus(mDatabase, id1, 1); + + activityController.pause(); + activityController.resume(); + + Integer[] idsAfterFav = getShortcutIds(mainActivity); + + assertArrayEquals(new Integer[] {id1, id4, id3}, idsAfterFav); + + DBHelper.updateLoyaltyCardStarStatus(mDatabase, id1, 0); + + activityController.pause(); + activityController.resume(); + + Integer[] idsAfterUnfav = getShortcutIds(mainActivity); + + assertArrayEquals(new Integer[] {id4, id3, id2}, idsAfterUnfav); + } +}