From d5d921a1c8e669f6be1131bda1000db7dc9c0da0 Mon Sep 17 00:00:00 2001 From: Katharine Date: Thu, 28 Oct 2021 01:25:50 +0800 Subject: [PATCH] Group management POC --- app/src/main/AndroidManifest.xml | 91 ++--- .../java/protect/card_locker/DBHelper.java | 54 +++ .../main/java/protect/card_locker/Group.java | 35 +- .../card_locker/ManageGroupActivity.java | 355 +++++++++++++++++ .../ManageGroupActivityInGroupState.java | 58 +++ .../card_locker/ManageGroupCursorAdapter.java | 365 ++++++++++++++++++ .../card_locker/ManageGroupLoyaltyCard.java | 168 ++++++++ .../card_locker/ManageGroupsActivity.java | 9 +- .../main/res/layout/activity_manage_group.xml | 56 +++ .../main/res/layout/content_manage_group.xml | 49 +++ .../manage_group_loyalty_card_layout.xml | 147 +++++++ app/src/main/res/menu/manage_group_menu.xml | 10 + app/src/main/res/values/strings.xml | 7 +- 13 files changed, 1358 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/ManageGroupActivity.java create mode 100644 app/src/main/java/protect/card_locker/ManageGroupActivityInGroupState.java create mode 100644 app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java create mode 100644 app/src/main/java/protect/card_locker/ManageGroupLoyaltyCard.java create mode 100644 app/src/main/res/layout/activity_manage_group.xml create mode 100644 app/src/main/res/layout/content_manage_group.xml create mode 100644 app/src/main/res/layout/manage_group_loyalty_card_layout.xml create mode 100644 app/src/main/res/menu/manage_group_menu.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3602f9e97..4c6e441ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,14 +1,13 @@ - + - - - + + + + + - - + + android:theme="@style/Theme.App.Starting"> - + - + - - + android:theme="@style/AppTheme.NoActionBar"> - + android:theme="@style/AppTheme.NoActionBar"> + android:windowSoftInputMode="stateHidden" /> - + android:windowSoftInputMode="stateHidden"> + + - - + - - - + android:theme="@style/AppTheme.NoActionBar" /> + android:windowSoftInputMode="stateHidden" /> + android:theme="@style/AppTheme.NoActionBar" /> + android:theme="@style/AppTheme.NoActionBar" /> + android:theme="@style/AppTheme.NoActionBar"> - - + + + + + android:grantUriPermissions="true"> + android:resource="@xml/file_provider_paths" /> - + \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 505f3be52..a55ae71fe 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -613,6 +613,21 @@ public class DBHelper extends SQLiteOpenHelper } } + public void addLoyaltyCardToGroup(int cardId, String groupId){ + SQLiteDatabase db = getWritableDatabase(); + ContentValues contentValues = new ContentValues(); + contentValues.put(LoyaltyCardDbIdsGroups.cardID, cardId); + contentValues.put(LoyaltyCardDbIdsGroups.groupID, groupId); + db.insert(LoyaltyCardDbIdsGroups.TABLE, null, contentValues); + } + + public void removeLoyaltyCardFromGroup(int cardId, String groupId){ + SQLiteDatabase db = getWritableDatabase(); + db.delete(LoyaltyCardDbIdsGroups.TABLE, + whereAttrs(LoyaltyCardDbIdsGroups.cardID, LoyaltyCardDbIdsGroups.groupID), + withArgs(cardId, groupId)); + } + public boolean deleteLoyaltyCard(final int id) { SQLiteDatabase db = getWritableDatabase(); @@ -718,6 +733,45 @@ public class DBHelper extends SQLiteOpenHelper limitString, filter.trim().isEmpty() ? null : new String[] { TextUtils.join("* ", filter.split(" ")) + '*' }, null); } + /** + * Returns a cursor to all loyalty cards with the filter text in either the store and whether card is in the provided group + * + * @param filter + * @param group + * @param order + * @return Cursor + */ + public Cursor getIfLoyaltyCardsAreInGroupCursor(String filter, Group group, LoyaltyCardOrder order, LoyaltyCardOrderDirection direction) { + SQLiteDatabase db = getReadableDatabase(); + + if (group == null) { + throw new IllegalArgumentException("group cannot be null"); + } + String orderField = getFieldForOrder(order); + String[] selectionArgs; + if(filter.trim().isEmpty()) { + selectionArgs = new String[]{ + group._id + }; + }else{ + selectionArgs = new String[]{ + group._id, + TextUtils.join("* ", filter.split(" ")) + '*' + }; + } + return db.rawQuery("SELECT " + "*" + + " FROM " + LoyaltyCardDbIds.TABLE + " LEFT OUTER JOIN " + + " (SELECT " + LoyaltyCardDbIdsGroups.TABLE + ".*" + + " FROM " + LoyaltyCardDbIdsGroups.TABLE + + " WHERE " + LoyaltyCardDbIdsGroups.groupID + " = ? " + + " ) " + " AS " + LoyaltyCardDbIdsGroups.TABLE + + " ON " + LoyaltyCardDbIds.TABLE + "." + LoyaltyCardDbIds.ID + " = " + LoyaltyCardDbIdsGroups.TABLE + "." + LoyaltyCardDbIdsGroups.cardID + + (filter.trim().isEmpty() ? "" : " WHERE " + LoyaltyCardDbIds.TABLE + "." + LoyaltyCardDbIds.STORE + " MATCH ? ") + + " ORDER BY " + LoyaltyCardDbIds.TABLE + "." + orderField, + selectionArgs, + null); + } + /** * Returns the amount of loyalty cards. * diff --git a/app/src/main/java/protect/card_locker/Group.java b/app/src/main/java/protect/card_locker/Group.java index 1d5de8603..0120dd229 100644 --- a/app/src/main/java/protect/card_locker/Group.java +++ b/app/src/main/java/protect/card_locker/Group.java @@ -1,8 +1,10 @@ package protect.card_locker; import android.database.Cursor; +import android.os.Parcel; +import android.os.Parcelable; -public class Group +public class Group implements Parcelable { public final String _id; public final int order; @@ -12,6 +14,25 @@ public class Group this.order = order; } + protected Group(Parcel in){ + this._id = in.readString(); + this.order = in.readInt(); + } + + + @Override + public void writeToParcel(Parcel parcel, int i){ + parcel.writeString(_id); + parcel.writeInt(order); + } + + + @Override + public int describeContents() { + // group table does not have an integer ID + return 0; + } + public static Group toGroup(Cursor cursor) { String _id = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbGroups.ID)); @@ -19,4 +40,16 @@ public class Group return new Group(_id, order); } + + public static final Creator CREATOR = new Creator() { + @Override + public Group createFromParcel(Parcel in) { + return new Group(in); + } + + @Override + public Group[] newArray(int size) { + return new Group[size]; + } + }; } diff --git a/app/src/main/java/protect/card_locker/ManageGroupActivity.java b/app/src/main/java/protect/card_locker/ManageGroupActivity.java new file mode 100644 index 000000000..f993ddfd4 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ManageGroupActivity.java @@ -0,0 +1,355 @@ +package protect.card_locker; + +import android.app.SearchManager; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.CursorIndexOutOfBoundsException; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.tabs.TabLayout; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.view.ActionMode; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.Toolbar; +import androidx.core.graphics.BlendModeColorFilterCompat; +import androidx.core.graphics.BlendModeCompat; +import androidx.core.splashscreen.SplashScreen; +import androidx.recyclerview.widget.RecyclerView; +import protect.card_locker.preferences.SettingsActivity; + +public class ManageGroupActivity extends CatimaAppCompatActivity implements ManageGroupCursorAdapter.CardAdapterListener +{ + private static final String TAG = "Catima"; + + private final DBHelper mDB = new DBHelper(this); + private ManageGroupCursorAdapter mAdapter; + private ActionMode mCurrentActionMode; + private Menu mMenu; + + // currently unused + protected String mFilter = ""; + protected DBHelper.LoyaltyCardOrderDirection mOrderDirection = DBHelper.LoyaltyCardOrderDirection.Ascending; + protected DBHelper.LoyaltyCardOrder mOrder = DBHelper.LoyaltyCardOrder.Alpha; + + protected Group mGroup = null; + private RecyclerView mCardList; + private View mHelpText; + private View mNoMatchingCardsText; + private View mNoGroupCardsText; + private EditText mGroupNameText; + private TextView mGroupNameLabel; + private ActionBar mActionBar; + + private HashMap mAdapterState; + private String mCurrentGroupName; + + private boolean mGroupNameNotInUse; + + private boolean mDarkMode; + + @Override + protected void onCreate(Bundle inputSavedInstanceState) + { + super.onCreate(inputSavedInstanceState); + setContentView(R.layout.activity_manage_group); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + mHelpText = findViewById(R.id.helpText); + mNoMatchingCardsText = findViewById(R.id.noMatchingCardsText); + mNoGroupCardsText = findViewById(R.id.noGroupCardsText); + mCardList = findViewById(R.id.list); + + mGroupNameText = findViewById(R.id.editTextGroupName); + mGroupNameLabel = findViewById(R.id.textViewEditGroupName); + + mAdapter = new ManageGroupCursorAdapter(this, null, this); + mCardList.setAdapter(mAdapter); + registerForContextMenu(mCardList); + + mGroup = null; + + mDarkMode = Utils.isDarkModeEnabled(getApplicationContext()); + + if (inputSavedInstanceState != null) { + ManageGroupActivityInGroupState adapterState = inputSavedInstanceState.getParcelable("mAdapterState"); + if (adapterState != null) { + mAdapterState = adapterState.getMap(); + } + mCurrentGroupName = inputSavedInstanceState.getString("mCurrentGroupName"); + } + + mActionBar = getSupportActionBar(); + + mActionBar.setDisplayHomeAsUpEnabled(true); + mActionBar.setDisplayShowHomeEnabled(true); + + } + + private void resetGroupNameTextColor() { + if (mDarkMode) { + mGroupNameText.setTextColor(Color.WHITE); + } else{ + mGroupNameText.setTextColor(Color.BLACK); + } + } + + private void checkIfGroupNameIsInUse(){ + mGroupNameNotInUse = false; + String currentGroupName = mGroupNameText.getText().toString(); + if (!mGroup._id.equals(currentGroupName.trim())) { + Group group = mDB.getGroup(currentGroupName.trim()); + if (group != null) { + mGroupNameNotInUse = false; + mGroupNameText.setTextColor(Color.RED); + } else { + mGroupNameNotInUse = true; + } + } + } + + @Override + protected void onResume() + { + super.onResume(); + + Intent intent = getIntent(); + mGroup = intent.getParcelableExtra("group"); + if (mGroup == null){ + throw(new IllegalArgumentException("this activity expects a group loaded into it's intent")); + } + + setTitle(getString(R.string.edit) + ": " + mGroup._id); + + if (mCurrentGroupName == null){ + mGroupNameText.setText(mGroup._id); + }else{ + mGroupNameText.setText(mCurrentGroupName); + checkIfGroupNameIsInUse(); + } + + mGroupNameText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + return; + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + return; + } + + @Override + public void afterTextChanged(Editable s) { + resetGroupNameTextColor(); + checkIfGroupNameIsInUse(); + } + }); + + if (mAdapterState != null){ + mAdapter.importInGroupState(mAdapterState); + } + updateLoyaltyCardList(); + } + + @Override + protected void onSaveInstanceState (Bundle outState){ + super.onSaveInstanceState(outState); + if(mAdapterState != null){ + ManageGroupActivityInGroupState state = new ManageGroupActivityInGroupState(mAdapterState); + outState.putParcelable("mAdapterState", state); + } + if(mCurrentGroupName != null){ + outState.putString("mCurrentGroupName", mCurrentGroupName); + } + + } + + @Override + protected void onPause(){ + super.onPause(); + + mAdapterState = mAdapter.exportInGroupState(); + mCurrentGroupName = mGroupNameText.getText().toString(); + } + + private void updateLoyaltyCardList() { + mAdapter.swapCursor(mDB.getIfLoyaltyCardsAreInGroupCursor(mFilter, mGroup, mOrder, mOrderDirection)); + + if(mAdapter.getCountFromCursor() > 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 + mHelpText.setVisibility(View.GONE); + mNoGroupCardsText.setVisibility(View.GONE); + if(mAdapter.getItemCount() > 0) + { + mCardList.setVisibility(View.VISIBLE); + mNoMatchingCardsText.setVisibility(View.GONE); + } + else + { + mCardList.setVisibility(View.GONE); + if (!mFilter.isEmpty()) { + // Actual Empty Search Result + mNoMatchingCardsText.setVisibility(View.VISIBLE); + mNoGroupCardsText.setVisibility(View.GONE); + } else { + // Group Tab with no Group Cards + mNoMatchingCardsText.setVisibility(View.GONE); + mNoGroupCardsText.setVisibility(View.VISIBLE); + } + } + } + else + { + mCardList.setVisibility(View.GONE); + mHelpText.setVisibility(View.VISIBLE); + mNoMatchingCardsText.setVisibility(View.GONE); + mNoGroupCardsText.setVisibility(View.GONE); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu inputMenu) + { + this.mMenu = inputMenu; + + getMenuInflater().inflate(R.menu.manage_group_menu, inputMenu); + + MenuItem confirmButton = inputMenu.findItem(R.id.action_confirm); + Drawable icon = confirmButton.getIcon(); + icon.mutate(); + icon.setTint(Color.WHITE); + confirmButton.setIcon(icon); + return super.onCreateOptionsMenu(inputMenu); + } + + private void leaveWithoutSaving(){ + if (hasChanged()){ + AlertDialog.Builder builder = new AlertDialog.Builder(ManageGroupActivity.this); + builder.setTitle(R.string.discard_changes); + builder.setMessage(R.string.discard_changes_confirm); + builder.setPositiveButton(R.string.confirm, (dialog, which) -> finish()); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + AlertDialog dialog = builder.create(); + dialog.show(); + }else{ + finish(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem inputItem) + { + int id = inputItem.getItemId(); + + if (id == R.id.action_confirm) + { + String currentGroupName = mGroupNameText.getText().toString(); + if(!currentGroupName.trim().equals(mGroup._id)){ + if(!mGroupNameNotInUse) { + Toast toast = Toast.makeText(getApplicationContext(), R.string.group_name_already_in_use, Toast.LENGTH_SHORT); + toast.show(); + return true; + } + if(currentGroupName.trim().length() == 0){ + Toast toast = Toast.makeText(getApplicationContext(), R.string.group_name_is_empty, Toast.LENGTH_SHORT); + toast.show(); + return true; + } + } + + mAdapter.commitToDatabase(getApplicationContext(), mGroup._id); + Toast toast = Toast.makeText(getApplicationContext(), R.string.group_updated, Toast.LENGTH_SHORT); + if(!currentGroupName.trim().equals(mGroup._id)){ + mDB.updateGroup(mGroup._id, currentGroupName.trim()); + } + toast.show(); + finish(); + return true; + } + + return super.onOptionsItemSelected(inputItem); + } + @Override + public void onBackPressed() + { + leaveWithoutSaving(); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + + private boolean hasChanged(){ + return mAdapter.hasChanged() || !mGroup._id.equals(mGroupNameText.getText().toString().trim()); + } + + private void setSort(DBHelper.LoyaltyCardOrder order, DBHelper.LoyaltyCardOrderDirection direction) { + // Update values + mOrder = order; + mOrderDirection = direction; + + // Store in Shared Preference to restore next app launch + SharedPreferences sortPref = getApplicationContext().getSharedPreferences( + getString(R.string.sharedpreference_sort), + Context.MODE_PRIVATE); + SharedPreferences.Editor sortPrefEditor = sortPref.edit(); + sortPrefEditor.putString(getString(R.string.sharedpreference_sort_order), order.name()); + sortPrefEditor.putString(getString(R.string.sharedpreference_sort_direction), direction.name()); + sortPrefEditor.apply(); + + // Update card list + updateLoyaltyCardList(); + } + + @Override + public void onRowLongClicked(int inputPosition) + { + // do nothing for now + return; + } + + @Override + public void onRowClicked(int inputPosition) + { + Cursor selected = mAdapter.getCursor(); + mAdapter.toggleSelection(inputPosition); + } +} diff --git a/app/src/main/java/protect/card_locker/ManageGroupActivityInGroupState.java b/app/src/main/java/protect/card_locker/ManageGroupActivityInGroupState.java new file mode 100644 index 000000000..e6c30f793 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ManageGroupActivityInGroupState.java @@ -0,0 +1,58 @@ +package protect.card_locker; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.HashMap; +import java.util.Map; + +public class ManageGroupActivityInGroupState implements Parcelable { + HashMap map; + + + + public ManageGroupActivityInGroupState(HashMap in){ + map = (HashMap)in.clone(); + } + + @Override + public int describeContents(){ + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(map.size()); + + for(Map.Entry entry : map.entrySet()){ + dest.writeInt(entry.getKey()); + dest.writeInt(entry.getValue() ? 1 : 0); + } + } + + protected ManageGroupActivityInGroupState(Parcel in){ + map = new HashMap(); + int length = in.readInt(); + for (int i = 0;i < length; i++){ + int key = in.readInt(); + boolean value = in.readInt() == 1 ? true : false; + map.put(key, value); + } + } + + public static final Creator CREATOR = new Creator() { + @Override + public ManageGroupActivityInGroupState createFromParcel(Parcel in) { + return new ManageGroupActivityInGroupState(in); + } + + @Override + public ManageGroupActivityInGroupState[] newArray(int size) { + return new ManageGroupActivityInGroupState[size]; + } + }; + + public HashMap getMap(){ + return (HashMap)map.clone(); + } +} diff --git a/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java b/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java new file mode 100644 index 000000000..c1a10d6d0 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java @@ -0,0 +1,365 @@ +package protect.card_locker; + +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.util.TypedValue; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.google.android.material.card.MaterialCardView; + +import java.math.BigDecimal; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.BlendModeColorFilterCompat; +import androidx.core.graphics.BlendModeCompat; +import androidx.recyclerview.widget.RecyclerView; + +import protect.card_locker.preferences.Settings; + +public class ManageGroupCursorAdapter extends BaseCursorAdapter { + private 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; + private HashMap mIndexCardMap; + private HashMap mInGroupOverlay; + + + public ManageGroupCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener) { + super(inputCursor); + setHasStableIds(true); + mSettings = new Settings(inputContext); + mContext = inputContext; + mListener = inputListener; + mSelectedItems = new SparseBooleanArray(); + mAnimationItemsIndex = new SparseBooleanArray(); + + mDarkModeEnabled = Utils.isDarkModeEnabled(inputContext); + + + mInGroupOverlay = new HashMap(); + swapCursor(mCursor); + } + + @Override + public void swapCursor(Cursor inputCursor) { + mIndexCardMap = new HashMap(); + + super.swapCursor(inputCursor); + mCursor = inputCursor; + + } + + @Override + public ManageGroupListItemViewHolder onCreateViewHolder(ViewGroup inputParent, int inputViewType) { + View itemView = LayoutInflater.from(inputParent.getContext()).inflate(R.layout.manage_group_loyalty_card_layout, inputParent, false); + return new ManageGroupListItemViewHolder(itemView, mListener); + } + + public Cursor getCursor() { + return mCursor; + } + + public void onBindViewHolder(ManageGroupListItemViewHolder inputHolder, Cursor inputCursor) { + // Invisible until we want to show something more + inputHolder.mDivider.setVisibility(View.GONE); + + int size = mSettings.getFontSizeMax(mSettings.getSmallFont()); + + if (mDarkModeEnabled) { + inputHolder.mStarIcon.setColorFilter(BlendModeColorFilterCompat.createBlendModeColorFilterCompat(Color.WHITE, BlendModeCompat.SRC_ATOP)); + } + + ManageGroupLoyaltyCard cardEntry = ManageGroupLoyaltyCard.toCard(inputCursor); + + inputHolder.mStoreField.setText(cardEntry.store); + inputHolder.mStoreField.setTextSize(mSettings.getFontSizeMax(mSettings.getMediumFont())); + if (!cardEntry.note.isEmpty()) { + inputHolder.mNoteField.setVisibility(View.VISIBLE); + inputHolder.mNoteField.setText(cardEntry.note); + inputHolder.mNoteField.setTextSize(size); + } else { + inputHolder.mNoteField.setVisibility(View.GONE); + } + + if (!cardEntry.balance.equals(new BigDecimal("0"))) { + int drawableSize = dpToPx((size*24)/14, mContext); + inputHolder.mDivider.setVisibility(View.VISIBLE); + inputHolder.mBalanceField.setVisibility(View.VISIBLE); + Drawable balanceIcon = inputHolder.mBalanceField.getCompoundDrawables()[0]; + balanceIcon.setBounds(0,0,drawableSize,drawableSize); + inputHolder.mBalanceField.setCompoundDrawablesRelative(balanceIcon, null, null, null); + if (mDarkModeEnabled) { + balanceIcon.setColorFilter(BlendModeColorFilterCompat.createBlendModeColorFilterCompat(Color.WHITE, BlendModeCompat.SRC_ATOP)); + } + inputHolder.mBalanceField.setText(Utils.formatBalance(mContext, cardEntry.balance, cardEntry.balanceType)); + inputHolder.mBalanceField.setTextSize(size); + } else { + inputHolder.mBalanceField.setVisibility(View.GONE); + } + + if (cardEntry.expiry != null) { + int drawableSize = dpToPx((size*24)/14, mContext); + inputHolder.mDivider.setVisibility(View.VISIBLE); + inputHolder.mExpiryField.setVisibility(View.VISIBLE); + Drawable expiryIcon = inputHolder.mExpiryField.getCompoundDrawables()[0]; + expiryIcon.setBounds(0,0, drawableSize, drawableSize); + inputHolder.mExpiryField.setCompoundDrawablesRelative(expiryIcon, null, null, null); + if (Utils.hasExpired(cardEntry.expiry)) { + expiryIcon.setColorFilter(BlendModeColorFilterCompat.createBlendModeColorFilterCompat(Color.RED, BlendModeCompat.SRC_ATOP)); + inputHolder.mExpiryField.setTextColor(Color.RED); + } else if (mDarkModeEnabled) { + expiryIcon.setColorFilter(BlendModeColorFilterCompat.createBlendModeColorFilterCompat(Color.WHITE, BlendModeCompat.SRC_ATOP)); + } + inputHolder.mExpiryField.setText(DateFormat.getDateInstance(DateFormat.LONG).format(cardEntry.expiry)); + inputHolder.mExpiryField.setTextSize(size); + } else { + inputHolder.mExpiryField.setVisibility(View.GONE); + } + + // inputHolder.mStarIcon.setVisibility(cardEntry.starStatus != 0 ? View.VISIBLE : View.GONE); + inputHolder.mCardIcon.setImageBitmap(Utils.generateIcon(mContext, cardEntry.store, cardEntry.headerColor).getLetterTile()); + int imageSize = dpToPx( (size*46)/14, mContext); + inputHolder.mCardIcon.getLayoutParams().height = imageSize; + inputHolder.mCardIcon.getLayoutParams().width = imageSize; + inputHolder.mStarIcon.getLayoutParams().height = imageSize; + inputHolder.mStarIcon.getLayoutParams().width = imageSize; + inputHolder.mTickIcon.getLayoutParams().height = imageSize; + inputHolder.mTickIcon.getLayoutParams().width = imageSize; + + /* Changing Padding and Margin of different views according to font size + * Views Included: + * a) InformationContainer padding + * b) Store left padding + * c) Divider Margin + * d) note top margin + * e) row margin + * */ + int marginPaddingSize = dpToPx((size*16)/14, mContext ); + inputHolder.mInformationContainer.setPadding(marginPaddingSize, marginPaddingSize, marginPaddingSize, marginPaddingSize); + inputHolder.mStoreField.setPadding(marginPaddingSize, 0, 0, 0); + LinearLayout.LayoutParams lpDivider = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT ); + lpDivider.setMargins(0, marginPaddingSize, 0, marginPaddingSize); + inputHolder.mDivider.setLayoutParams(lpDivider); + LinearLayout.LayoutParams lpNoteField = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT ); + lpNoteField.setMargins(0, marginPaddingSize/2, 0, 0); + inputHolder.mNoteField.setLayoutParams(lpNoteField); + LinearLayout.LayoutParams lpRow = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT ); + lpRow.setMargins(marginPaddingSize/2, marginPaddingSize/2, marginPaddingSize/2, marginPaddingSize/2); + inputHolder.mRow.setLayoutParams(lpRow); + + inputHolder.itemView.setActivated(mSelectedItems.get(inputCursor.getPosition(), false)); + + Boolean overlayValue = mInGroupOverlay.get(cardEntry.id); + if((overlayValue != null? overlayValue: cardEntry.is_in_group)) { + mAnimationItemsIndex.put(inputCursor.getPosition(), true); + mSelectedItems.put(inputCursor.getPosition(), true); + } + + applyIconAnimation(inputHolder, inputCursor.getPosition()); + applyClickEvents(inputHolder, inputCursor.getPosition()); + + mIndexCardMap.put(inputCursor.getPosition(), cardEntry); + } + + private void applyClickEvents(ManageGroupListItemViewHolder inputHolder, final int inputPosition) { + inputHolder.mRow.setOnClickListener(inputView -> mListener.onRowClicked(inputPosition)); + inputHolder.mInformationContainer.setOnClickListener(inputView -> mListener.onRowClicked(inputPosition)); + + inputHolder.mRow.setOnLongClickListener(inputView -> { + mListener.onRowLongClicked(inputPosition); + inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + return true; + }); + + inputHolder.mInformationContainer.setOnLongClickListener(inputView -> { + mListener.onRowLongClicked(inputPosition); + inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + return true; + }); + } + + private void applyIconAnimation(ManageGroupListItemViewHolder 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(); + } + } else { + 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(); + } + + + 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); + } + ManageGroupLoyaltyCard cardEntry = mIndexCardMap.get(inputPosition); + Boolean overlayValue = mInGroupOverlay.get(cardEntry.id); + if (overlayValue == null){ + mInGroupOverlay.put(cardEntry.id, !cardEntry.is_in_group); + }else{ + mInGroupOverlay.put(cardEntry.id, !overlayValue); + } + + notifyDataSetChanged(); + } + + private void resetCurrentIndex() { + mCurrentSelectedIndex = -1; + } + + public interface CardAdapterListener { + void onRowClicked(int inputPosition); + + void onRowLongClicked(int inputPosition); + } + + private HashMap fetchWholeQuery (){ + HashMap res = new HashMap(); + int oldPosition = mCursor.getPosition(); + mCursor.moveToFirst(); + while(!mCursor.isAfterLast()){ + ManageGroupLoyaltyCard cardEntry = ManageGroupLoyaltyCard.toCard(mCursor); + res.put(cardEntry.id, cardEntry); + mCursor.moveToNext(); + } + mCursor.moveToPosition(oldPosition); + return res; + } + + public void commitToDatabase(Context context, String groupId){ + DBHelper dbHelper = new DBHelper(context); + HashMap cache = fetchWholeQuery(); + for(Map.Entry entry : mInGroupOverlay.entrySet()){ + ManageGroupLoyaltyCard cardEntry = cache.get(entry.getKey()); + if (cardEntry == null){ + Log.d("commitToDatabase", "card with id " + entry.getKey() + " was removed from database unexpectedly"); + continue; + } + if (entry.getValue() != cardEntry.is_in_group) { + if (entry.getValue()) { + dbHelper.addLoyaltyCardToGroup(entry.getKey(), groupId); + } else { + dbHelper.removeLoyaltyCardFromGroup(entry.getKey(), groupId); + } + } + } + } + + public boolean hasChanged(){ + HashMap cache = fetchWholeQuery(); + for(Map.Entry entry : mInGroupOverlay.entrySet()){ + ManageGroupLoyaltyCard cardEntry = cache.get(entry.getKey()); + if(cardEntry.is_in_group != entry.getValue()){ + return true; + } + } + return false; + } + + public int getCountFromCursor(){ + return mCursor.getCount(); + } + + public HashMap exportInGroupState(){ + return (HashMap)mInGroupOverlay.clone(); + } + + public void importInGroupState(HashMap cardIdInGroupMap){ + mInGroupOverlay = (HashMap)cardIdInGroupMap.clone(); + } + + + public static class ManageGroupListItemViewHolder extends RecyclerView.ViewHolder { + + public TextView mStoreField, mNoteField, mBalanceField, mExpiryField; + public LinearLayout mInformationContainer; + public ImageView mCardIcon, mStarIcon, mTickIcon; + public MaterialCardView mRow; + public View mDivider; + public RelativeLayout mThumbnailFrontContainer, mThumbnailBackContainer; + + public ManageGroupListItemViewHolder(View inputView, CardAdapterListener inputListener) { + super(inputView); + mRow = inputView.findViewById(R.id.row); + mDivider = inputView.findViewById(R.id.info_divider); + 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); + mTickIcon = inputView.findViewById(R.id.selected_thumbnail); + inputView.setOnLongClickListener(view -> { + inputListener.onRowClicked(getAdapterPosition()); + inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + return true; + }); + } + } + + public int dpToPx(int dp, Context mContext){ + Resources r = mContext.getResources(); + int px = (int)TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()); + return px; + } +} diff --git a/app/src/main/java/protect/card_locker/ManageGroupLoyaltyCard.java b/app/src/main/java/protect/card_locker/ManageGroupLoyaltyCard.java new file mode 100644 index 000000000..0010d1f19 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ManageGroupLoyaltyCard.java @@ -0,0 +1,168 @@ +package protect.card_locker; +// Was thinking about extending LoyaltyCard but this makes things more modular..? +import android.database.Cursor; +import android.os.Parcel; +import android.os.Parcelable; + +import java.math.BigDecimal; +import java.util.Currency; +import java.util.Date; + +import androidx.annotation.Nullable; + +public class ManageGroupLoyaltyCard implements Parcelable { + public final int id; + public final String store; + public final String note; + public final Date expiry; + public final BigDecimal balance; + public final Currency balanceType; + public final String cardId; + + @Nullable + public final String barcodeId; + + @Nullable + public final CatimaBarcode barcodeType; + + @Nullable + public final Integer headerColor; + + public final int starStatus; + public final long lastUsed; + public int zoomLevel; + + public final boolean is_in_group; + + public ManageGroupLoyaltyCard(final int id, final String store, final String note, final Date expiry, + final BigDecimal balance, final Currency balanceType, final String cardId, + @Nullable final String barcodeId, @Nullable final CatimaBarcode barcodeType, + @Nullable final Integer headerColor, final int starStatus, final long lastUsed,final int zoomLevel, + final boolean is_in_group) + { + this.id = id; + this.store = store; + this.note = note; + this.expiry = expiry; + this.balance = balance; + this.balanceType = balanceType; + this.cardId = cardId; + this.barcodeId = barcodeId; + this.barcodeType = barcodeType; + this.headerColor = headerColor; + this.starStatus = starStatus; + this.lastUsed = lastUsed; + this.zoomLevel = zoomLevel; + this.is_in_group = is_in_group; + } + + protected ManageGroupLoyaltyCard(Parcel in) { + id = in.readInt(); + store = in.readString(); + note = in.readString(); + long tmpExpiry = in.readLong(); + expiry = tmpExpiry != -1 ? new Date(tmpExpiry) : null; + balance = (BigDecimal) in.readValue(BigDecimal.class.getClassLoader()); + balanceType = (Currency) in.readValue(Currency.class.getClassLoader()); + cardId = in.readString(); + barcodeId = in.readString(); + String tmpBarcodeType = in.readString(); + barcodeType = !tmpBarcodeType.isEmpty() ? CatimaBarcode.fromName(tmpBarcodeType) : null; + int tmpHeaderColor = in.readInt(); + headerColor = tmpHeaderColor != -1 ? tmpHeaderColor : null; + starStatus = in.readInt(); + lastUsed = in.readLong(); + zoomLevel = in.readInt(); + if (in.readInt() == 1) { + is_in_group = true; + }else{ + is_in_group = false; + } + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeInt(id); + parcel.writeString(store); + parcel.writeString(note); + parcel.writeLong(expiry != null ? expiry.getTime() : -1); + parcel.writeValue(balance); + parcel.writeValue(balanceType); + parcel.writeString(cardId); + parcel.writeString(barcodeId); + parcel.writeString(barcodeType != null ? barcodeType.name() : ""); + parcel.writeInt(headerColor != null ? headerColor : -1); + parcel.writeInt(starStatus); + parcel.writeLong(lastUsed); + parcel.writeInt(zoomLevel); + if (is_in_group) { + parcel.writeInt(1); + }else{ + parcel.writeInt(0); + } + } + + public static ManageGroupLoyaltyCard toCard(Cursor cursor) + { + int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID)); + String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE)); + String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE)); + long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY)); + BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE))); + String cardId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID)); + String barcodeId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_ID)); + int starred = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS)); + long lastUsed = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.LAST_USED)); + int zoomLevel = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ZOOM_LEVEL)); + + int barcodeTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE); + int balanceTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE); + int headerColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_COLOR); + + boolean was_in_group = !cursor.isNull(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIdsGroups.groupID)); + + CatimaBarcode barcodeType = null; + Currency balanceType = null; + Date expiry = null; + Integer headerColor = null; + + if (cursor.isNull(barcodeTypeColumn) == false) + { + barcodeType = CatimaBarcode.fromName(cursor.getString(barcodeTypeColumn)); + } + + if (cursor.isNull(balanceTypeColumn) == false) + { + balanceType = Currency.getInstance(cursor.getString(balanceTypeColumn)); + } + + if(expiryLong > 0) + { + expiry = new Date(expiryLong); + } + + if(cursor.isNull(headerColorColumn) == false) + { + headerColor = cursor.getInt(headerColorColumn); + } + + return new ManageGroupLoyaltyCard(id, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starred, lastUsed,zoomLevel,was_in_group); + } + + @Override + public int describeContents() { + return id; + } + + public static final Creator CREATOR = new Creator() { + @Override + public LoyaltyCard createFromParcel(Parcel in) { + return new LoyaltyCard(in); + } + + @Override + public LoyaltyCard[] newArray(int size) { + return new LoyaltyCard[size]; + } + }; +} diff --git a/app/src/main/java/protect/card_locker/ManageGroupsActivity.java b/app/src/main/java/protect/card_locker/ManageGroupsActivity.java index 60e6dd35f..9140ef7de 100644 --- a/app/src/main/java/protect/card_locker/ManageGroupsActivity.java +++ b/app/src/main/java/protect/card_locker/ManageGroupsActivity.java @@ -1,6 +1,7 @@ package protect.card_locker; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.text.InputType; @@ -176,7 +177,12 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro @Override public void onEditButtonClicked(View view) { - final String groupName = getGroupName(view); + Group group = mDb.getGroup(getGroupName(view)); + Intent intent = new Intent(this, ManageGroupActivity.class); + intent.putExtra("group", group); + startActivity(intent); + /* + final String groupName = c; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.enter_group_name); @@ -195,6 +201,7 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro dialog.show(); dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); input.requestFocus(); + */ } @Override diff --git a/app/src/main/res/layout/activity_manage_group.xml b/app/src/main/res/layout/activity_manage_group.xml new file mode 100644 index 000000000..32f696a4c --- /dev/null +++ b/app/src/main/res/layout/activity_manage_group.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/content_manage_group.xml b/app/src/main/res/layout/content_manage_group.xml new file mode 100644 index 000000000..71a305bcb --- /dev/null +++ b/app/src/main/res/layout/content_manage_group.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/manage_group_loyalty_card_layout.xml b/app/src/main/res/layout/manage_group_loyalty_card_layout.xml new file mode 100644 index 000000000..4c3af71d5 --- /dev/null +++ b/app/src/main/res/layout/manage_group_loyalty_card_layout.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/manage_group_menu.xml b/app/src/main/res/menu/manage_group_menu.xml new file mode 100644 index 000000000..ca6a8b551 --- /dev/null +++ b/app/src/main/res/menu/manage_group_menu.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e14f6b22..b29e5858c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - + Search Add @@ -129,6 +129,9 @@ %d card %d cards + Group name already in use! + Group name cannot be empty! + Group updated! All Delete group? Install a file manager first. @@ -245,4 +248,6 @@ Rate this app on Google Play Report Error + Discard Changes + There are unsaved changes that were made, do you wish to leave without saving?