From dfa1c29fa0d8a42fe3c0bbdbb45fd3935675f5f8 Mon Sep 17 00:00:00 2001 From: NightShiftNexus <109205988+NightShiftNexus@users.noreply.github.com> Date: Sat, 2 May 2026 16:10:03 +0200 Subject: [PATCH] feat: split complex loyaltyCardViewActivity (#3072) --- app/src/main/AndroidManifest.xml | 2 +- .../card_locker/BarcodeImageWriterTask.java | 2 +- .../CardsOnPowerScreenService.java | 2 + .../java/protect/card_locker/ListWidget.kt | 3 +- .../card_locker/LoyaltyCardEditActivity.java | 8 + .../java/protect/card_locker/MainActivity.kt | 1 + .../protect/card_locker/ShortcutHelper.java | 39 +- .../cardview/LoyaltyCardImageNavigator.java | 103 ++ .../LoyaltyCardMainImageRenderer.java | 183 ++++ .../LoyaltyCardViewActivity.java | 898 +++++++----------- .../cardview/LoyaltyCardViewDialogs.java | 246 +++++ .../LoyaltyCardViewActivityTest.java | 449 +++++---- .../java/protect/card_locker/TestHelpers.java | 21 +- 13 files changed, 1150 insertions(+), 807 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/cardview/LoyaltyCardImageNavigator.java create mode 100644 app/src/main/java/protect/card_locker/cardview/LoyaltyCardMainImageRenderer.java rename app/src/main/java/protect/card_locker/{ => cardview}/LoyaltyCardViewActivity.java (53%) create mode 100644 app/src/main/java/protect/card_locker/cardview/LoyaltyCardViewDialogs.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d26f252c5..ed2c6f41f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,7 +98,7 @@ android:theme="@style/AppTheme.NoActionBar" android:windowSoftInputMode="adjustResize"/> { private final boolean showFallback; private final BarcodeImageWriterResultCallback callback; - BarcodeImageWriterTask( + public BarcodeImageWriterTask( Context context, ImageView imageView, String cardIdString, CatimaBarcode barcodeFormat, @NonNull Charset barcodeEncoding, TextView textView, boolean showFallback, BarcodeImageWriterResultCallback callback, boolean roundCornerPadding, boolean isFullscreen diff --git a/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java b/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java index 46deaf467..3f54dfafe 100644 --- a/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java +++ b/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.concurrent.Flow; import java.util.function.Consumer; +import protect.card_locker.cardview.LoyaltyCardViewActivity; + @RequiresApi(Build.VERSION_CODES.R) public class CardsOnPowerScreenService extends ControlsProviderService { diff --git a/app/src/main/java/protect/card_locker/ListWidget.kt b/app/src/main/java/protect/card_locker/ListWidget.kt index be5ac4886..f8245aeb4 100644 --- a/app/src/main/java/protect/card_locker/ListWidget.kt +++ b/app/src/main/java/protect/card_locker/ListWidget.kt @@ -13,6 +13,7 @@ import android.view.View import android.widget.RemoteViews import androidx.core.widget.RemoteViewsCompat import protect.card_locker.DBHelper.LoyaltyCardArchiveFilter +import protect.card_locker.cardview.LoyaltyCardViewActivity class ListWidget : AppWidgetProvider() { fun updateAll(context: Context) { @@ -129,4 +130,4 @@ class ListWidget : AppWidgetProvider() { return remoteViews } -} \ No newline at end of file +} diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 83fc94972..efcdee670 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -719,6 +719,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements }); } + @Override + protected void onDestroy() { + if (mDatabase != null && mDatabase.isOpen()) { + mDatabase.close(); + } + super.onDestroy(); + } + private void selectTab(int index) { binding.tabs.selectTab(binding.tabs.getTabAt(index)); viewModel.setTabIndex(index); diff --git a/app/src/main/java/protect/card_locker/MainActivity.kt b/app/src/main/java/protect/card_locker/MainActivity.kt index 2cf536fba..d153823f5 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.kt +++ b/app/src/main/java/protect/card_locker/MainActivity.kt @@ -31,6 +31,7 @@ import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import protect.card_locker.DBHelper.LoyaltyCardOrder import protect.card_locker.DBHelper.LoyaltyCardOrderDirection import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener +import protect.card_locker.cardview.LoyaltyCardViewActivity import protect.card_locker.databinding.ContentMainBinding import protect.card_locker.databinding.MainActivityBinding import protect.card_locker.databinding.SortingOptionBinding diff --git a/app/src/main/java/protect/card_locker/ShortcutHelper.java b/app/src/main/java/protect/card_locker/ShortcutHelper.java index 0b1231235..f9085bb56 100644 --- a/app/src/main/java/protect/card_locker/ShortcutHelper.java +++ b/app/src/main/java/protect/card_locker/ShortcutHelper.java @@ -15,11 +15,11 @@ import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import org.jetbrains.annotations.NotNull; - -import java.util.Collections; import java.util.LinkedList; -class ShortcutHelper { +import protect.card_locker.cardview.LoyaltyCardViewActivity; + +public class ShortcutHelper { /** * This variable controls the maximum number of shortcuts available. * It is made public only to make testing easier and should not be @@ -43,33 +43,36 @@ class ShortcutHelper { * 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) { + public static void updateShortcuts(Context context) { if (maxShortcuts == -1) { maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context); } LinkedList finalList = new LinkedList<>(); - SQLiteDatabase database = new DBHelper(context).getReadableDatabase(); - Cursor loyaltyCardCursor = DBHelper.getLoyaltyCardCursor( + DBHelper dbHelper = new DBHelper(context); + SQLiteDatabase database = dbHelper.getReadableDatabase(); + try (Cursor loyaltyCardCursor = DBHelper.getLoyaltyCardCursor( database, "", null, DBHelper.LoyaltyCardOrder.LastUsed, DBHelper.LoyaltyCardOrderDirection.Ascending, DBHelper.LoyaltyCardArchiveFilter.Unarchived - ); + )) { + int rank = 0; - int rank = 0; + while (rank < maxShortcuts && loyaltyCardCursor.moveToNext()) { + int id = loyaltyCardCursor.getInt(loyaltyCardCursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID)); + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, id); - while (rank < maxShortcuts && loyaltyCardCursor.moveToNext()) { - int id = loyaltyCardCursor.getInt(loyaltyCardCursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID)); - LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, id); + ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard) + .setRank(rank) + .build(); - ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard) - .setRank(rank) - .build(); - - finalList.addLast(updatedShortcut); - rank++; + finalList.addLast(updatedShortcut); + rank++; + } + } finally { + dbHelper.close(); } ShortcutManagerCompat.setDynamicShortcuts(context, finalList); @@ -110,4 +113,4 @@ class ShortcutHelper { .setIntent(intent) .setIcon(icon); } -} +} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/cardview/LoyaltyCardImageNavigator.java b/app/src/main/java/protect/card_locker/cardview/LoyaltyCardImageNavigator.java new file mode 100644 index 000000000..556593e85 --- /dev/null +++ b/app/src/main/java/protect/card_locker/cardview/LoyaltyCardImageNavigator.java @@ -0,0 +1,103 @@ +package protect.card_locker.cardview; + +import java.util.ArrayList; +import java.util.List; + +final class LoyaltyCardImageNavigator { + private final List imageTypes; + private int currentIndex; + + LoyaltyCardImageNavigator(List imageTypes, int currentIndex) { + this.imageTypes = new ArrayList<>(imageTypes); + this.currentIndex = clampIndex(currentIndex); + } + + LoyaltyCardImageType getCurrent() { + if (isEmpty()) { + return LoyaltyCardImageType.NONE; + } + return imageTypes.get(currentIndex); + } + + boolean isEmpty() { + return imageTypes.isEmpty(); + } + + int size() { + return imageTypes.size(); + } + + boolean remove(LoyaltyCardImageType type) { + int removedIndex = imageTypes.indexOf(type); + if (removedIndex == -1) { + return false; + } + + imageTypes.remove(removedIndex); + currentIndex = clampIndex(currentIndex); + return true; + } + + boolean canGoPrevious() { + return currentIndex > 0; + } + + boolean canGoNext() { + return currentIndex < imageTypes.size() - 1; + } + + boolean movePrevious() { + if (!canGoPrevious()) { + return false; + } + currentIndex--; + return true; + } + + boolean moveNext(boolean overflow) { + if (isEmpty()) { + return false; + } + if (canGoNext()) { + currentIndex++; + return true; + } + if (overflow) { + currentIndex = 0; + return true; + } + return false; + } + + int getCurrentIndex() { + return currentIndex; + } + + LoyaltyCardImageType peekNext(boolean overflow) { + if (isEmpty()) { + return LoyaltyCardImageType.NONE; + } + + if (canGoNext()) { + return imageTypes.get(currentIndex + 1); + } + + return overflow ? imageTypes.get(0) : getCurrent(); + } + + private int clampIndex(int index) { + if (imageTypes.isEmpty()) { + return 0; + } + + return Math.max(0, Math.min(index, imageTypes.size() - 1)); + } +} + +enum LoyaltyCardImageType { + NONE, + ICON, + BARCODE, + IMAGE_FRONT, + IMAGE_BACK +} diff --git a/app/src/main/java/protect/card_locker/cardview/LoyaltyCardMainImageRenderer.java b/app/src/main/java/protect/card_locker/cardview/LoyaltyCardMainImageRenderer.java new file mode 100644 index 000000000..7c266a1d8 --- /dev/null +++ b/app/src/main/java/protect/card_locker/cardview/LoyaltyCardMainImageRenderer.java @@ -0,0 +1,183 @@ +package protect.card_locker.cardview; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.ImageView; +import android.widget.TextView; + +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.color.MaterialColors; + +import java.nio.charset.Charset; + +import protect.card_locker.*; +import protect.card_locker.async.TaskHandler; + +final class LoyaltyCardMainImageRenderer { + private final Context context; + private final TaskHandler tasks = new TaskHandler(); + private final BarcodeImageWriterResultCallback barcodeCallback; + + LoyaltyCardMainImageRenderer( + Context context, + BarcodeImageWriterResultCallback barcodeCallback + ) { + this.context = context; + this.barcodeCallback = barcodeCallback; + } + + void renderCurrent( + LoyaltyCardImageType imageType, + Bitmap frontImageBitmap, + Bitmap backImageBitmap, + CatimaBarcode format, + Charset barcodeEncoding, + String cardIdString, + String barcodeIdString, + ImageView barcodeRenderTarget, + TextView mainImageDescription, + MaterialCardView mainCardView, + boolean isFullscreen, + boolean waitForResize + ) { + if (imageType == LoyaltyCardImageType.NONE) { + // With no renderable media left, show the raw card ID instead of an empty card area. + barcodeRenderTarget.setVisibility(View.GONE); + mainCardView.setCardBackgroundColor(Color.TRANSPARENT); + mainImageDescription.setTextColor( + MaterialColors.getColor( + mainImageDescription, + com.google.android.material.R.attr.colorOnSurfaceVariant + ) + ); + mainImageDescription.setText(cardIdString); + return; + } + + if (imageType == LoyaltyCardImageType.BARCODE) { + barcodeRenderTarget.setBackgroundColor(Color.WHITE); + mainCardView.setCardBackgroundColor(Color.WHITE); + mainImageDescription.setTextColor(context.getResources().getColor(R.color.md_theme_light_onSurfaceVariant)); + + if (waitForResize) { + redrawBarcodeAfterResize( + barcodeRenderTarget, + barcodeIdString, + cardIdString, + format, + barcodeEncoding, + !isFullscreen, + isFullscreen + ); + } else { + drawBarcode( + barcodeRenderTarget, + barcodeIdString, + cardIdString, + format, + barcodeEncoding, + !isFullscreen, + isFullscreen + ); + } + + mainImageDescription.setText(cardIdString); + barcodeRenderTarget.setContentDescription( + context.getString(R.string.barcodeImageDescriptionWithType, format.prettyName()) + ); + } else if (imageType == LoyaltyCardImageType.IMAGE_FRONT) { + barcodeRenderTarget.setImageBitmap(frontImageBitmap); + barcodeRenderTarget.setBackgroundColor(Color.TRANSPARENT); + mainCardView.setCardBackgroundColor(Color.TRANSPARENT); + mainImageDescription.setTextColor( + MaterialColors.getColor( + mainImageDescription, + com.google.android.material.R.attr.colorOnSurfaceVariant + ) + ); + mainImageDescription.setText(context.getString(R.string.frontImageDescription)); + barcodeRenderTarget.setContentDescription(context.getString(R.string.frontImageDescription)); + } else if (imageType == LoyaltyCardImageType.IMAGE_BACK) { + barcodeRenderTarget.setImageBitmap(backImageBitmap); + barcodeRenderTarget.setBackgroundColor(Color.TRANSPARENT); + mainCardView.setCardBackgroundColor(Color.TRANSPARENT); + mainImageDescription.setTextColor( + MaterialColors.getColor( + mainImageDescription, + com.google.android.material.R.attr.colorOnSurfaceVariant + ) + ); + mainImageDescription.setText(context.getString(R.string.backImageDescription)); + barcodeRenderTarget.setContentDescription(context.getString(R.string.backImageDescription)); + } else { + throw new IllegalArgumentException("Unknown image type: " + imageType); + } + + barcodeRenderTarget.setVisibility(View.VISIBLE); + } + + private void redrawBarcodeAfterResize( + ImageView barcodeRenderTarget, + String barcodeIdString, + String cardIdString, + CatimaBarcode format, + Charset barcodeEncoding, + boolean addPadding, + boolean isFullscreen + ) { + if (format == null) { + return; + } + + // Barcode dimensions depend on the final ImageView size, so wait for layout before rendering. + barcodeRenderTarget.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + barcodeRenderTarget.getViewTreeObserver().removeOnGlobalLayoutListener(this); + drawBarcode( + barcodeRenderTarget, + barcodeIdString, + cardIdString, + format, + barcodeEncoding, + addPadding, + isFullscreen + ); + } + }); + } + + private void drawBarcode( + ImageView barcodeRenderTarget, + String barcodeIdString, + String cardIdString, + CatimaBarcode format, + Charset barcodeEncoding, + boolean addPadding, + boolean isFullscreen + ) { + // Barcodes are regenerated eagerly because zoom/fullscreen changes affect the output bitmap. + tasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); + if (format == null) { + return; + } + + BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask( + context.getApplicationContext(), + barcodeRenderTarget, + barcodeIdString != null ? barcodeIdString : cardIdString, + format, + barcodeEncoding, + null, + false, + barcodeCallback, + addPadding, + isFullscreen + ); + tasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); + } +} diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/cardview/LoyaltyCardViewActivity.java similarity index 53% rename from app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java rename to app/src/main/java/protect/card_locker/cardview/LoyaltyCardViewActivity.java index 676725481..82a724b3a 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/cardview/LoyaltyCardViewActivity.java @@ -1,4 +1,4 @@ -package protect.card_locker; +package protect.card_locker.cardview; import android.content.ActivityNotFoundException; import android.content.ClipData; @@ -13,12 +13,6 @@ import android.graphics.drawable.Drawable; import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; -import android.text.InputType; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.method.DigitsKeyListener; -import android.text.style.ForegroundColorSpan; import android.text.util.Linkify; import android.util.Log; import android.view.KeyEvent; @@ -26,22 +20,17 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; import android.view.WindowManager; -import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; @@ -52,25 +41,18 @@ import androidx.core.graphics.ColorUtils; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; -import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; -import com.google.android.material.textfield.TextInputEditText; -import com.google.zxing.BarcodeFormat; import java.io.File; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.nio.charset.Charset; -import java.text.DateFormat; -import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Date; import java.util.List; -import java.util.function.Predicate; -import protect.card_locker.async.TaskHandler; +import protect.card_locker.*; import protect.card_locker.databinding.LoyaltyCardViewLayoutBinding; import protect.card_locker.preferences.Settings; import protect.card_locker.preferences.SettingsActivity; @@ -100,34 +82,37 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements boolean backgroundNeedsDarkIcons; boolean isFullscreen = false; ImageView barcodeRenderTarget; - int mainImageIndex = 0; - List imageTypes; + LoyaltyCardImageNavigator cardNavigator = new LoyaltyCardImageNavigator(new ArrayList<>(), 0); + private LoyaltyCardMainImageRenderer mainImageRenderer; + private final LoyaltyCardViewDialogs dialogs = new LoyaltyCardViewDialogs(); + // Used only to seed the first navigator after recreation, before card data has been reloaded. + private Integer restoredImageIndex = null; - static final String STATE_IMAGEINDEX = "imageIndex"; - static final String STATE_FULLSCREEN = "isFullscreen"; + public static final String STATE_IMAGEINDEX = "imageIndex"; + public static final String STATE_FULLSCREEN = "isFullscreen"; - static final String BUNDLE_ID = "id"; - static final String BUNDLE_CARDLIST = "cardList"; - static final String BUNDLE_TRANSITION_RIGHT = "transition_right"; - - final private TaskHandler mTasks = new TaskHandler(); - Runnable barcodeImageGenerationFinishedCallback; + public static final String BUNDLE_ID = "id"; + public static final String BUNDLE_CARDLIST = "cardList"; + public static final String BUNDLE_TRANSITION_RIGHT = "transition_right"; private long initTime = System.currentTimeMillis(); + private enum AdjacentCardDirection { + PREVIOUS, + NEXT + } + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (settings.useVolumeKeysForNavigation()) { if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - // Navigate to the previous card if (initTime < (System.currentTimeMillis() - 1000)) { - prevNextCard(false); + navigateToAdjacentCard(AdjacentCardDirection.PREVIOUS); } return true; } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - // Navigate to the next card if (initTime < (System.currentTimeMillis() - 1000)) { - prevNextCard(true); + navigateToAdjacentCard(AdjacentCardDirection.NEXT); } return true; } @@ -142,10 +127,10 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements return; } - ImageType imageType = imageTypes.get(mainImageIndex); + LoyaltyCardImageType imageType = cardNavigator.getCurrent(); - // If the barcode is shown, switch to fullscreen layout - if (imageType == ImageType.BARCODE) { + // Fullscreen exists mainly to make barcodes easier to scan, not as a separate screen flow. + if (imageType == LoyaltyCardImageType.BARCODE) { setFullscreen(true); return; @@ -155,10 +140,12 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements openImageInGallery(imageType); } - private void openImageInGallery(ImageType imageType) { + private void openImageInGallery(LoyaltyCardImageType imageType) { File file = null; switch (imageType) { + case NONE: + return; case ICON: file = Utils.retrieveCardImageAsFile(this, loyaltyCardId, ImageLocationType.icon); break; @@ -171,8 +158,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements case BARCODE: Toast.makeText(this, R.string.barcodeLongPressMessage, Toast.LENGTH_SHORT).show(); return; - default: - // Empty default case for now to keep the spotBugsRelease job happy } // Do nothing if there is no file @@ -196,7 +181,8 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements @Override public void onBarcodeImageWriterResult(boolean success) { if (!success) { - imageTypes.remove(ImageType.BARCODE); + // If barcode rendering fails, drop that slot so the user falls back to working content. + cardNavigator.remove(LoyaltyCardImageType.BARCODE); setStateBasedOnImageTypes(); @@ -207,14 +193,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } } - enum ImageType { - NONE, - ICON, - BARCODE, - IMAGE_FRONT, - IMAGE_BACK - } - private void extractIntentFields(Intent intent) { final Bundle b = intent.getExtras(); loyaltyCardId = b != null ? b.getInt(BUNDLE_ID) : 0; @@ -252,10 +230,8 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements int transitionRight = incomingIntentExtras.getInt(BUNDLE_TRANSITION_RIGHT, -1); if (transitionRight == 1) { - // right side transition overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); } else if (transitionRight == 0) { - // left side transition overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right); } } @@ -270,16 +246,15 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements settings = new Settings(this); if (savedInstanceState != null) { - mainImageIndex = savedInstanceState.getInt(STATE_IMAGEINDEX); + restoredImageIndex = savedInstanceState.getInt(STATE_IMAGEINDEX, 0); isFullscreen = savedInstanceState.getBoolean(STATE_FULLSCREEN); } extractIntentFields(getIntent()); - setContentView(binding.getRoot()); - database = new DBHelper(this).getWritableDatabase(); importURIHelper = new ImportURIHelper(this); + mainImageRenderer = new LoyaltyCardMainImageRenderer(this, this); binding.barcodeScaler.setOnSeekBarChangeListener(setOnSeekBarChangeListenerUnifiedFunction()); binding.barcodeWidthscaler.setOnSeekBarChangeListener(setOnSeekBarChangeListenerUnifiedFunction()); @@ -298,14 +273,25 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements }); binding.fabEdit.bringToFront(); - binding.bottomAppBarInfoButton.setOnClickListener(view -> showInfoDialog()); - binding.bottomAppBarPreviousButton.setOnClickListener(view -> prevNextCard(false)); - binding.bottomAppBarNextButton.setOnClickListener(view -> prevNextCard(true)); - binding.bottomAppBarUpdateBalanceButton.setOnClickListener(view -> showBalanceUpdateDialog()); + binding.bottomAppBarInfoButton.setOnClickListener(view -> + dialogs.showInfoDialog(this, loyaltyCard, loyaltyCardGroups) + ); + binding.bottomAppBarPreviousButton.setOnClickListener(view -> + navigateToAdjacentCard(AdjacentCardDirection.PREVIOUS) + ); + binding.bottomAppBarNextButton.setOnClickListener(view -> + navigateToAdjacentCard(AdjacentCardDirection.NEXT) + ); + binding.bottomAppBarUpdateBalanceButton.setOnClickListener(view -> + dialogs.showBalanceUpdateDialog(this, loyaltyCard, newBalance -> { + DBHelper.updateLoyaltyCardBalance(database, loyaltyCardId, newBalance); + onResume(); + }) + ); binding.iconContainer.setOnClickListener(view -> { if (loyaltyCard.getImageThumbnail(this) != null) { - openImageInGallery(ImageType.ICON); + openImageInGallery(LoyaltyCardImageType.ICON); } else { Toast.makeText(LoyaltyCardViewActivity.this, R.string.icon_header_click_text, Toast.LENGTH_LONG).show(); } @@ -324,8 +310,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements }); binding.mainImage.setOnClickListener(view -> onMainImageTap()); - // This long-press was originally only intended for when Talkback was used but sadly limiting - // this doesn't seem to work well + // This shortcut started as a TalkBack aid, but it is still useful as a quick way to cycle images. binding.mainImage.setOnLongClickListener(view -> { setMainImage(true, true); return true; @@ -373,7 +358,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } DBHelper.updateLoyaltyCardZoomLevel(database, loyaltyCardId, loyaltyCard.zoomLevel, loyaltyCard.zoomLevelWidth); - drawMainImage(mainImageIndex, true, isFullscreen); + renderCurrentMainImage(true); } @Override public void onStartTrackingTouch(SeekBar seekBar) { @@ -384,189 +369,10 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements }; } - private SpannableStringBuilder padSpannableString(SpannableStringBuilder spannableStringBuilder) { - if (spannableStringBuilder.length() > 0) { - spannableStringBuilder.append("\n\n"); - } - - return spannableStringBuilder; - } - private boolean hasBalance(LoyaltyCard loyaltyCard) { return !loyaltyCard.balance.equals(new BigDecimal(0)); } - private void showInfoDialog() { - AlertDialog.Builder infoDialog = new MaterialAlertDialogBuilder(this); - - int dialogContentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding); - infoDialog.setTitle(loyaltyCard.store); - - TextView infoTextview = new TextView(this); - infoTextview.setPadding( - dialogContentPadding, - dialogContentPadding / 2, - dialogContentPadding, - 0 - ); - infoTextview.setAutoLinkMask(Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS); - infoTextview.setTextIsSelectable(true); - - SpannableStringBuilder infoText = new SpannableStringBuilder(); - if (!loyaltyCard.note.isEmpty()) { - infoText.append(loyaltyCard.note); - } - - if (loyaltyCardGroups.size() > 0) { - List groupNames = new ArrayList<>(); - for (Group group : loyaltyCardGroups) { - groupNames.add(group._id); - } - - padSpannableString(infoText); - infoText.append(getString(R.string.groupsList, TextUtils.join(", ", groupNames))); - } - - if (hasBalance(loyaltyCard)) { - padSpannableString(infoText); - infoText.append(getString(R.string.balanceSentence, Utils.formatBalance(this, loyaltyCard.balance, loyaltyCard.balanceType))); - } - - appendDateInfo(infoText, loyaltyCard.validFrom, (Utils::isNotYetValid), R.string.validFromSentence, R.string.validFromSentence); - - appendDateInfo(infoText, loyaltyCard.expiry, (Utils::hasExpired), R.string.expiryStateSentenceExpired, R.string.expiryStateSentence); - - infoTextview.setText(infoText); - - infoDialog.setView(infoTextview); - infoDialog.setPositiveButton(R.string.ok, (dialogInterface, i) -> dialogInterface.dismiss()); - infoDialog.create().show(); - } - - private void appendDateInfo(SpannableStringBuilder infoText, Date date, Predicate dateCheck, @StringRes int dateCheckTrueString, @StringRes int dateCheckFalseString) { - if (date != null) { - String formattedDate = DateFormat.getDateInstance(DateFormat.LONG).format(date); - - padSpannableString(infoText); - if (dateCheck.test(date)) { - int start = infoText.length(); - infoText.append(getString(dateCheckTrueString, formattedDate)); - infoText.setSpan(new ForegroundColorSpan(Color.RED), start, infoText.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - } else { - infoText.append(getString(dateCheckFalseString, formattedDate)); - } - } - } - - private void showBalanceUpdateDialog() { - AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); - - // Header - builder.setTitle(R.string.updateBalanceTitle); - - // Layout - FrameLayout container = new FrameLayout(this); - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ); - int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding); - params.leftMargin = contentPadding; - params.topMargin = contentPadding / 2; - params.rightMargin = contentPadding; - - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - - TextView currentTextview = new TextView(this); - currentTextview.setText(getString(R.string.currentBalanceSentence, Utils.formatBalance(this, loyaltyCard.balance, loyaltyCard.balanceType))); - layout.addView(currentTextview); - - final TextInputEditText input = new TextInputEditText(this); - input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); - input.setKeyListener(DigitsKeyListener.getInstance("0123456789,.")); - input.setHint(R.string.updateBalanceHint); - - layout.addView(input); - layout.setLayoutParams(params); - container.addView(layout); - - // Set layout - builder.setView(container); - - // Buttons - builder.setPositiveButton(R.string.spend, (dialogInterface, i) -> { - // Calculate and update balance - try { - BigDecimal balanceChange = Utils.parseBalance(input.getText().toString(), loyaltyCard.balanceType); - BigDecimal newBalance = loyaltyCard.balance.subtract(balanceChange).max(new BigDecimal(0)); - DBHelper.updateLoyaltyCardBalance(database, loyaltyCardId, newBalance); - } catch (ParseException e) { - Toast.makeText(getApplicationContext(), R.string.amountParsingFailed, Toast.LENGTH_LONG).show(); - } - - // Reload state - this.onResume(); - - // Show new balance - Toast.makeText(getApplicationContext(), getString(R.string.newBalanceSentence, Utils.formatBalance(this, loyaltyCard.balance, loyaltyCard.balanceType)), Toast.LENGTH_LONG).show(); - }); - builder.setNegativeButton(R.string.receive, (dialogInterface, i) -> { - // Calculate and update balance - try { - BigDecimal balanceChange = Utils.parseBalance(input.getText().toString(), loyaltyCard.balanceType); - BigDecimal newBalance = loyaltyCard.balance.add(balanceChange); - DBHelper.updateLoyaltyCardBalance(database, loyaltyCardId, newBalance); - } catch (ParseException e) { - Toast.makeText(getApplicationContext(), R.string.amountParsingFailed, Toast.LENGTH_LONG).show(); - } - - // Reload state - this.onResume(); - - // Show new balance - Toast.makeText(getApplicationContext(), getString(R.string.newBalanceSentence, Utils.formatBalance(this, loyaltyCard.balance, loyaltyCard.balanceType)), Toast.LENGTH_LONG).show(); - }); - builder.setNeutralButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel()); - AlertDialog dialog = builder.create(); - - // Now that the dialog exists, we can bind something that affects the buttons - input.addTextChangedListener(new SimpleTextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - BigDecimal balanceChange; - - try { - balanceChange = Utils.parseBalance(s.toString(), loyaltyCard.balanceType); - } catch (ParseException e) { - input.setError(getString(R.string.amountParsingFailed)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false); - return; - } - - input.setError(null); - if (balanceChange.equals(new BigDecimal(0))) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false); - } else { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(true); - } - } - }); - - dialog.show(); - - // Disable buttons (must be done **after** dialog is shown to prevent crash - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false); - - // Set focus on input field - dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - input.requestFocus(); - } - private void setBottomAppBarButtonState() { if (!loyaltyCard.note.isEmpty() || !loyaltyCardGroups.isEmpty() || hasBalance(loyaltyCard) || loyaltyCard.validFrom != null || loyaltyCard.expiry != null) { binding.bottomAppBarInfoButton.setVisibility(View.VISIBLE); @@ -585,12 +391,12 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements binding.bottomAppBarUpdateBalanceButton.setVisibility(hasBalance(loyaltyCard) ? View.VISIBLE : View.GONE); } - private void prevNextCard(boolean next) { - // If there are no other cards, don't bother switching + private void navigateToAdjacentCard(AdjacentCardDirection direction) { if (cardList == null || cardList.size() == 1) { return; } + boolean next = direction == AdjacentCardDirection.NEXT; // If we're in RTL layout, we want the "left" button to be "next" instead of "previous" // So we swap next around boolean transitionRight = next; @@ -636,12 +442,11 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements @Override public void onSaveInstanceState(Bundle savedInstanceState) { - savedInstanceState.putInt(STATE_IMAGEINDEX, mainImageIndex); + savedInstanceState.putInt(STATE_IMAGEINDEX, cardNavigator.getCurrentIndex()); savedInstanceState.putBoolean(STATE_FULLSCREEN, isFullscreen); super.onSaveInstanceState(savedInstanceState); } - @Override protected void onResume() { activityOverridesNavBarColor = true; @@ -650,82 +455,116 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements Log.i(TAG, "To view card: " + loyaltyCardId); Window window = getWindow(); - if (window != null) { - // Hide the keyboard if still shown (could be the case when returning from edit activity - window.setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN - ); + applyWindowPreferences(window); + enablePausedNfcIfConfigured(); - WindowManager.LayoutParams attributes = window.getAttributes(); - - // The brightness value is on a scale from [0, ..., 1], where - // '1' is the brightest. We attempt to maximize the brightness - // to help barcode readers scan the barcode. - if (settings.useMaxBrightnessDisplayingBarcode()) { - attributes.screenBrightness = 1F; - } - - if (settings.getKeepScreenOn()) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - if (settings.getDisableLockscreenWhileViewingCard()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setShowWhenLocked(true); - } else { - showWhenLockedSdkLessThan27(window); - } - } - - window.setAttributes(attributes); - } - - // Pause NFC to prevent NFC payments from triggering while showing a barcode - if (settings.getDisableNfcWhileViewingCard()) { - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); - if (nfcAdapter != null) { - nfcAdapter.enableReaderMode(this, tag -> { - Snackbar snackbar = Snackbar.make(binding.container, R.string.nfc_blocked_while_viewing_card, Snackbar.LENGTH_LONG) - .setAnchorView(binding.fabEdit) - .setAction(R.string.change_settings, view -> { - // Open settings activity - Intent intent = new Intent(getApplicationContext(), SettingsActivity.class); - startActivity(intent); - }); - snackbar.show(); - }, NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B - | NfcAdapter.FLAG_READER_NFC_F | NfcAdapter.FLAG_READER_NFC_V - | NfcAdapter.FLAG_READER_NFC_BARCODE - | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK - | NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS, null); - } - } - - loyaltyCard = DBHelper.getLoyaltyCard(this, database, loyaltyCardId); - if (loyaltyCard == null) { - Log.w(TAG, "Could not lookup loyalty card " + loyaltyCardId); - Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show(); + if (!loadCurrentCardFromDatabase()) { finish(); return; } - setTitle(loyaltyCard.store); - - loyaltyCardGroups = DBHelper.getLoyaltyCardGroups(database, loyaltyCardId); - + populateStateFromCurrentCard(); showHideElementsForScreenSize(); + bindCardIdDescriptionInteractions(); + applyCardStyling(window); + refreshDisplayedCardMedia(); + DBHelper.updateLoyaltyCardLastUsed(database, loyaltyCard.id); + + invalidateOptionsMenu(); + + ShortcutHelper.updateShortcuts(this); + } + + @Override + protected void onDestroy() { + if (database != null && database.isOpen()) { + database.close(); + } + super.onDestroy(); + } + + private void applyWindowPreferences(Window window) { + if (window == null) { + return; + } + + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + WindowManager.LayoutParams attributes = window.getAttributes(); + + // Brightening the screen improves scan reliability when the barcode is displayed on-device. + if (settings.useMaxBrightnessDisplayingBarcode()) { + attributes.screenBrightness = 1F; + } + + if (settings.getKeepScreenOn()) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + // Some users scan cards directly from the lock screen, so keep the historical unlock behavior. + if (settings.getDisableLockscreenWhileViewingCard()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true); + } else { + showWhenLockedSdkLessThan27(window); + } + } + + window.setAttributes(attributes); + } + + private void enablePausedNfcIfConfigured() { + // Pause NFC to prevent NFC payments from triggering while showing a barcode + if (!settings.getDisableNfcWhileViewingCard()) { + return; + } + + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (nfcAdapter == null) { + return; + } + + nfcAdapter.enableReaderMode(this, tag -> { + Snackbar snackbar = Snackbar.make(binding.container, R.string.nfc_blocked_while_viewing_card, Snackbar.LENGTH_LONG) + .setAnchorView(binding.fabEdit) + .setAction(R.string.change_settings, view -> { + // Open settings activity + Intent intent = new Intent(getApplicationContext(), SettingsActivity.class); + startActivity(intent); + }); + snackbar.show(); + }, NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B + | NfcAdapter.FLAG_READER_NFC_F | NfcAdapter.FLAG_READER_NFC_V + | NfcAdapter.FLAG_READER_NFC_BARCODE + | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK + | NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS, null); + } + + private boolean loadCurrentCardFromDatabase() { + loyaltyCard = DBHelper.getLoyaltyCard(this, database, loyaltyCardId); + if (loyaltyCard != null) { + return true; + } + + Log.w(TAG, "Could not lookup loyalty card " + loyaltyCardId); + Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show(); + return false; + } + + private void populateStateFromCurrentCard() { + setTitle(loyaltyCard.store); + loyaltyCardGroups = DBHelper.getLoyaltyCardGroups(database, loyaltyCardId); format = loyaltyCard.barcodeType; cardIdString = loyaltyCard.cardId; barcodeIdString = loyaltyCard.barcodeId; barcodeEncoding = loyaltyCard.barcodeEncoding; - binding.mainImageDescription.setText(loyaltyCard.cardId); + } - // Display full text on click in case it doesn't fit in a single line + private void bindCardIdDescriptionInteractions() { binding.mainImageDescription.setOnClickListener(v -> { - if (mainImageIndex != 0) { - // Don't show cardId dialog, we're displaying something else + // Only the barcode/card-id state exposes the full value and copy action. + if (!isShowingCardIdDescription()) { return; } @@ -740,25 +579,22 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements builder.setTitle(R.string.cardId); builder.setView(cardIdView); builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); - builder.setNeutralButton(R.string.copy_value, (dialog, which) -> { - copyCardIdToClipboard(); - }); + builder.setNeutralButton(R.string.copy_value, (dialog, which) -> copyCardIdToClipboard()); AlertDialog dialog = builder.create(); dialog.show(); }); binding.mainImageDescription.setOnLongClickListener(view -> { - if (mainImageIndex != 0) { - // Don't copy to clipboard, we're showing something else + if (!isShowingCardIdDescription()) { return false; } copyCardIdToClipboard(); return true; }); + } + private void applyCardStyling(Window window) { int backgroundHeaderColor = Utils.getHeaderColor(this, loyaltyCard); - - // Also apply colours to UI elements int darkenedColor = ColorUtils.blendARGB(backgroundHeaderColor, Color.BLACK, 0.1f); binding.barcodeScaler.setProgressTintList(ColorStateList.valueOf(darkenedColor)); binding.barcodeScaler.setThumbTintList(ColorStateList.valueOf(darkenedColor)); @@ -787,63 +623,95 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements fixBottomAppBarImageButtonColor(binding.bottomAppBarNextButton); fixBottomAppBarImageButtonColor(binding.bottomAppBarUpdateBalanceButton); setBottomAppBarButtonState(); + } - boolean isBarcodeSupported; - if (format != null && !format.isSupported()) { - isBarcodeSupported = false; - - Toast.makeText(this, getString(R.string.unsupportedBarcodeType), Toast.LENGTH_LONG).show(); - } else if (format == null) { - isBarcodeSupported = false; - } else { - isBarcodeSupported = true; - } - - imageTypes = new ArrayList<>(); - - if (isBarcodeSupported) { - imageTypes.add(ImageType.BARCODE); - } - - frontImageBitmap = loyaltyCard.getImageFront(this); - if (frontImageBitmap != null) { - imageTypes.add(ImageType.IMAGE_FRONT); - } - - backImageBitmap = loyaltyCard.getImageBack(this); - if (backImageBitmap != null) { - imageTypes.add(ImageType.IMAGE_BACK); - } - + private void refreshDisplayedCardMedia() { + cardNavigator = createCardNavigator(isBarcodeSupported()); setStateBasedOnImageTypes(); - + // Call correct drawMainImage setFullscreen(isFullscreen); + } - DBHelper.updateLoyaltyCardLastUsed(database, loyaltyCard.id); + private boolean isBarcodeSupported() { + if (format == null) { + return false; + } - invalidateOptionsMenu(); + if (format.isSupported()) { + return true; + } - ShortcutHelper.updateShortcuts(this); + Toast.makeText(this, getString(R.string.unsupportedBarcodeType), Toast.LENGTH_LONG).show(); + return false; } private void setStateBasedOnImageTypes() { - // Decrease the card holder size to only fit the value if there is no barcode - // This looks much cleaner ViewGroup.LayoutParams cardHolderLayoutParams = binding.cardHolder.getLayoutParams(); - if (imageTypes.isEmpty()) { + // A card without barcode/front/back media should shrink to its text fallback instead of filling the screen. + if (cardNavigator.isEmpty()) { cardHolderLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; } else { cardHolderLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; } binding.cardHolder.setLayoutParams(cardHolderLayoutParams); - // Ensure buttons and accessibility are correct - setMainImagePreviousNextButtons(); - setMainImageAccessibility(); + updateMainImageUiState(); + } + + private LoyaltyCardImageNavigator createCardNavigator(boolean isBarcodeSupported) { + List availableImageTypes = new ArrayList<>(); + + if (isBarcodeSupported) { + availableImageTypes.add(LoyaltyCardImageType.BARCODE); + } + + frontImageBitmap = loyaltyCard.getImageFront(this); + if (frontImageBitmap != null) { + availableImageTypes.add(LoyaltyCardImageType.IMAGE_FRONT); + } + + backImageBitmap = loyaltyCard.getImageBack(this); + if (backImageBitmap != null) { + availableImageTypes.add(LoyaltyCardImageType.IMAGE_BACK); + } + + // Card edits may remove barcode/front/back images, so keep the previously selected index in range. + int initialIndex = cardNavigator.isEmpty() + ? (restoredImageIndex != null ? restoredImageIndex : 0) + : cardNavigator.getCurrentIndex(); + LoyaltyCardImageNavigator navigator = + new LoyaltyCardImageNavigator(availableImageTypes, initialIndex); + restoredImageIndex = null; + return navigator; + } + + private void renderCurrentMainImage(boolean waitForResize) { + mainImageRenderer.renderCurrent( + cardNavigator.getCurrent(), + frontImageBitmap, + backImageBitmap, + format, + barcodeEncoding, + cardIdString, + barcodeIdString, + barcodeRenderTarget, + binding.mainImageDescription, + binding.mainCardView, + isFullscreen, + waitForResize + ); + } + + private void syncFullscreenScalers() { + binding.barcodeScaler.setProgress(loyaltyCard.zoomLevel); + setScalerGuideline(loyaltyCard.zoomLevel); + binding.barcodeWidthscaler.setProgress(loyaltyCard.zoomLevelWidth); + setScalerWidthGuideline(loyaltyCard.zoomLevelWidth); } @SuppressWarnings("deprecation") private void showWhenLockedSdkLessThan27(Window window) { + // Pre-O_MR1 devices still need the legacy window flags because setShowWhenLocked(true) is unavailable. window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); } @@ -859,13 +727,11 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements return super.onCreateOptionsMenu(menu); } - @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); if (loyaltyCard != null) { - // Update star status if (loyaltyCard.starStatus == 1) { menu.findItem(R.id.action_star_unstar).setIcon(R.drawable.ic_starred); menu.findItem(R.id.action_star_unstar).setTitle(R.string.unstar); @@ -874,7 +740,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements menu.findItem(R.id.action_star_unstar).setTitle(R.string.star); } - // Update archive/unarchive button if (loyaltyCard.archiveStatus != 0) { menu.findItem(R.id.action_unarchive).setVisible(true); menu.findItem(R.id.action_archive).setVisible(false); @@ -967,10 +832,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements private void showHideElementsForScreenSize() { int orientation = getResources().getConfiguration().orientation; - // Detect square-ish screens like the Unihertz Titan + // Treat square-ish devices such as the Unihertz Titan like landscape to avoid a cramped header layout. boolean isSmallHeight = getResources().getDisplayMetrics().heightPixels < (getResources().getDisplayMetrics().widthPixels * 1.5); - // Treat sqaure-ish screens as if (orientation == Configuration.ORIENTATION_LANDSCAPE || isSmallHeight) { Log.d(TAG, "Detected landscape mode or square-ish screen"); binding.iconContainer.setVisibility(View.GONE); @@ -982,144 +846,53 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements enableToolbarBackButton(); } - private void drawBarcode(boolean addPadding) { - mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); - - if (format != null) { - BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask( - getApplicationContext(), - barcodeRenderTarget, - barcodeIdString != null ? barcodeIdString : cardIdString, - format, - barcodeEncoding, - null, - false, - this, - addPadding, - isFullscreen); - mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); - } - } - - private void redrawBarcodeAfterResize(boolean addPadding) { - if (format != null) { - barcodeRenderTarget.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - barcodeRenderTarget.getViewTreeObserver().removeOnGlobalLayoutListener(this); - - Log.d(TAG, "ImageView size now known"); - drawBarcode(addPadding); - } - }); - } - } - - private void drawMainImage(int index, boolean waitForResize, boolean isFullscreen) { - if (imageTypes.isEmpty()) { - barcodeRenderTarget.setVisibility(View.GONE); - binding.mainCardView.setCardBackgroundColor(Color.TRANSPARENT); - binding.mainImageDescription.setTextColor(MaterialColors.getColor(binding.mainImageDescription, com.google.android.material.R.attr.colorOnSurfaceVariant)); - - binding.mainImageDescription.setText(loyaltyCard.cardId); - return; - } - - ImageType wantedImageType = imageTypes.get(index); - - if (wantedImageType == ImageType.BARCODE) { - barcodeRenderTarget.setBackgroundColor(Color.WHITE); - binding.mainCardView.setCardBackgroundColor(Color.WHITE); - binding.mainImageDescription.setTextColor(getResources().getColor(R.color.md_theme_light_onSurfaceVariant)); - - if (waitForResize) { - redrawBarcodeAfterResize(!isFullscreen); - } else { - drawBarcode(!isFullscreen); - } - - binding.mainImageDescription.setText(loyaltyCard.cardId); - barcodeRenderTarget.setContentDescription(getString(R.string.barcodeImageDescriptionWithType, format.prettyName())); - } else if (wantedImageType == ImageType.IMAGE_FRONT) { - barcodeRenderTarget.setImageBitmap(frontImageBitmap); - barcodeRenderTarget.setBackgroundColor(Color.TRANSPARENT); - binding.mainCardView.setCardBackgroundColor(Color.TRANSPARENT); - binding.mainImageDescription.setTextColor(MaterialColors.getColor(binding.mainImageDescription, com.google.android.material.R.attr.colorOnSurfaceVariant)); - - binding.mainImageDescription.setText(getString(R.string.frontImageDescription)); - barcodeRenderTarget.setContentDescription(getString(R.string.frontImageDescription)); - } else if (wantedImageType == ImageType.IMAGE_BACK) { - barcodeRenderTarget.setImageBitmap(backImageBitmap); - barcodeRenderTarget.setBackgroundColor(Color.TRANSPARENT); - binding.mainCardView.setCardBackgroundColor(Color.TRANSPARENT); - binding.mainImageDescription.setTextColor(MaterialColors.getColor(binding.mainImageDescription, com.google.android.material.R.attr.colorOnSurfaceVariant)); - - binding.mainImageDescription.setText(getString(R.string.backImageDescription)); - barcodeRenderTarget.setContentDescription(getString(R.string.backImageDescription)); - } else { - throw new IllegalArgumentException("Unknown image type: " + wantedImageType); - } - - barcodeRenderTarget.setVisibility(View.VISIBLE); - } - private void setMainImage(boolean next, boolean overflow) { - int newIndex = mainImageIndex + (next ? 1 : -1); - - if (newIndex >= imageTypes.size() && overflow) { - newIndex = 0; - } - - if (newIndex == -1 || newIndex >= imageTypes.size()) { + boolean moved = next ? cardNavigator.moveNext(overflow) : cardNavigator.movePrevious(); + if (!moved) { return; } - mainImageIndex = newIndex; + renderCurrentMainImage(false); - drawMainImage(newIndex, false, isFullscreen); - - setMainImagePreviousNextButtons(); - setMainImageAccessibility(); + updateMainImageUiState(); } - private void setMainImageAccessibility() { - // Single-click actions - if (mainImageIndex == 0) { - ViewCompat.replaceAccessibilityAction( - binding.mainImage, - AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, - getString(R.string.moveBarcodeToTopOfScreen), - null - ); - } else { - int accessibilityClickAction; - if (mainImageIndex == 1) { - accessibilityClickAction = R.string.openFrontImageInGalleryApp; - } else if (mainImageIndex == 2) { - accessibilityClickAction = R.string.openBackImageInGalleryApp; - } else { - throw new IndexOutOfBoundsException("setMainImageAccessibility was out of range (action_click)"); - } + private boolean isShowingCardIdDescription() { + return cardNavigator.isEmpty() || cardNavigator.getCurrent() == LoyaltyCardImageType.BARCODE; + } - ViewCompat.replaceAccessibilityAction( - binding.mainImage, - AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, - getString(accessibilityClickAction), - null - ); + private void updateMainImageUiState() { + updateMainImagePreviousNextButtons(); + updateMainImageAccessibility(); + } + + private void updateMainImageAccessibility() { + // The same image view can represent barcode/front/back states, so accessibility actions must track that role. + int accessibilityClickAction; + LoyaltyCardImageType currentImageType = cardNavigator.getCurrent(); + if (currentImageType == LoyaltyCardImageType.IMAGE_FRONT) { + accessibilityClickAction = R.string.openFrontImageInGalleryApp; + } else if (currentImageType == LoyaltyCardImageType.IMAGE_BACK) { + accessibilityClickAction = R.string.openBackImageInGalleryApp; + } else { + accessibilityClickAction = R.string.moveBarcodeToTopOfScreen; } - // Long-press actions + ViewCompat.replaceAccessibilityAction( + binding.mainImage, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + getString(accessibilityClickAction), + null + ); + int accessibilityLongPressAction; - if (mainImageIndex == 0) { + LoyaltyCardImageType nextImageType = cardNavigator.peekNext(true); + if (nextImageType == LoyaltyCardImageType.IMAGE_FRONT) { accessibilityLongPressAction = R.string.switchToFrontImage; - } else if (mainImageIndex == 1) { + } else if (nextImageType == LoyaltyCardImageType.IMAGE_BACK) { accessibilityLongPressAction = R.string.switchToBackImage; - } else if (mainImageIndex == 2) { - accessibilityLongPressAction = R.string.switchToBarcode; } else { - throw new IndexOutOfBoundsException("setMainImageAccessibility was out of range (action_long_click)"); + accessibilityLongPressAction = R.string.switchToBarcode; } ViewCompat.replaceAccessibilityAction( @@ -1130,45 +903,35 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements ); } - private void setMainImagePreviousNextButtons() { - // Ensure the main image index is valid. After a card update, some images (front/back/barcode) - // may have been removed, so the index should not exceed the number of available images. - if(mainImageIndex > imageTypes.size() - 1){ - mainImageIndex = 0; - } - - if (imageTypes.size() < 2) { + private void updateMainImagePreviousNextButtons() { + if (cardNavigator.size() < 2) { binding.mainLeftButton.setVisibility(View.INVISIBLE); binding.mainRightButton.setVisibility(View.INVISIBLE); - binding.mainLeftButton.setOnClickListener(null); binding.mainRightButton.setOnClickListener(null); - return; } - final ImageButton prevButton; + final ImageButton previousButton; final ImageButton nextButton; - + // In RTL, the visual left/right buttons map to opposite logical navigation directions. if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - prevButton = binding.mainRightButton; + previousButton = binding.mainRightButton; nextButton = binding.mainLeftButton; } else { - prevButton = binding.mainLeftButton; + previousButton = binding.mainLeftButton; nextButton = binding.mainRightButton; } - // Enable left button if we can go further left - if (mainImageIndex > 0) { - prevButton.setVisibility(View.VISIBLE); - prevButton.setOnClickListener(view -> setMainImage(false, false)); + if (cardNavigator.canGoPrevious()) { + previousButton.setVisibility(View.VISIBLE); + previousButton.setOnClickListener(view -> setMainImage(false, false)); } else { - prevButton.setVisibility(View.INVISIBLE); - prevButton.setOnClickListener(null); + previousButton.setVisibility(View.INVISIBLE); + previousButton.setOnClickListener(null); } - // Enable right button if we can go further right - if (mainImageIndex < (imageTypes.size() - 1)) { + if (cardNavigator.canGoNext()) { nextButton.setVisibility(View.VISIBLE); nextButton.setOnClickListener(view -> setMainImage(true, false)); } else { @@ -1178,124 +941,99 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } /** - * When enabled, hides the status bar and moves the barcode to the top of the screen. - *

- * The purpose of this function is to make sure the barcode can be scanned from the phone - * by machines which offer no space to insert the complete device. + * Fullscreen hides system chrome and moves the barcode higher on screen so scanners can read + * it even when the whole device does not fit cleanly in front of the reader. */ private void setFullscreen(boolean enabled) { - ActionBar actionBar = getSupportActionBar(); isFullscreen = enabled; + ActionBar actionBar = getSupportActionBar(); - if (enabled && !imageTypes.isEmpty()) { - Log.d(TAG, "Move into fullscreen"); - + if (enabled && !cardNavigator.isEmpty()) { barcodeRenderTarget = binding.fullscreenImage; - - // Show only fullscreen view binding.container.setVisibility(View.GONE); binding.fullscreenLayout.setVisibility(View.VISIBLE); + // Square barcodes resize uniformly, and Data Matrix behaves similarly, so width-only scaling adds no value. + binding.setWidthLayout.setVisibility( + format == null || format.isSquare() || format.format() == com.google.zxing.BarcodeFormat.DATA_MATRIX + ? View.GONE + : View.VISIBLE + ); + renderCurrentMainImage(true); + syncFullscreenScalers(); - // Only show width slider if the barcode isn't square (square barcodes will resize height and width together) - // or if the internals of the barcode are squares, like DATA_MATRIX - binding.setWidthLayout.setVisibility((format.isSquare() || format.format() == BarcodeFormat.DATA_MATRIX) ? View.GONE : View.VISIBLE); - - drawMainImage(mainImageIndex, true, isFullscreen); - - binding.barcodeScaler.setProgress(loyaltyCard.zoomLevel); - setScalerGuideline(loyaltyCard.zoomLevel); - - binding.barcodeWidthscaler.setProgress(loyaltyCard.zoomLevelWidth); - setScalerWidthGuideline(loyaltyCard.zoomLevelWidth); - - // Hide actionbar if (actionBar != null) { actionBar.hide(); } - // Hide other UI elements binding.bottomAppBar.setVisibility(View.GONE); binding.fabEdit.setVisibility(View.GONE); - - // Set Android to fullscreen mode - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Window window = getWindow(); - if (window != null) { - window.setDecorFitsSystemWindows(false); - WindowInsetsController wic = window.getInsetsController(); - if (wic != null) { - wic.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars()); - wic.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } - } - } else { - setFullscreenModeSdkLessThan30(); - } + setFullscreenMode(); } else { - Log.d(TAG, "Move out of fullscreen"); - barcodeRenderTarget = binding.mainImage; - - // Show only regular view binding.container.setVisibility(View.VISIBLE); binding.fullscreenLayout.setVisibility(View.GONE); + renderCurrentMainImage(true); - drawMainImage(mainImageIndex, true, isFullscreen); - - // Show actionbar if (actionBar != null) { actionBar.show(); } - // Show other UI elements binding.bottomAppBar.setVisibility(View.VISIBLE); binding.fabEdit.setVisibility(View.VISIBLE); - - // Unset fullscreen mode - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Window window = getWindow(); - if (window != null) { - window.setDecorFitsSystemWindows(true); - WindowInsetsController wic = window.getInsetsController(); - if (wic != null) { - wic.show(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars()); - wic.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_DEFAULT); - } - } - } else { - unsetFullscreenModeSdkLessThan30(); - } + unsetFullscreenMode(); } - Log.d("setFullScreen", "Is full screen enabled? " + enabled + " Zoom Level = " + binding.barcodeScaler.getProgress()); } @SuppressWarnings("deprecation") - private void unsetFullscreenModeSdkLessThan30() { + private void setFullscreenMode() { Window window = getWindow(); - if (window != null) { - window.getDecorView().setSystemUiVisibility( - window.getDecorView().getSystemUiVisibility() - & ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - & ~View.SYSTEM_UI_FLAG_FULLSCREEN - ); + if (window == null) { + return; } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false); + WindowInsetsController wic = window.getInsetsController(); + if (wic != null) { + wic.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars()); + wic.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + return; + } + + window.getDecorView().setSystemUiVisibility( + window.getDecorView().getSystemUiVisibility() + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_FULLSCREEN + ); } @SuppressWarnings("deprecation") - private void setFullscreenModeSdkLessThan30() { + private void unsetFullscreenMode() { Window window = getWindow(); - if (window != null) { - window.getDecorView().setSystemUiVisibility( - window.getDecorView().getSystemUiVisibility() - | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_FULLSCREEN - ); + if (window == null) { + return; } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(true); + WindowInsetsController wic = window.getInsetsController(); + if (wic != null) { + wic.show(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars()); + wic.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_DEFAULT); + } + return; + } + + window.getDecorView().setSystemUiVisibility( + window.getDecorView().getSystemUiVisibility() + & ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + & ~View.SYSTEM_UI_FLAG_FULLSCREEN + ); } private void copyCardIdToClipboard() { - // Take the value that’s already displayed to the user String value = loyaltyCard.cardId; if (value == null || value.isEmpty()) { diff --git a/app/src/main/java/protect/card_locker/cardview/LoyaltyCardViewDialogs.java b/app/src/main/java/protect/card_locker/cardview/LoyaltyCardViewDialogs.java new file mode 100644 index 000000000..2c7daf68f --- /dev/null +++ b/app/src/main/java/protect/card_locker/cardview/LoyaltyCardViewDialogs.java @@ -0,0 +1,246 @@ +package protect.card_locker.cardview; + +import android.content.Context; +import android.graphics.Color; +import android.text.InputType; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.DigitsKeyListener; +import android.text.style.ForegroundColorSpan; +import android.text.util.Linkify; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.TextInputEditText; + +import java.math.BigDecimal; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.function.Predicate; + +import protect.card_locker.*; + +final class LoyaltyCardViewDialogs { + interface BalanceUpdateListener { + void onBalanceUpdated(BigDecimal newBalance); + } + + void showInfoDialog(Context context, LoyaltyCard loyaltyCard, List loyaltyCardGroups) { + AlertDialog.Builder infoDialog = new MaterialAlertDialogBuilder(context); + + int dialogContentPadding = context.getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding); + infoDialog.setTitle(loyaltyCard.store); + + TextView infoTextview = new TextView(context); + infoTextview.setPadding( + dialogContentPadding, + dialogContentPadding / 2, + dialogContentPadding, + 0 + ); + infoTextview.setAutoLinkMask(Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS); + infoTextview.setTextIsSelectable(true); + + SpannableStringBuilder infoText = new SpannableStringBuilder(); + if (!loyaltyCard.note.isEmpty()) { + infoText.append(loyaltyCard.note); + } + + if (!loyaltyCardGroups.isEmpty()) { + List groupNames = new ArrayList<>(); + for (Group group : loyaltyCardGroups) { + groupNames.add(group._id); + } + + padSpannableString(infoText); + infoText.append(context.getString(R.string.groupsList, TextUtils.join(", ", groupNames))); + } + + if (hasBalance(loyaltyCard)) { + padSpannableString(infoText); + infoText.append(context.getString( + R.string.balanceSentence, + Utils.formatBalance(context, loyaltyCard.balance, loyaltyCard.balanceType) + )); + } + + appendDateInfo( + context, + infoText, + loyaltyCard.validFrom, + Utils::isNotYetValid, + R.string.validFromSentence, + R.string.validFromSentence + ); + appendDateInfo( + context, + infoText, + loyaltyCard.expiry, + Utils::hasExpired, + R.string.expiryStateSentenceExpired, + R.string.expiryStateSentence + ); + + infoTextview.setText(infoText); + infoDialog.setView(infoTextview); + infoDialog.setPositiveButton(R.string.ok, (dialogInterface, i) -> dialogInterface.dismiss()); + infoDialog.create().show(); + } + + void showBalanceUpdateDialog( + Context context, + LoyaltyCard loyaltyCard, + BalanceUpdateListener balanceUpdateListener + ) { + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(R.string.updateBalanceTitle); + + FrameLayout container = new FrameLayout(context); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + int contentPadding = context.getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding); + params.leftMargin = contentPadding; + params.topMargin = contentPadding / 2; + params.rightMargin = contentPadding; + + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + + TextView currentTextview = new TextView(context); + currentTextview.setText(context.getString( + R.string.currentBalanceSentence, + Utils.formatBalance(context, loyaltyCard.balance, loyaltyCard.balanceType) + )); + layout.addView(currentTextview); + + final TextInputEditText input = new TextInputEditText(context); + input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + input.setKeyListener(DigitsKeyListener.getInstance("0123456789,.")); + input.setHint(R.string.updateBalanceHint); + + layout.addView(input); + layout.setLayoutParams(params); + container.addView(layout); + builder.setView(container); + + builder.setPositiveButton(R.string.spend, (dialogInterface, i) -> { + try { + BigDecimal balanceChange = Utils.parseBalance(input.getText().toString(), loyaltyCard.balanceType); + BigDecimal newBalance = loyaltyCard.balance.subtract(balanceChange).max(new BigDecimal(0)); + balanceUpdateListener.onBalanceUpdated(newBalance); + Toast.makeText( + context, + context.getString( + R.string.newBalanceSentence, + Utils.formatBalance(context, newBalance, loyaltyCard.balanceType) + ), + Toast.LENGTH_LONG + ).show(); + } catch (ParseException e) { + Toast.makeText(context, R.string.amountParsingFailed, Toast.LENGTH_LONG).show(); + } + }); + builder.setNegativeButton(R.string.receive, (dialogInterface, i) -> { + try { + BigDecimal balanceChange = Utils.parseBalance(input.getText().toString(), loyaltyCard.balanceType); + BigDecimal newBalance = loyaltyCard.balance.add(balanceChange); + balanceUpdateListener.onBalanceUpdated(newBalance); + Toast.makeText( + context, + context.getString( + R.string.newBalanceSentence, + Utils.formatBalance(context, newBalance, loyaltyCard.balanceType) + ), + Toast.LENGTH_LONG + ).show(); + } catch (ParseException e) { + Toast.makeText(context, R.string.amountParsingFailed, Toast.LENGTH_LONG).show(); + } + }); + builder.setNeutralButton(context.getString(R.string.cancel), (dialog, which) -> dialog.cancel()); + AlertDialog dialog = builder.create(); + + // Button state depends on the parsed input, so listeners must be bound after the dialog exists. + input.addTextChangedListener(new SimpleTextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + BigDecimal balanceChange; + + try { + balanceChange = Utils.parseBalance(s.toString(), loyaltyCard.balanceType); + } catch (ParseException e) { + input.setError(context.getString(R.string.amountParsingFailed)); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false); + return; + } + + input.setError(null); + boolean hasNonZeroValue = !balanceChange.equals(new BigDecimal(0)); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(hasNonZeroValue); + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(hasNonZeroValue); + } + }); + + dialog.show(); + // Touching dialog buttons before show() can crash because they are not created yet. + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false); + + if (dialog.getWindow() != null) { + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + // Focus immediately so the keyboard opens on the amount field. + input.requestFocus(); + } + + private boolean hasBalance(LoyaltyCard loyaltyCard) { + return !loyaltyCard.balance.equals(new BigDecimal(0)); + } + + private SpannableStringBuilder padSpannableString(SpannableStringBuilder spannableStringBuilder) { + if (spannableStringBuilder.length() > 0) { + spannableStringBuilder.append("\n\n"); + } + + return spannableStringBuilder; + } + + private void appendDateInfo( + Context context, + SpannableStringBuilder infoText, + Date date, + Predicate dateCheck, + @StringRes int dateCheckTrueString, + @StringRes int dateCheckFalseString + ) { + if (date == null) { + return; + } + + String formattedDate = DateFormat.getDateInstance(DateFormat.LONG).format(date); + + padSpannableString(infoText); + if (dateCheck.test(date)) { + int start = infoText.length(); + infoText.append(context.getString(dateCheckTrueString, formattedDate)); + infoText.setSpan(new ForegroundColorSpan(Color.RED), start, infoText.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } else { + infoText.append(context.getString(dateCheckFalseString, formattedDate)); + } + } +} diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index 0b7cbf9a8..d43d78893 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -51,6 +51,7 @@ import com.google.android.material.textfield.TextInputLayout; import com.google.zxing.BarcodeFormat; import com.google.zxing.client.android.Intents; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -71,13 +72,19 @@ import java.text.DateFormat; import java.text.ParseException; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Currency; import java.util.Date; +import java.util.List; + +import protect.card_locker.cardview.LoyaltyCardViewActivity; @RunWith(RobolectricTestRunner.class) public class LoyaltyCardViewActivityTest { private final String BARCODE_DATA = "428311627547"; private final CatimaBarcode BARCODE_TYPE = CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A); + private final List> activityControllers = new ArrayList<>(); + private final List dbHelpers = new ArrayList<>(); private final String EAN_BARCODE_DATA = "4763705295336"; private final CatimaBarcode EAN_BARCODE_TYPE = CatimaBarcode.fromBarcode(BarcodeFormat.EAN_13); @@ -101,6 +108,30 @@ public class LoyaltyCardViewActivityTest { ShadowLog.stream = System.out; } + @After + public void tearDown() { + for (int index = activityControllers.size() - 1; index >= 0; index--) { + ActivityController activityController = activityControllers.get(index); + try { + activityController.pause().stop().destroy(); + } catch (RuntimeException ignored) { + try { + activityController.destroy(); + } catch (RuntimeException ignoredAgain) { + } + } + } + activityControllers.clear(); + + for (int index = dbHelpers.size() - 1; index >= 0; index--) { + try { + dbHelpers.get(index).close(); + } catch (RuntimeException ignored) { + } + } + dbHelpers.clear(); + } + /** * Register a handler in the package manager for a image capture intent */ @@ -137,87 +168,110 @@ public class LoyaltyCardViewActivityTest { final String barcodeType, final String barcodeEncoding, boolean creatingNewCard) throws ParseException { - SQLiteDatabase database = new DBHelper(activity).getWritableDatabase(); - if (creatingNewCard) { - assertEquals(0, DBHelper.getLoyaltyCardCount(database)); - } else { + DBHelper dbHelper = new DBHelper(activity); + SQLiteDatabase database = dbHelper.getWritableDatabase(); + try { + if (creatingNewCard) { + assertEquals(0, DBHelper.getLoyaltyCardCount(database)); + } else { + assertEquals(1, DBHelper.getLoyaltyCardCount(database)); + } + + final EditText storeField = activity.findViewById(R.id.storeNameEdit); + final EditText noteField = activity.findViewById(R.id.noteEdit); + final TextInputLayout validFromView = activity.findViewById(R.id.validFromView); + final TextInputLayout expiryView = activity.findViewById(R.id.expiryView); + final EditText balanceView = activity.findViewById(R.id.balanceField); + final EditText balanceCurrencyField = activity.findViewById(R.id.balanceCurrencyField); + final TextView cardIdField = activity.findViewById(R.id.cardIdView); + final TextView barcodeIdField = activity.findViewById(R.id.barcodeIdField); + final TextView barcodeTypeField = activity.findViewById(R.id.barcodeTypeField); + final TextView barcodeEncodingField = activity.findViewById(R.id.barcodeEncodingField); + + storeField.setText(store); + noteField.setText(note); + validFromView.setTag(validFrom); + expiryView.setTag(expiry); + balanceView.setText(balance.toPlainString()); + balanceCurrencyField.setText(balanceType); + cardIdField.setText(cardId); + barcodeIdField.setText(barcodeId); + barcodeTypeField.setText(barcodeType); + barcodeEncodingField.setText(barcodeEncoding); + + assertEquals(false, activity.isFinishing()); + activity.findViewById(R.id.fabSave).performClick(); + assertEquals(true, activity.isFinishing()); + assertEquals(1, DBHelper.getLoyaltyCardCount(database)); + + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), database, 1); + assertEquals(store, card.store); + assertEquals(note, card.note); + assertEquals(balance, card.balance); + + // The special "Any date" string shouldn't actually be written to the loyalty card + if (validFrom.equals(activity.getApplicationContext().getString(R.string.anyDate))) { + assertEquals(null, card.validFrom); + } else { + assertEquals(DateFormat.getDateInstance().parse(validFrom), card.validFrom); + } + + // The special "Never" string shouldn't actually be written to the loyalty card + if (expiry.equals(activity.getApplicationContext().getString(R.string.never))) { + assertEquals(null, card.expiry); + } else { + assertEquals(DateFormat.getDateInstance().parse(expiry), card.expiry); + } + + // The special "Points" string shouldn't actually be written to the loyalty card + if (balanceType.equals(activity.getApplicationContext().getString(R.string.points))) { + assertEquals(null, card.balanceType); + } else { + assertEquals(Currency.getInstance(balanceType), card.balanceType); + } + assertEquals(cardId, card.cardId); + + // The special "Same as barcode ID" string shouldn't actually be written to the loyalty card + if (barcodeId.equals(activity.getApplicationContext().getString(R.string.sameAsCardId))) { + assertEquals(null, card.barcodeId); + } else { + assertEquals(barcodeId, card.barcodeId); + } + + // The special "No barcode" string shouldn't actually be written to the loyalty card + if (barcodeType.equals(activity.getApplicationContext().getString(R.string.noBarcode))) { + assertEquals(null, card.barcodeType); + } else { + assertEquals(CatimaBarcode.fromName(barcodeType).format(), card.barcodeType.format()); + } + + assertEquals(Charset.forName(barcodeEncoding), card.barcodeEncoding); + + assertNotNull(card.headerColor); + } finally { + dbHelper.close(); } + } - final EditText storeField = activity.findViewById(R.id.storeNameEdit); - final EditText noteField = activity.findViewById(R.id.noteEdit); - final TextInputLayout validFromView = activity.findViewById(R.id.validFromView); - final TextInputLayout expiryView = activity.findViewById(R.id.expiryView); - final EditText balanceView = activity.findViewById(R.id.balanceField); - final EditText balanceCurrencyField = activity.findViewById(R.id.balanceCurrencyField); - final TextView cardIdField = activity.findViewById(R.id.cardIdView); - final TextView barcodeIdField = activity.findViewById(R.id.barcodeIdField); - final TextView barcodeTypeField = activity.findViewById(R.id.barcodeTypeField); - final TextView barcodeEncodingField = activity.findViewById(R.id.barcodeEncodingField); + private SQLiteDatabase getWritableEmptyDatabase(Context context) { + DBHelper dbHelper = TestHelpers.getEmptyDb(context); + dbHelpers.add(dbHelper); + return dbHelper.getWritableDatabase(); + } - storeField.setText(store); - noteField.setText(note); - validFromView.setTag(validFrom); - expiryView.setTag(expiry); - balanceView.setText(balance.toPlainString()); - balanceCurrencyField.setText(balanceType); - cardIdField.setText(cardId); - barcodeIdField.setText(barcodeId); - barcodeTypeField.setText(barcodeType); - barcodeEncodingField.setText(barcodeEncoding); + private ActivityController trackActivityController(ActivityController activityController) { + activityControllers.add(activityController); + return activityController; + } - assertEquals(false, activity.isFinishing()); - activity.findViewById(R.id.fabSave).performClick(); - assertEquals(true, activity.isFinishing()); - - assertEquals(1, DBHelper.getLoyaltyCardCount(database)); - - LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), database, 1); - assertEquals(store, card.store); - assertEquals(note, card.note); - assertEquals(balance, card.balance); - - // The special "Any date" string shouldn't actually be written to the loyalty card - if (validFrom.equals(activity.getApplicationContext().getString(R.string.anyDate))) { - assertEquals(null, card.validFrom); + private void checkFieldType(final Activity activity, final int id, FieldTypeView fieldTypeView) { + final View view = activity.findViewById(id); + if (fieldTypeView == FieldTypeView.TextView) { + assertTrue(view instanceof TextView); } else { - assertEquals(DateFormat.getDateInstance().parse(validFrom), card.validFrom); + assertTrue(view instanceof TextInputLayout || view instanceof ImageView); } - - // The special "Never" string shouldn't actually be written to the loyalty card - if (expiry.equals(activity.getApplicationContext().getString(R.string.never))) { - assertEquals(null, card.expiry); - } else { - assertEquals(DateFormat.getDateInstance().parse(expiry), card.expiry); - } - - // The special "Points" string shouldn't actually be written to the loyalty card - if (balanceType.equals(activity.getApplicationContext().getString(R.string.points))) { - assertEquals(null, card.balanceType); - } else { - assertEquals(Currency.getInstance(balanceType), card.balanceType); - } - assertEquals(cardId, card.cardId); - - // The special "Same as barcode ID" string shouldn't actually be written to the loyalty card - if (barcodeId.equals(activity.getApplicationContext().getString(R.string.sameAsCardId))) { - assertEquals(null, card.barcodeId); - } else { - assertEquals(barcodeId, card.barcodeId); - } - - // The special "No barcode" string shouldn't actually be written to the loyalty card - if (barcodeType.equals(activity.getApplicationContext().getString(R.string.noBarcode))) { - assertEquals(null, card.barcodeType); - } else { - assertEquals(CatimaBarcode.fromName(barcodeType).format(), card.barcodeType.format()); - } - - assertEquals(Charset.forName(barcodeEncoding), card.barcodeEncoding); - - assertNotNull(card.headerColor); - - database.close(); } /** @@ -357,7 +411,7 @@ public class LoyaltyCardViewActivityTest { @Test @Config(qualifiers="de") public void noCrashOnRegionlessLocale() { - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class).create()); LoyaltyCardEditActivity activity = (LoyaltyCardEditActivity) activityController.get(); final Context context = activity.getApplicationContext(); @@ -375,107 +429,110 @@ public class LoyaltyCardViewActivityTest { @Test public void noDataLossOnResumeOrRotate() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); + try { + registerMediaStoreIntentHandler(); - registerMediaStoreIntentHandler(); + Integer cardId; - Integer cardId; + for (boolean newCard : new boolean[]{false, true}) { + System.out.println(); + System.out.println("====="); + System.out.println("New card? " + newCard); + System.out.println("====="); + System.out.println(); - for (boolean newCard : new boolean[]{false, true}) { - System.out.println(); - System.out.println("====="); - System.out.println("New card? " + newCard); - System.out.println("====="); - System.out.println(); + if (!newCard) { + cardId = (int) DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.UTF_8, Color.BLACK, 0, null, 0); + } else { + cardId = null; + } - if (!newCard) { - cardId = (int) DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.UTF_8, Color.BLACK, 0, null, 0); - } else { - cardId = null; + ActivityController activityController = createActivityWithLoyaltyCard(true, cardId); + LoyaltyCardEditActivity activity = (LoyaltyCardEditActivity) activityController.get(); + + activityController.start(); + activityController.visible(); + activityController.resume(); + + shadowOf(getMainLooper()).idle(); + + // Check default settings + checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, newCard ? "" : "store", newCard ? "" : "note", context.getString(R.string.anyDate), context.getString(R.string.never), "0", context.getString(R.string.points), newCard ? "" : EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), newCard ? context.getString(R.string.noBarcode) : EAN_BARCODE_TYPE.prettyName(), newCard ? "ISO-8859-1" : "UTF-8", null, null); + + // Change everything + final EditText storeField = activity.findViewById(R.id.storeNameEdit); + final EditText noteField = activity.findViewById(R.id.noteEdit); + final EditText validFromField = activity.findViewById(R.id.validFromField); + final EditText expiryField = activity.findViewById(R.id.expiryField); + final EditText balanceField = activity.findViewById(R.id.balanceField); + final EditText balanceTypeField = activity.findViewById(R.id.balanceCurrencyField); + final EditText cardIdField = activity.findViewById(R.id.cardIdView); + final EditText barcodeField = activity.findViewById(R.id.barcodeIdField); + final EditText barcodeTypeField = activity.findViewById(R.id.barcodeTypeField); + final EditText barcodeEncodingField = activity.findViewById(R.id.barcodeEncodingField); + final ImageView frontImageView = activity.findViewById(R.id.frontImage); + final ImageView backImageView = activity.findViewById(R.id.backImage); + + Currency currency = Currency.getInstance("EUR"); + Date validFromDate = Date.from(Instant.now().minus(20, ChronoUnit.DAYS)); + Date expiryDate = new Date(); + Bitmap frontBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.circle); + Bitmap backBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_done); + + storeField.setText("correct store"); + noteField.setText("correct note"); + LoyaltyCardEditActivity.formatDateField(context, validFromField, validFromDate); + activity.setLoyaltyCardValidFrom(validFromDate); + LoyaltyCardEditActivity.formatDateField(context, expiryField, expiryDate); + activity.setLoyaltyCardExpiry(expiryDate); + balanceField.setText("100"); + balanceTypeField.setText(currency.getSymbol()); + cardIdField.setText("12345678"); + barcodeField.setText("87654321"); + barcodeTypeField.setText(CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName()); + barcodeEncodingField.setText(StandardCharsets.ISO_8859_1.name()); + activity.setCardImage(ImageLocationType.front, frontImageView, frontBitmap, true); + activity.setCardImage(ImageLocationType.back, backImageView, backBitmap, true); + + shadowOf(getMainLooper()).idle(); + + // Check if changed + checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), StandardCharsets.ISO_8859_1.name(), frontBitmap, backBitmap); + + // Resume + activityController.pause(); + activityController.resume(); + + shadowOf(getMainLooper()).idle(); + + // Check if no changes lost + checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), StandardCharsets.ISO_8859_1.name(), frontBitmap, backBitmap); + + // Rotate to landscape + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + activity.recreate(); + shadowOf(getMainLooper()).idle(); + + // Check if no changes lost + checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), StandardCharsets.ISO_8859_1.name(), frontBitmap, backBitmap); + + // Rotate to portrait + shadowOf(getMainLooper()).idle(); + activity.recreate(); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + + // Check if no changes lost + checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), StandardCharsets.ISO_8859_1.name(), frontBitmap, backBitmap); } - - ActivityController activityController = createActivityWithLoyaltyCard(true, cardId); - LoyaltyCardEditActivity activity = (LoyaltyCardEditActivity) activityController.get(); - - activityController.start(); - activityController.visible(); - activityController.resume(); - - shadowOf(getMainLooper()).idle(); - - // Check default settings - checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, newCard ? "" : "store", newCard ? "" : "note", context.getString(R.string.anyDate), context.getString(R.string.never), "0", context.getString(R.string.points), newCard ? "" : EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), newCard ? context.getString(R.string.noBarcode) : EAN_BARCODE_TYPE.prettyName(), newCard ? "ISO-8859-1" : "UTF-8", null, null); - - // Change everything - final EditText storeField = activity.findViewById(R.id.storeNameEdit); - final EditText noteField = activity.findViewById(R.id.noteEdit); - final EditText validFromField = activity.findViewById(R.id.validFromField); - final EditText expiryField = activity.findViewById(R.id.expiryField); - final EditText balanceField = activity.findViewById(R.id.balanceField); - final EditText balanceTypeField = activity.findViewById(R.id.balanceCurrencyField); - final EditText cardIdField = activity.findViewById(R.id.cardIdView); - final EditText barcodeField = activity.findViewById(R.id.barcodeIdField); - final EditText barcodeTypeField = activity.findViewById(R.id.barcodeTypeField); - final EditText barcodeEncodingField = activity.findViewById(R.id.barcodeEncodingField); - final ImageView frontImageView = activity.findViewById(R.id.frontImage); - final ImageView backImageView = activity.findViewById(R.id.backImage); - - Currency currency = Currency.getInstance("EUR"); - Date validFromDate = Date.from(Instant.now().minus(20, ChronoUnit.DAYS)); - Date expiryDate = new Date(); - Bitmap frontBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.circle); - Bitmap backBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_done); - - storeField.setText("correct store"); - noteField.setText("correct note"); - LoyaltyCardEditActivity.formatDateField(context, validFromField, validFromDate); - activity.setLoyaltyCardValidFrom(validFromDate); - LoyaltyCardEditActivity.formatDateField(context, expiryField, expiryDate); - activity.setLoyaltyCardExpiry(expiryDate); - balanceField.setText("100"); - balanceTypeField.setText(currency.getSymbol()); - cardIdField.setText("12345678"); - barcodeField.setText("87654321"); - barcodeTypeField.setText(CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName()); - barcodeEncodingField.setText(StandardCharsets.ISO_8859_1.name()); - activity.setCardImage(ImageLocationType.front, frontImageView, frontBitmap, true); - activity.setCardImage(ImageLocationType.back, backImageView, backBitmap, true); - - shadowOf(getMainLooper()).idle(); - - // Check if changed - checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), StandardCharsets.ISO_8859_1.name(), frontBitmap, backBitmap); - - // Resume - activityController.pause(); - activityController.resume(); - - shadowOf(getMainLooper()).idle(); - - // Check if no changes lost - checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), StandardCharsets.ISO_8859_1.name(), frontBitmap, backBitmap); - - // Rotate to landscape - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - activity.recreate(); - shadowOf(getMainLooper()).idle(); - - // Check if no changes lost - checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), StandardCharsets.ISO_8859_1.name(), frontBitmap, backBitmap); - - // Rotate to portrait - shadowOf(getMainLooper()).idle(); - activity.recreate(); - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - - // Check if no changes lost - checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), StandardCharsets.ISO_8859_1.name(), frontBitmap, backBitmap); + } finally { + database.close(); } } @Test public void startWithoutParametersCheckFieldsAvailable() { - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class).create()); activityController.start(); activityController.visible(); activityController.resume(); @@ -489,9 +546,9 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithoutParametersCannotCreateLoyaltyCard() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class).create()); activityController.start(); activityController.visible(); activityController.resume(); @@ -519,7 +576,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithoutParametersBack() { - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class).create()); activityController.start(); activityController.visible(); activityController.resume(); @@ -535,7 +592,7 @@ public class LoyaltyCardViewActivityTest { public void startWithoutParametersCaptureBarcodeCreateLoyaltyCard() throws IOException, ParseException { registerMediaStoreIntentHandler(); - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class).create()); activityController.start(); activityController.visible(); activityController.resume(); @@ -559,7 +616,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithoutParametersCaptureBarcodeFailure() throws IOException { - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class).create()); activityController.start(); activityController.visible(); activityController.resume(); @@ -580,7 +637,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithoutParametersCaptureBarcodeCancel() throws IOException { - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class).create()); activityController.start(); activityController.visible(); activityController.resume(); @@ -631,13 +688,13 @@ public class LoyaltyCardViewActivityTest { intent.putExtras(bundle); - return Robolectric.buildActivity(clazz, intent).create(); + return trackActivityController(Robolectric.buildActivity(clazz, intent).create()); } @Test public void startWithLoyaltyCardEditModeCheckDisplay() throws IOException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -656,7 +713,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardViewModeCheckDisplay() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.UTF_8, Color.BLACK, 0, null, 0); @@ -675,7 +732,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardWithBarcodeUpdateBarcode() throws IOException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.UTF_8, Color.BLACK, 0, null, 0); @@ -700,7 +757,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardWithReceiptUpdateReceiptCancel() throws IOException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -739,7 +796,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardNoExpirySetExpiry() throws IOException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -772,7 +829,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardExpirySetNoExpiry() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, new Date(), new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -797,7 +854,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardNoBalanceSetBalance() throws IOException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -848,7 +905,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardBalanceSetNoBalance() throws IOException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("10.00"), Currency.getInstance("USD"), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -889,7 +946,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardSameAsCardIDUpdateBarcodeID() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -921,7 +978,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardSameAsCardIDUpdateCardID() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -955,7 +1012,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardDifferentFromCardIDUpdateCardIDUpdate() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, "123456", EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -992,7 +1049,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithLoyaltyCardDifferentFromCardIDUpdateCardIDDoNotUpdate() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, "123456", EAN_BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -1029,7 +1086,7 @@ public class LoyaltyCardViewActivityTest { @Test public void checkMenu() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -1074,7 +1131,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithoutParametersViewBack() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -1095,7 +1152,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startWithoutColors() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.ISO_8859_1, null, 0, null, 0); @@ -1116,7 +1173,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startLoyaltyCardWithoutColorsSave() throws IOException, ParseException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.ISO_8859_1, null, 0, null, 0); @@ -1136,7 +1193,7 @@ public class LoyaltyCardViewActivityTest { @Test public void startLoyaltyCardWithExplicitNoBarcodeSave() throws IOException, ParseException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, null, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -1156,7 +1213,7 @@ public class LoyaltyCardViewActivityTest { @Test public void removeBarcodeFromLoyaltyCard() throws IOException, ParseException { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -1187,7 +1244,7 @@ public class LoyaltyCardViewActivityTest { @Test public void checkPushStarIcon() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -1224,7 +1281,7 @@ public class LoyaltyCardViewActivityTest { @Test public void checkBarcodeFullscreenWorkflow() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -1320,7 +1377,7 @@ public class LoyaltyCardViewActivityTest { @Test public void checkNoBarcodeFullscreenWorkflow() { final Context context = ApplicationProvider.getApplicationContext(); - SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase(); + SQLiteDatabase database = getWritableEmptyDatabase(context); long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, BARCODE_DATA, null, null, StandardCharsets.ISO_8859_1, Color.BLACK, 0, null, 0); @@ -1366,7 +1423,7 @@ public class LoyaltyCardViewActivityTest { Intent intent = new Intent(); intent.setData(importUri); - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class, intent).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class, intent).create()); activityController.start(); activityController.visible(); @@ -1388,7 +1445,7 @@ public class LoyaltyCardViewActivityTest { Intent intent = new Intent(); intent.setData(importUri); - ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class, intent).create(); + ActivityController activityController = trackActivityController(Robolectric.buildActivity(LoyaltyCardEditActivity.class, intent).create()); activityController.start(); activityController.visible(); diff --git a/app/src/test/java/protect/card_locker/TestHelpers.java b/app/src/test/java/protect/card_locker/TestHelpers.java index 084b04752..1963ead2d 100644 --- a/app/src/test/java/protect/card_locker/TestHelpers.java +++ b/app/src/test/java/protect/card_locker/TestHelpers.java @@ -22,19 +22,20 @@ public class TestHelpers { SQLiteDatabase database = db.getWritableDatabase(); // Make sure no files remain - Cursor cursor = DBHelper.getLoyaltyCardCursor(database); - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - int cardID = cursor.getColumnIndex(DBHelper.LoyaltyCardDbIds.ID); + try (Cursor cursor = DBHelper.getLoyaltyCardCursor(database)) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + int cardID = cursor.getColumnIndex(DBHelper.LoyaltyCardDbIds.ID); - for (ImageLocationType imageLocationType : ImageLocationType.values()) { - try { - Utils.saveCardImage(context.getApplicationContext(), null, cardID, imageLocationType); - } catch (FileNotFoundException ignored) { + for (ImageLocationType imageLocationType : ImageLocationType.values()) { + try { + Utils.saveCardImage(context.getApplicationContext(), null, cardID, imageLocationType); + } catch (FileNotFoundException ignored) { + } } - } - cursor.moveToNext(); + cursor.moveToNext(); + } } // Make sure DB is empty