From 4bbeb27714fbb4c717a1155e06a346a98fc51d61 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Fri, 6 Nov 2020 18:12:06 +0100 Subject: [PATCH] Feature/groups (#71) * Basic group management * Assign cards to groups * Fix lint * Fix findbugs 'dodgy code' * Group name as unique key * More group tests * Import/export groups * Implement group renaming and deleting * Fix findBugs * Fix chip marking in edit activity * Group import/export tests * Fix some state bugs * Some last fixes * Remove redundant if statement * Fix findBugs * Deduplicate code * Cleanup * Fix groups not showing up with new card * Fix capture and enter button touching * Fix dialog button look --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 5 + .../card_locker/CsvDatabaseExporter.java | 62 +++- .../card_locker/CsvDatabaseImporter.java | 172 +++++++++- .../java/protect/card_locker/DBHelper.java | 322 +++++++++++++++++- .../main/java/protect/card_locker/Group.java | 20 ++ .../card_locker/GroupCursorAdapter.java | 53 +++ .../java/protect/card_locker/LoyaltyCard.java | 4 +- .../card_locker/LoyaltyCardEditActivity.java | 49 +++ .../card_locker/LoyaltyCardViewActivity.java | 2 - .../protect/card_locker/MainActivity.java | 131 ++++++- .../card_locker/ManageGroupsActivity.java | 186 ++++++++++ .../res/drawable-hdpi/ic_folder_white.png | Bin 0 -> 163 bytes .../res/drawable-mdpi/ic_folder_white.png | Bin 0 -> 135 bytes .../res/drawable-xhdpi/ic_folder_white.png | Bin 0 -> 207 bytes .../res/drawable-xxhdpi/ic_folder_white.png | Bin 0 -> 252 bytes app/src/main/res/layout/group_layout.xml | 68 ++++ app/src/main/res/layout/group_main.xml | 25 ++ .../main/res/layout/layout_chip_choice.xml | 9 + .../res/layout/loyalty_card_edit_activity.xml | 58 ++++ app/src/main/res/layout/main_activity.xml | 7 + .../res/layout/manage_groups_activity.xml | 36 ++ app/src/main/res/menu/main_menu.xml | 5 + app/src/main/res/values-v21/styles.xml | 2 +- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 9 +- app/src/main/res/values/styles.xml | 11 +- .../protect/card_locker/DatabaseTest.java | 161 ++++++++- .../card_locker/ImportExportActivityTest.java | 2 - .../protect/card_locker/ImportExportTest.java | 143 +++++++- .../protect/card_locker/MainActivityTest.java | 193 ++++++++++- 31 files changed, 1682 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/Group.java create mode 100644 app/src/main/java/protect/card_locker/GroupCursorAdapter.java create mode 100644 app/src/main/java/protect/card_locker/ManageGroupsActivity.java create mode 100644 app/src/main/res/drawable-hdpi/ic_folder_white.png create mode 100644 app/src/main/res/drawable-mdpi/ic_folder_white.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_folder_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_folder_white.png create mode 100644 app/src/main/res/layout/group_layout.xml create mode 100644 app/src/main/res/layout/group_main.xml create mode 100644 app/src/main/res/layout/layout_chip_choice.xml create mode 100644 app/src/main/res/layout/manage_groups_activity.xml diff --git a/app/build.gradle b/app/build.gradle index 73e10bf37..ba2f7c362 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,7 +45,7 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'androidx.appcompat:appcompat:1.2.0' - compile 'com.google.android.material:material:1.2.0-alpha03' + compile 'com.google.android.material:material:1.2.1' compile 'androidx.legacy:legacy-support-v4:1.0.0' compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' compile 'com.google.zxing:core:3.3.0' @@ -58,9 +58,6 @@ dependencies { implementation 'androidx.cardview:cardview:1.0.0' testCompile 'junit:junit:4.12' testCompile "org.robolectric:robolectric:4.0.2" - - - } task findbugs(type: FindBugs, dependsOn: 'assembleDebug') { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 54d70c2d6..82a908124 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,11 @@ + + cardGroups = helper.getLoyaltyCardGroups(cardId); + cardGroups.add(helper.getGroup(groupId)); + helper.setLoyaltyCardGroups(database, cardId, cardGroups); + } +} \ 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 2bf42a606..66acad49d 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -1,18 +1,26 @@ package protect.card_locker; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import java.util.ArrayList; +import java.util.List; + public class DBHelper extends SQLiteOpenHelper { public static final String DATABASE_NAME = "Catima.db"; public static final int ORIGINAL_DATABASE_VERSION = 1; public static final int DATABASE_VERSION = 4; + static class LoyaltyCardDbGroups + { + public static final String TABLE = "groups"; + public static final String ID = "_id"; + } + static class LoyaltyCardDbIds { public static final String TABLE = "cards"; @@ -26,6 +34,13 @@ public class DBHelper extends SQLiteOpenHelper public static final String STAR_STATUS = "starstatus"; } + static class LoyaltyCardDbIdsGroups + { + public static final String TABLE = "cardsGroups"; + public static final String cardID = "cardId"; + public static final String groupID = "groupId"; + } + public DBHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -34,7 +49,11 @@ public class DBHelper extends SQLiteOpenHelper @Override public void onCreate(SQLiteDatabase db) { - // create table for gift cards + // create table for card groups + db.execSQL("create table " + LoyaltyCardDbGroups.TABLE + "(" + + LoyaltyCardDbGroups.ID + " TEXT primary key not null)"); + + // create table for cards db.execSQL("create table " + LoyaltyCardDbIds.TABLE + "(" + LoyaltyCardDbIds.ID + " INTEGER primary key autoincrement," + LoyaltyCardDbIds.STORE + " TEXT not null," + @@ -44,6 +63,12 @@ public class DBHelper extends SQLiteOpenHelper LoyaltyCardDbIds.CARD_ID + " TEXT not null," + LoyaltyCardDbIds.BARCODE_TYPE + " TEXT not null," + LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0' )"); + + // create associative table for cards in groups + db.execSQL("create table " + LoyaltyCardDbIdsGroups.TABLE + "(" + + LoyaltyCardDbIdsGroups.cardID + " INTEGER," + + LoyaltyCardDbIdsGroups.groupID + " TEXT," + + "primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID +"))"); } @Override @@ -64,12 +89,24 @@ public class DBHelper extends SQLiteOpenHelper db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.HEADER_TEXT_COLOR + " INTEGER"); } + // Upgrade from version 3 to version 4 if(oldVersion < 4 && newVersion >= 4) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'"); + } + // Upgrade from version 4 to version 5 + if(oldVersion < 5 && newVersion >= 5) + { + db.execSQL("create table " + LoyaltyCardDbGroups.TABLE + "(" + + LoyaltyCardDbGroups.ID + " TEXT primary key not null)"); + + db.execSQL("create table " + LoyaltyCardDbIdsGroups.TABLE + "(" + + LoyaltyCardDbIdsGroups.cardID + " INTEGER," + + LoyaltyCardDbIdsGroups.groupID + " TEXT," + + "primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID +"))"); } } @@ -90,8 +127,8 @@ public class DBHelper extends SQLiteOpenHelper return newId; } - public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id, - final String store, final String note, final String cardId, + public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id, final String store, + final String note, final String cardId, final String barcodeType, final Integer headerColor, final Integer headerTextColor, final int starStatus) { @@ -108,7 +145,6 @@ public class DBHelper extends SQLiteOpenHelper return (newId != -1); } - public boolean updateLoyaltyCard(final int id, final String store, final String note, final String cardId, final String barcodeType, final Integer headerColor, final Integer headerTextColor) @@ -149,6 +185,7 @@ public class DBHelper extends SQLiteOpenHelper if(data.getCount() == 1) { data.moveToFirst(); + card = LoyaltyCard.toLoyaltyCard(data); } @@ -157,12 +194,76 @@ public class DBHelper extends SQLiteOpenHelper return card; } + public List getLoyaltyCardGroups(final int id) + { + SQLiteDatabase db = getReadableDatabase(); + Cursor data = db.rawQuery("select * from " + LoyaltyCardDbGroups.TABLE + " g " + + " LEFT JOIN " + LoyaltyCardDbIdsGroups.TABLE + " ig ON ig." + LoyaltyCardDbIdsGroups.groupID + " = g." + LoyaltyCardDbGroups.ID + + " where " + LoyaltyCardDbIdsGroups.cardID + "=?" + + " ORDER BY " + LoyaltyCardDbIdsGroups.groupID, new String[]{String.format("%d", id)}); + + List groups = new ArrayList<>(); + + if (!data.moveToFirst()) { + return groups; + } + + groups.add(Group.toGroup(data)); + + while (data.moveToNext()) { + groups.add(Group.toGroup(data)); + } + + return groups; + } + + public void setLoyaltyCardGroups(final int id, List groups) + { + SQLiteDatabase db = getWritableDatabase(); + + // First delete lookup table entries associated with this card + db.delete(LoyaltyCardDbIdsGroups.TABLE, + LoyaltyCardDbIdsGroups.cardID + " = ? ", + new String[]{String.format("%d", id)}); + + // Then create entries for selected values + for (Group group : groups) { + ContentValues contentValues = new ContentValues(); + contentValues.put(LoyaltyCardDbIdsGroups.cardID, id); + contentValues.put(LoyaltyCardDbIdsGroups.groupID, group._id); + db.insert(LoyaltyCardDbIdsGroups.TABLE, null, contentValues); + } + } + + public void setLoyaltyCardGroups(final SQLiteDatabase db, final int id, List groups) + { + // First delete lookup table entries associated with this card + db.delete(LoyaltyCardDbIdsGroups.TABLE, + LoyaltyCardDbIdsGroups.cardID + " = ? ", + new String[]{String.format("%d", id)}); + + // Then create entries for selected values + for (Group group : groups) { + ContentValues contentValues = new ContentValues(); + contentValues.put(LoyaltyCardDbIdsGroups.cardID, id); + contentValues.put(LoyaltyCardDbIdsGroups.groupID, group._id); + db.insert(LoyaltyCardDbIdsGroups.TABLE, null, contentValues); + } + } + public boolean deleteLoyaltyCard (final int id) { SQLiteDatabase db = getWritableDatabase(); - int rowsDeleted = db.delete(LoyaltyCardDbIds.TABLE, + // Delete card + int rowsDeleted = db.delete(LoyaltyCardDbIds.TABLE, LoyaltyCardDbIds.ID + " = ? ", new String[]{String.format("%d", id)}); + + // And delete lookup table entries associated with this card + db.delete(LoyaltyCardDbIdsGroups.TABLE, + LoyaltyCardDbIdsGroups.cardID + " = ? ", + new String[]{String.format("%d", id)}); + return (rowsDeleted == 1); } @@ -179,16 +280,52 @@ public class DBHelper extends SQLiteOpenHelper * @return Cursor */ public Cursor getLoyaltyCardCursor(final String filter) + { + return getLoyaltyCardCursor(filter, null); + } + + /** + * Returns a cursor to all loyalty cards with the filter text in either the store or note in a certain group. + * + * @param filter + * @param group + * @return Cursor + */ + public Cursor getLoyaltyCardCursor(final String filter, Group group) { String actualFilter = String.format("%%%s%%", filter); String[] selectionArgs = { actualFilter, actualFilter }; + StringBuilder groupFilter = new StringBuilder(); + String limitString = ""; SQLiteDatabase db = getReadableDatabase(); + if (group != null) { + List allowedIds = getGroupCardIds(group._id); + + // Empty group + if (allowedIds.size() > 0) { + groupFilter.append("AND ("); + + for (int i = 0; i < allowedIds.size(); i++) { + groupFilter.append(LoyaltyCardDbIds.ID + " = " + allowedIds.get(i)); + if (i != allowedIds.size() - 1) { + groupFilter.append(" OR "); + } + } + groupFilter.append(") "); + } else { + limitString = "LIMIT 0"; + } + } + Cursor res = db.rawQuery("select * from " + LoyaltyCardDbIds.TABLE + - " WHERE " + LoyaltyCardDbIds.STORE + " LIKE ? " + - " OR " + LoyaltyCardDbIds.NOTE + " LIKE ? " + - " ORDER BY " + LoyaltyCardDbIds.STAR_STATUS + " DESC," + LoyaltyCardDbIds.STORE + " COLLATE NOCASE ASC", selectionArgs, null); + " WHERE (" + LoyaltyCardDbIds.STORE + " LIKE ? " + + " OR " + LoyaltyCardDbIds.NOTE + " LIKE ? )" + + groupFilter.toString() + + " ORDER BY " + LoyaltyCardDbIds.STAR_STATUS + " DESC," + LoyaltyCardDbIds.STORE + " COLLATE NOCASE ASC " + + limitString, selectionArgs, null); + return res; } @@ -227,5 +364,172 @@ public class DBHelper extends SQLiteOpenHelper return numItems; } + + /** + * Returns a cursor to all groups. + * + * @return Cursor + */ + public Cursor getGroupCursor() + { + SQLiteDatabase db = getReadableDatabase(); + + Cursor res = db.rawQuery("select * from " + LoyaltyCardDbGroups.TABLE + + " ORDER BY " + LoyaltyCardDbGroups.ID + " COLLATE NOCASE ASC", null, null); + return res; + } + + public List getGroups() { + Cursor data = getGroupCursor(); + + List groups = new ArrayList<>(); + + if (!data.moveToFirst()) { + return groups; + } + + groups.add(Group.toGroup(data)); + + while (data.moveToNext()) { + groups.add(Group.toGroup(data)); + } + + return groups; + } + + public Group getGroup(final String groupName) + { + SQLiteDatabase db = getReadableDatabase(); + Cursor data = db.rawQuery("select * from " + LoyaltyCardDbGroups.TABLE + + " where " + LoyaltyCardDbGroups.ID + "=?", new String[]{groupName}); + + Group group = null; + + if(data.getCount() == 1) + { + data.moveToFirst(); + + group = Group.toGroup(data); + } + + data.close(); + + return group; + } + + public int getGroupCount() + { + SQLiteDatabase db = getReadableDatabase(); + Cursor data = db.rawQuery("SELECT Count(*) FROM " + LoyaltyCardDbGroups.TABLE, null); + + int numItems = 0; + + if(data.getCount() == 1) + { + data.moveToFirst(); + numItems = data.getInt(0); + } + + data.close(); + + return numItems; + } + + public List getGroupCardIds(final String groupName) + { + SQLiteDatabase db = getReadableDatabase(); + Cursor data = db.rawQuery("SELECT " + LoyaltyCardDbIdsGroups.cardID + + " FROM " + LoyaltyCardDbIdsGroups.TABLE + + " WHERE " + LoyaltyCardDbIdsGroups.groupID + " =? ", new String[]{groupName}); + + List cardIds = new ArrayList<>(); + + if (!data.moveToFirst()) { + return cardIds; + } + + cardIds.add(data.getInt(0)); + + while (data.moveToNext()) { + cardIds.add(data.getInt(0)); + } + + data.close(); + + return cardIds; + } + + public long insertGroup(final String name) + { + if (name.isEmpty()) return -1; + + SQLiteDatabase db = getWritableDatabase(); + ContentValues contentValues = new ContentValues(); + contentValues.put(LoyaltyCardDbGroups.ID, name); + final long newId = db.insert(LoyaltyCardDbGroups.TABLE, null, contentValues); + return newId; + } + + public boolean insertGroup(final SQLiteDatabase db, final String name) + { + ContentValues contentValues = new ContentValues(); + contentValues.put(LoyaltyCardDbGroups.ID, name); + final long newId = db.insert(LoyaltyCardDbGroups.TABLE, null, contentValues); + return (newId != -1); + } + + public boolean updateGroup(final String groupName, final String newName) + { + if (newName.isEmpty()) return false; + + SQLiteDatabase db = getWritableDatabase(); + ContentValues contentValues = new ContentValues(); + contentValues.put(LoyaltyCardDbGroups.ID, newName); + try { + int rowsUpdated = db.update(LoyaltyCardDbGroups.TABLE, contentValues, + LoyaltyCardDbGroups.ID + "=?", + new String[]{groupName}); + return (rowsUpdated == 1); + } catch (android.database.sqlite.SQLiteConstraintException _e) { + return false; + } + } + + public boolean deleteGroup(final String groupName) + { + SQLiteDatabase db = getWritableDatabase(); + // Delete group + int rowsDeleted = db.delete(LoyaltyCardDbGroups.TABLE, + LoyaltyCardDbGroups.ID + " = ? ", + new String[]{groupName}); + + // And delete lookup table entries associated with this group + db.delete(LoyaltyCardDbIdsGroups.TABLE, + LoyaltyCardDbIdsGroups.groupID + " = ? ", + new String[]{groupName}); + + return (rowsDeleted == 1); + } + + public int getGroupCardCount(final String groupName) + { + SQLiteDatabase db = getReadableDatabase(); + + Cursor data = db.rawQuery("SELECT Count(*) FROM " + LoyaltyCardDbIdsGroups.TABLE + + " where " + LoyaltyCardDbIdsGroups.groupID + "=?", + new String[]{groupName}); + + int numItems = 0; + + if(data.getCount() == 1) + { + data.moveToFirst(); + numItems = data.getInt(0); + } + + data.close(); + + return numItems; + } } diff --git a/app/src/main/java/protect/card_locker/Group.java b/app/src/main/java/protect/card_locker/Group.java new file mode 100644 index 000000000..9846abe73 --- /dev/null +++ b/app/src/main/java/protect/card_locker/Group.java @@ -0,0 +1,20 @@ +package protect.card_locker; + +import android.database.Cursor; + +public class Group +{ + public final String _id; + + public Group(final String _id) + { + this._id = _id; + } + + public static Group toGroup(Cursor cursor) + { + String _id = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbGroups.ID)); + + return new Group(_id); + } +} diff --git a/app/src/main/java/protect/card_locker/GroupCursorAdapter.java b/app/src/main/java/protect/card_locker/GroupCursorAdapter.java new file mode 100644 index 000000000..e06ea7ad6 --- /dev/null +++ b/app/src/main/java/protect/card_locker/GroupCursorAdapter.java @@ -0,0 +1,53 @@ +package protect.card_locker; + +import android.content.Context; +import android.database.Cursor; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.TextView; + +import protect.card_locker.preferences.Settings; + +class GroupCursorAdapter extends CursorAdapter +{ + Settings settings; + DBHelper db; + + public GroupCursorAdapter(Context context, Cursor cursor) + { + super(context, cursor, 0); + settings = new Settings(context); + + db = new DBHelper(context); + } + + // 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) + { + return LayoutInflater.from(context).inflate(R.layout.group_layout, parent, false); + } + + // 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) + { + // Find fields to populate in inflated template + TextView nameField = (TextView) view.findViewById(R.id.name); + TextView countField = (TextView) view.findViewById(R.id.cardCount); + + // Extract properties from cursor + Group group = Group.toGroup(cursor); + + // Populate fields with extracted properties + nameField.setText(group._id); + countField.setText(String.format(context.getString(R.string.groupCardCount), db.getGroupCardCount(group._id))); + + nameField.setTextSize(settings.getCardTitleListFontSize()); + countField.setTextSize(settings.getCardNoteListFontSize()); + } +} diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index 6c21b3a38..67cb4de64 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -1,6 +1,7 @@ package protect.card_locker; import android.database.Cursor; + import androidx.annotation.Nullable; public class LoyaltyCard @@ -20,7 +21,8 @@ public class LoyaltyCard public final int starStatus; public LoyaltyCard(final int id, final String store, final String note, final String cardId, - final String barcodeType, final Integer headerColor, final Integer headerTextColor,final int starStatus) + final String barcodeType, final Integer headerColor, final Integer headerTextColor, + final int starStatus) { this.id = id; this.store = store; diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 3a36ee7ec..8af102bf8 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -9,6 +9,8 @@ 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; @@ -33,6 +35,8 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialog; import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; import java.io.InvalidObjectException; +import java.util.ArrayList; +import java.util.List; public class LoyaltyCardEditActivity extends AppCompatActivity { @@ -43,6 +47,7 @@ public class LoyaltyCardEditActivity extends AppCompatActivity EditText storeFieldEdit; EditText noteFieldEdit; + ChipGroup groupsChips; ImageView headingColorSample; Button headingColorSelectButton; ImageView headingStoreTextColorSample; @@ -99,6 +104,7 @@ public class LoyaltyCardEditActivity extends AppCompatActivity storeFieldEdit = findViewById(R.id.storeNameEdit); noteFieldEdit = findViewById(R.id.noteEdit); + groupsChips = findViewById(R.id.groupChips); headingColorSample = findViewById(R.id.headingColorSample); headingColorSelectButton = findViewById(R.id.headingColorSelectButton); headingStoreTextColorSample = findViewById(R.id.headingStoreTextColorSample); @@ -213,6 +219,40 @@ public class LoyaltyCardEditActivity extends AppCompatActivity hideBarcode(); } + if(groupsChips.getChildCount() == 0) + { + List existingGroups = db.getGroups(); + + List loyaltyCardGroups = db.getLoyaltyCardGroups(loyaltyCardId); + + View groupsView = findViewById(R.id.groupsView); + View groupsTableRow = findViewById(R.id.groupsTableRow); + + if (existingGroups.isEmpty()) { + groupsView.setVisibility(View.GONE); + groupsTableRow.setVisibility(View.GONE); + } else { + groupsView.setVisibility(View.VISIBLE); + groupsTableRow.setVisibility(View.VISIBLE); + } + + for (Group group : db.getGroups()) { + Chip chip = (Chip) getLayoutInflater().inflate(R.layout.layout_chip_choice, groupsChips, false); + chip.setText(group._id); + chip.setTag(group); + + chip.setChecked(false); + for (Group loyaltyCardGroup : loyaltyCardGroups) { + if (loyaltyCardGroup._id.equals(group._id)) { + chip.setChecked(true); + break; + } + } + + groupsChips.addView(chip); + } + } + if(headingColorValue == null) { // Select a random color to start out with. @@ -404,6 +444,13 @@ public class LoyaltyCardEditActivity extends AppCompatActivity return; } + List selectedGroups = new ArrayList<>(); + + for (Integer chipId : groupsChips.getCheckedChipIds()) { + Chip chip = groupsChips.findViewById(chipId); + selectedGroups.add((Group) chip.getTag()); + } + if(updateLoyaltyCard) { //update of "starStatus" not necessary, since it cannot be changed in this activity (only in ViewActivity) db.updateLoyaltyCard(loyaltyCardId, store, note, cardId, barcodeType, headingColorValue, headingStoreTextColorValue); @@ -414,6 +461,8 @@ public class LoyaltyCardEditActivity extends AppCompatActivity loyaltyCardId = (int)db.insertLoyaltyCard(store, note, cardId, barcodeType, headingColorValue, headingStoreTextColorValue, 0); } + db.setLoyaltyCardGroups(loyaltyCardId, selectedGroups); + finish(); } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index b7c213924..7faada421 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -1,6 +1,5 @@ package protect.card_locker; - import android.content.Intent; import android.content.pm.ActivityInfo; import android.graphics.Color; @@ -34,7 +33,6 @@ import com.google.zxing.BarcodeFormat; import protect.card_locker.preferences.Settings; - public class LoyaltyCardViewActivity extends AppCompatActivity { private static final String TAG = "Catima"; diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index 8dee2a883..64c45ae38 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -11,6 +11,10 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; import android.os.Bundle; +import androidx.appcompat.app.AlertDialog; +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.Menu; @@ -22,13 +26,12 @@ import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.Toolbar; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.tabs.TabLayout; import com.google.common.collect.ImmutableMap; + import java.util.Calendar; +import java.util.List; import java.util.Map; import protect.card_locker.preferences.SettingsActivity; @@ -39,6 +42,7 @@ public class MainActivity extends AppCompatActivity private Menu menu; protected String filter = ""; + protected int selectedTab = 0; @Override protected void onCreate(Bundle savedInstanceState) @@ -48,7 +52,26 @@ public class MainActivity extends AppCompatActivity Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - updateLoyaltyCardList(""); + updateLoyaltyCardList(filter, 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()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + + } + }); } @Override @@ -64,7 +87,22 @@ public class MainActivity extends AppCompatActivity } } - updateLoyaltyCardList(filter); + TabLayout groupsTabLayout = findViewById(R.id.groups); + boolean hasReset = updateTabGroups(groupsTabLayout); + + Object group = null; + + if (groupsTabLayout.getTabCount() != 0) { + TabLayout.Tab tab = groupsTabLayout.getTabAt(0); + + if (!hasReset) { + tab = groupsTabLayout.getTabAt(selectedTab); + } + + groupsTabLayout.selectTab(tab); + group = tab.getTag(); + } + updateLoyaltyCardList(filter, group); FloatingActionButton addButton = findViewById(R.id.fabAdd); addButton.setOnClickListener(new View.OnClickListener() { @@ -109,17 +147,31 @@ public class MainActivity extends AppCompatActivity if (!searchView.isIconified()) { searchView.setIconified(true); } else { - super.onBackPressed(); + TabLayout groupsTabLayout = findViewById(R.id.groups); + + if (groupsTabLayout.getVisibility() == View.VISIBLE && selectedTab != 0) { + selectedTab = 0; + groupsTabLayout.selectTab(groupsTabLayout.getTabAt(0)); + } else { + super.onBackPressed(); + } } } - private void updateLoyaltyCardList(String filterText) + private void updateLoyaltyCardList(String filterText, Object tag) { + Group group = null; + if (tag != null) { + group = (Group) tag; + } + final ListView cardList = findViewById(R.id.list); 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); + if(db.getLoyaltyCardCount() > 0) { // We want the cardList to be visible regardless of the filtered match count @@ -127,7 +179,7 @@ public class MainActivity extends AppCompatActivity // the keyboard cardList.setVisibility(View.VISIBLE); helpText.setVisibility(View.GONE); - if(db.getLoyaltyCardCount(filterText) > 0) + if(cardCursor.getCount() > 0) { noMatchingCardsText.setVisibility(View.GONE); } @@ -143,8 +195,6 @@ public class MainActivity extends AppCompatActivity noMatchingCardsText.setVisibility(View.GONE); } - Cursor cardCursor = db.getLoyaltyCardCursor(filterText); - final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor); cardList.setAdapter(adapter); @@ -171,6 +221,53 @@ public class MainActivity extends AppCompatActivity }); } + public boolean updateTabGroups(TabLayout groupsTabLayout) + { + final DBHelper db = new DBHelper(this); + + List newGroups = db.getGroups(); + + if (newGroups.size() == 0) { + groupsTabLayout.removeAllTabs(); + groupsTabLayout.setVisibility(View.GONE); + return true; + } + + // -1 because there is an "All" tab + boolean isChanged = groupsTabLayout.getTabCount() - 1 != newGroups.size(); + + if (!isChanged) { + for (int i = 0; i < newGroups.size(); i++) { + if (!((Group) groupsTabLayout.getTabAt(i + 1).getTag())._id.equals(newGroups.get(i)._id)) { + isChanged = true; + break; + } + } + } + + if (isChanged) { + groupsTabLayout.removeAllTabs(); + + TabLayout.Tab allTab = groupsTabLayout.newTab(); + allTab.setText(R.string.all); + allTab.setTag(null); + groupsTabLayout.addTab(allTab); + + for (Group group : newGroups) { + TabLayout.Tab tab = groupsTabLayout.newTab(); + tab.setText(group._id); + tab.setTag(group); + groupsTabLayout.addTab(tab); + } + + groupsTabLayout.setVisibility(View.VISIBLE); + + return true; + } + + return false; + } + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { @@ -240,7 +337,10 @@ public class MainActivity extends AppCompatActivity @Override public boolean onQueryTextChange(String newText) { filter = newText; - updateLoyaltyCardList(newText); + + TabLayout groupsTabLayout = findViewById(R.id.groups); + + updateLoyaltyCardList(newText, groupsTabLayout.getTabAt(groupsTabLayout.getSelectedTabPosition()).getTag()); return true; } }); @@ -254,6 +354,13 @@ public class MainActivity extends AppCompatActivity { int id = item.getItemId(); + if (id == R.id.action_manage_groups) + { + Intent i = new Intent(getApplicationContext(), ManageGroupsActivity.class); + startActivityForResult(i, MAIN_REQUEST_CODE); + return true; + } + if(id == R.id.action_import_export) { Intent i = new Intent(getApplicationContext(), ImportExportActivity.class); diff --git a/app/src/main/java/protect/card_locker/ManageGroupsActivity.java b/app/src/main/java/protect/card_locker/ManageGroupsActivity.java new file mode 100644 index 000000000..1b83723ba --- /dev/null +++ b/app/src/main/java/protect/card_locker/ManageGroupsActivity.java @@ -0,0 +1,186 @@ +package protect.card_locker; + +import android.content.DialogInterface; +import android.database.Cursor; +import android.os.Bundle; +import android.text.InputType; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +public class ManageGroupsActivity extends AppCompatActivity +{ + private static final String TAG = "Catima"; + + private AlertDialog newGroupDialog; + private final DBHelper db = new DBHelper(this); + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.manage_groups_activity); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + if(actionBar != null) + { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + newGroupDialog = createNewGroupDialog(); + + updateGroupList(); + } + + @Override + protected void onResume() { + super.onResume(); + + updateGroupList(); + + FloatingActionButton addButton = findViewById(R.id.fabAdd); + addButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + newGroupDialog.show(); + } + }); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + } + + private void updateGroupList() + { + final ListView groupList = findViewById(R.id.list); + final TextView helpText = findViewById(R.id.helpText); + final DBHelper db = new DBHelper(this); + + if(db.getGroupCount() > 0) + { + groupList.setVisibility(View.VISIBLE); + helpText.setVisibility(View.GONE); + } + else + { + groupList.setVisibility(View.GONE); + helpText.setVisibility(View.VISIBLE); + } + + Cursor groupCursor = db.getGroupCursor(); + + final GroupCursorAdapter adapter = new GroupCursorAdapter(this, groupCursor); + groupList.setAdapter(adapter); + + registerForContextMenu(groupList); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + int id = item.getItemId(); + + if (id == android.R.id.home) { + finish(); + } + + return super.onOptionsItemSelected(item); + } + + public void editGroup(View view) { + LinearLayout parentRow = (LinearLayout) view.getParent(); + + TextView groupNameTextView = (TextView) parentRow.findViewById(R.id.name); + + final String groupName = (String) groupNameTextView.getText(); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.enter_group_name); + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setText(groupName); + builder.setView(input); + + builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + db.updateGroup(groupName, input.getText().toString()); + updateGroupList(); + } + }); + builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + public void deleteGroup(View view) { + LinearLayout parentRow = (LinearLayout) view.getParent(); + + TextView groupNameTextView = (TextView) parentRow.findViewById(R.id.name); + + final String groupName = (String) groupNameTextView.getText(); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.deleteConfirmationGroup); + builder.setMessage(groupName); + + builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + db.deleteGroup(groupName); + updateGroupList(); + } + }); + builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + private AlertDialog createNewGroupDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.enter_group_name); + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT); + builder.setView(input); + + builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + db.insertGroup(input.getText().toString()); + updateGroupList(); + } + }); + builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + AlertDialog dialog = builder.create(); + + return dialog; + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_folder_white.png b/app/src/main/res/drawable-hdpi/ic_folder_white.png new file mode 100644 index 0000000000000000000000000000000000000000..a667cd078f903d1ba45e1851cc209568d33b2ded GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBvOHZJLn>~)y>^hdL4n6DaoJm` zH!k7@3}ve-3_dOsh@Sl;?$iDK0(`oy9Zkl@lG(|B4`#6}^IuohRKobAJ?hAscR$1? zahCXPYK$@qU}xbFP;h8q_$#@kmf_O&{+j*WADb?9Oy7A_V4|~iOV literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_folder_white.png b/app/src/main/res/drawable-mdpi/ic_folder_white.png new file mode 100644 index 0000000000000000000000000000000000000000..56b8a6895c19a2b7475a6e626c4eca396f42efd9 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gjex5FlAr-fh6C_xh+5Y_he_x`* zobjWi%#L|M+-8pr=M;H&7|e;_;bKjWielK-a!qWGy@dsJm? i%5yyGMsE`%Gs8~F443szbv8hg89ZJ6T-G@yGywodn=2Cl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_folder_white.png b/app/src/main/res/drawable-xhdpi/ic_folder_white.png new file mode 100644 index 0000000000000000000000000000000000000000..12f88a2b324ac3533789f07d42ff992c0c21444c GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtX`U{QAr-gY-Z;qHpupo0C|1y@ zx0?Bdkp%_!*OSJ$1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_folder_white.png b/app/src/main/res/drawable-xxhdpi/ic_folder_white.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb3a8df9314b99b813dd57d11ab51340863ba71 GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!hdo^!Ln>~)y?vMSu!2ZKp!kWj z!e!?!GQKjoc{1unvZA8OwfUb9)=qCedD)c3bCQZ@(B!9ct{bg4y50I&f-!9;W17s8 z*@eH{wAlMa3x2t3wbzBQyy9PJHPQ3Ke^nv{RXek~wHi&cfGFBJG VH7~gF_7%`O44$rjF6*2Ung9w{TMqyL literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/group_layout.xml b/app/src/main/res/layout/group_layout.xml new file mode 100644 index 000000000..787cd38d4 --- /dev/null +++ b/app/src/main/res/layout/group_layout.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/group_main.xml b/app/src/main/res/layout/group_main.xml new file mode 100644 index 000000000..11afef1b6 --- /dev/null +++ b/app/src/main/res/layout/group_main.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/layout/layout_chip_choice.xml b/app/src/main/res/layout/layout_chip_choice.xml new file mode 100644 index 000000000..fe580c27d --- /dev/null +++ b/app/src/main/res/layout/layout_chip_choice.xml @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/app/src/main/res/layout/loyalty_card_edit_activity.xml b/app/src/main/res/layout/loyalty_card_edit_activity.xml index 3093f189c..7ace87adb 100644 --- a/app/src/main/res/layout/loyalty_card_edit_activity.xml +++ b/app/src/main/res/layout/loyalty_card_edit_activity.xml @@ -145,6 +145,62 @@ android:background="@color/inputBorder" /> + + + + + + + + + + + + + + + +