Files
Android/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java
Matthias Paulmier ace353d71d Fix several bugs related to shortcut handling (#2919)
* Improve ShortcuHelper.updateShortcuts to take all actions into account

* Remove now useless calls to removeShortcut

* Add doc to explain the usage of maxShortcut

* Fix typo in doc of maxShortcuts
2026-01-09 17:24:29 +01:00

1284 lines
51 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package protect.card_locker;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
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;
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;
import androidx.core.content.FileProvider;
import androidx.core.graphics.BlendModeColorFilterCompat;
import androidx.core.graphics.BlendModeCompat;
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.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.databinding.LoyaltyCardViewLayoutBinding;
import protect.card_locker.preferences.Settings;
public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements BarcodeImageWriterResultCallback {
private LoyaltyCardViewLayoutBinding binding;
private static final String TAG = "Catima";
int loyaltyCardId;
ArrayList<Integer> cardList;
LoyaltyCard loyaltyCard;
List<Group> loyaltyCardGroups;
boolean rotationEnabled;
SQLiteDatabase database;
ImportURIHelper importURIHelper;
Settings settings;
String cardIdString;
String barcodeIdString;
CatimaBarcode format;
@Nullable
Charset barcodeEncoding;
Bitmap frontImageBitmap;
Bitmap backImageBitmap;
boolean backgroundNeedsDarkIcons;
boolean isFullscreen = false;
ImageView barcodeRenderTarget;
int mainImageIndex = 0;
List<ImageType> imageTypes;
static final String STATE_IMAGEINDEX = "imageIndex";
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;
private long initTime = System.currentTimeMillis();
@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);
}
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
// Navigate to the next card
if (initTime < (System.currentTimeMillis() - 1000)) {
prevNextCard(true);
}
return true;
}
}
return super.onKeyDown(keyCode, event);
}
public void onMainImageTap() {
// If we're in fullscreen, leave fullscreen
if (isFullscreen) {
setFullscreen(false);
return;
}
ImageType imageType = imageTypes.get(mainImageIndex);
// If the barcode is shown, switch to fullscreen layout
if (imageType == ImageType.BARCODE) {
setFullscreen(true);
return;
}
// If this is an image, open it in the gallery.
openImageInGallery(imageType);
}
private void openImageInGallery(ImageType imageType) {
File file = null;
switch (imageType) {
case ICON:
file = Utils.retrieveCardImageAsFile(this, loyaltyCardId, ImageLocationType.icon);
break;
case IMAGE_FRONT:
file = Utils.retrieveCardImageAsFile(this, loyaltyCardId, ImageLocationType.front);
break;
case IMAGE_BACK:
file = Utils.retrieveCardImageAsFile(this, loyaltyCardId, ImageLocationType.back);
break;
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
if (file == null) {
Toast.makeText(this, R.string.failedToRetrieveImageFile, Toast.LENGTH_SHORT).show();
return;
}
try {
Intent intent = new Intent(Intent.ACTION_VIEW)
.setDataAndType(FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID, file), "image/*")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
} catch (ActivityNotFoundException e) {
// Display a toast message if an image viewer is not installed on device
Toast.makeText(this, R.string.failedLaunchingPhotoPicker, Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@Override
public void onBarcodeImageWriterResult(boolean success) {
if (!success) {
imageTypes.remove(ImageType.BARCODE);
setStateBasedOnImageTypes();
// Call correct drawMainImage
setFullscreen(isFullscreen);
Toast.makeText(LoyaltyCardViewActivity.this, getString(R.string.wrongValueForBarcodeType), Toast.LENGTH_LONG).show();
}
}
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;
cardList = b != null ? b.getIntegerArrayList(BUNDLE_CARDLIST) : null;
Log.d(TAG, "View activity: id=" + loyaltyCardId);
}
private void setScalerGuideline(int zoomLevel) {
float scale = zoomLevel / 100f;
if (format != null && format.isSquare()) {
binding.scalerGuideline.setGuidelinePercent(0.75f * scale);
} else {
binding.scalerGuideline.setGuidelinePercent(0.5f * scale);
}
}
private void setScalerWidthGuideline(int zoomLevelWidth) {
float halfscale = zoomLevelWidth / 200f;
binding.scalerEndwidthguideline.setGuidelinePercent(0.5f + halfscale);
binding.scalerStartwidthguideline.setGuidelinePercent(0.5f - halfscale);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState == null) {
Bundle incomingIntentExtras = getIntent().getExtras();
if (incomingIntentExtras == null) {
Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show();
finish();
return;
}
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);
}
}
super.onCreate(savedInstanceState);
binding = LoyaltyCardViewLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
settings = new Settings(this);
if (savedInstanceState != null) {
mainImageIndex = savedInstanceState.getInt(STATE_IMAGEINDEX);
isFullscreen = savedInstanceState.getBoolean(STATE_FULLSCREEN);
}
extractIntentFields(getIntent());
setContentView(binding.getRoot());
database = new DBHelper(this).getWritableDatabase();
importURIHelper = new ImportURIHelper(this);
binding.barcodeScaler.setOnSeekBarChangeListener(setOnSeekBarChangeListenerUnifiedFunction());
binding.barcodeWidthscaler.setOnSeekBarChangeListener(setOnSeekBarChangeListenerUnifiedFunction());
rotationEnabled = true;
binding.fullscreenButtonMinimize.setOnClickListener(v -> setFullscreen(false));
binding.fabEdit.setOnClickListener(v -> {
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle bundle = new Bundle();
bundle.putInt(LoyaltyCardEditActivity.BUNDLE_ID, loyaltyCardId);
bundle.putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true);
intent.putExtras(bundle);
startActivity(intent);
});
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.iconContainer.setOnClickListener(view -> {
if (loyaltyCard.getImageThumbnail(this) != null) {
openImageInGallery(ImageType.ICON);
} else {
Toast.makeText(LoyaltyCardViewActivity.this, R.string.icon_header_click_text, Toast.LENGTH_LONG).show();
}
});
binding.iconContainer.setOnLongClickListener(view -> {
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle bundle = new Bundle();
bundle.putInt(LoyaltyCardEditActivity.BUNDLE_ID, loyaltyCardId);
bundle.putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true);
bundle.putBoolean(LoyaltyCardEditActivity.BUNDLE_OPEN_SET_ICON_MENU, true);
intent.putExtras(bundle);
startActivity(intent);
finish();
return true;
});
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
binding.mainImage.setOnLongClickListener(view -> {
setMainImage(true, true);
return true;
});
binding.fullscreenImage.setOnClickListener(view -> onMainImageTap());
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (isFullscreen) {
setFullscreen(false);
} else {
finish();
}
}
});
}
private SeekBar.OnSeekBarChangeListener setOnSeekBarChangeListenerUnifiedFunction() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (!fromUser) {
Log.d(TAG, "non user triggered onProgressChanged, ignoring, progress is " + progress);
return;
}
Log.d(TAG, "Progress is " + progress);
if (seekBar.getId() == binding.barcodeScaler.getId()) {
Log.d(TAG, "Max is " + binding.barcodeScaler.getMax());
float scale = (float) progress / (float) binding.barcodeScaler.getMax();
Log.d(TAG, "Scaling to " + scale);
}
else {
Log.d(TAG, "Max is " + binding.barcodeWidthscaler.getMax());
float scale = (float) progress / (float) binding.barcodeWidthscaler.getMax();
Log.d(TAG, "Scaling to " + scale);
}
if (seekBar.getId() == binding.barcodeScaler.getId()) {
loyaltyCard.zoomLevel = progress;
setScalerGuideline(loyaltyCard.zoomLevel);
}
else {
loyaltyCard.zoomLevelWidth = progress;
setScalerWidthGuideline(loyaltyCard.zoomLevelWidth);
}
DBHelper.updateLoyaltyCardZoomLevel(database, loyaltyCardId, loyaltyCard.zoomLevel, loyaltyCard.zoomLevelWidth);
drawMainImage(mainImageIndex, true, isFullscreen);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
};
}
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.ALL);
infoTextview.setTextIsSelectable(true);
SpannableStringBuilder infoText = new SpannableStringBuilder();
if (!loyaltyCard.note.isEmpty()) {
infoText.append(loyaltyCard.note);
}
if (loyaltyCardGroups.size() > 0) {
List<String> 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<Date> 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);
} else {
binding.bottomAppBarInfoButton.setVisibility(View.GONE);
}
if (cardList == null || cardList.size() == 1) {
binding.bottomAppBarPreviousButton.setVisibility(View.GONE);
binding.bottomAppBarNextButton.setVisibility(View.GONE);
} else {
binding.bottomAppBarPreviousButton.setVisibility(View.VISIBLE);
binding.bottomAppBarNextButton.setVisibility(View.VISIBLE);
}
binding.bottomAppBarUpdateBalanceButton.setVisibility(hasBalance(loyaltyCard) ? View.VISIBLE : View.GONE);
}
private void prevNextCard(boolean 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;
if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
next = !next;
}
int cardListPosition = cardList.indexOf(loyaltyCardId);
if (next) {
if (cardListPosition == cardList.size() - 1) {
cardListPosition = 0;
} else {
cardListPosition = cardListPosition + 1;
}
} else {
if (cardListPosition == 0) {
cardListPosition = cardList.size() - 1;
} else {
cardListPosition = cardListPosition - 1;
}
}
loyaltyCardId = cardList.get(cardListPosition);
// Restart activity with new card id and index
Intent intent = getIntent();
Bundle b = intent.getExtras();
b.putInt(BUNDLE_ID, loyaltyCardId);
b.putInt(BUNDLE_TRANSITION_RIGHT, transitionRight ? 1 : 0);
intent.putExtras(b);
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.i(TAG, "Received new intent");
extractIntentFields(intent);
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt(STATE_IMAGEINDEX, mainImageIndex);
savedInstanceState.putBoolean(STATE_FULLSCREEN, isFullscreen);
super.onSaveInstanceState(savedInstanceState);
}
@Override
protected void onResume() {
activityOverridesNavBarColor = true;
super.onResume();
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
);
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);
}
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();
finish();
return;
}
setTitle(loyaltyCard.store);
loyaltyCardGroups = DBHelper.getLoyaltyCardGroups(database, loyaltyCardId);
showHideElementsForScreenSize();
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
binding.mainImageDescription.setOnClickListener(v -> {
if (mainImageIndex != 0) {
// Don't show cardId dialog, we're displaying something else
return;
}
TextView cardIdView = new TextView(LoyaltyCardViewActivity.this);
cardIdView.setText(loyaltyCard.cardId);
cardIdView.setTextIsSelectable(true);
int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
cardIdView.setPadding(contentPadding, contentPadding / 2, contentPadding, 0);
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(LoyaltyCardViewActivity.this);
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();
});
AlertDialog dialog = builder.create();
dialog.show();
});
binding.mainImageDescription.setOnLongClickListener(view -> {
if (mainImageIndex != 0) {
// Don't copy to clipboard, we're showing something else
return false;
}
copyCardIdToClipboard();
return true;
});
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));
binding.barcodeWidthscaler.setProgressTintList(ColorStateList.valueOf(darkenedColor));
binding.barcodeWidthscaler.setThumbTintList(ColorStateList.valueOf(darkenedColor));
// Set bottomAppBar and system navigation bar color
binding.bottomAppBar.setBackgroundColor(darkenedColor);
Utils.setNavigationBarColor(null, window, darkenedColor, Utils.needsDarkForeground(darkenedColor));
int complementaryColor = Utils.getComplementaryColor(darkenedColor);
binding.fabEdit.setBackgroundTintList(ColorStateList.valueOf(complementaryColor));
Drawable editButtonIcon = binding.fabEdit.getDrawable();
editButtonIcon.mutate();
editButtonIcon.setTint(Utils.needsDarkForeground(complementaryColor) ? Color.BLACK : Color.WHITE);
binding.fabEdit.setImageDrawable(editButtonIcon);
Bitmap icon = loyaltyCard.getImageThumbnail(this);
Utils.setIconOrTextWithBackground(this, loyaltyCard, icon, binding.iconImage, binding.iconText, 1);
// If the background is very bright, we should use dark icons
backgroundNeedsDarkIcons = Utils.needsDarkForeground(backgroundHeaderColor);
fixBottomAppBarImageButtonColor(binding.bottomAppBarInfoButton);
fixBottomAppBarImageButtonColor(binding.bottomAppBarPreviousButton);
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);
}
setStateBasedOnImageTypes();
setFullscreen(isFullscreen);
DBHelper.updateLoyaltyCardLastUsed(database, loyaltyCard.id);
invalidateOptionsMenu();
ShortcutHelper.updateShortcuts(this);
}
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()) {
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();
}
@SuppressWarnings("deprecation")
private void showWhenLockedSdkLessThan27(Window window) {
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
}
private void fixBottomAppBarImageButtonColor(ImageButton imageButton) {
imageButton.setColorFilter(BlendModeColorFilterCompat.createBlendModeColorFilterCompat(backgroundNeedsDarkIcons ? Color.BLACK : Color.WHITE, BlendModeCompat.SRC_ATOP));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.card_view_menu, menu);
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);
} else {
menu.findItem(R.id.action_star_unstar).setIcon(R.drawable.ic_unstarred);
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);
} else {
menu.findItem(R.id.action_unarchive).setVisible(false);
menu.findItem(R.id.action_archive).setVisible(true);
}
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
} else if (id == R.id.action_share) {
try {
importURIHelper.startShareIntent(Arrays.asList(loyaltyCard));
} catch (UnsupportedEncodingException e) {
Toast.makeText(LoyaltyCardViewActivity.this, R.string.failedGeneratingShareURL, Toast.LENGTH_LONG).show();
e.printStackTrace();
}
return true;
} else if (id == R.id.action_duplicate) {
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle bundle = new Bundle();
bundle.putInt(LoyaltyCardEditActivity.BUNDLE_ID, loyaltyCardId);
bundle.putBoolean(LoyaltyCardEditActivity.BUNDLE_DUPLICATE_ID, true);
intent.putExtras(bundle);
startActivity(intent);
return true;
} else if (id == R.id.action_star_unstar) {
DBHelper.updateLoyaltyCardStarStatus(database, loyaltyCardId, loyaltyCard.starStatus == 0 ? 1 : 0);
new ListWidget().updateAll(LoyaltyCardViewActivity.this);
// Re-init loyaltyCard with new data from DB
onResume();
invalidateOptionsMenu();
return true;
} else if (id == R.id.action_archive) {
DBHelper.updateLoyaltyCardArchiveStatus(database, loyaltyCardId, 1);
Toast.makeText(LoyaltyCardViewActivity.this, R.string.archived, Toast.LENGTH_LONG).show();
new ListWidget().updateAll(LoyaltyCardViewActivity.this);
// Re-init loyaltyCard with new data from DB
onResume();
invalidateOptionsMenu();
return true;
} else if (id == R.id.action_unarchive) {
DBHelper.updateLoyaltyCardArchiveStatus(database, loyaltyCardId, 0);
Toast.makeText(LoyaltyCardViewActivity.this, R.string.unarchived, Toast.LENGTH_LONG).show();
// Re-init loyaltyCard with new data from DB
onResume();
invalidateOptionsMenu();
return true;
} else if (id == R.id.action_delete) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.deleteTitle);
builder.setMessage(R.string.deleteConfirmation);
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
Log.e(TAG, "Deleting card: " + loyaltyCardId);
DBHelper.deleteLoyaltyCard(database, LoyaltyCardViewActivity.this, loyaltyCardId);
new ListWidget().updateAll(LoyaltyCardViewActivity.this);
finish();
dialog.dismiss();
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
return true;
}
return super.onOptionsItemSelected(item);
}
private void showHideElementsForScreenSize() {
int orientation = getResources().getConfiguration().orientation;
// Detect square-ish screens like the Unihertz Titan
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);
} else {
Log.d(TAG, "Detected portrait mode on non square-ish screen");
binding.iconContainer.setVisibility(View.VISIBLE);
}
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()) {
return;
}
mainImageIndex = newIndex;
drawMainImage(newIndex, false, isFullscreen);
setMainImagePreviousNextButtons();
setMainImageAccessibility();
}
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)");
}
ViewCompat.replaceAccessibilityAction(
binding.mainImage,
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
getString(accessibilityClickAction),
null
);
}
// Long-press actions
int accessibilityLongPressAction;
if (mainImageIndex == 0) {
accessibilityLongPressAction = R.string.switchToFrontImage;
} else if (mainImageIndex == 1) {
accessibilityLongPressAction = R.string.switchToBackImage;
} else if (mainImageIndex == 2) {
accessibilityLongPressAction = R.string.switchToBarcode;
} else {
throw new IndexOutOfBoundsException("setMainImageAccessibility was out of range (action_long_click)");
}
ViewCompat.replaceAccessibilityAction(
binding.mainImage,
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
getString(accessibilityLongPressAction),
null
);
}
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) {
binding.mainLeftButton.setVisibility(View.INVISIBLE);
binding.mainRightButton.setVisibility(View.INVISIBLE);
binding.mainLeftButton.setOnClickListener(null);
binding.mainRightButton.setOnClickListener(null);
return;
}
final ImageButton prevButton;
final ImageButton nextButton;
if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
prevButton = binding.mainRightButton;
nextButton = binding.mainLeftButton;
} else {
prevButton = 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));
} else {
prevButton.setVisibility(View.INVISIBLE);
prevButton.setOnClickListener(null);
}
// Enable right button if we can go further right
if (mainImageIndex < (imageTypes.size() - 1)) {
nextButton.setVisibility(View.VISIBLE);
nextButton.setOnClickListener(view -> setMainImage(true, false));
} else {
nextButton.setVisibility(View.INVISIBLE);
nextButton.setOnClickListener(null);
}
}
/**
* When enabled, hides the status bar and moves the barcode to the top of the screen.
* <p>
* 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.
*/
private void setFullscreen(boolean enabled) {
ActionBar actionBar = getSupportActionBar();
isFullscreen = enabled;
if (enabled && !imageTypes.isEmpty()) {
Log.d(TAG, "Move into fullscreen");
barcodeRenderTarget = binding.fullscreenImage;
// Show only fullscreen view
binding.container.setVisibility(View.GONE);
binding.fullscreenLayout.setVisibility(View.VISIBLE);
// 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();
}
} 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);
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();
}
}
Log.d("setFullScreen", "Is full screen enabled? " + enabled + " Zoom Level = " + binding.barcodeScaler.getProgress());
}
@SuppressWarnings("deprecation")
private void unsetFullscreenModeSdkLessThan30() {
Window window = getWindow();
if (window != null) {
window.getDecorView().setSystemUiVisibility(
window.getDecorView().getSystemUiVisibility()
& ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
& ~View.SYSTEM_UI_FLAG_FULLSCREEN
);
}
}
@SuppressWarnings("deprecation")
private void setFullscreenModeSdkLessThan30() {
Window window = getWindow();
if (window != null) {
window.getDecorView().setSystemUiVisibility(
window.getDecorView().getSystemUiVisibility()
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_FULLSCREEN
);
}
}
private void copyCardIdToClipboard() {
// Take the value thats already displayed to the user
String value = loyaltyCard.cardId;
if (value == null || value.isEmpty()) {
Toast.makeText(this, R.string.nothing_to_copy, Toast.LENGTH_SHORT).show();
return;
}
ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.cardId), value);
cm.setPrimaryClip(clip);
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}