commit 618ae4e7fd3f80af7d66c99eeda153273a16bded truex <truex@equwal.com> 2026-09-26 13:52:12 -0700 Tell which apps of the SubRead suite are installed The new plugin method suite gives, for a checklist: SubRead Overlay (the release build and the debug build), the version name of SubRead or null, SubRead Anki, and the number of apps in the text selection menu without this app. The package manager gives the packages that the manifest queries. On the web, all are false, null and 0.
.../java/com/equwal/scansubread/SubReadPlugin.java | 43 ++++++++++++++++++++++ src/subread.ts | 28 ++++++++++++++ test/subread.test.ts | 10 +++++ 3 files changed, 81 insertions(+)
diff --git a/android/app/src/main/java/com/equwal/scansubread/SubReadPlugin.java b/android/app/src/main/java/com/equwal/scansubread/SubReadPlugin.java index b7f9f8f..709aa35 100644 --- a/android/app/src/main/java/com/equwal/scansubread/SubReadPlugin.java +++ b/android/app/src/main/java/com/equwal/scansubread/SubReadPlugin.java @@ -8,6 +8,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; @@ -47,6 +48,7 @@ import java.util.List; * (`space.subread.app.action.ALIGN`). * - SubRead Anki, through `space.subread.anki.action.ADD`: a card for a * word and its sentence. + * - The package manager: which apps of the suite are installed. * - The screen, which stays on during read-along. */ @CapacitorPlugin(name = "SubRead") @@ -60,6 +62,9 @@ public class SubReadPlugin extends Plugin { private static final String COLUMN_STATE = "state"; private static final String NO_OVERLAY = "error=no_overlay"; + private static final String OVERLAY_PACKAGE = "space.subread.overlay"; + private static final String OVERLAY_DEBUG_PACKAGE = "space.subread.overlay.debug"; + private static final String PREFS = "subread"; private static final String PREF_DICTIONARY = "dictionary"; @@ -332,6 +337,44 @@ public class SubReadPlugin extends Plugin { }); } + // --- The suite --- + + /** The package info of an installed app, or null when the app is not installed. */ + private PackageInfo packageInfo(String name) { + try { + return getContext().getPackageManager().getPackageInfo(name, 0); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + /** + * Tells which apps of the suite are installed, for a checklist. The + * packages are in the queries of the manifest, else Android hides them. + */ + @PluginMethod + public void suite(PluginCall call) { + // The version name of SubRead, or null when SubRead is not installed. + PackageInfo subread = packageInfo(SUBREAD_PACKAGE); + Object version = JSObject.NULL; + if (subread != null) version = subread.versionName == null ? "" : subread.versionName; + int dictionaries = 0; + PackageManager pm = getContext().getPackageManager(); + for (ResolveInfo info : pm.queryIntentActivities(probe(), PackageManager.MATCH_DEFAULT_ONLY)) { + // This app is not a dictionary. + if (info.activityInfo != null && !info.activityInfo.packageName.equals(getContext().getPackageName())) { + dictionaries++; + } + } + JSObject ret = new JSObject(); + ret.put("overlay", packageInfo(OVERLAY_PACKAGE) != null); + ret.put("overlayDebug", packageInfo(OVERLAY_DEBUG_PACKAGE) != null); + ret.put("subread", version); + ret.put("anki", packageInfo(ANKI_PACKAGE) != null); + ret.put("dictionaries", dictionaries); + call.resolve(ret); + } + // --- The subtitle maker, through the SubRead app --- /** Writes `text` to cacheDir/share/<name> and returns its FileProvider Uri. */ diff --git a/src/subread.ts b/src/subread.ts index 4a3bc20..d4cc101 100644 --- a/src/subread.ts +++ b/src/subread.ts @@ -53,6 +53,20 @@ export interface AnkiResult { error?: 'not_installed'; } +/** The apps of the SubRead suite on the device. */ +export interface SuiteApps { + /** SubRead Overlay, the release build. */ + overlay: boolean; + /** SubRead Overlay, the debug build. */ + overlayDebug: boolean; + /** The version name of SubRead, for example "0.10.0", or null without SubRead. */ + subread: string | null; + /** SubRead Anki. */ + anki: boolean; + /** The number of apps in the text selection menu (the `dictionaries` list), without this app. */ + dictionaries: number; +} + export interface SubReadPlugin { playerState(): Promise<StateLine>; play(): Promise<StateLine>; @@ -84,6 +98,11 @@ export interface SubReadPlugin { * `{ error: 'not_installed' }`. */ ankiAdd(options: AnkiCard): Promise<AnkiResult>; + /** + * Tells which apps of the SubRead suite are installed, for a checklist. + * On the web: all false, null and 0. + */ + suite(): Promise<SuiteApps>; makeSubtitles(options: { audio: string; bookText: string; @@ -142,6 +161,15 @@ class SubReadWeb implements SubReadPlugin { ankiAdd(): Promise<AnkiResult> { return Promise.resolve({ error: 'not_installed' }); } + suite(): Promise<SuiteApps> { + return Promise.resolve({ + overlay: false, + overlayDebug: false, + subread: null, + anki: false, + dictionaries: 0, + }); + } makeSubtitles(): Promise<SubtitlesResult> { return Promise.resolve({ error: 'not_installed' }); } diff --git a/test/subread.test.ts b/test/subread.test.ts index 4e6b8f2..7a8224e 100644 --- a/test/subread.test.ts +++ b/test/subread.test.ts @@ -53,6 +53,16 @@ describe('the web fallback of the plugin', () => { }); }); + it('has no app of the suite', async () => { + await expect(SubRead.suite()).resolves.toEqual({ + overlay: false, + overlayDebug: false, + subread: null, + anki: false, + dictionaries: 0, + }); + }); + it('ignores a browser with no wake lock, and a refused request', async () => { vi.stubGlobal('navigator', {}); await expect(SubRead.keepAwake({ on: true })).resolves.toBeUndefined();