Skip to content

Commit fbcb47e

Browse files
committed
Introduce read-only ContentProvider for cards
1 parent bdab862 commit fbcb47e

7 files changed

Lines changed: 555 additions & 43 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:tools="http://schemas.android.com/tools">
44

5+
<permission
6+
android:description="@string/permissionReadCardsDescription"
7+
android:icon="@drawable/ic_launcher_foreground"
8+
android:label="@string/permissionReadCardsLabel"
9+
android:name="me.hackerchick.catima.READ_CARDS"
10+
android:protectionLevel="dangerous" />
11+
512
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
613

714
<uses-permission android:name="android.permission.CAMERA" />
@@ -155,6 +162,12 @@
155162
android:name=".UCropWrapper"
156163
android:theme="@style/AppTheme.NoActionBar" />
157164

165+
<provider
166+
android:name=".contentprovider.CardsContentProvider"
167+
android:authorities="${applicationId}.contentprovider.cards"
168+
android:exported="true"
169+
android:readPermission="me.hackerchick.catima.READ_CARDS"/>
170+
158171
<provider
159172
android:name="androidx.core.content.FileProvider"
160173
android:authorities="${applicationId}"
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package protect.card_locker.contentprovider;
2+
3+
import static protect.card_locker.DBHelper.LoyaltyCardDbIds;
4+
5+
import android.content.ContentProvider;
6+
import android.content.ContentValues;
7+
import android.content.UriMatcher;
8+
import android.database.Cursor;
9+
import android.database.MatrixCursor;
10+
import android.database.sqlite.SQLiteDatabase;
11+
import android.net.Uri;
12+
import android.util.Log;
13+
14+
import androidx.annotation.NonNull;
15+
import androidx.annotation.Nullable;
16+
17+
import java.util.Arrays;
18+
import java.util.HashSet;
19+
import java.util.Set;
20+
21+
import protect.card_locker.BuildConfig;
22+
import protect.card_locker.DBHelper;
23+
24+
public class CardsContentProvider extends ContentProvider {
25+
private static final String TAG = "Catima";
26+
27+
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".contentprovider.cards";
28+
29+
public static class Version {
30+
public static final String MAJOR_COLUMN = "major";
31+
public static final String MINOR_COLUMN = "minor";
32+
public static final int MAJOR = 1;
33+
public static final int MINOR = 0;
34+
}
35+
36+
private static final int URI_VERSION = 0;
37+
private static final int URI_CARDS = 1;
38+
private static final int URI_GROUPS = 2;
39+
private static final int URI_CARD_GROUPS = 3;
40+
41+
private static final String[] CARDS_DEFAULT_PROJECTION = new String[]{
42+
LoyaltyCardDbIds.ID,
43+
LoyaltyCardDbIds.STORE,
44+
LoyaltyCardDbIds.VALID_FROM,
45+
LoyaltyCardDbIds.EXPIRY,
46+
LoyaltyCardDbIds.BALANCE,
47+
LoyaltyCardDbIds.BALANCE_TYPE,
48+
LoyaltyCardDbIds.NOTE,
49+
LoyaltyCardDbIds.HEADER_COLOR,
50+
LoyaltyCardDbIds.CARD_ID,
51+
LoyaltyCardDbIds.BARCODE_ID,
52+
LoyaltyCardDbIds.BARCODE_TYPE,
53+
LoyaltyCardDbIds.STAR_STATUS,
54+
LoyaltyCardDbIds.LAST_USED,
55+
LoyaltyCardDbIds.ARCHIVE_STATUS,
56+
};
57+
58+
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH) {{
59+
addURI(AUTHORITY, "version", URI_VERSION);
60+
addURI(AUTHORITY, "cards", URI_CARDS);
61+
addURI(AUTHORITY, "groups", URI_GROUPS);
62+
addURI(AUTHORITY, "card_groups", URI_CARD_GROUPS);
63+
}};
64+
65+
@Override
66+
public boolean onCreate() {
67+
return true;
68+
}
69+
70+
@Nullable
71+
@Override
72+
public Cursor query(@NonNull final Uri uri,
73+
@Nullable final String[] projection,
74+
@Nullable final String selection,
75+
@Nullable final String[] selectionArgs,
76+
@Nullable final String sortOrder) {
77+
final String table;
78+
String[] updatedProjection = projection;
79+
80+
switch (uriMatcher.match(uri)) {
81+
case URI_VERSION:
82+
return queryVersion();
83+
case URI_CARDS:
84+
table = DBHelper.LoyaltyCardDbIds.TABLE;
85+
// Restrict columns to the default projection (omit internal columns such as zoom level)
86+
if (projection == null) {
87+
updatedProjection = CARDS_DEFAULT_PROJECTION;
88+
} else {
89+
final Set<String> defaultProjection = new HashSet<>(Arrays.asList(CARDS_DEFAULT_PROJECTION));
90+
updatedProjection = Arrays.stream(projection).filter(defaultProjection::contains).toArray(String[]::new);
91+
}
92+
break;
93+
case URI_GROUPS:
94+
table = DBHelper.LoyaltyCardDbGroups.TABLE;
95+
break;
96+
case URI_CARD_GROUPS:
97+
table = DBHelper.LoyaltyCardDbIdsGroups.TABLE;
98+
break;
99+
default:
100+
Log.w(TAG, "Unrecognized URI " + uri);
101+
return null;
102+
}
103+
104+
final DBHelper dbHelper = new DBHelper(getContext());
105+
final SQLiteDatabase database = dbHelper.getReadableDatabase();
106+
107+
return database.query(
108+
table,
109+
updatedProjection,
110+
selection,
111+
selectionArgs,
112+
null,
113+
null,
114+
sortOrder
115+
);
116+
}
117+
118+
private Cursor queryVersion() {
119+
final String[] columns = new String[]{Version.MAJOR_COLUMN, Version.MINOR_COLUMN};
120+
final MatrixCursor matrixCursor = new MatrixCursor(columns);
121+
matrixCursor.addRow(new Object[]{Version.MAJOR, Version.MINOR});
122+
123+
return matrixCursor;
124+
}
125+
126+
@Nullable
127+
@Override
128+
public String getType(@NonNull final Uri uri) {
129+
// MIME types are not relevant (for now at least)
130+
return null;
131+
}
132+
133+
@Nullable
134+
@Override
135+
public Uri insert(@NonNull final Uri uri,
136+
@Nullable final ContentValues values) {
137+
// This content provider is read-only for now, so we always return null
138+
return null;
139+
}
140+
141+
@Override
142+
public int delete(@NonNull final Uri uri,
143+
@Nullable final String selection,
144+
@Nullable final String[] selectionArgs) {
145+
// This content provider is read-only for now, so we always return 0
146+
return 0;
147+
}
148+
149+
@Override
150+
public int update(@NonNull final Uri uri,
151+
@Nullable final ContentValues values,
152+
@Nullable final String selection,
153+
@Nullable final String[] selectionArgs) {
154+
// This content provider is read-only for now, so we always return 0
155+
return 0;
156+
}
157+
}

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
<string name="exporting">Exporting…</string>
6868
<string name="storageReadPermissionRequired">Permission to read storage needed for this action…</string>
6969
<string name="cameraPermissionRequired">Permission to access camera needed for this action…</string>
70+
<string name="permissionReadCardsLabel">Read Catima Cards</string>
71+
<string name="permissionReadCardsDescription">Read your cards and all its details, including notes and images</string>
7072
<string name="cameraPermissionDeniedTitle">Could not access the camera</string>
7173
<string name="noCameraPermissionDirectToSystemSetting">To scan barcodes, Catima will need access to your camera. Tap here to change your permission settings.</string>
7274
<string name="exportOptionExplanation">The data will be written to a location of your choice.</string>

