diff --git a/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java b/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java index 2c4b0c9cd..191a6e010 100644 --- a/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java +++ b/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java @@ -4,9 +4,6 @@ import android.app.Activity; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -28,6 +25,10 @@ import java.util.Collections; import java.util.LinkedList; import java.util.Map; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + /** * This activity is callable and will allow a user to enter * barcode data and generate all barcodes possible for diff --git a/app/src/main/java/protect/card_locker/BaseCursorAdapter.java b/app/src/main/java/protect/card_locker/BaseCursorAdapter.java new file mode 100644 index 000000000..a20041e1d --- /dev/null +++ b/app/src/main/java/protect/card_locker/BaseCursorAdapter.java @@ -0,0 +1,87 @@ +package protect.card_locker; + +import android.database.Cursor; + +import androidx.recyclerview.widget.RecyclerView; + +public abstract class BaseCursorAdapter extends RecyclerView.Adapter +{ + private Cursor mCursor; + private boolean mDataValid; + private int mRowIDColumn; + + public BaseCursorAdapter(Cursor inputCursor) + { + setHasStableIds(true); + swapCursor(inputCursor); + } + + public abstract void onBindViewHolder(V inputHolder, Cursor inputCursor); + + @Override + public void onBindViewHolder(V inputHolder, int inputPosition) + { + if (!mDataValid) + { + throw new IllegalStateException("Cannot bind view holder when cursor is in invalid state."); + } + + if (!mCursor.moveToPosition(inputPosition)) + { + throw new IllegalStateException("Could not move cursor to position " + inputPosition + " when trying to bind view holder"); + } + + onBindViewHolder(inputHolder, mCursor); + } + + @Override + public int getItemCount() + { + if (mDataValid) + { + return mCursor.getCount(); + } + else + { + return 0; + } + } + + @Override + public long getItemId(int inputPosition) + { + if (!mDataValid) + { + throw new IllegalStateException("Cannot lookup item id when cursor is in invalid state."); + } + + if (!mCursor.moveToPosition(inputPosition)) + { + throw new IllegalStateException("Could not move cursor to position " + inputPosition + " when trying to get an item id"); + } + + return mCursor.getLong(mRowIDColumn); + } + + public void swapCursor(Cursor inputCursor) + { + if (inputCursor == mCursor) + { + return; + } + + if (inputCursor != null) + { + mCursor = inputCursor; + mDataValid = true; + notifyDataSetChanged(); + } + else + { + notifyItemRangeRemoved(0, getItemCount()); + mCursor = null; + mRowIDColumn = -1; + mDataValid = false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/CardShortcutConfigure.java b/app/src/main/java/protect/card_locker/CardShortcutConfigure.java index 0310e51f0..c6a8cee52 100644 --- a/app/src/main/java/protect/card_locker/CardShortcutConfigure.java +++ b/app/src/main/java/protect/card_locker/CardShortcutConfigure.java @@ -2,32 +2,28 @@ package protect.card_locker; import android.content.Intent; import android.database.Cursor; -import android.graphics.Bitmap; import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.pm.ShortcutInfoCompat; -import androidx.core.content.pm.ShortcutManagerCompat; -import androidx.core.graphics.drawable.IconCompat; - +import android.os.Parcelable; import android.util.Log; import android.view.View; -import android.widget.AdapterView; -import android.widget.ListView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.RecyclerView; + /** * The configuration screen for creating a shortcut. */ -public class CardShortcutConfigure extends AppCompatActivity +public class CardShortcutConfigure extends AppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener { static final String TAG = "Catima"; + final DBHelper mDb = new DBHelper(this); @Override - public void onCreate(Bundle bundle) - { + public void onCreate(Bundle bundle) { super.onCreate(bundle); // Set the result to CANCELED. This will cause nothing to happen if the @@ -45,53 +41,59 @@ public class CardShortcutConfigure extends AppCompatActivity final DBHelper db = new DBHelper(this); // If there are no cards, bail - if(db.getLoyaltyCardCount() == 0) - { + if (db.getLoyaltyCardCount() == 0) { Toast.makeText(this, R.string.noCardsMessage, Toast.LENGTH_LONG).show(); finish(); } - final ListView cardList = findViewById(R.id.list); + final RecyclerView cardList = findViewById(R.id.list); cardList.setVisibility(View.VISIBLE); Cursor cardCursor = db.getLoyaltyCardCursor(); - final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor); + final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor, this); cardList.setAdapter(adapter); + } - cardList.setOnItemClickListener(new AdapterView.OnItemClickListener() - { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) - { - Cursor selected = (Cursor) parent.getItemAtPosition(position); - LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected); + private void onClickAction(int position) { + Cursor selected = mDb.getLoyaltyCardCursor(); + selected.moveToPosition(position); + LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected); - Log.d(TAG, "Creating shortcut for card " + loyaltyCard.store + "," + loyaltyCard.id); + Log.d(TAG, "Creating shortcut for card " + loyaltyCard.store + "," + loyaltyCard.id); - Intent shortcutIntent = new Intent(CardShortcutConfigure.this, LoyaltyCardViewActivity.class); - shortcutIntent.setAction(Intent.ACTION_MAIN); - // Prevent instances of the view activity from piling up; if one exists let this - // one replace it. - shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - Bundle bundle = new Bundle(); - bundle.putInt("id", loyaltyCard.id); - bundle.putBoolean("view", true); - shortcutIntent.putExtras(bundle); + Intent shortcutIntent = new Intent(CardShortcutConfigure.this, LoyaltyCardViewActivity.class); + shortcutIntent.setAction(Intent.ACTION_MAIN); + // Prevent instances of the view activity from piling up; if one exists let this + // one replace it. + shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + Bundle bundle = new Bundle(); + bundle.putInt("id", loyaltyCard.id); + bundle.putBoolean("view", true); + shortcutIntent.putExtras(bundle); - Bitmap icon = Utils.generateIcon(CardShortcutConfigure.this, loyaltyCard.store, loyaltyCard.headerColor, true).getLetterTile(); + Parcelable icon = Intent.ShortcutIconResource.fromContext(CardShortcutConfigure.this, R.mipmap.ic_launcher); + Intent intent = new Intent(); + intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, loyaltyCard.store); + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon); + setResult(RESULT_OK, intent); - ShortcutInfoCompat shortcutInfo = new ShortcutInfoCompat.Builder(CardShortcutConfigure.this, String.valueOf(loyaltyCard.id)) - .setIntent(shortcutIntent) - .setIcon(IconCompat.createWithAdaptiveBitmap(icon)) - .setShortLabel(loyaltyCard.store) - .build(); + finish(); + } - Intent intent = ShortcutManagerCompat.createShortcutResultIntent(CardShortcutConfigure.this, shortcutInfo); - setResult(RESULT_OK, intent); + @Override + public void onIconClicked(int inputPosition) { + onClickAction(inputPosition); + } - finish(); - } - }); + @Override + public void onRowClicked(int inputPosition) { + onClickAction(inputPosition); + } + + @Override + public void onRowLongClicked(int inputPosition) { + // do nothing } } diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 140a786df..bf7ad087a 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -6,14 +6,13 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; -import android.graphics.Color; import com.google.zxing.BarcodeFormat; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Currency; import java.util.Date; -import java.util.ArrayList; import java.util.List; public class DBHelper extends SQLiteOpenHelper diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index 97b243b75..2445605a9 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -8,15 +8,6 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; - -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; - -import android.provider.ContactsContract; import android.util.Log; import android.view.MenuItem; import android.view.View; @@ -29,7 +20,13 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; -import java.text.DateFormat; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; public class ImportExportActivity extends AppCompatActivity { diff --git a/app/src/main/java/protect/card_locker/ImportExportTask.java b/app/src/main/java/protect/card_locker/ImportExportTask.java index 9ea99c627..8e288b345 100644 --- a/app/src/main/java/protect/card_locker/ImportExportTask.java +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -8,7 +8,6 @@ import android.util.Log; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; diff --git a/app/src/main/java/protect/card_locker/ImportURIHelper.java b/app/src/main/java/protect/card_locker/ImportURIHelper.java index ed2d7f1df..3d2f8e481 100644 --- a/app/src/main/java/protect/card_locker/ImportURIHelper.java +++ b/app/src/main/java/protect/card_locker/ImportURIHelper.java @@ -10,6 +10,7 @@ import java.io.InvalidObjectException; import java.math.BigDecimal; import java.util.Currency; import java.util.Date; +import java.util.List; public class ImportURIHelper { private static final String STORE = DBHelper.LoyaltyCardDbIds.STORE; @@ -29,6 +30,7 @@ public class ImportURIHelper { private final String oldHost; private final String oldPath; private final String shareText; + private final String shareMultipleText; public ImportURIHelper(Context context) { this.context = context; @@ -37,6 +39,7 @@ public class ImportURIHelper { oldHost = "brarcher.github.io"; oldPath = "/loyalty-card-locker/share"; shareText = context.getResources().getString(R.string.intent_import_card_from_url_share_text); + shareMultipleText = context.getResources().getString(R.string.intent_import_card_from_url_share_multiple_text); } private boolean isImportUri(Uri uri) { @@ -126,17 +129,33 @@ public class ImportURIHelper { return uriBuilder.build(); } - private void startShareIntent(Uri uri) { + public void startShareIntent(List loyaltyCards) { + int loyaltyCardCount = loyaltyCards.size(); + + StringBuilder text = new StringBuilder(); + if (loyaltyCardCount == 1) { + text.append(shareText); + } else { + text.append(shareMultipleText); + } + text.append("\n\n"); + + for (int i = 0; i < loyaltyCardCount; i++) { + LoyaltyCard loyaltyCard = loyaltyCards.get(i); + + text.append(loyaltyCard.store + ": " + toUri(loyaltyCard)); + + if (i < (loyaltyCardCount - 1)) { + text.append("\n\n"); + } + } + Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, shareText + "\n" + uri.toString()); + sendIntent.putExtra(Intent.EXTRA_TEXT, text.toString()); sendIntent.setType("text/plain"); Intent shareIntent = Intent.createChooser(sendIntent, null); context.startActivity(shareIntent); } - - public void startShareIntent(LoyaltyCard loyaltyCard) { - startShareIntent(toUri(loyaltyCard)); - } } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardAnimator.java b/app/src/main/java/protect/card_locker/LoyaltyCardAnimator.java new file mode 100644 index 000000000..43e6ed6e8 --- /dev/null +++ b/app/src/main/java/protect/card_locker/LoyaltyCardAnimator.java @@ -0,0 +1,37 @@ +package protect.card_locker; + +import android.animation.AnimatorInflater; +import android.animation.AnimatorSet; +import android.content.Context; +import android.view.View; + +public class LoyaltyCardAnimator { + + private static AnimatorSet selectedViewIn, defaultViewOut, selectedViewOut, defaultViewIn; + + public static void flipView(Context inputContext, final View inputSelectedView, final View inputDefaultView, boolean inputItemSelected) { + + selectedViewIn = (AnimatorSet) AnimatorInflater.loadAnimator(inputContext, R.animator.flip_left_in); + defaultViewOut = (AnimatorSet) AnimatorInflater.loadAnimator(inputContext, R.animator.flip_right_out); + selectedViewOut = (AnimatorSet) AnimatorInflater.loadAnimator(inputContext, R.animator.flip_left_out); + defaultViewIn = (AnimatorSet) AnimatorInflater.loadAnimator(inputContext, R.animator.flip_right_in); + + final AnimatorSet showFrontAnim = new AnimatorSet(); + final AnimatorSet showBackAnim = new AnimatorSet(); + + selectedViewIn.setTarget(inputSelectedView); + defaultViewOut.setTarget(inputDefaultView); + showFrontAnim.playTogether(selectedViewIn, defaultViewOut); + + selectedViewOut.setTarget(inputSelectedView); + defaultViewIn.setTarget(inputDefaultView); + showBackAnim.playTogether(defaultViewIn, selectedViewOut); + + if (inputItemSelected) { + showFrontAnim.start(); + } else { + showBackAnim.start(); + } + } + +} diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java index e828ddfce..f3b0f3f7d 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java @@ -2,98 +2,296 @@ package protect.card_locker; import android.content.Context; import android.database.Cursor; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.util.SparseBooleanArray; +import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.CursorAdapter; import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import java.math.BigDecimal; import java.text.DateFormat; -import java.util.Date; +import java.util.ArrayList; +import androidx.cardview.widget.CardView; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.recyclerview.widget.RecyclerView; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import protect.card_locker.preferences.Settings; -class LoyaltyCardCursorAdapter extends CursorAdapter +public class LoyaltyCardCursorAdapter extends BaseCursorAdapter { - Settings settings; + private static int mCurrentSelectedIndex = -1; + private Cursor mCursor; + Settings mSettings; + boolean mDarkModeEnabled; + private Context mContext; + private CardAdapterListener mListener; + private SparseBooleanArray mSelectedItems; + private SparseBooleanArray mAnimationItemsIndex; + private boolean mReverseAllAnimations = false; - public LoyaltyCardCursorAdapter(Context context, Cursor cursor) + public LoyaltyCardCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener) { - super(context, cursor, 0); - settings = new Settings(context); + super(inputCursor); + mSettings = new Settings(inputContext); + mCursor = inputCursor; + mContext = inputContext; + mListener = inputListener; + mSelectedItems = new SparseBooleanArray(); + mAnimationItemsIndex = new SparseBooleanArray(); + + mDarkModeEnabled = MainActivity.isDarkModeEnabled(inputContext); } - // The newView method is used to inflate a new view and return it, - // you don't bind any data to the view at this point. @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) + public LoyaltyCardListItemViewHolder onCreateViewHolder(ViewGroup inputParent, int inputViewType) { - return LayoutInflater.from(context).inflate(R.layout.loyalty_card_layout, parent, false); + View itemView = LayoutInflater.from(inputParent.getContext()).inflate(R.layout.loyalty_card_layout, inputParent, false); + return new LoyaltyCardListItemViewHolder(itemView); } - // The bindView method is used to bind all data to a given view - // such as setting the text on a TextView. - @Override - public void bindView(View view, Context context, Cursor cursor) + public Cursor getCursor() { - // Find fields to populate in inflated template - ImageView thumbnail = view.findViewById(R.id.thumbnail); - TextView storeField = view.findViewById(R.id.store); - TextView noteField = view.findViewById(R.id.note); - TextView balanceField = view.findViewById(R.id.balance); - TextView expiryField = view.findViewById(R.id.expiry); - ImageView star = view.findViewById(R.id.star); + return mCursor; + } - // Extract properties from cursor - LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(cursor); - - // Populate fields with extracted properties - storeField.setText(loyaltyCard.store); - - storeField.setTextSize(settings.getFontSizeMax(settings.getMediumFont())); - - if(!loyaltyCard.note.isEmpty()) - { - noteField.setVisibility(View.VISIBLE); - noteField.setText(loyaltyCard.note); - noteField.setTextSize(settings.getFontSizeMax(settings.getSmallFont())); - } - else - { - noteField.setVisibility(View.GONE); + public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor) { + if (mDarkModeEnabled) { + inputHolder.mStarIcon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP); } - if(!loyaltyCard.balance.equals(new BigDecimal("0"))) { - balanceField.setVisibility(View.VISIBLE); - balanceField.setText(context.getString(R.string.balanceSentence, Utils.formatBalance(context, loyaltyCard.balance, loyaltyCard.balanceType))); - balanceField.setTextSize(settings.getFontSizeMax(settings.getSmallFont())); - } - else - { - balanceField.setVisibility(View.GONE); + LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(inputCursor); + + inputHolder.mStoreField.setText(loyaltyCard.store); + inputHolder.mStoreField.setTextSize(mSettings.getFontSizeMax(mSettings.getMediumFont())); + if (!loyaltyCard.note.isEmpty()) { + inputHolder.mNoteField.setVisibility(View.VISIBLE); + inputHolder.mNoteField.setText(loyaltyCard.note); + inputHolder.mNoteField.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont())); + } else { + inputHolder.mNoteField.setVisibility(View.GONE); } - if(loyaltyCard.expiry != null) + if (!loyaltyCard.balance.equals(new BigDecimal("0"))) { + inputHolder.mBalanceField.setVisibility(View.VISIBLE); + inputHolder.mBalanceField.setText(mContext.getString(R.string.balanceSentence, Utils.formatBalance(mContext, loyaltyCard.balance, loyaltyCard.balanceType))); + inputHolder.mBalanceField.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont())); + } else { + inputHolder.mBalanceField.setVisibility(View.GONE); + } + + if (loyaltyCard.expiry != null) { - expiryField.setVisibility(View.VISIBLE); + inputHolder.mExpiryField.setVisibility(View.VISIBLE); int expiryString = R.string.expiryStateSentence; if(Utils.hasExpired(loyaltyCard.expiry)) { expiryString = R.string.expiryStateSentenceExpired; - expiryField.setTextColor(context.getResources().getColor(R.color.alert)); + inputHolder.mExpiryField.setTextColor(mContext.getResources().getColor(R.color.alert)); + } + inputHolder.mExpiryField.setText(mContext.getString(expiryString, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry))); + inputHolder.mExpiryField.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont())); + } else { + inputHolder.mExpiryField.setVisibility(View.GONE); + } + + inputHolder.mStarIcon.setVisibility((loyaltyCard.starStatus != 0) ? View.VISIBLE : View.GONE); + inputHolder.mCardIcon.setImageBitmap(Utils.generateIcon(mContext, loyaltyCard.store, loyaltyCard.headerColor).getLetterTile()); + + inputHolder.itemView.setActivated(mSelectedItems.get(inputCursor.getPosition(), false)); + applyIconAnimation(inputHolder, inputCursor.getPosition()); + applyClickEvents(inputHolder, inputCursor.getPosition()); + + } + + private void applyClickEvents(LoyaltyCardListItemViewHolder inputHolder, final int inputPosition) + { + inputHolder.mThumbnailContainer.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View inputView) + { + mListener.onIconClicked(inputPosition); + } + }); + + inputHolder.mRow.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View inputView) + { + mListener.onRowClicked(inputPosition); + } + }); + + inputHolder.mInformationContainer.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View inputView) + { + mListener.onRowClicked(inputPosition); + } + }); + + inputHolder.mRow.setOnLongClickListener(new View.OnLongClickListener() + { + @Override + public boolean onLongClick(View inputView) + { + mListener.onRowLongClicked(inputPosition); + inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + return true; + } + }); + + inputHolder.mInformationContainer.setOnLongClickListener(new View.OnLongClickListener() + { + @Override + public boolean onLongClick(View inputView) + { + mListener.onRowLongClicked(inputPosition); + inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + return true; + } + }); + } + + private void applyIconAnimation(LoyaltyCardListItemViewHolder inputHolder, int inputPosition) + { + if (mSelectedItems.get(inputPosition, false)) + { + inputHolder.mThumbnailFrontContainer.setVisibility(View.GONE); + resetIconYAxis(inputHolder.mThumbnailBackContainer); + inputHolder.mThumbnailBackContainer.setVisibility(View.VISIBLE); + inputHolder.mThumbnailBackContainer.setAlpha(1); + if (mCurrentSelectedIndex == inputPosition) + { + LoyaltyCardAnimator.flipView(mContext, inputHolder.mThumbnailBackContainer, inputHolder.mThumbnailFrontContainer, true); + resetCurrentIndex(); } - expiryField.setText(context.getString(expiryString, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry))); - expiryField.setTextSize(settings.getFontSizeMax(settings.getSmallFont())); } else { - expiryField.setVisibility(View.GONE); + inputHolder.mThumbnailBackContainer.setVisibility(View.GONE); + resetIconYAxis(inputHolder.mThumbnailFrontContainer); + inputHolder.mThumbnailFrontContainer.setVisibility(View.VISIBLE); + inputHolder.mThumbnailFrontContainer.setAlpha(1); + if ((mReverseAllAnimations && mAnimationItemsIndex.get(inputPosition, false)) || mCurrentSelectedIndex == inputPosition) + { + LoyaltyCardAnimator.flipView(mContext, inputHolder.mThumbnailBackContainer, inputHolder.mThumbnailFrontContainer, false); + resetCurrentIndex(); + } + } + } + + private void resetIconYAxis(View inputView) + { + if (inputView.getRotationY() != 0) + { + inputView.setRotationY(0); + } + } + + public void resetAnimationIndex() + { + mReverseAllAnimations = false; + mAnimationItemsIndex.clear(); + } + + @SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD") + public void toggleSelection(int inputPosition) + { + mCurrentSelectedIndex = inputPosition; + if (mSelectedItems.get(inputPosition, false)) + { + mSelectedItems.delete(inputPosition); + mAnimationItemsIndex.delete(inputPosition); + } + else + { + mSelectedItems.put(inputPosition, true); + mAnimationItemsIndex.put(inputPosition, true); + } + notifyItemChanged(inputPosition); + } + + public void clearSelections() + { + mReverseAllAnimations = true; + mSelectedItems.clear(); + notifyDataSetChanged(); + } + + public int getSelectedItemCount() + { + return mSelectedItems.size(); + } + + public ArrayList getSelectedItems() + { + + ArrayList result = new ArrayList<>(); + + int i; + for(i = 0; i < mSelectedItems.size(); i++) + { + mCursor.moveToPosition(mSelectedItems.keyAt(i)); + result.add(LoyaltyCard.toLoyaltyCard(mCursor)); } - if (loyaltyCard.starStatus!=0) star.setVisibility(View.VISIBLE); - else star.setVisibility(View.GONE); - - thumbnail.setImageBitmap(Utils.generateIcon(context, loyaltyCard.store, loyaltyCard.headerColor).getLetterTile()); + return result; } -} + + private void resetCurrentIndex() + { + mCurrentSelectedIndex = -1; + } + + public interface CardAdapterListener + { + void onIconClicked(int inputPosition); + void onRowClicked(int inputPosition); + void onRowLongClicked(int inputPosition); + } + + public class LoyaltyCardListItemViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener + { + + public TextView mStoreField, mNoteField, mBalanceField, mExpiryField; + public LinearLayout mInformationContainer; + public ImageView mCardIcon, mStarIcon; + public CardView mThumbnailContainer; + public ConstraintLayout mRow; + public RelativeLayout mThumbnailFrontContainer, mThumbnailBackContainer; + + public LoyaltyCardListItemViewHolder(View inputView) + { + super(inputView); + mThumbnailContainer = inputView.findViewById(R.id.thumbnail_container); + mRow = inputView.findViewById(R.id.row); + mThumbnailFrontContainer = inputView.findViewById(R.id.thumbnail_front); + mThumbnailBackContainer = inputView.findViewById(R.id.thumbnail_back); + mInformationContainer = inputView.findViewById(R.id.information_container); + mStoreField = inputView.findViewById(R.id.store); + mNoteField = inputView.findViewById(R.id.note); + mBalanceField = inputView.findViewById(R.id.balance); + mExpiryField = inputView.findViewById(R.id.expiry); + mCardIcon = inputView.findViewById(R.id.thumbnail); + mStarIcon = inputView.findViewById(R.id.star); + inputView.setOnLongClickListener(this); + } + + @Override + public boolean onLongClick(View inputView) + { + mListener.onRowLongClicked(getAdapterPosition()); + inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + return true; + } + } +} \ 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 7d1430fb8..39ef4424d 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -6,22 +6,10 @@ import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.content.res.TypedArray; -import android.graphics.Bitmap; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; - -import com.google.android.material.chip.Chip; -import com.google.android.material.chip.ChipGroup; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.DialogFragment; - import android.os.LocaleList; import android.text.Editable; import android.text.InputType; @@ -29,7 +17,6 @@ import android.text.TextWatcher; import android.util.Log; import android.view.Menu; import android.view.MenuItem; -import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; import android.view.WindowManager; @@ -42,6 +29,10 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayout; import com.google.zxing.BarcodeFormat; import com.jaredrummler.android.colorpicker.ColorPickerDialog; @@ -50,7 +41,6 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; import java.io.InvalidObjectException; import java.math.BigDecimal; import java.text.DateFormat; -import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; @@ -60,6 +50,12 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.DialogFragment; + public class LoyaltyCardEditActivity extends AppCompatActivity { private static final String TAG = "Catima"; diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardLockerApplication.java b/app/src/main/java/protect/card_locker/LoyaltyCardLockerApplication.java index 0705435fc..43d950cc0 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardLockerApplication.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardLockerApplication.java @@ -1,8 +1,8 @@ package protect.card_locker; import android.app.Application; -import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.app.AppCompatDelegate; import protect.card_locker.preferences.Settings; public class LoyaltyCardLockerApplication extends Application { diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 308151e51..28eb59169 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -7,24 +7,12 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.Guideline; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.widget.TextViewCompat; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.appcompat.widget.Toolbar; - import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; 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.WindowManager; @@ -41,8 +29,17 @@ import com.google.zxing.BarcodeFormat; import java.math.BigDecimal; import java.text.DateFormat; +import java.util.Arrays; import java.util.List; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.Toolbar; +import androidx.constraintlayout.widget.Guideline; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.widget.TextViewCompat; import protect.card_locker.preferences.Settings; public class LoyaltyCardViewActivity extends AppCompatActivity @@ -491,7 +488,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity break; case R.id.action_share: - importURIHelper.startShareIntent(loyaltyCard); + importURIHelper.startShareIntent(Arrays.asList(loyaltyCard)); return true; case R.id.action_lock_unlock: diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index 433cd9a60..0b1f43990 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -1,63 +1,157 @@ package protect.card_locker; -import android.app.AlertDialog; import android.app.SearchManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.Toolbar; import android.util.Log; -import android.view.ContextMenu; import android.view.GestureDetector; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.widget.AdapterView; -import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; + import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; + import java.util.List; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.view.ActionMode; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import protect.card_locker.preferences.SettingsActivity; -public class MainActivity extends AppCompatActivity implements GestureDetector.OnGestureListener +public class MainActivity extends AppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener, GestureDetector.OnGestureListener { private static final String TAG = "Catima"; - private Menu menu; - private GestureDetector gestureDetector; - protected String filter = ""; + private final DBHelper mDB = new DBHelper(this); + private LoyaltyCardCursorAdapter mAdapter; + private ActionMode mCurrentActionMode; + private Menu mMenu; + private GestureDetector mGestureDetector; + protected String mFilter = ""; protected int selectedTab = 0; + private RecyclerView mCardList; + + private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() + { + @Override + public boolean onCreateActionMode(ActionMode inputMode, Menu inputMenu) + { + inputMode.getMenuInflater().inflate(R.menu.card_longclick_menu, inputMenu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode inputMode, Menu inputMenu) + { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode inputMode, MenuItem inputItem) + { + if (inputItem.getItemId() == R.id.action_copy_to_clipboard) + { + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + + String clipboardData; + int cardCount = mAdapter.getSelectedItemCount(); + + if (cardCount == 1) { + clipboardData = mAdapter.getSelectedItems().get(0).cardId; + } else { + StringBuilder cardIds = new StringBuilder(); + + for (int i = 0; i < cardCount; i++) { + LoyaltyCard loyaltyCard = mAdapter.getSelectedItems().get(i); + + cardIds.append(loyaltyCard.store + ": " + loyaltyCard.cardId); + if (i < (cardCount - 1)) { + cardIds.append("\n"); + } + } + + clipboardData = cardIds.toString(); + } + + ClipData clip = ClipData.newPlainText(getString(R.string.card_ids_copied), clipboardData); + clipboard.setPrimaryClip(clip); + Toast.makeText(MainActivity.this, cardCount > 1 ? R.string.copy_to_clipboard_multiple_toast : R.string.copy_to_clipboard_toast, Toast.LENGTH_LONG).show(); + inputMode.finish(); + return true; + } + else if (inputItem.getItemId() == R.id.action_share) + { + final ImportURIHelper importURIHelper = new ImportURIHelper(MainActivity.this); + importURIHelper.startShareIntent(mAdapter.getSelectedItems()); + inputMode.finish(); + return true; + } + else if(inputItem.getItemId() == R.id.action_edit) + { + if (mAdapter.getSelectedItemCount() != 1) { + throw new IllegalArgumentException("Cannot edit more than 1 card at a time"); + } + + Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class); + Bundle bundle = new Bundle(); + bundle.putInt("id", mAdapter.getSelectedItems().get(0).id); + bundle.putBoolean("update", true); + intent.putExtras(bundle); + startActivity(intent); + inputMode.finish(); + return true; + } + + return false; + } + + @Override + public void onDestroyActionMode(ActionMode inputMode) + { + mAdapter.clearSelections(); + mCurrentActionMode = null; + mCardList.post(new Runnable() + { + @Override + public void run() + { + mAdapter.resetAnimationIndex(); + } + }); + } + }; @Override - protected void onCreate(Bundle savedInstanceState) + protected void onCreate(Bundle inputSavedInstanceState) { - super.onCreate(savedInstanceState); + super.onCreate(inputSavedInstanceState); setContentView(R.layout.main_activity); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - updateLoyaltyCardList(filter, null); + updateLoyaltyCardList(mFilter, null); TabLayout groupsTabLayout = findViewById(R.id.groups); groupsTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { selectedTab = tab.getPosition(); - updateLoyaltyCardList(filter, tab.getTag()); + updateLoyaltyCardList(mFilter, tab.getTag()); // Store active tab in Shared Preference to restore next app launch SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences( @@ -79,12 +173,12 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O } }); - gestureDetector = new GestureDetector(this, this); + mGestureDetector = new GestureDetector(this, this); View.OnTouchListener gestureTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(final View v, final MotionEvent event){ - return gestureDetector.onTouchEvent(event); + return mGestureDetector.onTouchEvent(event); } }; @@ -129,15 +223,23 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O } @Override - protected void onResume() { + protected void onResume() + { super.onResume(); - if (menu != null) + if(mCurrentActionMode != null) { - SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView(); + mAdapter.clearSelections(); + mCurrentActionMode.finish(); + } - if (!searchView.isIconified()) { - filter = searchView.getQuery().toString(); + if (mMenu != null) + { + SearchView searchView = (SearchView) mMenu.findItem(R.id.action_search).getActionView(); + + if (!searchView.isIconified()) + { + mFilter = searchView.getQuery().toString(); } } @@ -163,11 +265,12 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O assert tab != null; group = tab.getTag(); } - updateLoyaltyCardList(filter, group); + updateLoyaltyCardList(mFilter, group); // End of active tab logic FloatingActionButton addButton = findViewById(R.id.fabAdd); - addButton.setOnClickListener(new View.OnClickListener() { + addButton.setOnClickListener(new View.OnClickListener() + { @Override public void onClick(View v) { Intent i = new Intent(getApplicationContext(), ScanActivity.class); @@ -183,14 +286,12 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O if (requestCode == Utils.MAIN_REQUEST) { // We're coming back from another view so clear the search // We only do this now to prevent a flash of all entries right after picking one - filter = ""; - if (menu != null) + mFilter = ""; + if (mMenu != null) { - MenuItem searchItem = menu.findItem(R.id.action_search); + MenuItem searchItem = mMenu.findItem(R.id.action_search); searchItem.collapseActionView(); } - - // In case the theme changed recreate(); return; @@ -209,16 +310,18 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O } @Override - public void onBackPressed() { - if (menu == null) + public void onBackPressed() + { + if (mMenu == null) { super.onBackPressed(); return; } - SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView(); + SearchView searchView = (SearchView) mMenu.findItem(R.id.action_search).getActionView(); - if (!searchView.isIconified()) { + if (!searchView.isIconified()) + { searchView.setIconified(true); } else { TabLayout groupsTabLayout = findViewById(R.id.groups); @@ -239,21 +342,29 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O group = (Group) tag; } - final ListView cardList = findViewById(R.id.list); + mCardList = findViewById(R.id.list); + RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext()); + mCardList.setLayoutManager(mLayoutManager); + mCardList.setItemAnimator(new DefaultItemAnimator()); + final TextView helpText = findViewById(R.id.helpText); final TextView noMatchingCardsText = findViewById(R.id.noMatchingCardsText); - final DBHelper db = new DBHelper(this); - Cursor cardCursor = db.getLoyaltyCardCursor(filterText, group); + Cursor cardCursor = mDB.getLoyaltyCardCursor(filterText, group); - if(db.getLoyaltyCardCount() > 0) + mAdapter = new LoyaltyCardCursorAdapter(this, cardCursor, this); + mCardList.setAdapter(mAdapter); + + registerForContextMenu(mCardList); + + if(mDB.getLoyaltyCardCount() > 0) { // We want the cardList to be visible regardless of the filtered match count // to ensure that the noMatchingCardsText doesn't end up being shown below // the keyboard - cardList.setVisibility(View.VISIBLE); + mCardList.setVisibility(View.VISIBLE); helpText.setVisibility(View.GONE); - if(cardCursor.getCount() > 0) + if(mAdapter.getItemCount() > 0) { noMatchingCardsText.setVisibility(View.GONE); } @@ -264,35 +375,14 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O } else { - cardList.setVisibility(View.GONE); + mCardList.setVisibility(View.GONE); helpText.setVisibility(View.VISIBLE); noMatchingCardsText.setVisibility(View.GONE); } - final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor); - cardList.setAdapter(adapter); - - registerForContextMenu(cardList); - - cardList.setOnItemClickListener(new AdapterView.OnItemClickListener() - { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) - { - Cursor selected = (Cursor) parent.getItemAtPosition(position); - LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected); - - Intent i = new Intent(view.getContext(), LoyaltyCardViewActivity.class); - i.setAction(""); - final Bundle b = new Bundle(); - b.putInt("id", loyaltyCard.id); - i.putExtras(b); - - ShortcutHelper.updateShortcuts(MainActivity.this, loyaltyCard, i); - - startActivityForResult(i, Utils.MAIN_REQUEST); - } - }); + if (mCurrentActionMode != null) { + mCurrentActionMode.finish(); + } } public void updateTabGroups(TabLayout groupsTabLayout) @@ -333,80 +423,47 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O } @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) + public boolean onCreateOptionsMenu(Menu inputMenu) { - super.onCreateContextMenu(menu, v, menuInfo); - if (v.getId()==R.id.list) - { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.card_longclick_menu, menu); - } - } + this.mMenu = inputMenu; - @Override - public boolean onContextItemSelected(MenuItem item) - { - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); - ListView listView = findViewById(R.id.list); - - Cursor cardCursor = (Cursor)listView.getItemAtPosition(info.position); - LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor); - - if(item.getItemId() == R.id.action_clipboard) - { - ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(card.store, card.cardId); - clipboard.setPrimaryClip(clip); - - Toast.makeText(this, R.string.copy_to_clipboard_toast, Toast.LENGTH_LONG).show(); - return true; - } - else if(item.getItemId() == R.id.action_share) - { - final ImportURIHelper importURIHelper = new ImportURIHelper(this); - importURIHelper.startShareIntent(card); - return true; - } - - return super.onContextItemSelected(item); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) - { - this.menu = menu; - - getMenuInflater().inflate(R.menu.main_menu, menu); + getMenuInflater().inflate(R.menu.main_menu, inputMenu); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); - if (searchManager != null) { - SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView(); + if (searchManager != null) + { + SearchView searchView = (SearchView) inputMenu.findItem(R.id.action_search).getActionView(); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); searchView.setSubmitButtonEnabled(false); - searchView.setOnCloseListener(new SearchView.OnCloseListener() { + searchView.setOnCloseListener(new SearchView.OnCloseListener() + { @Override - public boolean onClose() { + public boolean onClose() + { invalidateOptionsMenu(); return false; } }); - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() + { @Override - public boolean onQueryTextSubmit(String query) { + public boolean onQueryTextSubmit(String query) + { return false; } @Override - public boolean onQueryTextChange(String newText) { - filter = newText; + public boolean onQueryTextChange(String newText) + { + mFilter = newText; TabLayout groupsTabLayout = findViewById(R.id.groups); TabLayout.Tab currentTab = groupsTabLayout.getTabAt(groupsTabLayout.getSelectedTabPosition()); updateLoyaltyCardList( - newText, + mFilter, currentTab != null ? currentTab.getTag() : null ); @@ -414,14 +471,13 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O } }); } - - return super.onCreateOptionsMenu(menu); + return super.onCreateOptionsMenu(inputMenu); } @Override - public boolean onOptionsItemSelected(MenuItem item) + public boolean onOptionsItemSelected(MenuItem inputItem) { - int id = item.getItemId(); + int id = inputItem.getItemId(); if (id == R.id.action_manage_groups) { @@ -430,14 +486,14 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O return true; } - if(id == R.id.action_import_export) + if (id == R.id.action_import_export) { Intent i = new Intent(getApplicationContext(), ImportExportActivity.class); startActivityForResult(i, Utils.MAIN_REQUEST); return true; } - if(id == R.id.action_settings) + if (id == R.id.action_settings) { Intent i = new Intent(getApplicationContext(), SettingsActivity.class); startActivityForResult(i, Utils.MAIN_REQUEST); @@ -450,14 +506,14 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O return true; } - if(id == R.id.action_about) + if (id == R.id.action_about) { Intent i = new Intent(getApplicationContext(), AboutActivity.class); startActivityForResult(i, Utils.MAIN_REQUEST); return true; } - return super.onOptionsItemSelected(item); + return super.onOptionsItemSelected(inputItem); } protected static boolean isDarkModeEnabled(Context inputContext) @@ -536,4 +592,79 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O return false; } + + @Override + public void onRowLongClicked(int inputPosition) + { + enableActionMode(inputPosition); + } + + private void enableActionMode(int inputPosition) + { + if (mCurrentActionMode == null) + { + mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback); + } + toggleSelection(inputPosition); + } + + private void toggleSelection(int inputPosition) + { + mAdapter.toggleSelection(inputPosition); + int count = mAdapter.getSelectedItemCount(); + + if (count == 0) { + mCurrentActionMode.finish(); + } else { + mCurrentActionMode.setTitle("Selected: " + count + " Cards"); + + MenuItem editItem = mCurrentActionMode.getMenu().findItem(R.id.action_edit); + if (count == 1) { + editItem.setVisible(true); + editItem.setEnabled(true); + } else { + editItem.setVisible(false); + editItem.setEnabled(false); + } + + mCurrentActionMode.invalidate(); + } + } + + @Override + public void onIconClicked(int inputPosition) + { + if (mCurrentActionMode == null) + { + mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback); + } + + toggleSelection(inputPosition); + } + + @Override + public void onRowClicked(int inputPosition) + { + + if (mAdapter.getSelectedItemCount() > 0) + { + enableActionMode(inputPosition); + } + else + { + Cursor selected = mAdapter.getCursor(); + selected.moveToPosition(inputPosition); + LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected); + + Intent i = new Intent(this, LoyaltyCardViewActivity.class); + i.setAction(""); + final Bundle b = new Bundle(); + b.putInt("id", loyaltyCard.id); + i.putExtras(b); + + ShortcutHelper.updateShortcuts(MainActivity.this, loyaltyCard, i); + + startActivityForResult(i, Utils.MAIN_REQUEST); + } + } } \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 9f7a91f3b..23690cd00 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -9,6 +9,14 @@ import android.provider.MediaStore; import android.util.Log; import android.widget.Toast; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.LuminanceSource; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.NotFoundException; +import com.google.zxing.RGBLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + import java.io.IOException; import java.math.BigDecimal; import java.text.NumberFormat; @@ -19,15 +27,6 @@ import java.util.GregorianCalendar; import androidx.core.graphics.ColorUtils; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.BinaryBitmap; -import com.google.zxing.LuminanceSource; -import com.google.zxing.MultiFormatReader; -import com.google.zxing.NotFoundException; -import com.google.zxing.RGBLuminanceSource; -import com.google.zxing.Result; -import com.google.zxing.common.HybridBinarizer; - public class Utils { private static final String TAG = "Catima"; diff --git a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java index f1aa773e2..6836cb5eb 100644 --- a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java @@ -7,23 +7,17 @@ import com.google.zxing.BarcodeFormat; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; -import org.json.JSONException; import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.math.BigDecimal; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.text.ParseException; import java.util.Currency; import java.util.Date; import java.util.List; -import java.util.zip.ZipFile; import protect.card_locker.DBHelper; import protect.card_locker.FormatException; diff --git a/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java index afb320eda..427b56535 100644 --- a/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java @@ -2,10 +2,8 @@ package protect.card_locker.importexport; import org.json.JSONException; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.text.ParseException; import protect.card_locker.DBHelper; diff --git a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java index 05fa9051e..d7a1b84bf 100644 --- a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java @@ -1,33 +1,21 @@ package protect.card_locker.importexport; -import android.annotation.SuppressLint; import android.database.sqlite.SQLiteDatabase; -import android.graphics.Color; import com.google.zxing.BarcodeFormat; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; -import org.json.JSONArray; import org.json.JSONException; -import org.json.JSONObject; -import java.io.BufferedReader; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.StringReader; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Currency; -import java.util.Date; -import java.util.TimeZone; import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import protect.card_locker.DBHelper; diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java index fedc446e7..ed0ee525b 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java @@ -7,8 +7,6 @@ import java.io.OutputStreamWriter; import protect.card_locker.DBHelper; import protect.card_locker.DataFormat; -import protect.card_locker.importexport.CsvDatabaseExporter; -import protect.card_locker.importexport.DatabaseExporter; public class MultiFormatExporter { diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java index 4d5324521..187ee03fb 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java @@ -6,14 +6,11 @@ import org.json.JSONException; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.text.ParseException; import protect.card_locker.DBHelper; import protect.card_locker.DataFormat; import protect.card_locker.FormatException; -import protect.card_locker.importexport.CsvDatabaseImporter; -import protect.card_locker.importexport.DatabaseImporter; public class MultiFormatImporter { diff --git a/app/src/main/java/protect/card_locker/preferences/Settings.java b/app/src/main/java/protect/card_locker/preferences/Settings.java index 19836fa59..ede946634 100644 --- a/app/src/main/java/protect/card_locker/preferences/Settings.java +++ b/app/src/main/java/protect/card_locker/preferences/Settings.java @@ -2,11 +2,11 @@ package protect.card_locker.preferences; import android.content.Context; import android.content.SharedPreferences; -import androidx.preference.PreferenceManager; + import androidx.annotation.IntegerRes; import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatDelegate; - +import androidx.preference.PreferenceManager; import protect.card_locker.R; public class Settings diff --git a/app/src/main/java/protect/card_locker/preferences/SettingsActivity.java b/app/src/main/java/protect/card_locker/preferences/SettingsActivity.java index 9280f8d5f..c6f7fb776 100644 --- a/app/src/main/java/protect/card_locker/preferences/SettingsActivity.java +++ b/app/src/main/java/protect/card_locker/preferences/SettingsActivity.java @@ -1,15 +1,15 @@ package protect.card_locker.preferences; import android.os.Bundle; +import android.view.MenuItem; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.app.AppCompatDelegate; -import android.view.MenuItem; - import nl.invissvenska.numberpickerpreference.NumberDialogPreference; import nl.invissvenska.numberpickerpreference.NumberPickerPreferenceDialogFragment; import protect.card_locker.R; diff --git a/app/src/main/res/animator/flip_left_in.xml b/app/src/main/res/animator/flip_left_in.xml new file mode 100644 index 000000000..79fd3bf85 --- /dev/null +++ b/app/src/main/res/animator/flip_left_in.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/flip_left_out.xml b/app/src/main/res/animator/flip_left_out.xml new file mode 100644 index 000000000..4ff5709ed --- /dev/null +++ b/app/src/main/res/animator/flip_left_out.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/flip_right_in.xml b/app/src/main/res/animator/flip_right_in.xml new file mode 100644 index 000000000..3d252a741 --- /dev/null +++ b/app/src/main/res/animator/flip_right_in.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/flip_right_out.xml b/app/src/main/res/animator/flip_right_out.xml new file mode 100644 index 000000000..bebc16c1e --- /dev/null +++ b/app/src/main/res/animator/flip_right_out.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..874303af5 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_copy.xml b/app/src/main/res/drawable/ic_copy.xml new file mode 100644 index 000000000..00d235484 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml new file mode 100644 index 000000000..a9a5ae203 --- /dev/null +++ b/app/src/main/res/drawable/ic_done.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..9d692c216 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..2b7b5f35f --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/list_divider.xml b/app/src/main/res/drawable/list_divider.xml new file mode 100644 index 000000000..5afbb0145 --- /dev/null +++ b/app/src/main/res/drawable/list_divider.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_row.xml b/app/src/main/res/drawable/list_row.xml new file mode 100644 index 000000000..62ca0f43c --- /dev/null +++ b/app/src/main/res/drawable/list_row.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 46422b10f..0384a349a 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -27,9 +27,11 @@ android:text="@string/noMatchingGiftCards" android:visibility="gone"/> - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" + android:visibility="gone" /> + diff --git a/app/src/main/res/layout/loyalty_card_layout.xml b/app/src/main/res/layout/loyalty_card_layout.xml index f54b902ea..aad759f5c 100644 --- a/app/src/main/res/layout/loyalty_card_layout.xml +++ b/app/src/main/res/layout/loyalty_card_layout.xml @@ -1,80 +1,118 @@ - - - - - - - + android:layout_marginStart="@dimen/activity_margin" + android:layout_marginLeft="@dimen/activity_margin" + android:layout_toEndOf="@+id/thumbnail_container" + android:layout_toRightOf="@+id/thumbnail_container" + android:layout_weight="1" + android:clickable="true" + android:focusable="true" + android:orientation="vertical" + app:layout_constraintBottom_toBottomOf="@+id/thumbnail_container" + app:layout_constraintEnd_toStartOf="@+id/star" + app:layout_constraintStart_toEndOf="@+id/thumbnail_container" + app:layout_constraintTop_toTopOf="@+id/thumbnail_container"> - - - - - + android:textSize="@dimen/storeNameTextSize" + android:textStyle="bold" /> + android:lines="1" + android:textSize="@dimen/noteTextSize" /> + android:lines="1" + android:textSize="@dimen/noteTextSize" /> + android:lines="1" + android:textSize="@dimen/noteTextSize" /> + + + + + + + + + + + + + + + + + - + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:layout_marginStart="@dimen/activity_margin" + android:contentDescription="@string/starImage" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="@android:color/black" /> + + \ No newline at end of file diff --git a/app/src/main/res/menu/card_add_menu.xml b/app/src/main/res/menu/card_add_menu.xml index 9eb7100fa..4159f22dd 100644 --- a/app/src/main/res/menu/card_add_menu.xml +++ b/app/src/main/res/menu/card_add_menu.xml @@ -1,4 +1,3 @@ - + \ No newline at end of file diff --git a/app/src/main/res/menu/card_longclick_menu.xml b/app/src/main/res/menu/card_longclick_menu.xml index 5ff953855..2e23a87de 100644 --- a/app/src/main/res/menu/card_longclick_menu.xml +++ b/app/src/main/res/menu/card_longclick_menu.xml @@ -3,11 +3,24 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 745041949..c8dc32230 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -11,6 +11,8 @@ #000000 #ffffff + #88000000 + #070707 #000000 #222222 diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index 775af5f96..0efcd9f3b 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -1,8 +1,9 @@ -> + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 01c87830f..5bc7daeb5 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -10,6 +10,8 @@ #ffffff #000000 + #44000000 + #F8F8F8 #FFFFFF #DDDDDD diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 45c05623b..60fda819d 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -2,4 +2,6 @@ https://github.com/TheLastProject/Catima/releases https://github.com/TheLastProject/Catima + 100 + 50 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57f10246e..545fad587 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ Search Add + Selected:\u0020 Click the + plus button to add a card, or import some from the ⋮ menu first. Didn\'t find anything. Try changing your search. @@ -36,6 +37,7 @@ Scan Card Barcode Card Shortcut Add a card first + Copied Card ID(s) Image of card barcode @@ -76,7 +78,7 @@ Select Barcode Enter the card ID, and either pick its barcode type below, or “This card has no barcode”. - Card ID copied to clipboard + Card ID(s) copied to clipboard Thumbnail for card Favorite star @@ -173,4 +175,6 @@ Set barcode value This barcode type can\'t yet be displayed. It may be supported in a newer version of the app. The value is not valid for the selected barcode type + Copied card IDs to clipboard + I want to share some cards with you diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index fba2c047c..5ad402cd7 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,6 +7,9 @@ @color/colorPrimaryDark @color/colorSecondary @color/colorSecondary + @color/colorPrimary + true + @drawable/ic_close