Skip to content

Commit db39af4

Browse files
authored
fix(setup): warm ML Kit barcode scanner at startup (v0.2.4.6-alpha)
Squash-merge of #53.
2 parents aae9486 + 1deeacc commit db39af4

4 files changed

Lines changed: 96 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,29 @@ If an upstream merge changes either of these, revert immediately and do not ship
1313

1414
## Git workflow
1515

16-
After completing any fix or feature on the `glucodroid` branch, always commit and push the changes immediately.
17-
Use a clear commit message, then run `git push`.
16+
Never push directly to `origin/glucodroid`. All fixes and features go through a PR:
17+
18+
1. Create a feature branch off `glucodroid` (e.g. `fix/<short-name>` or `feat/<short-name>`).
19+
2. Commit the changes on that branch.
20+
3. Push the feature branch: `git push -u origin <branch>`.
21+
4. Open a PR with `gh pr create --base glucodroid --head <branch> --title "..." --body "..."`.
22+
5. Merge with `gh pr merge <n> --squash --delete-branch` once CI is green.
23+
6. The `glucodroid` branch itself is updated by the squash merge — never `git push` to it directly.
24+
25+
`origin` is `robster7674/glucodroid` (this repo). `upstream` is `ctqvva/JugglucoNG`**never push to upstream** under any circumstances; it is read-only mirror reference.
26+
27+
## Release process
28+
29+
Every release follows these steps in order — do not skip any:
30+
31+
1. Bump `versionName` / `versionCode` in `Common/build.gradle` defaultConfig.
32+
2. Build: `./gradlew assembleMobileLibre3SiDexNogoogleRelease -Pno_x86 -Pno_x86_64`.
33+
3. Copy APK to `~/Downloads/glucodroid.apk` (exact filename — never rename).
34+
4. Commit the version bump + any other release-blocker fixes on the feature branch.
35+
5. Open and merge the PR into `glucodroid` (squash, delete branch).
36+
6. Tag the merged commit on `glucodroid`: `git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z`.
37+
7. Create the GitHub release with `gh release create vX.Y.Z --prerelease --title "vX.Y.Z" --notes-file <notes.md> --target glucodroid`. (If `gh release create` fails on scope, use the REST API with `gh auth token`.)
38+
8. **Upload the APK as a release asset**`~/Downloads/glucodroid.apk` MUST be attached to the release as `glucodroid.apk`. A release with notes but no APK is incomplete and the release is not done. Verify `browser_download_url` is present in the upload response.
1839

1940
## Fresh clone setup
2041