app/src/test/java/protect/card_locker/ImportExportTest.java

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,6 @@ public void setUp() {
6767
mDatabase = TestHelpers.getEmptyDb(activity).getWritableDatabase();
6868
}
6969

70-
/**
71-
* Add the given number of cards, each with
72-
* an index in the store name.
73-
*
74-
* @param cardsToAdd
75-
*/
76-
private void addLoyaltyCards(int cardsToAdd) {
77-
// Add in reverse order to test sorting
78-
for (int index = cardsToAdd; index > 0; index--) {
79-
String storeName = String.format("store, \"%4d", index);
80-
String note = String.format("note, \"%4d", index);
81-
long id = DBHelper.insertLoyaltyCard(mDatabase, storeName, note, null, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, null, BARCODE_TYPE, index, 0, null,0);
82-
boolean result = (id != -1);
83-
assertTrue(result);
84-
}
85-
86-
assertEquals(cardsToAdd, DBHelper.getLoyaltyCardCount(mDatabase));
87-
}
88-
8970
private void addLoyaltyCardsFiveStarred() {
9071
int cardsToAdd = 9;
9172
// Add in reverse order to test sorting
@@ -183,18 +164,6 @@ public void addLoyaltyCardsWithExpiryNeverPastTodayFuture() {
183164
assertEquals(4, DBHelper.getLoyaltyCardCount(mDatabase));
184165
}
185166

186-
private void addGroups(int groupsToAdd) {
187-
// Add in reverse order to test sorting
188-
for (int index = groupsToAdd; index > 0; index--) {
189-
String groupName = String.format("group, \"%4d", index);
190-
long id = DBHelper.insertGroup(mDatabase, groupName);
191-
boolean result = (id != -1);
192-
assertTrue(result);
193-
}
194-
195-
assertEquals(groupsToAdd, DBHelper.getGroupCount(mDatabase));
196-
}
197-
198167
/**
199168
* Check that all of the cards follow the pattern
200169
* specified in addLoyaltyCards(), and are in sequential order
@@ -285,7 +254,7 @@ private void checkLoyaltyCardsFiveStarred() {
285254

286255
/**
287256
* Check that all of the groups follow the pattern
288-
* specified in addGroups(), and are in sequential order
257+
* specified in {@link TestHelpers#addGroups}, and are in sequential order
289258
* where the smallest group's index is 1
290259
*/
291260
private void checkGroups() {
@@ -308,7 +277,7 @@ private void checkGroups() {
308277
public void multipleCardsExportImport() throws IOException {
309278
final int NUM_CARDS = 10;
310279

311-
addLoyaltyCards(NUM_CARDS);
280+
TestHelpers.addLoyaltyCards(mDatabase, NUM_CARDS);
312281

313282
ByteArrayOutputStream outData = new ByteArrayOutputStream();
314283
OutputStreamWriter outStream = new OutputStreamWriter(outData);
@@ -338,7 +307,7 @@ public void multipleCardsExportImportPasswordProtected() throws IOException {
338307
final int NUM_CARDS = 10;
339308
List<char[]> passwords = Arrays.asList(null, "123456789".toCharArray());
340309
for (char[] password : passwords) {
341-
addLoyaltyCards(NUM_CARDS);
310+
TestHelpers.addLoyaltyCards(mDatabase, NUM_CARDS);
342311

343312
ByteArrayOutputStream outData = new ByteArrayOutputStream();
344313
OutputStreamWriter outStream = new OutputStreamWriter(outData);
@@ -411,8 +380,8 @@ public void multipleCardsExportImportWithGroups() throws IOException {
411380
final int NUM_CARDS = 10;
412381
final int NUM_GROUPS = 3;
413382

414-
addLoyaltyCards(NUM_CARDS);
415-
addGroups(NUM_GROUPS);
383+
TestHelpers.addLoyaltyCards(mDatabase, NUM_CARDS);
384+
TestHelpers.addGroups(mDatabase, NUM_GROUPS);
416385

417386
List<Group> emptyGroup = new ArrayList<>();
418387

@@ -484,7 +453,7 @@ public void multipleCardsExportImportWithGroups() throws IOException {
484453
public void importExistingCardsNotReplace() throws IOException {
485454
final int NUM_CARDS = 10;
486455

487-
addLoyaltyCards(NUM_CARDS);
456+
TestHelpers.addLoyaltyCards(mDatabase, NUM_CARDS);
488457

489458
ByteArrayOutputStream outData = new ByteArrayOutputStream();
490459
OutputStreamWriter outStream = new OutputStreamWriter(outData);
@@ -513,7 +482,7 @@ public void corruptedImportNothingSaved() {
513482
final int NUM_CARDS = 10;
514483

515484
for (DataFormat format : DataFormat.values()) {
516-
addLoyaltyCards(NUM_CARDS);
485+
TestHelpers.addLoyaltyCards(mDatabase, NUM_CARDS);
517486

518487
ByteArrayOutputStream outData = new ByteArrayOutputStream();
519488
OutputStreamWriter outStream = new OutputStreamWriter(outData);
@@ -558,7 +527,7 @@ public void useImportExportTask() throws FileNotFoundException {
558527
final File sdcardDir = Environment.getExternalStorageDirectory();
559528
final File exportFile = new File(sdcardDir, "Catima.csv");
560529

561-
addLoyaltyCards(NUM_CARDS);
530+
TestHelpers.addLoyaltyCards(mDatabase, NUM_CARDS);
562531

563532
TestTaskCompleteListener listener = new TestTaskCompleteListener();
564533

app/src/test/java/protect/card_locker/TestHelpers.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package protect.card_locker;
22

3-
import android.app.Activity;
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import android.content.Context;
47
import android.database.Cursor;
58
import android.database.sqlite.SQLiteDatabase;
69

10+
import com.google.zxing.BarcodeFormat;
11+
712
import java.io.FileNotFoundException;
13+
import java.math.BigDecimal;
814

915
public class TestHelpers {
10-
static public DBHelper getEmptyDb(Activity activity) {
11-
DBHelper db = new DBHelper(activity);
16+
private static final String BARCODE_DATA = "428311627547";
17+
private static final CatimaBarcode BARCODE_TYPE = CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A);
18+
19+
public static DBHelper getEmptyDb(Context context) {
20+
DBHelper db = new DBHelper(context);
1221
SQLiteDatabase database = db.getWritableDatabase();
1322

1423
// Make sure no files remain
@@ -19,7 +28,7 @@ static public DBHelper getEmptyDb(Activity activity) {
1928

2029
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
2130
try {
22-
Utils.saveCardImage(activity.getApplicationContext(), null, cardID, imageLocationType);
31+
Utils.saveCardImage(context.getApplicationContext(), null, cardID, imageLocationType);
2332
} catch (FileNotFoundException ignored) {
2433
}
2534
}
@@ -34,4 +43,35 @@ static public DBHelper getEmptyDb(Activity activity) {
3443

3544
return db;
3645
}
46+
47+
/**
48+
* Add the given number of cards, each with an index in the store name.
49+
*
50+
* @param mDatabase
51+
* @param cardsToAdd
52+
*/
53+
public static void addLoyaltyCards(final SQLiteDatabase mDatabase, final int cardsToAdd) {
54+
// Add in reverse order to test sorting
55+
for (int index = cardsToAdd; index > 0; index--) {
56+
String storeName = String.format("store, \"%4d", index);
57+
String note = String.format("note, \"%4d", index);
58+
long id = DBHelper.insertLoyaltyCard(mDatabase, storeName, note, null, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, null, BARCODE_TYPE, index, 0, null,0);
59+
boolean result = (id != -1);
60+
assertTrue(result);
61+
}
62+
63+
assertEquals(cardsToAdd, DBHelper.getLoyaltyCardCount(mDatabase));
64+
}
65+
66+
public static void addGroups(final SQLiteDatabase mDatabase, int groupsToAdd) {
67+
// Add in reverse order to test sorting
68+
for (int index = groupsToAdd; index > 0; index--) {
69+
String groupName = String.format("group, \"%4d", index);
70+
long id = DBHelper.insertGroup(mDatabase, groupName);
71+
boolean result = (id != -1);
72+
assertTrue(result);
73+
}
74+
75+
assertEquals(groupsToAdd, DBHelper.getGroupCount(mDatabase));
76+
}
3777
}

0 commit comments

Comments
 (0)