Reem is an Expo / React Native AI assistant that runs directly on your Android phone. It combines cloud AI providers with on-device execution, a persistent memory system, background scheduling, notification triggers, a floating assistant button, and a home-screen widget — all packaged as a single APK built via GitHub Actions.
- Overview
- Key Features
- Architecture
- Getting the APK
- Manual Setup After Install
- Development Setup
- Building the APK
- CI/CD (GitHub Actions)
- Native Modules
- Configuration Plugins
- TTS Fallback Chain
- Environment Variables
- Troubleshooting
- License
Reem is an AI agent built specifically for mobile devices. It can run agentic tasks in the background, respond to notifications from other apps, schedule recurring runs, float an assistant button over any screen, and speak responses aloud — all without requiring a dedicated server.
- App ID:
com.reem.ai - Version:
2.1.7(seepackage.json) - Min SDK: 26 (Android 8.0)
- Target SDK: 36
- Framework: Expo SDK 57 + React Native + Expo Router
- Package manager: pnpm 12.16.1
- Android builds: GitHub Actions (
expo prebuild+./gradlew assembleRelease), not EAS
| Feature | Description |
|---|---|
| Agentic execution | Runs AI agent runs locally or via configured LLM providers (Gemini, OpenRouter, Anthropic, OpenAI, xAI, etc.) |
| Persistent memory | Multi-entry memory repository backed by SQLite (via expo-sqlite) |
| Background scheduler | Exact-alarm + foreground-service scheduling for recurring agent runs, even when the app is killed |
| Notification triggers | Listens to notifications from other apps and dispatches agent runs on matching packages |
| Floating button | Special-use foreground service with an overlay button that stays on top of other apps |
| Home-screen widget | App widget for quick launch, with APPWIDGET_UPDATE broadcast support |
| Default assistant | Can be set as the device's default assist app (AssistEntryActivity) |
| Device automation | Accessibility-service-driven UI automation (clicks, gestures, text entry, hierarchy dump) |
| TTS chain | Gemini TTS → OpenRouter TTS → on-device local TTS, with real audio playback via expo-audio |
| Wake word | Local wake-word detection for hands-free activation |
| Cloud storage | Backblaze B2 bucket (reemai) for model and asset sync |
| Share target | Android share-sheet integration (ShareTargetModule) |
| Process text | Android "Process text" text-selection integration |
| Saf file operations | Storage Access Framework (SAF) file operations |
| HTML video export | Client-side video encoding/export from HTML canvas frames |
src/
├── app/ # Expo Router routes (_layout.tsx, screens, etc.)
├── core/ # Database, repositories, services, app-state
├── modules/ # Feature modules (tts, notifications, scheduler, ai, chat, ...)
│ ├── tts/ # TTS providers + service (gemini, openrouter, local)
│ ├── notifications/ # Announcements, tts-service integration
│ ├── trigger-dispatch/# Headless task + registration for notification triggers
│ └── ...
├── plugins/ # Expo config plugins (native manifest/gradle injection)
└── ...
modules/ # Autolinked Expo modules with native Android (Kotlin) code
├── default-assistant/
├── device-automation/
├── floating-button/
├── home-screen-widget/
├── html-video-export/
├── notification-trigger/
├── scheduler-alarm/
├── trigger-dispatch/
├── wake-word/
├── background-agent-service/
├── persistent-model-download/
├── termux-stream/
├── saf-file-operations/
├── share-target/
├── process-text/
└── quote-selection/
.plugins/ # Config plugins wired into app.json
.github/workflows/ # android.yml — APK build + GitHub Release
Native modules are written in Kotlin and compiled as part of the app's assembleRelease Gradle task. Expo config plugins inject permissions, services, receivers, and packagingOptions into the generated AndroidManifest.xml and build.gradle during expo prebuild.
Prebuilt APKs are published as GitHub Release assets:
- Go to the Releases page of this repository:
https://github.com/NabinSharmaitgithub/Reem/releases - Download the latest
mobile-agent-<version>.apkasset. - Transfer the APK to your Android device and install it.
- Grant the required permissions (see below).
If no release is available yet, you can build locally (see Building the APK) or trigger the GitHub Actions workflow manually.
After installing the APK, you must manually grant several Android permissions — Android does not allow apps to self-grant most of these:
| Permission | Where to grant | Required for |
|---|---|---|
| Accessibility service | Settings → Accessibility → Reem Automation | Device automation (clicks, gestures, text entry) |
| Display over other apps | Settings → Apps → Special app access → Display over other apps → Reem | Floating assistant button overlay |
| Notifications | Settings → Apps → Reem → Notifications | Notification triggers + announcements |
| Microphone | App prompt / Settings → Apps → Reem → Permissions | Voice input, wake word, speech recognition |
| Camera | App prompt / Settings → Apps → Reem → Permissions | QR code scanning, photo attachment |
| Exact alarm & schedule | Settings → Apps → Special app access → Alarms & reminders → Reem | Background scheduler |
| Ignore battery optimizations | Settings → Battery → Battery optimization → Reem | Reliable background execution |
| Write settings | Settings → Apps → Special app access → Modify system settings → Reem | System setting changes via automation |
| Install unknown apps | Settings → Apps → Special app access → Install unknown apps | REQUEST_INSTALL_PACKAGES for APK updates |
The floating button requires Display over other apps; device automation requires the Accessibility service to be toggled on in system settings.
- Node.js 22+
- pnpm 12.16.1 (pinned in
packageManager) - No local Android SDK is required for typechecking/linting, but building the APK locally needs the Android SDK (cmdline-tools, platform-tools, android-36, build-tools-36.0.0).
pnpm installpnpm dlx expo lint # lint (0 errors expected)
pnpm dlx tsc --noEmit # typecheck (note: may OOM on low-RAM devices; rely on CI if needed)pnpm dlx vitest runandroid/ and ios/ are gitignored and regenerated by expo prebuild. Never edit them by hand — configure native behavior in app.json and config plugins under plugins/.
pnpm dlx expo prebuild --platform android --clean --no-installAndroid APK builds run on GitHub Actions ubuntu-latest:
-
Push to
main(or rungh workflow run "Android APK Release" --ref mainmanually). -
The workflow:
- Checks out the repo
- Runs
pnpm install --frozen-lockfile(pnpm 12.16.1, Node 22) - Sets up JDK 17 + Android SDK 36
- Runs
npx expo prebuild --platform android - Runs
./gradlew assembleRelease -x lint -x lintVitalRelease --continue --console=plain - Uploads the APK as a workflow artifact
- Publishes a GitHub Release (if
package.jsonversion differs from the latestv*tag)
-
Download the APK from the Actions run summary or the Releases page.
Note: Push builds are skipped when package.json version equals the latest v* tag (unless triggered via workflow_dispatch). Bump the version in package.json/app.json to force a new build.
pnpm dlx expo prebuild --platform android --clean --no-install
cd android
./gradlew assembleRelease -x lint -x lintVitalReleaseThe output APK will be at android/app/build/outputs/apk/release/app-release.apk.
Workflow: .github/workflows/android.yml — "Android APK Release"
Triggers: push to main and workflow_dispatch.
Key characteristics:
- pnpm 12.16.1 pinned via
packageManagerand the workflowversioninput. - Node 22, JDK 17, Android SDK 36 (
platforms;android-36,build-tools;36.0.0). --continueadded to Gradle so all Kotlin compile errors surface in a single run.- EAS builds were abandoned (all 14 prior attempts failed with
EAS_BUILD_UNKNOWN_GRADLE_ERROR); GitHub Actions is now the build path. android/andios/are gitignored;expo prebuildregenerates them on every run.
To trigger manually:
gh workflow run "Android APK Release" --ref mainTo check status:
gh run list --workflow "Android APK Release" --limit 5
gh run view <run-id> --json status,conclusion
gh run view <run-id> --logThe following autolinked Expo modules ship native Android (Kotlin) code under modules/:
| Module | Native responsibilities |
|---|---|
default-assistant |
DefaultAssistantModule + AssistEntryActivity — set Reem as the default assist app; voice-assistant invocation from the home button |
device-automation |
DeviceAutomationModule + DeviceAccessibilityService — accessibility-driven clicks, gestures, text entry, hierarchy dump, screenshots |
floating-button |
FloatingButtonModule + FloatingButtonService — special-use foreground service; overlay button; visibility state on service companion |
home-screen-widget |
HomeScreenWidgetModule + HomeScreenWidgetProvider — app widget; R.id.widget_root layout fix; APPWIDGET_UPDATE |
html-video-export |
HtmlVideoExportModule — client-side H.264 video encoding from HTML frames |
notification-trigger |
NotificationTriggerModule + NotificationTriggerListener — notification listener service; broadcasts NOTIFICATION_POSTED to trigger dispatch |
scheduler-alarm |
SchedulerAlarmManager, SchedulerWakeService, SchedulerHeadless — exact alarms, boot receiver, foreground dataSync service, headless JS scheduler task |
trigger-dispatch |
TriggerDispatchModule, TriggerBroadcastReceiver — manifest-declared broadcast receiver; TriggerDispatchTask headless handler; isAllowedInForeground = true |
wake-word |
WakeWordModule — local wake-word detection; OnDestroy lifecycle |
background-agent-service |
BackgroundAgentService + BackgroundAgentModule — background FGS (dataSync) for long-running agent work |
persistent-model-download |
ModelDownloadWorker — WorkManager-based model downloads with foreground notification |
termux-stream |
TermuxStreamModule + SseClient — Termux SSE streaming |
saf-file-operations |
SafFileOperationsModule — Storage Access Framework file ops |
share-target |
ShareTargetModule + ShareInbox — Android share-sheet target |
process-text |
ProcessTextModule + ProcessTextActivity — Android "Process text" selection |
quote-selection |
QuoteSelectionModule — text selection quotes |
Config plugins under plugins/ are wired into app.json and run during expo prebuild to inject manifest entries, permissions, and Gradle options:
| Plugin | Purpose |
|---|---|
with-firebase-resolution.js |
Aligns Firebase Firestore with the Firebase version used by Expo Notifications (no duplicate Timestamp classes) and adds Guava for React Native Firebase 18.9.0 firestore sources |
with-litert-lm.js |
LiteRT LM (on-device model) native config |
with-background-agent.js |
Background agent service + FOREGROUND_SERVICE_DATA_SYNC |
with-android-system-bars.js |
System bars / edge-to-edge handling |
with-gradle-build-settings.js |
Gradle build settings injection |
with-scheduler-alarm.js |
Scheduler permissions + SchedulerWakeService (dataSync FGS) |
with-whisper.js |
Whisper speech-to-text native config |
with-share-target.js |
Share target provider |
with-device-automation.js |
Accessibility service declaration (BIND_ACCESSIBILITY_SERVICE, exported) |
with-floating-button.js |
Floating button FGS (specialUse), SYSTEM_ALERT_WINDOW permission, PROPERTY_SPECIAL_USE_FGS_SUBTYPE |
with-default-assistant.js |
Default assist activity declaration |
with-notification-trigger.js |
Notification listener service (BIND_NOTIFICATION_LISTENER_SERVICE, exported) |
with-wake-word.js |
RECORD_AUDIO permission (no bogus service declaration) |
with-home-screen-widget.js |
App widget receiver (exported) + mirrored widget resources |
with-trigger-dispatch.js |
TriggerBroadcastReceiver declaration + intent filter |
Key manifest behaviors:
- Accessibility and notification-listener services are
android:exported="true"(required for system binding) but guarded byBIND_ACCESSIBILITY_SERVICE/BIND_NOTIFICATION_LISTENER_SERVICEpermissions. - The app widget receiver is
android:exported="true"so the system can deliverAPPWIDGET_UPDATE. - Widget resources are mirrored under
widget_*prefixed filenames so the app's ownvalues/strings.xmlis never clobbered.
Reem uses a provider-abstraction TTS chain (TtsProvider / TtsService singleton):
Order:
- Gemini TTS (Google Cloud / Gemini API
gemini-2.5-flash-preview-tts) — returns raw PCM, wrapped in a WAV container, played viaexpo-audio - OpenRouter TTS (OpenRouter chat completions with audio output,
openai/gpt-4o-audio-preview, MP3) — real audio before local fallback - Local TTS (
local-ttsnative module +expo-speech) — on-device final fallback
Keys are stored in expo-secure-store via ttsService.setGeminiApiKey() / ttsService.setOpenRouterApiKey(), with EXPO_PUBLIC_GEMINI_API_KEY / EXPO_PUBLIC_OPENROUTER_API_KEY as build-time fallback.
announcements.ts drives chunked speech through ttsService.speak(), which plays cloud audio to completion before proceeding to the next chunk, falling back to on-device speech if cloud audio cannot be produced or played.
| Variable | Purpose | Example |
|---|---|---|
FIREBASE_API_KEY |
Firebase project API key | AIza... |
FIREBASE_AUTH_DOMAIN |
Firebase auth domain | reem-8fd60.firebaseapp.com |
FIREBASE_PROJECT_ID |
Firebase project ID | reem-8fd60 |
FIREBASE_STORAGE_BUCKET |
Firebase storage bucket | reem-8fd60.appspot.com |
FIREBASE_MESSAGING_SENDER_ID |
Firebase sender ID | ... |
FIREBASE_APP_ID |
Firebase app ID | 1:... |
GEMINI_API_KEY |
Gemini API key (TTS + chat) | AQ.Ab8RN... |
OPENROUTER_API_KEY |
OpenRouter API key (TTS fallback) | sk-or-v1-... |
B2_APPLICATION_KEY |
Backblaze B2 application key | (set in CI/env) |
B2_KEY_ID |
Backblaze B2 key ID | (set in CI/env) |
B2_BUCKET_NAME |
Backblaze B2 bucket name | reemai |
EXPO_PUBLIC_* |
Public runtime config exposed to JS | see above |
Firebase project reem-8fd60 rules/indexes are deployed separately. .env is gitignored and populated locally; CI injects secrets via GitHub Actions encrypted variables.
| Issue | Likely cause / fix |
|---|---|
:default-assistant:compileReleaseKotlin fails |
reactContext must be a getter throwing ReactContextLost(); use sendEvent(...) not emit(...); no init {} / default lambda values in ModuleDefinition |
Duplicate com.google.firebase.Timestamp class |
with-firebase-resolution.js pins Firestore to the Firebase 26 line used with Expo Notifications' Firebase 25 messaging dependency |
| Accessibility service not binding | Manifest must declare exported="true" + BIND_ACCESSIBILITY_SERVICE; user must toggle it on in Settings |
| Floating button not appearing | Grant "Display over other apps"; service runs as specialUse FGS |
| Widget not updating | Receiver must be exported="true"; widget resources must exist in merged res/ |
| Notification triggers not firing | TriggerDispatchTask must be registered at bundle load (index.js imports headless-registration.ts); isAllowedInForeground = true |
| TTS silent (cloud providers) | Gemini now uses generativelanguage.googleapis.com TTS endpoint; OpenRouter requests modalities: ["text","audio"]; speak() plays audio via expo-audio before local fallback |
| Build OOMs on typecheck | tsc --noEmit needs ~2 GB RAM; skip locally and rely on CI / lint |
MIT License. See LICENSE for details.
Repository: NabinSharmaitgithub/Reem — branch main
Workflow: "Android APK Release" on GitHub Actions
Latest version: 2.1.7