strata-storage 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,10 +8,11 @@ import com.getcapacitor.PluginMethod;
8
8
  import com.getcapacitor.annotation.CapacitorPlugin;
9
9
  import com.strata.storage.SharedPreferencesStorage;
10
10
  import com.strata.storage.EncryptedStorage;
11
+ import com.strata.storage.FilesystemStorage;
11
12
  import com.strata.storage.SQLiteStorage;
12
13
  import android.util.Log;
13
14
  import org.json.JSONArray;
14
- import org.json.JSONException;
15
+ import java.util.HashMap;
15
16
  import java.util.List;
16
17
  import java.util.Map;
17
18
 
@@ -19,15 +20,24 @@ import java.util.Map;
19
20
  * Main Capacitor plugin for Strata Storage
20
21
  * Coordinates between different storage types on Android.
21
22
  *
22
- * Storage types with a real native backend: preferences, secure, sqlite.
23
- * `filesystem` is NOT implemented natively and is rejected explicitly rather
24
- * than silently falling through to SharedPreferences.
23
+ * Storage types with a real native backend: preferences, secure, sqlite,
24
+ * filesystem.
25
+ *
26
+ * SQLite supports multi-store: the `database` option selects the DB file and
27
+ * `table` selects the table within it. SQLite helper instances are cached per
28
+ * database name so the connection is not reopened per call.
25
29
  */
26
30
  @CapacitorPlugin(name = "StrataStorage")
