diff --git a/codemagic.yaml b/codemagic.yaml
index 3bc51fe..522e7c3 100644
--- a/codemagic.yaml
+++ b/codemagic.yaml
@@ -165,3 +165,76 @@ workflows:
# submit_to_app_store: false
# No email recipients here on purpose — build status is watched on the Codemagic dashboard. Add
# per-user notifications in the Codemagic UI (or a `publishing.email` block) later if you want them.
+
+ # ── Android test build ──────────────────────────────────────────────────────────────────────────
+ # Builds an INSTALLABLE debug APK of the same Capacitor shell (loads https://remote.bizgaze.com). This is
+ # the Android equivalent of the iOS TestFlight loop, but simpler — no Google Play account or signing is
+ # needed to test: download the APK artifact from the Codemagic build page (or wire an email/Slack in the
+ # UI), sideload it on a phone (enable "Install unknown apps"), and run.
+ #
+ # Runs on a Linux instance (has the Android SDK + JDK preinstalled) — much cheaper/faster than a Mac.
+ #
+ # Firebase push (FCM) is OPTIONAL for this test build: app/build.gradle only applies the google-services
+ # plugin when google-services.json is present, so the APK builds fine WITHOUT it (push/call-wake just
+ # won't fire). To enable push, base64 the google-services.json and store it as a secure Codemagic env var
+ # GOOGLE_SERVICES_JSON (group `android_config`); the step below decodes it into place.
+ android-apk:
+ name: Biz Connect Android → test APK
+ max_build_duration: 45
+ instance_type: linux_x2
+ environment:
+ # To enable FCM push later, create a Codemagic variable group holding GOOGLE_SERVICES_JSON (base64 of
+ # google-services.json, marked secure) and uncomment the two lines below. Left out for now so the first
+ # test build needs ZERO Codemagic setup.
+ # groups:
+ # - android_config
+ vars:
+ PACKAGE_NAME: "com.bizgaze.connect"
+ node: 22
+ java: 17
+ scripts:
+ - name: Install JS dependencies
+ script: |
+ cd mobile
+ npm install
+
+ - name: Generate the Android project (Capacitor)
+ script: |
+ cd mobile
+ # `cap add android` scaffolds android/; safe to re-run — it no-ops if it already exists (our committed
+ # project has custom manifest permissions, which cap sync preserves).
+ if [ ! -d "android" ]; then npx cap add android; fi
+ npx cap sync android
+ # App icon + splash from resources/icon.png & resources/splash*.png.
+ npx capacitor-assets generate --android || echo "capacitor-assets returned non-zero (see above)"
+ # The generated project points sdk.dir at wherever it was made; overwrite it with the CI SDK path
+ # (Codemagic exports ANDROID_SDK_ROOT) so Gradle finds the SDK.
+ echo "sdk.dir=$ANDROID_SDK_ROOT" > android/local.properties
+ echo "Android SDK -> $ANDROID_SDK_ROOT"
+ # android/ is gitignored and regenerated fresh in CI, so its manifest only has INTERNET. Inject the
+ # camera/mic/notification permissions the web UI needs (mirrors ios-patch.sh for iOS). Tolerant + idempotent.
+ node scripts/android-patch.js android/app/src/main/AndroidManifest.xml
+
+ - name: (Optional) Firebase google-services.json for FCM push
+ script: |
+ cd mobile/android/app
+ if [ -n "$GOOGLE_SERVICES_JSON" ]; then
+ echo "$GOOGLE_SERVICES_JSON" | base64 --decode > google-services.json
+ echo "google-services.json written — FCM push enabled in this build"
+ else
+ echo "No GOOGLE_SERVICES_JSON set — building WITHOUT FCM push (fine for a shell/UI test build)"
+ fi
+
+ - name: Build the debug APK
+ script: |
+ cd mobile/android
+ chmod +x ./gradlew
+ # Debug build type is auto-signed with the Android debug keystore → directly installable, no Play
+ # account or upload key needed. (A signed release AAB for the Play Store is a later, separate step.)
+ ./gradlew assembleDebug --stacktrace
+ echo "APK(s):"; find app/build/outputs -name "*.apk"
+ artifacts:
+ - mobile/android/app/build/outputs/**/*.apk
+ # Download the APK from the build page. To get it emailed like TestFlight, add a `publishing.email` block
+ # here (or notifications in the Codemagic UI). A signed release AAB → Google Play internal testing is a
+ # separate workflow we can add once the shell is verified on a device.
diff --git a/mobile/scripts/android-patch.js b/mobile/scripts/android-patch.js
new file mode 100644
index 0000000..4325a61
--- /dev/null
+++ b/mobile/scripts/android-patch.js
@@ -0,0 +1,43 @@
+// Inject the runtime permissions the web UI needs into the Capacitor-generated AndroidManifest.xml.
+// Run on Codemagic from the Android workflow:
+// node mobile/scripts/android-patch.js mobile/android/app/src/main/AndroidManifest.xml
+//
+// WHY: mobile/android/ is gitignored (regenerated in CI by `cap add android`, same as iOS ios/). The
+// freshly generated manifest only declares INTERNET, so without this the WebRTC calls in the web UI can't
+// get camera/mic and Android 13+ never prompts for notifications. This adds the same permissions listed in
+// mobile/resources/android-permissions.xml. (When the native-call Android plugin lands, it will contribute
+// its own manifest entries via Capacitor manifest-merging; this only covers the app-level WebView perms.)
+//
+// TOLERANT: exits 0 and no-ops if anything is off, so it can NEVER fail the build. Idempotent (keyed on
+// the bzcAndroidPerms marker), so re-runs never duplicate the block.
+const fs = require('fs');
+const p = process.argv[2];
+if (!p || !fs.existsSync(p)) { console.log(' (AndroidManifest.xml not found — permission patch skipped)'); process.exit(0); }
+try {
+ let s = fs.readFileSync(p, 'utf8');
+ if (s.includes('bzcAndroidPerms') || s.includes('android.permission.RECORD_AUDIO')) {
+ console.log(' Android permissions already present'); process.exit(0);
+ }
+ const block = [
+ '',
+ ' ',
+ ' ',
+ '
Show your screen with a 6-digit code
Enter a customer\'s code to help
Multi-party video — coming soon
Start or join a video meeting
Start or join a video meeting, or schedule one for later. Small group (mesh) for now — larger rooms coming with the SFU.
Start or join a video meeting, or schedule one for later.