Common/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ android {
8989
//compileSdk 36
9090
applicationId "cloud.glucodroid"
9191

92-
versionName '0.2.4.5'
93-
versionCode 8245
92+
versionName '0.2.4.6'
93+
versionCode 8246
9494
// multiDexEnabled true
9595

9696
buildConfigField 'String', 'BUILD_TIME', 'new java.text.SimpleDateFormat("HH:mm:ss dd-MMM-yyyy").format(new java.util.Date(' + System.currentTimeMillis() + 'L))'

Common/src/main/java/tk/glucodata/MainActivity.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ protected void onCreate(Bundle savedInstanceState) {
478478
super.onCreate(savedInstanceState);
479479
thisone = this;
480480

481+
warmUpBarcodeScanner();
482+
481483
if (android.os.Build.VERSION.SDK_INT >= 21) {
482484
Log.i(LOG_ID, "sdk 21 or larger");
483485
} else {
@@ -545,6 +547,50 @@ private void initComposeUI() {
545547
}
546548
}
547549

550+
// Warm the ML Kit BarcodeScanning static holder on the main thread, before
551+
// any wizard/Compose measure pass can reach it. The standalone ML Kit barcode
552+
// scanner (com.google.mlkit:barcode-scanning) statically delegates to
553+
// play-services-mlkit-barcode-scanning on the first BarcodeScanning.getClient
554+
// call; calling it from a Compose `remember { }` block that is also being
555+
// measured can race with the static initializer and NPE on the `zzi` field of
556+
// the internal `zzg` holder (see FINDINGS / agent rule for the
557+
// "standalone-MLKit initialization race"). Calling once here, in onCreate,
558+
// and closing immediately, makes the static `zzg.INSTANCE` non-null and its
559+
// `zza` field set, so subsequent wizard calls see a fully-initialized
560+
// scanner. Reflective on purpose: the mlkit classes are not on the classpath
561+
// of every flavor (no-google/wear), and a hard import would break those.
562+
private void warmUpBarcodeScanner() {
563+
try {
564+
Class<?> scanningCls = Class.forName("com.google.mlkit.vision.barcode.BarcodeScanning");
565+
Class<?> optionsCls = Class.forName("com.google.mlkit.vision.barcode.BarcodeScannerOptions");
566+
Class<?> optionsBuilderCls = Class.forName("com.google.mlkit.vision.barcode.BarcodeScannerOptions$Builder");
567+
Class<?> barcodeCls = Class.forName("com.google.mlkit.vision.barcode.common.Barcode");
568+
Object options = optionsBuilderCls.getMethod("build").invoke(
569+
optionsBuilderCls.getMethod("setBarcodeFormats", int[].class).invoke(
570+
optionsBuilderCls.getConstructor().newInstance(),
571+
new Object[]{ new int[]{
572+
barcodeCls.getField("FORMAT_QR_CODE").getInt(null),
573+
barcodeCls.getField("FORMAT_DATA_MATRIX").getInt(null)
574+
} }
575+
)
576+
);
577+
Object scanner = scanningCls.getMethod("getClient", optionsCls).invoke(null, options);
578+
try {
579+
scanner.getClass().getMethod("close").invoke(scanner);
580+
} catch (Throwable closeEx) {
581+
// best-effort
582+
}
583+
Log.i(LOG_ID, "ML Kit barcode scanner warmed up");
584+
} catch (ClassNotFoundException e) {
585+
// mlkit not on classpath for this flavor; nothing to do
586+
Log.i(LOG_ID, "ML Kit barcode scanner not present on classpath; skipping warm-up");
587+
} catch (Throwable t) {
588+
// Any init failure must not break app startup. The wizard's own
589+
// runCatching will surface this to the user as a scanner error.
590+
Log.w(LOG_ID, "ML Kit barcode scanner warm-up failed: " + t);
591+
}
592+
}
593+
548594
// GestureDetector mGestureDetector;
549595
void handleIntent(Intent intent) {
550596
if (intent == null)

Common/src/mobile/java/tk/glucodata/ui/setup/InlineQrScannerCard.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import androidx.compose.ui.viewinterop.AndroidView
6161
import androidx.core.content.ContextCompat
6262
import androidx.lifecycle.Lifecycle
6363
import androidx.lifecycle.LifecycleEventObserver
64+
import com.google.mlkit.vision.barcode.BarcodeScanner
6465
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
6566
import com.google.mlkit.vision.barcode.BarcodeScanning
6667
import com.google.mlkit.vision.barcode.common.Barcode
@@ -69,6 +70,7 @@ import java.util.concurrent.Executors
6970
import java.util.concurrent.TimeUnit
7071
import java.util.concurrent.atomic.AtomicBoolean
7172
import java.util.concurrent.atomic.AtomicInteger
73+
import tk.glucodata.Log
7274
import tk.glucodata.R
7375

7476
private fun Context.hasCameraPermission(): Boolean {
@@ -130,7 +132,7 @@ fun InlineQrScannerCard(
130132
)
131133
.build()
132134
}
133-
val barcodeScanner = remember { BarcodeScanning.getClient(scannerOptions) }
135+
var barcodeScanner by remember { mutableStateOf<BarcodeScanner?>(null) }
134136

135137
val permissionLauncher = rememberLauncherForActivityResult(
136138
contract = ActivityResultContracts.RequestPermission()
@@ -144,6 +146,23 @@ fun InlineQrScannerCard(
144146
}
145147
}
146148

149+
// Defer BarcodeScanning.getClient() out of the measure pass into a coroutine.
150+
// Doing this in `remember { }` races the lib's static <clinit> with Compose
151+
// measure on cold start and can NPE on the `zzi` field of the internal
152+
// `zzg` holder (see "standalone-MLKit initialization race" in
153+
// ~/.config/kilo/agent/glucodroid.md). MainActivity warm-up runs once at
154+
// process start; this LaunchedEffect is the per-composable safety net that
155+
// also recovers from a warm-up failure by surfacing it to `scannerError`.
156+
LaunchedEffect(Unit) {
157+
if (barcodeScanner != null) return@LaunchedEffect
158+
runCatching { BarcodeScanning.getClient(scannerOptions) }
159+
.onSuccess { barcodeScanner = it }
160+
.onFailure { t ->
161+
scannerError = t.message ?: t.javaClass.simpleName
162+
Log.e("InlineQrScannerCard", "Failed to acquire ML Kit barcode scanner: ${t.message}")
163+
}
164+
}
165+
147166
DisposableEffect(Unit) {
148167
onDispose {
149168
if (touchActive) {
@@ -153,7 +172,8 @@ fun InlineQrScannerCard(
153172
camera = null
154173
torchEnabled = false
155174
analyzerExecutor.shutdown()
156-
barcodeScanner.close()
175+
barcodeScanner?.close()
176+
barcodeScanner = null
157177
}
158178
}
159179

@@ -210,7 +230,8 @@ fun InlineQrScannerCard(
210230
.also { imageAnalysis ->
211231
imageAnalysis.setAnalyzer(analyzerExecutor) { imageProxy ->
212232
val mediaImage = imageProxy.image
213-
if (mediaImage == null || consumed) {
233+
val scanner = barcodeScanner
234+
if (mediaImage == null || consumed || scanner == null) {
214235
imageProxy.close()
215236
return@setAnalyzer
216237
}
@@ -224,7 +245,7 @@ fun InlineQrScannerCard(
224245
imageProxy.imageInfo.rotationDegrees
225246
)
226247

227-
barcodeScanner.process(inputImage)
248+
scanner.process(inputImage)
228249
.addOnSuccessListener(mainExecutor) { barcodes ->
229250
val rawValue = barcodes
230251
.firstNotNullOfOrNull { it.rawValue?.trim()?.takeIf { value -> value.isNotEmpty() } }

0 commit comments

Comments
 (0)