27
31
  public class StrataStoragePlugin extends Plugin {
32
+ private static final String DEFAULT_DATABASE = "strata_storage";
33
+ private static final String DEFAULT_TABLE = "storage";
34
+
28
35
  private SharedPreferencesStorage sharedPrefsStorage;
29
36
  private EncryptedStorage encryptedStorage;
30
- private SQLiteStorage sqliteStorage;
37
+ private FilesystemStorage filesystemStorage;
38
+
39
+ /** SQLite helper cache keyed by resolved database name (without ".db"). */
40
+ private final Map<String, SQLiteStorage> sqliteStores = new HashMap<>();
31
41
 
32
42
  @Override
33
43
  public void load() {
@@ -44,18 +54,43 @@ public class StrataStoragePlugin extends Plugin {
44
54
  Log.e("StrataStorage", "Failed to initialize encrypted storage", e);
45
55
  }
46
56
  try {
47
- sqliteStorage = new SQLiteStorage(getContext());
57
+ filesystemStorage = new FilesystemStorage(getContext());
48
58
  } catch (Exception e) {
49
- Log.e("StrataStorage", "Failed to initialize SQLite storage", e);
59
+ Log.e("StrataStorage", "Failed to initialize filesystem storage", e);
50
60
  }
51
61
  }
52
62
 
53
- private void rejectUnsupportedFilesystem(PluginCall call) {
54
- call.reject(
55
- "Filesystem storage is not implemented in the native Android plugin. " +
56
- "Use the 'preferences', 'secure', or 'sqlite' storage types, or a " +
57
- "web/Capacitor Filesystem-backed adapter."
58
- );
63
+ /**
64
+ * Sanitize a database name to a safe file stem ({@code ^[A-Za-z0-9_]+$})
65
+ * and return the resolved stem. The ".db" suffix is appended when opening.
66
+ */
67
+ private static String sanitizeDatabase(String database) {
68
+ if (database == null || database.isEmpty()) {
69
+ return DEFAULT_DATABASE;
70
+ }
71
+ String cleaned = database.replaceAll("[^A-Za-z0-9_]", "");
72
+ return cleaned.isEmpty() ? DEFAULT_DATABASE : cleaned;
73
+ }
74
+
75
+ /**
76
+ * Return (creating + caching on first use) the SQLite helper for the given
77
+ * database name. The cache is keyed by the sanitized DB stem so the
78
+ * connection is reused across bridge calls. May return {@code null} if the
79
+ * helper cannot be constructed.
80
+ */
81
+ private synchronized SQLiteStorage getSqliteStore(String database) {
82
+ String dbStem = sanitizeDatabase(database);
83
+ SQLiteStorage store = sqliteStores.get(dbStem);
84
+ if (store == null) {
85
+ try {
86
+ store = new SQLiteStorage(getContext(), dbStem + ".db");
87
+ sqliteStores.put(dbStem, store);
88
+ } catch (Exception e) {
89
+ Log.e("StrataStorage", "Failed to open SQLite database " + dbStem, e);
90
+ return null;
91
+ }
92
+ }
93
+ return store;
59
94
  }
60
95
 
61
96
  /**
@@ -71,11 +106,10 @@ public class StrataStoragePlugin extends Plugin {
71
106
  available = encryptedStorage != null;
72
107
  break;
73
108
  case "sqlite":
74
- available = sqliteStorage != null;
109
+ available = getSqliteStore(call.getString("database", DEFAULT_DATABASE)) != null;
75
110
  break;
76
111
  case "filesystem":
77
- // No native filesystem backend.
78
- available = false;
112
+ available = filesystemStorage != null;
79
113
  break;
80
114
  case "preferences":
81
115
  default:
@@ -89,7 +123,10 @@ public class StrataStoragePlugin extends Plugin {
89
123
  }
90
124
 
91
125
  /**
92
- * Get value from storage
126
+ * Get value from storage.
127
+ *
128
+ * For sqlite/filesystem the resolved `value` is the full wrapper object
129
+ * (parsed back from JSON). A miss resolves `value` = null.
93
130
  */
94
131
  @PluginMethod
95
132
  public void get(PluginCall call) {
@@ -102,46 +139,70 @@ public class StrataStoragePlugin extends Plugin {
102
139
  String storage = call.getString("storage", "preferences");
103
140
 
104
141
  try {
105
- Object value = null;
106
-
107
142
  switch (storage) {
108
- case "secure":
143
+ case "secure": {
109
144
  if (encryptedStorage == null) {
110
145
  call.reject("Encrypted storage not available");
111
146
  return;
112
147
  }
113
- value = encryptedStorage.get(key);
114
- break;
115
- case "sqlite":
116
- if (sqliteStorage == null) {
148
+ resolveValue(call, encryptedStorage.get(key));
149
+ return;
150
+ }
151
+ case "sqlite": {
152
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
153
+ if (store == null) {
117
154
  call.reject("SQLite storage not available");
118
155
  return;
119
156
  }
120
- value = sqliteStorage.get(key);
121
- break;
122
- case "filesystem":
123
- rejectUnsupportedFilesystem(call);
157
+ String table = call.getString("table", DEFAULT_TABLE);
158
+ resolveValue(call, store.get(table, key));
124
159
  return;
160
+ }
161
+ case "filesystem": {
162
+ if (filesystemStorage == null) {
163
+ call.reject("Filesystem storage not available");
164
+ return;
165
+ }
166
+ resolveValue(call, filesystemStorage.get(key));
167
+ return;
168
+ }
125
169
  case "preferences":
126
- default:
170
+ default: {
127
171
  if (sharedPrefsStorage == null) {
128
172
  call.reject("Preferences storage not available");
129
173
  return;
130
174
  }
131
- value = sharedPrefsStorage.get(key);
132
- break;
175
+ resolveValue(call, sharedPrefsStorage.get(key));
176
+ return;
177
+ }
133
178
  }
134
-
135
- JSObject result = new JSObject();
136
- result.put("value", value);
137
- call.resolve(result);
138
179
  } catch (Exception e) {
139
180
  call.reject("Failed to get value", e);
140
181
  }
141
182
  }
142
183
 
143
184
  /**
144
- * Set value in storage
185
+ * Resolve a get() call, mapping a Java {@code null} to a JS {@code null}
186
+ * value so the TS adapters treat it as a miss.
187
+ */
188
+ private void resolveValue(PluginCall call, Object value) {
189
+ JSObject result = new JSObject();
190
+ if (value == null) {
191
+ result.put("value", JSObject.NULL);
192
+ } else {
193
+ result.put("value", value);
194
+ }
195
+ call.resolve(result);
196
+ }
197
+
198
+ /**
199
+ * Set value in storage.
200
+ *
201
+ * For sqlite/filesystem the `value` is the FULL wrapper object
202
+ * ({ value, created, updated, expires?, tags?, metadata? }); it is read via
203
+ * call.getObject("value") and stored verbatim (with TTL/query columns
204
+ * extracted for sqlite). For preferences/secure the raw value is forwarded
205
+ * unchanged.
145
206
  */
146
207
  @PluginMethod
147
208
  public void set(PluginCall call) {
@@ -151,7 +212,6 @@ public class StrataStoragePlugin extends Plugin {
151
212
  return;
152
213
  }
153
214
 
154
- Object value = call.getData().opt("value");
155
215
  String storage = call.getString("storage", "preferences");
156
216
 
157
217
  try {
@@ -162,25 +222,42 @@ public class StrataStoragePlugin extends Plugin {
162
222
  call.reject("Encrypted storage not available");
163
223
  return;
164
224
  }
165
- ok = encryptedStorage.set(key, value);
225
+ ok = encryptedStorage.set(key, call.getData().opt("value"));
166
226
  break;
167
- case "sqlite":
168
- if (sqliteStorage == null) {
227
+ case "sqlite": {
228
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
229
+ if (store == null) {
169
230
  call.reject("SQLite storage not available");
170
231
  return;
171
232
  }
172
- ok = sqliteStorage.set(key, value);
233
+ JSObject wrapper = call.getObject("value");
234
+ if (wrapper == null) {
235
+ call.reject("A wrapper object 'value' is required for sqlite storage");
236
+ return;
237
+ }
238
+ ok = store.set(call.getString("table", DEFAULT_TABLE), key, wrapper);
173
239
  break;
174
- case "filesystem":
175
- rejectUnsupportedFilesystem(call);
176
- return;
240
+ }
241
+ case "filesystem": {
242
+ if (filesystemStorage == null) {
243
+ call.reject("Filesystem storage not available");
244
+ return;
245
+ }
246
+ JSObject wrapper = call.getObject("value");
247
+ if (wrapper == null) {
248
+ call.reject("A wrapper object 'value' is required for filesystem storage");
249
+ return;
250
+ }
251
+ ok = filesystemStorage.set(key, wrapper);
252
+ break;
253
+ }
177
254
  case "preferences":
178
255
  default:
179
256
  if (sharedPrefsStorage == null) {
180
257
  call.reject("Preferences storage not available");
181
258
  return;
182
259
  }
183
- ok = sharedPrefsStorage.set(key, value);
260
+ ok = sharedPrefsStorage.set(key, call.getData().opt("value"));
184
261
  break;
185
262
  }
186
263
 
@@ -216,16 +293,22 @@ public class StrataStoragePlugin extends Plugin {
216
293
  }
217
294
  encryptedStorage.remove(key);
218
295
  break;
219
- case "sqlite":
220
- if (sqliteStorage == null) {
296
+ case "sqlite": {
297
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
298
+ if (store == null) {
221
299
  call.reject("SQLite storage not available");
222
300
  return;
223
301
  }
224
- sqliteStorage.remove(key);
302
+ store.remove(call.getString("table", DEFAULT_TABLE), key);
225
303
  break;
304
+ }
226
305
  case "filesystem":
227
- rejectUnsupportedFilesystem(call);
228
- return;
306
+ if (filesystemStorage == null) {
307
+ call.reject("Filesystem storage not available");
308
+ return;
309
+ }
310
+ filesystemStorage.remove(key);
311
+ break;
229
312
  case "preferences":
230
313
  default:
231
314
  if (sharedPrefsStorage == null) {
@@ -263,16 +346,22 @@ public class StrataStoragePlugin extends Plugin {
263
346
  }
264
347
  encryptedStorage.clear(prefix);
265
348
  break;
266
- case "sqlite":
267
- if (sqliteStorage == null) {
349
+ case "sqlite": {
350
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
351
+ if (store == null) {
268
352
  call.reject("SQLite storage not available");
269
353
  return;
270
354
  }
271
- sqliteStorage.clear(prefix);
355
+ store.clear(call.getString("table", DEFAULT_TABLE), prefix);
272
356
  break;
357
+ }
273
358
  case "filesystem":
274
- rejectUnsupportedFilesystem(call);
275
- return;
359
+ if (filesystemStorage == null) {
360
+ call.reject("Filesystem storage not available");
361
+ return;
362
+ }
363
+ filesystemStorage.clear(prefix);
364
+ break;
276
365
  case "preferences":
277
366
  default:
278
367
  if (sharedPrefsStorage == null) {
@@ -298,7 +387,7 @@ public class StrataStoragePlugin extends Plugin {
298
387
  String pattern = call.getString("pattern");
299
388
 
300
389
  try {
301
- List<String> keys = null;
390
+ List<String> keys;
302
391
 
303
392
  switch (storage) {
304
393
  case "secure":
@@ -308,16 +397,22 @@ public class StrataStoragePlugin extends Plugin {
308
397
  }
309
398
  keys = encryptedStorage.keys(pattern);
310
399
  break;
311
- case "sqlite":
312
- if (sqliteStorage == null) {
400
+ case "sqlite": {
401
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
402
+ if (store == null) {
313
403
  call.reject("SQLite storage not available");
314
404
  return;
315
405
  }
316
- keys = sqliteStorage.keys(pattern);
406
+ keys = store.keys(call.getString("table", DEFAULT_TABLE), pattern);
317
407
  break;
408
+ }
318
409
  case "filesystem":
319
- rejectUnsupportedFilesystem(call);
320
- return;
410
+ if (filesystemStorage == null) {
411
+ call.reject("Filesystem storage not available");
412
+ return;
413
+ }
414
+ keys = filesystemStorage.keys(pattern);
415
+ break;
321
416
  case "preferences":
322
417
  default:
323
418
  if (sharedPrefsStorage == null) {
@@ -337,17 +432,21 @@ public class StrataStoragePlugin extends Plugin {
337
432
  }
338
433
 
339
434
  /**
340
- * Get storage size information
435
+ * Get storage size information.
436
+ *
437
+ * When `detailed` is true the result also carries a byte breakdown
438
+ * { detailed: { keys, values, metadata } }.
341
439
  */
342
440
  @PluginMethod
343
441
  public void size(PluginCall call) {
344
442
  String storage = call.getString("storage", "preferences");
443
+ boolean detailed = Boolean.TRUE.equals(call.getBoolean("detailed", false));
345
444
 
346
445
  try {
347
446
  JSObject result = new JSObject();
348
447
 
349
448
  switch (storage) {
350
- case "secure":
449
+ case "secure": {
351
450
  if (encryptedStorage == null) {
352
451
  call.reject("Encrypted storage not available");
353
452
  return;
@@ -356,20 +455,29 @@ public class StrataStoragePlugin extends Plugin {
356
455
  result.put("total", encryptedSizeInfo.total);
357
456
  result.put("count", encryptedSizeInfo.count);
358
457
  break;
359
- case "sqlite":
360
- if (sqliteStorage == null) {
458
+ }
459
+ case "sqlite": {
460
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
461
+ if (store == null) {
361
462
  call.reject("SQLite storage not available");
362
463
  return;
363
464
  }
364
- SQLiteStorage.SizeInfo sqliteSizeInfo = sqliteStorage.size();
365
- result.put("total", sqliteSizeInfo.total);
366
- result.put("count", sqliteSizeInfo.count);
465
+ SQLiteStorage.SizeInfo sqliteSizeInfo =
466
+ store.size(call.getString("table", DEFAULT_TABLE), detailed);
467
+ putSizeInfo(result, sqliteSizeInfo);
367
468
  break;
368
- case "filesystem":
369
- rejectUnsupportedFilesystem(call);
370
- return;
469
+ }
470
+ case "filesystem": {
471
+ if (filesystemStorage == null) {
472
+ call.reject("Filesystem storage not available");
473
+ return;
474
+ }
475
+ SQLiteStorage.SizeInfo fsSizeInfo = filesystemStorage.size(detailed);
476
+ putSizeInfo(result, fsSizeInfo);
477
+ break;
478
+ }
371
479
  case "preferences":
372
- default:
480
+ default: {
373
481
  if (sharedPrefsStorage == null) {
374
482
  call.reject("Preferences storage not available");
375
483
  return;
@@ -378,6 +486,7 @@ public class StrataStoragePlugin extends Plugin {
378
486
  result.put("total", prefsSizeInfo.total);
379
487
  result.put("count", prefsSizeInfo.count);
380
488
  break;
489
+ }
381
490
  }
382
491
 
383
492
  call.resolve(result);
@@ -386,12 +495,27 @@ public class StrataStoragePlugin extends Plugin {
386
495
  }
387
496
  }
388
497
 
498
+ /**
499
+ * Populate a size result, including the detailed byte breakdown when the
500
+ * {@link SQLiteStorage.SizeInfo} carries one.
501
+ */
502
+ private void putSizeInfo(JSObject result, SQLiteStorage.SizeInfo info) {
503
+ result.put("total", info.total);
504
+ result.put("count", info.count);
505
+ if (info.detailed) {
506
+ JSObject breakdown = new JSObject();
507
+ breakdown.put("keys", info.keysBytes);
508
+ breakdown.put("values", info.valuesBytes);
509
+ breakdown.put("metadata", info.metadataBytes);
510
+ result.put("detailed", breakdown);
511
+ }
512
+ }
513
+
389
514
  /**
390
515
  * Query SQLite-backed storage.
391
516
  * Matches the optional `query` method in the JS contract:
392
- * resolves { results: [{ key, value }] }. The JS SqliteAdapter applies the
393
- * real condition by re-fetching/filtering, so this enumerates candidate
394
- * rows.
517
+ * resolves { results: [{ key }] }. The JS SqliteAdapter applies the real
518
+ * condition by re-fetching/filtering, so this enumerates candidate keys.
395
519
  */
396
520
  @PluginMethod
397
521
  public void query(PluginCall call) {
@@ -400,18 +524,18 @@ public class StrataStoragePlugin extends Plugin {
400
524
  call.reject("Query is only supported for the 'sqlite' storage type");
401
525
  return;
402
526
  }
403
- if (sqliteStorage == null) {
527
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
528
+ if (store == null) {
404
529
  call.reject("SQLite storage not available");
405
530
  return;
406
531
  }
407
532
 
408
533
  try {
409
- List<Map<String, Object>> rows = sqliteStorage.query();
534
+ List<Map<String, Object>> rows = store.query(call.getString("table", DEFAULT_TABLE));
410
535
  JSArray results = new JSArray();
411
536
  for (Map<String, Object> row : rows) {
412
537
  JSObject obj = new JSObject();
413
538
  obj.put("key", row.get("key"));
414
- obj.put("value", row.get("value"));
415
539
  results.put(obj);
416
540
  }
417
541
  JSObject result = new JSObject();
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md — android/
2
2
 
3
- Last Updated: 2026-04-03
3
+ Last Updated: 2026-05-27
4
4
 
5
5
  > Agent instructions for Android native development.
6
6
 
@@ -8,27 +8,44 @@ Last Updated: 2026-04-03
8
8
 
9
9
  | File | Purpose |
10
10
  |------|---------|
11
- | `StrataStoragePlugin.java` | Capacitor plugin entry (com.stratastorage) |
12
- | `SharedPreferencesStorage.java` | General key-value storage (com.strata.storage) |
13
- | `EncryptedStorage.java` | Secure storage (com.strata.storage) |
14
- | `SQLiteStorage.java` | Database storage (com.strata.storage) |
11
+ | `StrataStoragePlugin.java` | Capacitor plugin entry (`com.stratastorage`) |
12
+ | `SharedPreferencesStorage.java` | General key-value storage (`com.strata.storage`) |
13
+ | `EncryptedStorage.java` | Secure storage via EncryptedSharedPreferences (`com.strata.storage`); requires `androidx.security:security-crypto 1.1.0` (stable, upgraded in v2.6.0) |
14
+ | `SQLiteStorage.java` | SQLite database storage — multi-store (v2.6.0): `database`/`table` options honoured, full `StorageValue` wrapper round-trip, `size(detailed)` supported (`com.strata.storage`) |
15
+ | `FilesystemStorage.java` | File-per-key native storage under `getFilesDir()/strata_storage/` (new in v2.6.0); atomic writes via staging rename; `isAvailable()` returns `true`; no external permissions needed (`com.strata.storage`) |
16
+
17
+ ## v2.6.0 Notes
18
+
19
+ - **`androidx.security 1.1.0`:** upgraded from `1.1.0-alpha06`. Requires `compileSdkVersion 34`. If Gradle sync fails, bump `compileSdkVersion` and `targetSdkVersion` to 34 in `android/app/build.gradle`.
20
+ - **SQLite multi-store:** `call.getString("database")` and `call.getString("table")` are now read and used. Each `(database, table)` pair → distinct `.db` file. Previously ignored.
21
+ - **SQLite value shape:** native `get` returns full `StorageValue` wrapper. Corrupt rows → treated as miss (no throw).
22
+ - **FilesystemStorage.java:** new class. Writes are atomic (staging temp → `renameTo`). `keys()` excludes `.staging/` artifacts. `size(detailed)` → `{ keys, values, metadata }`.
23
+ - **Pending on-device verification** — see `docs/guides/platforms/device-verification.md`.
15
24
 
16
25
  ## Agent Rules
17
26
 
18
27
  ### Security (IRON-SOLID)
19
28
  - Sensitive data MUST use `EncryptedStorage`
20
29
  - NEVER store secrets in `SharedPreferencesStorage`
30
+ - `androidx.security:security-crypto` version MUST be `1.1.0` (stable) — not alpha
21
31
 
22
32
  ### SQL Safety (IRON-SOLID)
23
33
  - ALWAYS use parameterized queries
24
34
  - NEVER concatenate user input into SQL strings
25
35
 
36
+ ### Filesystem Safety
37
+ - NEVER read from `strata_storage/.staging/` — staging files are transient
38
+ - Key sanitisation must be consistent between `set`, `get`, `remove`, and `keys`
39
+
26
40
  ### Plugin Architecture
27
41
  - Methods exposed through `StrataStoragePlugin.java`
28
42
  - Each storage backend is a separate Java class
29
43
  - Two package paths: `com.stratastorage` (plugin) and `com.strata.storage` (impl)
30
44
 
45
+ ### Build
46
+ - `compileSdkVersion 34` required for `androidx.security 1.1.0`
47
+ - Verify Gradle build succeeds after any change
48
+
31
49
  ### Before Modifying
32
50
  - Understand Capacitor plugin protocol for Android
33
- - Test on Android emulator after changes
34
- - Verify Gradle build succeeds
51
+ - Test on Android emulator after changes; follow device-verification guide
@@ -1,6 +1,6 @@
1
1
  # CLAUDE.md — android/
2
2
 
3
- Last Updated: 2026-04-03
3
+ Last Updated: 2026-05-27
4
4
 
5
5
  ## Android Native Plugin
6
6
 
@@ -12,8 +12,9 @@ Java implementation of native storage backends for Capacitor.
12
12
  |------|------|---------|
13
13
  | `StrataStoragePlugin.java` | `src/main/java/com/stratastorage/` | Main Capacitor plugin entry |
14
14
  | `SharedPreferencesStorage.java` | `src/main/java/com/strata/storage/` | SharedPreferences general storage |
15
- | `EncryptedStorage.java` | `src/main/java/com/strata/storage/` | EncryptedSharedPreferences secure storage |
16
- | `SQLiteStorage.java` | `src/main/java/com/strata/storage/` | SQLite database storage |
15
+ | `EncryptedStorage.java` | `src/main/java/com/strata/storage/` | EncryptedSharedPreferences secure storage (`androidx.security 1.1.0`) |
16
+ | `SQLiteStorage.java` | `src/main/java/com/strata/storage/` | SQLite database storage — multi-store (v2.6.0) |
17
+ | `FilesystemStorage.java` | `src/main/java/com/strata/storage/` | File-per-key storage under `Context.getFilesDir()/strata_storage/` (new in v2.6.0) |
17
18
 
18
19
  ### Configuration
19
20
 
@@ -23,27 +24,64 @@ Java implementation of native storage backends for Capacitor.
23
24
 
24
25
  **Note**: There are two Java package paths — `com.stratastorage` (plugin) and `com.strata.storage` (implementations).
25
26
 
27
+ ## v2.6.0 Changes
28
+
29
+ ### `androidx.security:security-crypto` 1.1.0 (stable)
30
+ `EncryptedStorage.java` depends on `androidx.security:security-crypto`. The
31
+ dependency was upgraded from `1.1.0-alpha06` to `1.1.0` (stable) in v2.6.0.
32
+ The `EncryptedSharedPreferences` / `MasterKey` API is unchanged. The stable
33
+ version requires `compileSdkVersion 34` — if Gradle sync fails after this
34
+ change, bump `compileSdkVersion` and `targetSdkVersion` in
35
+ `android/app/build.gradle` to 34.
36
+
37
+ ### SQLite multi-store
38
+ `SQLiteStorage.java` now accepts `database` and `table` parameters from
39
+ `call.getString("database")` / `call.getString("table")`. Each unique pair
40
+ opens a distinct `.db` file in the app's internal storage. Table identifiers
41
+ are sanitised to `[A-Za-z0-9_]`. The full `StorageValue` wrapper is serialised
42
+ to JSON and stored in a single `value` column; native `get` returns the full
43
+ wrapper so TTL, tags, and metadata survive the round-trip. `size(detailed)`
44
+ returns per-column byte breakdown.
45
+
46
+ ### FilesystemStorage.java (new)
47
+ Stores each key as `getFilesDir()/strata_storage/<sanitised-key>.json`. Writes
48
+ are atomic: value is written to `strata_storage/.staging/<key>.tmp`, then
49
+ renamed into place using `renameTo()`. Staging artifacts are excluded from
50
+ `keys()`. No external filesystem permissions are needed (`getFilesDir()` is
51
+ app-private). `isAvailable()` returns `true`. `size(detailed)` supported.
52
+
53
+ > **Pending on-device verification** — native code is complete and reviewed; see
54
+ > `docs/guides/platforms/device-verification.md` for the test matrix.
55
+
26
56
  ## Rules
27
57
 
28
58
  ### Security (IRON-SOLID)
29
59
  - Sensitive data MUST use `EncryptedStorage` (EncryptedSharedPreferences)
30
60
  - NEVER store credentials, tokens, or secrets in `SharedPreferencesStorage`
31
61
  - Follow Android Keystore best practices
62
+ - `androidx.security:security-crypto 1.1.0` (stable) is the required version
32
63
 
33
64
  ### SQL Safety (IRON-SOLID)
34
65
  - ALWAYS use parameterized queries in `SQLiteStorage`
35
66
  - NEVER concatenate user input into SQL strings
36
67
  - Use `SQLiteDatabase.query()` or prepared statements
37
68
 
69
+ ### Filesystem Safety
70
+ - NEVER read from `strata_storage/.staging/` — staging files are transient
71
+ - Key sanitisation must be consistent between `set`, `get`, `remove`, and `keys`
72
+ - `getFilesDir()` is app-private; no external storage permissions required
73
+
38
74
  ### Plugin Architecture
39
75
  - All native methods exposed through `StrataStoragePlugin.java`
40
76
  - Plugin bridges Capacitor JS calls to Java implementations
41
77
  - Each storage backend is a separate Java class
78
+ - Two package paths: `com.stratastorage` (plugin) and `com.strata.storage` (impl)
42
79
 
43
80
  ### Build
44
81
  - Gradle-based build system
82
+ - `compileSdkVersion 34` required for `androidx.security 1.1.0`
45
83
  - Proguard rules configured for release builds
46
- - Test on Android emulator after changes
84
+ - Test on Android emulator after changes; follow device-verification guide
47
85
 
48
86
  ### Before Modifying
49
87
  - Understand the Capacitor plugin protocol for Android
@@ -51,7 +51,7 @@ dependencies {
51
51
  implementation fileTree(dir: 'libs', include: ['*.jar'])
52
52
  implementation project(':capacitor-android')
53
53
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
54
- implementation 'androidx.security:security-crypto:1.1.0-alpha06'
54
+ implementation 'androidx.security:security-crypto:1.1.0'
55
55
  testImplementation "junit:junit:$junitVersion"
56
56
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
57
57
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"