Skip to content

Commit bf3fdeb

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

File tree

10 files changed

+593
-43
lines changed

10 files changed

+593
-43
lines changed

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

app/src/main/java/protect/card_locker/preferences/Settings.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ public boolean getDisableLockscreenWhileViewingCard() {
7979
return getBoolean(R.string.settings_key_disable_lockscreen_while_viewing_card, true);
8080
}
8181

82+
public boolean getAllowContentProviderRead() {
83+
return getBoolean(R.string.settings_key_allow_content_provider_read, true);
84+
}
85+
8286
public boolean getOledDark() {
8387
return getBoolean(R.string.settings_key_oled_dark, false);
8488
}

app/src/main/java/protect/card_locker/preferences/SettingsActivity.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import android.app.Activity;
55
import android.content.Intent;
6+
import android.os.Build;
67
import android.os.Bundle;
78
import android.view.MenuItem;
89

@@ -150,6 +151,12 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
150151
colorPreference.setEntryValues(R.array.color_values_no_dynamic);
151152
colorPreference.setEntries(R.array.color_value_strings_no_dynamic);
152153
}
154+
155+
// Disable content provider on SDK < 23 since dangerous permissions
156+
// are granted at install-time
157+
Preference contentProviderReadPreference = findPreference(getResources().getString(R.string.settings_key_allow_content_provider_read));
158+
assert contentProviderReadPreference != null;
159+
contentProviderReadPreference.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M);
153160
}
154161

155162
private void refreshActivity(boolean reloadMain) {

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

Lines changed: 5 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>
@@ -115,7 +117,10 @@
115117
<string name="settings_keep_screen_on">Keep screen on</string>
116118
<string name="settings_key_keep_screen_on" translatable="false">pref_keep_screen_on</string>
117119
<string name="settings_disable_lockscreen_while_viewing_card">Prevent screen lock</string>
120+
<string name="settings_allow_content_provider_read_title">Allow other apps to access my data</string>
121+
<string name="settings_allow_content_provider_read_summary">Apps will still have to request permissions to be granted access</string>
118122
<string name="settings_key_disable_lockscreen_while_viewing_card" translatable="false">pref_disable_lockscreen_while_viewing_card</string>
123+
<string name="settings_key_allow_content_provider_read" translatable="false">pref_allow_content_provider_read</string>
119124
<string name="settings_key_oled_dark" translatable="false">pref_oled_dark</string>
120125
<string name="sharedpreference_active_tab" translatable="false">sharedpreference_active_tab</string>
121126
<string name="sharedpreference_privacy_policy_shown" translatable="false">sharedpreference_privacy_policy_shown</string>

app/src/main/res/xml/preferences.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@
7474
app:iconSpaceReserved="false"
7575
app:singleLineTitle="false" />
7676

77+
<SwitchPreferenceCompat
78+
android:widgetLayout="@layout/preference_material_switch"
79+
android:defaultValue="true"
80+
android:key="@string/settings_key_allow_content_provider_read"
81+
android:summary="@string/settings_allow_content_provider_read_summary"
82+
android:title="@string/settings_allow_content_provider_read_title"
83+
app:iconSpaceReserved="false"
84+
app:singleLineTitle="false" />
85+
7786
</PreferenceCategory>
7887

7988
</PreferenceScreen>

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

0 commit comments

Comments
 (0)