mirror of
https://github.com/status-im/status-app.git
synced 2026-08-27 07:01:14 +00:00
feat(android): status-go as a service
Add an option to run status-go as a service on android using Androdi Binder as a transport. See ADR for more details docs/adr/0001-android-status-go-as-a-service.md
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
# ADR-0001: Android "status-go as a Service" (separate process + Binder IPC)
|
||||
|
||||
## Status
|
||||
- **Proposed**
|
||||
- **Date**: 2026-01-16
|
||||
- **Owners**: Status Desktop (Android target)
|
||||
|
||||
## Why
|
||||
|
||||
We need:
|
||||
|
||||
- A backend that survives UI process death (swipe-away, memory pressure).
|
||||
- Decrypted notifications when UI is not running.
|
||||
- Push notifications for de-googled phones (no firebase dependency).
|
||||
- A design that matches Android’s process model and OEM restrictions.
|
||||
|
||||
Historically, `status-go` ran in the UI process; killing the UI killed messaging and decryption. This ADR documents the separate-process service architecture now implemented.
|
||||
|
||||
## How (implementation)
|
||||
|
||||
### Transport and contract
|
||||
|
||||
- **IPC:** Binder (AIDL).
|
||||
- **RPC shape:** `call(method, argsJson)` and `callToFile(...)` for large payloads.
|
||||
- **Signals:** service emits JSON to UI via `RemoteCallbackList` + `IStatusGoSignalListener`.
|
||||
- **Login state:** UI uses `status_account.getActiveAccount()` over the service RPC path as the authoritative check for resume vs onboarding.
|
||||
|
||||
### Process split
|
||||
|
||||
- **Service process:** Android `Service` in its own process, loads `libstatus_service.so` which links real `libstatus.so`.
|
||||
- **UI process:** links a stub library implementing the C API; stub forwards all calls to the service via Binder and re-injects signals into the existing pipeline.
|
||||
|
||||
### Compilation and running
|
||||
|
||||
- The UI process links the **stub** library and starts/binds the service early (Activity startup).
|
||||
- The service process loads **real** `libstatus.so` via the JNI wrapper and handles all status-go calls.
|
||||
- The AIDL surface is intentionally minimal to avoid churn.
|
||||
|
||||
### Lifetime behavior and mitigations
|
||||
|
||||
- Service starts **foreground immediately** to avoid `ForegroundServiceDidNotStartInTime`.
|
||||
- Uses `START_STICKY` so the OS can restart it.
|
||||
- On logout, clears session markers and stops itself.
|
||||
- `DeadObjectException` is handled in the client with a rebind and single retry.
|
||||
- Resume marker file exists for telemetry only and is deleted on service start; it must not be used for resume gating.
|
||||
|
||||
## What was considered (transport options)
|
||||
|
||||
- **Binder (AIDL)**: chosen. Low latency, lifecycle-aware, Android-native.
|
||||
- **Local TCP/Unix + JSON-RPC**: more security hardening and weaker lifecycle integration.
|
||||
- **Typed AIDL per method**: too much churn as status-go evolves.
|
||||
- **gRPC/protobuf IPC**: heavier stack without solving Android lifecycle constraints.
|
||||
|
||||
## What next
|
||||
|
||||
- **Lower battery impact:** add a true sleep mode in status-go (suspend Waku), with wake on app open or push.
|
||||
- **Transport replaceability:** keep the generic call/args shape as a stable boundary so Binder could be swapped for another IPC later with minimal churn.
|
||||
|
||||
## References (code)
|
||||
|
||||
- Service process: [mobile/android/qt6/src/app/status/mobile/ipc/StatusGoService.java](https://github.com/status-im/status-app/blob/master/mobile/android/qt6/src/app/status/mobile/ipc/StatusGoService.java)
|
||||
- AIDL: [mobile/android/qt6/aidl/app/status/mobile/ipc/IStatusGoService.aidl](https://github.com/status-im/status-app/blob/master/mobile/android/qt6/aidl/app/status/mobile/ipc/IStatusGoService.aidl)
|
||||
- UI client + stub: [mobile/android/qt6/src/app/status/mobile/ipc/StatusGoServiceClient.java](https://github.com/status-im/status-app/blob/master/mobile/android/qt6/src/app/status/mobile/ipc/StatusGoServiceClient.java),
|
||||
[mobile/android/qt6/src/app/status/mobile/StatusGoStub.java](https://github.com/status-im/status-app/blob/master/mobile/android/qt6/src/app/status/mobile/StatusGoStub.java)
|
||||
- Resume gating: [src/app/modules/onboarding/module.nim](https://github.com/status-im/status-app/blob/master/src/app/modules/onboarding/module.nim), [src/app/boot/app_controller.nim](https://github.com/status-im/status-app/blob/master/src/app/boot/app_controller.nim)
|
||||
- Stub generator: [vendor/status-go/tools/generate-stub-bindings](https://github.com/status-im/status-app/blob/master/vendor/status-go/tools/generate-stub-bindings)
|
||||
|
||||
### Data/control flow (signals)
|
||||
|
||||
1. status-go emits a JSON signal in the service process.
|
||||
2. JNI calls back into `StatusGoService.onNativeSignal(json)`.
|
||||
3. Service broadcasts to registered `IStatusGoSignalListener` clients.
|
||||
4. UI client forwards into native `StatusGoStub.nativeDeliverSignal(json)` so existing signal pipelines keep working.
|
||||
|
||||
### Login state detection (“should we show onboarding or AppMain?”)
|
||||
|
||||
Goal: avoid file-based heuristics and decide based on the **live** service state.
|
||||
|
||||
- UI startup performs a short poll loop before loading `main.qml`:
|
||||
- if a keyUid is returned and matches a local account, UI resumes into AppMain.
|
||||
- else UI proceeds to onboarding.
|
||||
|
||||
Key design constraint:
|
||||
|
||||
- **Never claim “logged in” after OS-kill** when the service process was actually terminated.
|
||||
- Keys are in memory; if the process died, they are gone.
|
||||
- Therefore the service must not “restore logged-in” from a stale file marker.
|
||||
|
||||
## Service lifetime design
|
||||
|
||||
Android lifetime is not a single mode; this proposal outlines multiple modes depending on user settings and device capabilities.
|
||||
|
||||
### Mode A: Foreground “keepalive” while logged in (status-mobile style)
|
||||
|
||||
**When**: user enables messenger notifications / wants background message processing.
|
||||
|
||||
**How**:
|
||||
|
||||
- Service promotes itself to a foreground service *only while logged in*.
|
||||
- Foreground notification indicates “Status is running”.
|
||||
- Use `START_STICKY` so system can restart the service process if needed.
|
||||
|
||||
**Pros**
|
||||
- Best reliability across OEMs.
|
||||
- Allows local decrypted notifications without push round-trips.
|
||||
- UI process can die and be restarted; service stays alive.
|
||||
|
||||
**Cons**
|
||||
- Always-on notification (acceptable by product decision, but not ideal UX for all users).
|
||||
- Power usage can be non-trivial if Waku stays connected.
|
||||
|
||||
### Mode B: “Push-driven / on-demand” service (sleep until wake)
|
||||
|
||||
**When**: user disables background messaging or wants reduced resource usage; also useful for “push enabled” devices.
|
||||
|
||||
**How (conceptual)**:
|
||||
|
||||
- Keep the service process **not running** most of the time (or running but “suspended”).
|
||||
- Wake it only when needed:
|
||||
- app is opened, or
|
||||
- a push notification arrives (GMS/FCM path).
|
||||
|
||||
**Two implementation variants**
|
||||
|
||||
1) **Hard sleep (process not running)**
|
||||
- Don’t run the service until needed.
|
||||
- On push: start service, run minimal work, stop service.
|
||||
- **Constraint**: cannot keep keys unlocked across kills → this only supports “wake for sync” if keys can be unlocked.
|
||||
|
||||
2) **Soft sleep (process alive but networking suspended)**
|
||||
- Service remains alive (likely still foreground or exempted), but Waku/network subsystems are paused.
|
||||
- On push/app-open: resume networking and process.
|
||||
- Requires explicit status-go API support:
|
||||
- e.g. `wakuext_suspend()` / `wakuext_resume()` or a high-level “messenger sleep” toggle.
|
||||
|
||||
**Pros**
|
||||
- Reduced battery/network usage vs always-connected Waku.
|
||||
- Can be aligned with “push as wakeup” concept - similar to Signal.
|
||||
|
||||
**Cons / risks**
|
||||
- Android push delivery is not guaranteed (Doze, OEM restrictions, network).
|
||||
- FCM data messages may be delayed or dropped when app is background restricted.
|
||||
- For encrypted pushes: decryption still needs keys; if process was killed, you are effectively logged out for background work.
|
||||
|
||||
### Mode C: Hybrid (recommended long-term)
|
||||
|
||||
- Use Mode A for users who enable background messaging (most reliable).
|
||||
- Consider Mode B (soft sleep) as an optimization knob:
|
||||
- keep service alive (foreground) but pause Waku when idle,
|
||||
- wake on push or periodic lightweight alarms.
|
||||
|
||||
## App resilience
|
||||
|
||||
### Scenario matrix
|
||||
|
||||
| Scenario | UI process | Service process | Expected UX |
|
||||
|---|---:|---:|---|
|
||||
| User closes window / swipe away Recents (service is foreground) | killed | alive | Next open: **resume to AppMain** (no login) |
|
||||
| UI process killed by OS (service alive) | killed | alive | Next open: **resume to AppMain** |
|
||||
| OS kills service process (memory pressure / user force-stop) | any | killed | Next open: **onboarding** (must re-auth) |
|
||||
| App update/reinstall leads to stale Binder | restarted | restarted | Client must detect `DeadObjectException`, reconnect and retry once |
|
||||
|
||||
### Lifetime & failure analysis (detailed)
|
||||
|
||||
This section describes what happens for key lifecycle/failure events and how the design should behave.
|
||||
|
||||
#### UI crash (Qt/Nim/QML process crash)
|
||||
|
||||
- **What happens**
|
||||
- UI process terminates (SIGABRT/SIGSEGV/uncaught exception).
|
||||
- Binder callbacks to UI will fail; the service removes dead binder clients.
|
||||
- **Expected behavior**
|
||||
- **Service keeps running** (when foreground keepalive is enabled / policy keeps it alive).
|
||||
- Messages can continue syncing; local-notifications can still be produced by the service.
|
||||
- On next app open, UI should **query ``status_account.getActiveAccount()``** and resume if the service is logged in.
|
||||
- **Mitigations**
|
||||
- `RemoteCallbackList` on the service side to clean up dead callbacks.
|
||||
- UI binder client reconnects on restart (and retries once on `DeadObjectException`).
|
||||
|
||||
#### Service crash (Java Service process crash)
|
||||
|
||||
Examples: `StatusGoService` throws, process hits ANR, or the OS kills the service due to a crash.
|
||||
|
||||
- **What happens**
|
||||
- UI binder calls fail with `DeadObjectException`.
|
||||
- Any in-memory keys/session state are lost with the process.
|
||||
- **Expected behavior**
|
||||
- UI resets binder connection, rebinds, and retries once.
|
||||
- If the service comes back but is **not logged in**, UI falls back to onboarding.
|
||||
- UI must not auto-resume from any stale marker; “logged-in” is authoritative only when returned by the live service.
|
||||
- **Mitigations**
|
||||
- Reconnect+retry once on `DeadObjectException` (client-side).
|
||||
- Optional future: a “service ready” signal/handshake so UI does not call feature RPCs before status-go is initialized.
|
||||
|
||||
#### status-go crash (native crash inside the service process)
|
||||
|
||||
This is a special case of service crash where the crash occurs in `libstatus.so` (JNI/native).
|
||||
|
||||
- **What happens**
|
||||
- The service process dies (tombstone / fatal signal).
|
||||
- On restart, the service process is “fresh” and **not logged in** (keys lost).
|
||||
- **Expected behavior**
|
||||
- UI treats this as “service not logged in” and shows onboarding.
|
||||
- If configured `START_STICKY`, Android may restart the service, but it still won’t have unlocked keys.
|
||||
- **Mitigations**
|
||||
- Optional: telemetry and a UI-visible “backend crashed” banner when this happens while UI is active.
|
||||
- Optional: explicit version/handshake checks so UI can detect a “fresh” backend.
|
||||
|
||||
#### App reinstall / update
|
||||
|
||||
We distinguish two common cases:
|
||||
|
||||
1) **Update install (same package, data typically preserved)**
|
||||
- **What happens**
|
||||
- UI process and service process may be restarted during update.
|
||||
- Existing Binder handles can become stale → `DeadObjectException`.
|
||||
- **Expected behavior**
|
||||
- UI reconnects/retries once.
|
||||
- If the service process was restarted, it is not logged in; UI should show onboarding.
|
||||
- If the service survived (rare during update), UI can resume if `status_account.getActiveAccount()` returns a non-empty account.
|
||||
- **Mitigations**
|
||||
- Keep reconnection logic in the client.
|
||||
|
||||
2) **Uninstall + install (fresh install)**
|
||||
- **What happens**
|
||||
- Android removes the app package and kills all its processes.
|
||||
- App-private data directory is removed; the service cannot persist.
|
||||
- **Expected behavior**
|
||||
- Fresh install behaves as logged-out; onboarding shown.
|
||||
- **Mitigations**
|
||||
- None required; this is expected platform behavior.
|
||||
|
||||
#### App uninstall
|
||||
|
||||
- **What happens**
|
||||
- Android kills UI + service processes.
|
||||
- All app private storage is removed.
|
||||
- **Expected behavior**
|
||||
- No background work remains; no notifications from the app after uninstall.
|
||||
- **Mitigations**
|
||||
- None; ensure no exported components allow external restarts after uninstall (standard Android packaging).
|
||||
|
||||
### Why “logged-in” must be an in-memory fact
|
||||
|
||||
If the service process is not alive, *there is no unlocked key material*. Any persistent marker would be misleading and can cause:
|
||||
|
||||
- UI “auto-resume” into logged-in flows,
|
||||
- immediate backend RPC failures,
|
||||
- unstable teardown (crashes) due to inconsistent state.
|
||||
|
||||
Therefore:
|
||||
|
||||
- The canonical “logged-in” state is the live service’s in-memory keyUid.
|
||||
- Persistence markers are allowed only as debugging/telemetry, not as a resume authority.
|
||||
|
||||
## Push notifications approach
|
||||
|
||||
### Current baseline (status-mobile style)
|
||||
|
||||
- Android chat notifications are primarily delivered via **status-go local-notifications** while the service is running.
|
||||
- Remote “generic chat push” is avoided to prevent duplicates and low-quality content.
|
||||
|
||||
### Proposed evolution: push as a wake-up signal
|
||||
|
||||
This ADR supports a roadmap where FCM acts as a wake signal when enabled.
|
||||
|
||||
#### Option 1: Wake service and resume Waku (no payload decryption in push handler)
|
||||
|
||||
- Push arrives (data message).
|
||||
- Start service (if not running) or signal it (if running).
|
||||
- Service wakes Waku and lets the normal message sync deliver messages.
|
||||
- status-go generates decrypted local-notifications as messages arrive.
|
||||
|
||||
**Pros**
|
||||
- Minimal cryptographic surface in the push handler.
|
||||
- Avoids needing to decrypt message payload directly from push.
|
||||
|
||||
**Cons**
|
||||
- Requires Waku to sync quickly after wake; might still be delayed by background constraints.
|
||||
|
||||
#### Option 2: Decrypt push payload directly (advanced)
|
||||
|
||||
- Push payload includes encrypted message preview.
|
||||
- Service decrypts immediately and posts a local notification without waiting for Waku.
|
||||
|
||||
**Pros**
|
||||
- Faster perceived notifications.
|
||||
|
||||
**Cons**
|
||||
- Key availability constraints remain (service must be alive + unlocked keys).
|
||||
- Higher complexity and security review surface.
|
||||
|
||||
### Non-GMS devices fallback
|
||||
|
||||
For non-GMS builds/devices, a push-driven model is not reliable. The fallback is:
|
||||
|
||||
- foreground keepalive (Mode A), or
|
||||
- periodic background work via WorkManager/AlarmManager (best-effort), understanding delivery limits.
|
||||
|
||||
## IPC / bindings analysis
|
||||
|
||||
### Proposed approach: Binder (AIDL) + “C API stub”
|
||||
|
||||
**What we would do**
|
||||
|
||||
- Keep the existing Nim ↔ C API boundary stable: Nim calls a C ABI “status-go” API.
|
||||
- Replace the implementation behind those symbols in the UI process with a stub that forwards to the service.
|
||||
- Use Android-native IPC (Binder + AIDL) for robustness and performance.
|
||||
|
||||
**Why this is a good fit (for Android)**
|
||||
|
||||
- Low latency IPC, lifecycle-aware, well-supported by Android tooling.
|
||||
- Easy to build “signals” using `RemoteCallbackList`.
|
||||
- AIDL gives a typed interface and a stable contract surface.
|
||||
|
||||
**Costs**
|
||||
|
||||
- Two binaries/processes to reason about.
|
||||
- Any new status-go API surface must be exposed through:
|
||||
- stub symbol, Java bridge, AIDL method (or generic `call`), service implementation.
|
||||
- More complex debugging (two PIDs, logcat filtering).
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
#### 1) In-process status-go (no IPC)
|
||||
|
||||
**Pros**
|
||||
- Simplest integration surface.
|
||||
- No IPC glue.
|
||||
|
||||
**Cons**
|
||||
- Cannot survive UI process death.
|
||||
- Decrypted notifications become unreliable.
|
||||
- Hard to make resilient to Android lifecycle.
|
||||
|
||||
Considered but not recommended for Android given the resilience requirements.
|
||||
|
||||
#### 2) Local TCP/Unix socket + JSON-RPC
|
||||
|
||||
**Pros**
|
||||
- Cross-platform story (same IPC on desktop/mobile).
|
||||
- Can reuse existing JSON-RPC patterns.
|
||||
|
||||
**Cons**
|
||||
- More security hardening required (authn, binding to loopback, file permissions).
|
||||
- Harder lifecycle integration vs Binder.
|
||||
- Performance overhead and more error-prone in Android background conditions.
|
||||
|
||||
Considered; not recommended as the primary Android path due to lifecycle/security/perf trade-offs vs Binder.
|
||||
|
||||
#### 3) AIDL with typed methods for every status-go call
|
||||
|
||||
**Pros**
|
||||
- Fully typed contract; compile-time safety.
|
||||
|
||||
**Cons**
|
||||
- Large API surface; heavy maintenance as status-go evolves.
|
||||
- Code generation churn.
|
||||
|
||||
We propose keeping a **generic `call(method, argsJson)`** RPC shape to contain churn.
|
||||
|
||||
#### 4) gRPC / protobuf IPC
|
||||
|
||||
**Pros**
|
||||
- Strongly typed; tooling ecosystem.
|
||||
|
||||
**Cons**
|
||||
- Significant additional stack; lifecycle and background constraints unchanged.
|
||||
- More complexity than needed for in-device IPC.
|
||||
|
||||
Considered; likely too heavy for on-device IPC in this context.
|
||||
|
||||
### Code generation strategy
|
||||
|
||||
The current approach minimizes codegen:
|
||||
|
||||
- AIDL generates `IStatusGoService` and `IStatusGoSignalListener`.
|
||||
- The C ABI stub bindings are generated from `libstatus.h` (exported C API) by `vendor/status-go/tools/generate-stub-bindings`.
|
||||
- Outputs are written to `vendor/status-go/build/bin/` and consumed by the mobile build.
|
||||
- Everything else stays “generic method + json args”.
|
||||
|
||||
If stronger typing becomes valuable later, we could add **a thin typed facade** on top of the generic call without changing the underlying transport.
|
||||
|
||||
## Operational notes / maintenance burden
|
||||
|
||||
### What this adds to day-to-day development
|
||||
|
||||
- Two-process debugging:
|
||||
- UI PID and service PID.
|
||||
- Need to filter logs per PID when diagnosing startup/resume.
|
||||
- Binder failure modes:
|
||||
- `DeadObjectException` must be handled with reconnect + retry.
|
||||
- Service version mismatch risks after upgrades.
|
||||
- Build complexity:
|
||||
- Separate JNI wrapper library (`status_service`) and stub library in the UI process.
|
||||
|
||||
### Mitigations
|
||||
|
||||
- Keep the AIDL surface minimal:
|
||||
- `call/callToFile`, `registerSignalListener`.
|
||||
- Use a single “gateway” for RPC errors and reconnect behavior:
|
||||
- `StatusGoServiceClient.call()` retries once on `DeadObjectException`.
|
||||
- Keep “login state” checks authoritative and cheap:
|
||||
- ``status_account.getActiveAccount()`` used during startup gating.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- UI can restart without losing the backend session (when service is alive).
|
||||
- Enables decrypted notifications via status-go local notifications pipeline.
|
||||
- Allows explicit lifetime policies (foreground keepalive vs on-demand).
|
||||
|
||||
### Negative / trade-offs
|
||||
|
||||
- Additional code and maintenance surface (service, IPC, stub).
|
||||
- “Always-on” notification required for the most reliable background mode.
|
||||
- If OS kills the service process, the user must re-authenticate (expected, secure behavior).
|
||||
|
||||
## Follow-ups / roadmap
|
||||
|
||||
### Near-term hardening
|
||||
|
||||
- Add structured telemetry for:
|
||||
- service start reasons (app open vs push)
|
||||
- time-to-ready
|
||||
- reconnect/retry counts
|
||||
|
||||
### Sleep / wake work (if desired)
|
||||
|
||||
- Define status-go API for suspend/resume of Waku/messenger (soft sleep).
|
||||
- Tie sleep policy to:
|
||||
- notifications enabled
|
||||
- device doze state
|
||||
- network type / battery saver
|
||||
- Implement “push wake” integration that triggers resume without decrypting push payload initially.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
|
||||
# Architecture of the Status App
|
||||
|
||||
## Architecture Decision Records (ADRs)
|
||||
|
||||
- [docs/adr/0001-android-status-go-as-a-service.md](/docs/adr/0001-android-status-go-as-a-service.md): Android architecture where `status-go` runs in a separate Service process and the UI talks to it via Binder IPC.
|
||||
|
||||
## Top level architecture
|
||||
|
||||
@@ -8,6 +11,7 @@ This shows the flow from the UI all the way to the backend.
|
||||
|
||||
We do not use servers. [status-go](https://github.com/status-im/status-go) is what our app considers the backend and as such, it has it's own local databases to contain the user data.
|
||||
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
qml["Frontend (QML)"] --> statusq(["StatusQ"])
|
||||
|
||||
+30
-3
@@ -1,8 +1,6 @@
|
||||
-include ./scripts/EnvVariables.mk
|
||||
-include ./scripts/Common.mk
|
||||
|
||||
STATUS_GO_LIB := $(LIB_PATH)/libstatus$(LIB_EXT)
|
||||
|
||||
# FLAG_KEYCARD_ENABLED: Controls NFC/Keycard support
|
||||
# - iOS: Default 1 (enabled) - Build with NFC support, works with free Apple Developer account
|
||||
# - Android: Default 1 (enabled) - NFC support doesn't require paid account on Android
|
||||
@@ -40,6 +38,7 @@ ifeq ($(OS),android)
|
||||
ANDROID_NDK_ROOT="$(ANDROID_NDK_ROOT)" \
|
||||
ANDROID_API="$(ANDROID_API)" \
|
||||
HOST_OS="$(HOST_OS)" \
|
||||
NIM_SDS_SOURCE_DIR="$(NIM_SDS_SOURCE_DIR)" \
|
||||
USE_SYSTEM_NIM=$(USE_SYSTEM_NIM) \
|
||||
GO_GENERATE_CMD="go generate" \
|
||||
SHELL=/bin/sh
|
||||
@@ -55,6 +54,29 @@ endif
|
||||
@cp $(NIM_SDS_SOURCE_DIR)/build/libsds$(LIB_EXT) $(LIB_PATH)/libsds$(LIB_EXT)
|
||||
@cp ../vendor/status-go/build/bin/libstatus$(LIB_EXT) $(LIB_PATH)/libstatus$(LIB_EXT)
|
||||
|
||||
$(STATUS_GO_STUB_LIB): $(STATUS_GO_LIB)
|
||||
@echo "Building status-go stub library (UI process)"
|
||||
@mkdir -p $(LIB_PATH)
|
||||
@$(MAKE) $(STATUS_GO_STUB_GEN)
|
||||
@$(CXX) -shared -fPIC -O2 \
|
||||
-o $(STATUS_GO_STUB_LIB) \
|
||||
$(STATUS_DESKTOP)/mobile/statusgo_stub/statusgo_stub.cpp \
|
||||
$(STATUS_GO_STUB_GEN) \
|
||||
-llog -lc++_shared
|
||||
|
||||
$(STATUS_GO_SERVICE_LIB): $(STATUS_GO_LIB)
|
||||
@echo "Building status-go service JNI library (service process)"
|
||||
@mkdir -p $(LIB_PATH)
|
||||
@$(MAKE) $(STATUS_GO_STUB_GEN)
|
||||
@$(CXX) -shared -fPIC -O2 \
|
||||
-o $(STATUS_GO_SERVICE_LIB) \
|
||||
$(STATUS_DESKTOP)/mobile/statusgo_service/statusgo_service_jni.cpp \
|
||||
$(STATUS_GO_SERVICE_GEN) \
|
||||
-L$(LIB_PATH) -lstatus -llog -lc++_shared
|
||||
|
||||
$(STATUS_GO_STUB_GEN):
|
||||
@$(MAKE) -C $(STATUS_DESKTOP)/vendor/status-go statusgo-stub-bindings
|
||||
|
||||
$(STATUS_Q_LIB): $(STATUS_Q_FILES) $(STATUS_Q_SCRIPT) $(STATUS_Q_UI_FILES)
|
||||
@echo "Building StatusQ"
|
||||
@STATUSQ=$(STATUSQ) QT_MAJOR=$(QT_MAJOR) LIB_SUFFIX=$(LIB_SUFFIX) LIB_EXT=$(LIB_EXT) $(STATUS_Q_SCRIPT) $(HANDLE_OUTPUT)
|
||||
@@ -84,7 +106,12 @@ $(STATUS_DESKTOP_RCC): $(STATUS_DESKTOP_UI_FILES) compile-translations
|
||||
@make -C $(STATUS_DESKTOP) rcc $(HANDLE_OUTPUT)
|
||||
@echo "Status Desktop rcc built"
|
||||
|
||||
$(NIM_STATUS_CLIENT_LIB): $(STATUS_DESKTOP_NIM_FILES) $(NIM_STATUS_CLIENT_SCRIPT) $(STATUS_DESKTOP_RCC) $(DOTHERSIDE_LIB) $(OPENSSL_LIB) $(STATUS_Q_LIB) $(STATUS_GO_LIB) $(QRCODEGEN_LIB)
|
||||
|
||||
ifeq ($(OS),android)
|
||||
$(NIM_STATUS_CLIENT_LIB): $(STATUS_GO_STUB_LIB) $(STATUS_GO_SERVICE_LIB)
|
||||
endif
|
||||
|
||||
$(NIM_STATUS_CLIENT_LIB): $(STATUS_DESKTOP_NIM_FILES) $(NIM_STATUS_CLIENT_SCRIPT) $(STATUS_DESKTOP_RCC) $(DOTHERSIDE_LIB) $(OPENSSL_LIB) $(STATUS_Q_LIB) $(QRCODEGEN_LIB)
|
||||
@echo "Building Status Desktop Lib"
|
||||
@STATUS_DESKTOP=$(STATUS_DESKTOP) \
|
||||
LIB_SUFFIX=$(LIB_SUFFIX) \
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.status.mobile"
|
||||
android:installLocation="auto">
|
||||
<!--
|
||||
Signature-level permission used to harden Binder IPC between the UI process and the
|
||||
separate-process StatusGoService. This is defense-in-depth on top of android:exported="false".
|
||||
-->
|
||||
<permission
|
||||
android:name="app.status.mobile.permission.STATUSGO_SERVICE"
|
||||
android:label="Status status-go service access"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<uses-permission android:name="app.status.mobile.permission.STATUSGO_SERVICE" />
|
||||
<!-- non-dangerous permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.NFC"/>
|
||||
@@ -10,6 +20,8 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<!-- features -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
@@ -116,5 +128,17 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/qtprovider_paths"/>
|
||||
</provider>
|
||||
|
||||
<!-- Separate-process status-go host service -->
|
||||
<service
|
||||
android:name=".ipc.StatusGoService"
|
||||
android:exported="false"
|
||||
android:permission="app.status.mobile.permission.STATUSGO_SERVICE"
|
||||
android:process=":statusgo"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="status-go background" />
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package app.status.mobile.ipc;
|
||||
|
||||
import app.status.mobile.ipc.IStatusGoSignalListener;
|
||||
|
||||
interface IStatusGoService {
|
||||
/** Generic call into status-go exports (method name is the C export name). */
|
||||
String call(String method, String argsJson);
|
||||
|
||||
/**
|
||||
* Same as call(), but writes the response to a file in the service's cache dir
|
||||
* and returns the absolute file path. This avoids Binder size limits for large JSON.
|
||||
*/
|
||||
String callToFile(String method, String argsJson);
|
||||
|
||||
/** Register a signal listener. */
|
||||
void registerSignalListener(IStatusGoSignalListener listener);
|
||||
|
||||
/** Unregister a signal listener. */
|
||||
void unregisterSignalListener(IStatusGoSignalListener listener);
|
||||
|
||||
/**
|
||||
* UI visibility hint used for notification suppression.
|
||||
*
|
||||
* If {@code visible=true}, the UI is in foreground; the service should not post OS
|
||||
* message notifications (to avoid duplicates / to match “only when background” behavior).
|
||||
*/
|
||||
void setUiVisible(boolean visible);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package app.status.mobile.ipc;
|
||||
|
||||
/** One-way signal stream from status-go service to UI process. */
|
||||
oneway interface IStatusGoSignalListener {
|
||||
void onSignal(String jsonSignal);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ android {
|
||||
buildToolsVersion androidBuildToolsVersion
|
||||
ndkVersion = androidNdkVersion
|
||||
|
||||
buildFeatures {
|
||||
aidl true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package app.status.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import app.status.mobile.ipc.StatusGoServiceClient;
|
||||
|
||||
/**
|
||||
* UI-process bridge used by the native status-go stub library (libstatus_stub.so).
|
||||
*
|
||||
* For now this is a placeholder: it provides a stable Java surface for the native stub
|
||||
* to call into. Next step is to back this by a Binder client that talks to the
|
||||
* separate status-go service process.
|
||||
*/
|
||||
public final class StatusGoStub {
|
||||
static {
|
||||
// Loads libstatus_stub.so (needed for JNI_OnLoad + nativeInit).
|
||||
System.loadLibrary("status_stub");
|
||||
}
|
||||
|
||||
private StatusGoStub() {}
|
||||
|
||||
// Native: supplies the Java class that implements call().
|
||||
private static native void nativeInit(Class<?> bridgeClass);
|
||||
|
||||
// Native: used later to deliver signals from Binder listener to Nim callback.
|
||||
public static native void nativeDeliverSignal(String jsonSignal);
|
||||
|
||||
/** Must be called early (e.g. Activity.onCreate) to bind the Java bridge. */
|
||||
public static void ensureInitialized(Context context) {
|
||||
nativeInit(StatusGoStub.class);
|
||||
// Start/bind the status-go service early so first RPC doesn't block too long.
|
||||
StatusGoServiceClient.get().ensureStartedAndBound(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from native status-go stub.
|
||||
* @param method status-go exported method name (e.g. "CallPrivateRPC")
|
||||
* @param argsJson JSON array of string args (placeholder encoding for now)
|
||||
*/
|
||||
public static String call(String method, String argsJson) {
|
||||
// Called from native stub exports; forward to separate-process service.
|
||||
if (sContext == null) {
|
||||
return "{\"error\":\"StatusGoStub not initialized\"}";
|
||||
}
|
||||
return StatusGoServiceClient.get().call(sContext, method, argsJson);
|
||||
}
|
||||
|
||||
/** Hint to the service whether the UI is currently visible (foreground). */
|
||||
public static void setUiVisible(boolean visible) {
|
||||
if (sContext == null) return;
|
||||
StatusGoServiceClient.get().setUiVisible(sContext, visible);
|
||||
}
|
||||
|
||||
private static volatile Context sContext;
|
||||
|
||||
/** Called by Activity. Keep application context for later native calls. */
|
||||
public static void setContext(Context context) {
|
||||
sContext = context.getApplicationContext();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package app.status.mobile;
|
||||
import org.qtproject.qt.android.bindings.QtActivity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.content.pm.PackageManager;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.Settings;
|
||||
import im.status.mobileui.PushNotificationHelper;
|
||||
|
||||
public class StatusQtActivity extends QtActivity {
|
||||
private static final AtomicBoolean splashShouldHide = new AtomicBoolean(false);
|
||||
@@ -21,6 +23,14 @@ public class StatusQtActivity extends QtActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
// Initialize the status-go UI stub bridge early.
|
||||
// (In the service-based architecture this forwards to the separate status-go process.)
|
||||
StatusGoStub.setContext(this);
|
||||
StatusGoStub.ensureInitialized(this);
|
||||
|
||||
// IMPORTANT: call super.onCreate() after starting/binding the service.
|
||||
// QtActivity may start the Qt (Nim) side during super.onCreate(), and the Nim
|
||||
// onboarding resume check queries the service immediately on startup.
|
||||
super.onCreate(savedInstanceState);
|
||||
sInstance = this;
|
||||
|
||||
@@ -38,11 +48,15 @@ public class StatusQtActivity extends QtActivity {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
ShakeDetector.onResume(this);
|
||||
// Inform the status-go service that UI is visible so it can suppress OS notifications.
|
||||
StatusGoStub.setUiVisible(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
ShakeDetector.onPause();
|
||||
// Inform the status-go service that UI is no longer in foreground.
|
||||
StatusGoStub.setUiVisible(false);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
package app.status.mobile.ipc;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteCallbackList;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import im.status.mobileui.PushNotificationHelper;
|
||||
|
||||
/**
|
||||
* Separate-process status-go host.
|
||||
*
|
||||
* Runs in its own Android process (see AndroidManifest.xml) and is intended to be the
|
||||
* only process that links/uses the real libstatus.so. UI process talks to it over Binder.
|
||||
*/
|
||||
public final class StatusGoService extends Service {
|
||||
private static final String TAG = "StatusGoService";
|
||||
|
||||
public static final String ACTION_START = "app.status.mobile.ipc.StatusGoService.START";
|
||||
public static final String ACTION_STOP = "app.status.mobile.ipc.StatusGoService.STOP";
|
||||
|
||||
private static final String CHANNEL_ID = "statusgo";
|
||||
private static final int NOTIFICATION_ID = 4242;
|
||||
|
||||
private final RemoteCallbackList<IStatusGoSignalListener> listeners = new RemoteCallbackList<>();
|
||||
private volatile boolean foregroundStarted = false;
|
||||
private volatile boolean uiVisible = false;
|
||||
private volatile long uiVisibleLastUpdateMs = 0L;
|
||||
|
||||
static {
|
||||
// Loads libstatus_service.so (JNI wrapper that links real libstatus.so).
|
||||
System.loadLibrary("status_service");
|
||||
}
|
||||
|
||||
private static native void nativeInit(StatusGoService self);
|
||||
private static native String nativeCall(String method, String argsJson);
|
||||
|
||||
/**
|
||||
* Defense-in-depth: ensure only our own app UID can invoke Binder methods.
|
||||
*
|
||||
* Note: this service is also declared with android:exported="false" and a signature-level
|
||||
* permission in AndroidManifest.xml. This runtime check protects against accidental manifest
|
||||
* changes and makes the security property explicit at the IPC boundary.
|
||||
*/
|
||||
private void enforceCallerIsSameApp() {
|
||||
final int callingUid = Binder.getCallingUid();
|
||||
final int myUid = getApplicationInfo() != null ? getApplicationInfo().uid : -1;
|
||||
if (callingUid != myUid) {
|
||||
throw new SecurityException("Unauthorized caller uid=" + callingUid);
|
||||
}
|
||||
}
|
||||
|
||||
/** Called from native (status-go callback). */
|
||||
@SuppressWarnings("unused")
|
||||
private void onNativeSignal(String jsonSignal) {
|
||||
maybeStartForegroundFromSignal(jsonSignal);
|
||||
maybeShowOsNotificationFromSignal(jsonSignal);
|
||||
|
||||
final int n = listeners.beginBroadcast();
|
||||
try {
|
||||
for (int i = 0; i < n; i++) {
|
||||
try {
|
||||
listeners.getBroadcastItem(i).onSignal(jsonSignal);
|
||||
} catch (RemoteException ignored) {
|
||||
// RemoteCallbackList handles dead clients.
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
listeners.finishBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show OS notifications from the service process.
|
||||
*
|
||||
* This is required to deliver OS notifications when the UI process is killed.
|
||||
* We suppress notifications when the UI is in foreground (uiVisible=true).
|
||||
*/
|
||||
private void maybeShowOsNotificationFromSignal(String jsonSignal) {
|
||||
if (jsonSignal == null || jsonSignal.isEmpty()) return;
|
||||
// If UI is (recently) visible, suppress OS notifications to avoid duplicates.
|
||||
// If UI crashed while visible, there will be no further heartbeats; fall back to showing
|
||||
// notifications after a short timeout.
|
||||
if (uiVisible) {
|
||||
final long now = System.currentTimeMillis();
|
||||
final long last = uiVisibleLastUpdateMs;
|
||||
if (last > 0 && (now - last) < 5000) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
final JSONObject root = new JSONObject(jsonSignal);
|
||||
final String type = root.optString("type", "");
|
||||
if ("local-notifications".equals(type)) {
|
||||
// Preferred path: status-go already computed title/body/deepLink for OS notifications.
|
||||
final JSONObject eventWrap = root.optJSONObject("event");
|
||||
if (eventWrap == null) return;
|
||||
|
||||
final boolean deleted = eventWrap.optBoolean("deleted", false);
|
||||
if (deleted) return;
|
||||
|
||||
final String title = eventWrap.optString("title", "");
|
||||
final String message = eventWrap.optString("message", "");
|
||||
final String deepLink = eventWrap.optString("deepLink", "");
|
||||
final String conversationId = eventWrap.optString("conversationId", "");
|
||||
|
||||
Log.d(TAG, "local-notifications received: title=" + title + " conversationId=" + conversationId);
|
||||
|
||||
final JSONObject identifier = new JSONObject();
|
||||
if (deepLink != null) identifier.put("deepLink", deepLink);
|
||||
if (conversationId != null) identifier.put("conversationId", conversationId);
|
||||
|
||||
PushNotificationHelper.showNotification(
|
||||
title != null ? title : "Status",
|
||||
message != null ? message : "",
|
||||
identifier.toString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// Best-effort only; do not crash the service for notification display.
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeStartForegroundFromSignal(String jsonSignal) {
|
||||
if (jsonSignal == null || jsonSignal.isEmpty()) return;
|
||||
try {
|
||||
final JSONObject root = new JSONObject(jsonSignal);
|
||||
final String type = root.optString("type", "");
|
||||
if (type.isEmpty()) return;
|
||||
|
||||
// On successful node login, keep this service as a foreground service so it survives
|
||||
// swipe-away from Recents.
|
||||
if ("node.login".equals(type)) {
|
||||
final JSONObject event = root.optJSONObject("event");
|
||||
if (event == null) return;
|
||||
final String err = event.optString("error", "");
|
||||
if (err != null && !err.isEmpty()) return;
|
||||
ensureForegroundStarted();
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// Best-effort only; don't crash the service.
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureForegroundStarted() {
|
||||
if (foregroundStarted) return;
|
||||
try {
|
||||
createNotificationChannel();
|
||||
startForeground(NOTIFICATION_ID, buildNotification());
|
||||
foregroundStarted = true;
|
||||
} catch (Throwable t) {
|
||||
// Best-effort only; don't crash service.
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeStopOnLogoutCall(String method, String respJson) {
|
||||
if (method == null) return;
|
||||
if (!method.equalsIgnoreCase("Logout")) return;
|
||||
if (respJson == null || respJson.isEmpty()) return;
|
||||
try {
|
||||
final JSONObject resp = new JSONObject(respJson);
|
||||
final String err = resp.optString("error", "");
|
||||
if (err != null && !err.isEmpty()) return;
|
||||
try {
|
||||
stopForeground(true);
|
||||
} catch (Throwable ignored) {}
|
||||
foregroundStarted = false;
|
||||
stopSelf();
|
||||
Log.i(TAG, "stopped after logout");
|
||||
} catch (Throwable t) {
|
||||
// Best-effort only.
|
||||
}
|
||||
}
|
||||
|
||||
private final IStatusGoService.Stub binder = new IStatusGoService.Stub() {
|
||||
@Override
|
||||
public String call(String method, String argsJson) {
|
||||
enforceCallerIsSameApp();
|
||||
String resp = nativeCall(method, argsJson);
|
||||
maybeStopOnLogoutCall(method, resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callToFile(String method, String argsJson) {
|
||||
enforceCallerIsSameApp();
|
||||
String resp = nativeCall(method, argsJson);
|
||||
if (resp == null) resp = "{\"error\":\"null response\"}";
|
||||
maybeStopOnLogoutCall(method, resp);
|
||||
try {
|
||||
File f = File.createTempFile("statusgo_", ".json", getCacheDir());
|
||||
try (FileOutputStream os = new FileOutputStream(f, false)) {
|
||||
os.write(resp.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return f.getAbsolutePath();
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "callToFile failed", t);
|
||||
return "{\"error\":\"callToFile failed\"}";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerSignalListener(IStatusGoSignalListener listener) {
|
||||
enforceCallerIsSameApp();
|
||||
if (listener != null) listeners.register(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterSignalListener(IStatusGoSignalListener listener) {
|
||||
enforceCallerIsSameApp();
|
||||
if (listener != null) listeners.unregister(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUiVisible(boolean visible) {
|
||||
enforceCallerIsSameApp();
|
||||
uiVisible = visible;
|
||||
uiVisibleLastUpdateMs = visible ? System.currentTimeMillis() : 0L;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.i(TAG, "onCreate()");
|
||||
// Do not automatically become a foreground service on creation. We only need to be
|
||||
// foreground while the user is logged in (then we survive swipe-away from Recents).
|
||||
PushNotificationHelper.initialize(this);
|
||||
nativeInit(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
final String action = intent != null ? intent.getAction() : null;
|
||||
Log.i(TAG, "onStartCommand action=" + action);
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
try {
|
||||
stopForeground(true);
|
||||
} catch (Throwable ignored) {}
|
||||
foregroundStarted = false;
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
// Ensure we can be started from background components (e.g. FCM) without risking
|
||||
// ForegroundServiceDidNotStartInTime. We can downgrade/stop later if needed.
|
||||
ensureForegroundStarted();
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.i(TAG, "onDestroy()");
|
||||
listeners.kill();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
if (nm == null) return;
|
||||
NotificationChannel ch = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Status background",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
ch.setDescription("Keeps Status background service running for messaging.");
|
||||
nm.createNotificationChannel(ch);
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Status is running")
|
||||
.setContentText("Background service for messaging and notifications")
|
||||
.setSmallIcon(android.R.drawable.stat_notify_chat)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
package app.status.mobile.ipc;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import app.status.mobile.StatusGoStub;
|
||||
|
||||
/** UI-process Binder client for {@link StatusGoService}. */
|
||||
public final class StatusGoServiceClient {
|
||||
private static final String TAG = "StatusGoServiceClient";
|
||||
private static final long CONNECT_TIMEOUT_MS = 8000;
|
||||
|
||||
private static volatile StatusGoServiceClient sInstance;
|
||||
|
||||
public static StatusGoServiceClient get() {
|
||||
if (sInstance == null) {
|
||||
synchronized (StatusGoServiceClient.class) {
|
||||
if (sInstance == null) sInstance = new StatusGoServiceClient();
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private final Object lock = new Object();
|
||||
private IStatusGoService service;
|
||||
private CountDownLatch connectedLatch;
|
||||
private boolean bound = false;
|
||||
|
||||
private final IStatusGoSignalListener signalListener = new IStatusGoSignalListener.Stub() {
|
||||
@Override
|
||||
public void onSignal(String jsonSignal) {
|
||||
// Forward into the native stub callback (SetSignalEventCallback).
|
||||
StatusGoStub.nativeDeliverSignal(jsonSignal);
|
||||
}
|
||||
};
|
||||
|
||||
private final ServiceConnection conn = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder binder) {
|
||||
synchronized (lock) {
|
||||
service = IStatusGoService.Stub.asInterface(binder);
|
||||
bound = true;
|
||||
try {
|
||||
service.registerSignalListener(signalListener);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "registerSignalListener failed", e);
|
||||
}
|
||||
if (connectedLatch != null) connectedLatch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
synchronized (lock) {
|
||||
service = null;
|
||||
connectedLatch = null;
|
||||
bound = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private StatusGoServiceClient() {}
|
||||
|
||||
private void resetConnection(Context appContext) {
|
||||
synchronized (lock) {
|
||||
service = null;
|
||||
connectedLatch = null;
|
||||
}
|
||||
try {
|
||||
if (bound) {
|
||||
appContext.getApplicationContext().unbindService(conn);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
} finally {
|
||||
synchronized (lock) {
|
||||
bound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureStartedAndBound(Context context) {
|
||||
final Context app = context.getApplicationContext();
|
||||
synchronized (lock) {
|
||||
if (service != null) return;
|
||||
if (connectedLatch == null) connectedLatch = new CountDownLatch(1);
|
||||
}
|
||||
|
||||
Intent i = new Intent(app, StatusGoService.class);
|
||||
i.setAction(StatusGoService.ACTION_START);
|
||||
try {
|
||||
// Start as a normal service; StatusGoService promotes itself to foreground only
|
||||
// when logged in.
|
||||
app.startService(i);
|
||||
} catch (Throwable t) {
|
||||
// Fallback: some OEMs are strict; best-effort start as foreground service.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(i);
|
||||
} else {
|
||||
app.startService(i);
|
||||
}
|
||||
}
|
||||
// Bind for request/response
|
||||
app.bindService(i, conn, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
public String call(Context context, String method, String argsJson) {
|
||||
final Context app = context.getApplicationContext();
|
||||
ensureStartedAndBound(app);
|
||||
IStatusGoService s;
|
||||
CountDownLatch latch;
|
||||
synchronized (lock) {
|
||||
s = service;
|
||||
latch = connectedLatch;
|
||||
}
|
||||
if (s == null && latch != null) {
|
||||
try {
|
||||
latch.await(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
synchronized (lock) {
|
||||
s = service;
|
||||
}
|
||||
if (s == null) {
|
||||
return "{\"error\":\"status-go service not connected\"}";
|
||||
}
|
||||
try {
|
||||
final String pathOrErr = s.callToFile(method, argsJson);
|
||||
if (pathOrErr == null) {
|
||||
return "{\"error\":\"status-go service returned null\"}";
|
||||
}
|
||||
if (!pathOrErr.startsWith("/")) {
|
||||
// service returned an error JSON (small)
|
||||
return pathOrErr;
|
||||
}
|
||||
final File f = new File(pathOrErr);
|
||||
byte[] data;
|
||||
try {
|
||||
data = Files.readAllBytes(f.toPath());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read response file: " + pathOrErr, e);
|
||||
return "{\"error\":\"failed to read response file\"}";
|
||||
} finally {
|
||||
// best-effort cleanup
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
f.delete();
|
||||
}
|
||||
return new String(data, StandardCharsets.UTF_8);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "call failed", e);
|
||||
// After reinstall/update (or service crash), binder can become a dead object.
|
||||
// Reset, rebind, and retry once to avoid cascading JSON parse errors upstream.
|
||||
if (e instanceof android.os.DeadObjectException) {
|
||||
resetConnection(app);
|
||||
ensureStartedAndBound(app);
|
||||
synchronized (lock) {
|
||||
s = service;
|
||||
latch = connectedLatch;
|
||||
}
|
||||
if (s == null && latch != null) {
|
||||
try {
|
||||
latch.await(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
synchronized (lock) {
|
||||
s = service;
|
||||
}
|
||||
if (s != null) {
|
||||
try {
|
||||
final String pathOrErr2 = s.callToFile(method, argsJson);
|
||||
if (pathOrErr2 == null) {
|
||||
return "{\"error\":\"status-go service returned null\"}";
|
||||
}
|
||||
if (!pathOrErr2.startsWith("/")) {
|
||||
return pathOrErr2;
|
||||
}
|
||||
final File f2 = new File(pathOrErr2);
|
||||
byte[] data2;
|
||||
try {
|
||||
data2 = Files.readAllBytes(f2.toPath());
|
||||
} catch (IOException io) {
|
||||
Log.w(TAG, "Failed to read response file: " + pathOrErr2, io);
|
||||
return "{\"error\":\"failed to read response file\"}";
|
||||
} finally {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
f2.delete();
|
||||
}
|
||||
return new String(data2, StandardCharsets.UTF_8);
|
||||
} catch (RemoteException e2) {
|
||||
Log.w(TAG, "call retry failed", e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "{\"error\":\"status-go service call failed\"}";
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort hint for whether UI is currently in foreground. */
|
||||
public void setUiVisible(Context context, boolean visible) {
|
||||
final Context app = context.getApplicationContext();
|
||||
ensureStartedAndBound(app);
|
||||
IStatusGoService s;
|
||||
synchronized (lock) {
|
||||
s = service;
|
||||
}
|
||||
if (s == null) return;
|
||||
try {
|
||||
s.setUiVisible(visible);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "setUiVisible failed", e);
|
||||
if (e instanceof android.os.DeadObjectException) {
|
||||
resetConnection(app);
|
||||
ensureStartedAndBound(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ DOTHERSIDE?=$(STATUS_DESKTOP)/vendor/DOtherSide
|
||||
OPENSSL?=$(ROOT_DIR)/vendors/openssl
|
||||
QRCODEGEN?=$(STATUS_DESKTOP)/vendor/QR-Code-generator/c
|
||||
STATUS_KEYCARD_QT?=$(STATUS_DESKTOP)/vendor/status-keycard-qt
|
||||
NIM_SDS_SOURCE_DIR ?= $(STATUS_DESKTOP)/vendor/nim-sds
|
||||
|
||||
# compile macros
|
||||
TARGET_PREFIX := Status
|
||||
@@ -62,6 +63,8 @@ OPENSSL_FILES := $(shell find $(OPENSSL) -type f \( -iname '*.c' -o -iname '*.h'
|
||||
QRCODEGEN_FILES := $(shell find $(QRCODEGEN) -type f \( -iname '*.c' -o -iname '*.h' \))
|
||||
STATUS_KEYCARD_QT_FILES := $(shell find $(STATUS_KEYCARD_QT) -type f \( -iname '*.cpp' -o -iname '*.h' \) 2>/dev/null || echo "")
|
||||
WRAPPER_APP_FILES := $(shell find $(WRAPPER_APP) -type f)
|
||||
STATUS_GO_STUB_GEN := $(STATUS_DESKTOP)/vendor/status-go/build/bin/statusgo_stub_exports.cpp
|
||||
STATUS_GO_SERVICE_GEN := $(STATUS_DESKTOP)/vendor/status-go/build/bin/statusgo_service_dispatch.cpp
|
||||
|
||||
# script files
|
||||
STATUS_Q_SCRIPT := $(SCRIPTS_PATH)/buildStatusQ.sh
|
||||
@@ -82,6 +85,8 @@ QRCODEGEN_LIB := $(LIB_PATH)/libqrcodegen.a
|
||||
STATUS_KEYCARD_QT_LIB := $(LIB_PATH)/libstatus-keycard-qt$(LIB_EXT)
|
||||
NIM_STATUS_CLIENT_LIB := $(LIB_PATH)/libnim_status_client$(LIB_EXT)
|
||||
STATUS_DESKTOP_RCC := $(STATUS_DESKTOP)/ui/resources.qrc
|
||||
STATUS_GO_STUB_LIB := $(LIB_PATH)/libstatus_stub$(LIB_EXT)
|
||||
STATUS_GO_SERVICE_LIB := $(LIB_PATH)/libstatus_service$(LIB_EXT)
|
||||
ifeq ($(OS), ios)
|
||||
DOTHERSIDE_LIB := $(LIB_PATH)/libDOtherSideStatic$(LIB_SUFFIX)$(LIB_EXT)
|
||||
LIB_ZXING := $(LIB_PATH)/libZXing$(LIB_SUFFIX)$(LIB_EXT)
|
||||
|
||||
@@ -67,6 +67,7 @@ if [[ "${OS}" == "android" ]]; then
|
||||
cp "$CWD/../android/qt${QT_MAJOR}"/{AndroidManifest.xml,build.gradle,settings.gradle,gradle.properties} "$BUILD_DIR/android-build/"
|
||||
rsync -a --exclude='libs.xml' "$CWD/../android/qt${QT_MAJOR}/res/" "$BUILD_DIR/android-build/res/" 2>/dev/null || true
|
||||
rsync -a "$CWD/../android/qt${QT_MAJOR}/src/" "$BUILD_DIR/android-build/src/" 2>/dev/null || true
|
||||
rsync -a "$CWD/../android/qt${QT_MAJOR}/aidl/" "$BUILD_DIR/android-build/aidl/" 2>/dev/null || true
|
||||
|
||||
if [[ -n "${MOBILEWEBVIEW_ANDROID_JAVA_SRC}" && \
|
||||
-f "${MOBILEWEBVIEW_ANDROID_JAVA_SRC}/org/mobilewebview/MobileWebView.java" ]]; then
|
||||
|
||||
@@ -32,7 +32,7 @@ if [[ "$OS" == "ios" ]]; then
|
||||
PLATFORM_SPECIFIC=(--app:staticlib -d:ios --os:ios)
|
||||
else
|
||||
PLATFORM_SPECIFIC=(--app:lib --os:android -d:android -d:androidNDK -d:chronicles_sinks=textlines[logcat],textlines[nocolors,dynamic],textlines[file,nocolors] \
|
||||
--passL="-L$LIB_DIR" --passL="-lstatus" --passL="-lStatusQ$LIB_SUFFIX" --passL="-lDOtherSide$LIB_SUFFIX" --passL="-lqrcodegen" --passL="-lssl_3" --passL="-lcrypto_3" --passL="-lstatus-keycard-qt" -d:taskpool)
|
||||
--passL="-L$LIB_DIR" --passL="-lstatus_stub" --passL="-lStatusQ$LIB_SUFFIX" --passL="-lDOtherSide$LIB_SUFFIX" --passL="-lqrcodegen" --passL="-lssl_3" --passL="-lcrypto_3" --passL="-lstatus-keycard-qt" -d:taskpool)
|
||||
fi
|
||||
|
||||
if [ -n "$USE_QML_SERVER" ]; then
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <cstring>
|
||||
|
||||
// Real status-go exports (from libstatus.so)
|
||||
extern "C" {
|
||||
typedef void (*SignalCallback)(const char* signalJson);
|
||||
void SetSignalEventCallback(SignalCallback cb);
|
||||
void Free(void* p);
|
||||
}
|
||||
|
||||
// Generated dispatcher (links against libstatus.so and calls real exports)
|
||||
extern "C" char* statusgo_service_dispatch(const char* method, const char** argv, size_t argc);
|
||||
|
||||
namespace {
|
||||
static JavaVM* g_vm = nullptr;
|
||||
static jobject g_serviceObj = nullptr; // Global ref
|
||||
static jmethodID g_onSignal = nullptr;
|
||||
static std::mutex g_lock;
|
||||
|
||||
static void loge(const char* msg) { __android_log_write(ANDROID_LOG_ERROR, "statusgo-service", msg); }
|
||||
|
||||
static JNIEnv* getEnv() {
|
||||
if (!g_vm) return nullptr;
|
||||
JNIEnv* env = nullptr;
|
||||
if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) return nullptr;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
static void signalCb(const char* signalJson) {
|
||||
std::lock_guard<std::mutex> guard(g_lock);
|
||||
if (!g_serviceObj || !g_onSignal) return;
|
||||
JNIEnv* env = getEnv();
|
||||
if (!env) return;
|
||||
jstring jSig = env->NewStringUTF(signalJson ? signalJson : "");
|
||||
env->CallVoidMethod(g_serviceObj, g_onSignal, jSig);
|
||||
env->DeleteLocalRef(jSig);
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal JSON array-of-strings parser:
|
||||
// Accepts the exact format produced by the UI stub runtime:
|
||||
// ["str1","str2",...]
|
||||
// with standard JSON escaping.
|
||||
static bool parseJsonString(const char*& p, std::string& out) {
|
||||
if (*p != '"') return false;
|
||||
++p;
|
||||
while (*p) {
|
||||
char c = *p++;
|
||||
if (c == '"') return true;
|
||||
if (c == '\\') {
|
||||
char e = *p++;
|
||||
switch (e) {
|
||||
case '"': out.push_back('"'); break;
|
||||
case '\\': out.push_back('\\'); break;
|
||||
case '/': out.push_back('/'); break;
|
||||
case 'b': out.push_back('\b'); break;
|
||||
case 'f': out.push_back('\f'); break;
|
||||
case 'n': out.push_back('\n'); break;
|
||||
case 'r': out.push_back('\r'); break;
|
||||
case 't': out.push_back('\t'); break;
|
||||
case 'u': {
|
||||
// Skip \uXXXX (best-effort; keep ASCII only for now)
|
||||
for (int i = 0; i < 4 && *p; i++) ++p;
|
||||
// Replace with '?'
|
||||
out.push_back('?');
|
||||
break;
|
||||
}
|
||||
default:
|
||||
out.push_back(e);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::vector<std::string> parseArgsJson(const char* argsJson) {
|
||||
std::vector<std::string> out;
|
||||
if (!argsJson) return out;
|
||||
const char* p = argsJson;
|
||||
while (*p && (*p == ' ' || *p == '\n' || *p == '\t' || *p == '\r')) ++p;
|
||||
if (*p != '[') return out;
|
||||
++p;
|
||||
while (*p) {
|
||||
while (*p && (*p == ' ' || *p == '\n' || *p == '\t' || *p == '\r')) ++p;
|
||||
if (*p == ']') break;
|
||||
std::string s;
|
||||
if (!parseJsonString(p, s)) break;
|
||||
out.push_back(std::move(s));
|
||||
while (*p && (*p == ' ' || *p == '\n' || *p == '\t' || *p == '\r')) ++p;
|
||||
if (*p == ',') { ++p; continue; }
|
||||
if (*p == ']') break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
|
||||
g_vm = vm;
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_app_status_mobile_ipc_StatusGoService_nativeInit(JNIEnv* env, jclass, jobject serviceObj) {
|
||||
std::lock_guard<std::mutex> guard(g_lock);
|
||||
if (g_serviceObj) {
|
||||
env->DeleteGlobalRef(g_serviceObj);
|
||||
g_serviceObj = nullptr;
|
||||
g_onSignal = nullptr;
|
||||
}
|
||||
g_serviceObj = env->NewGlobalRef(serviceObj);
|
||||
jclass cls = env->GetObjectClass(serviceObj);
|
||||
g_onSignal = env->GetMethodID(cls, "onNativeSignal", "(Ljava/lang/String;)V");
|
||||
if (!g_onSignal) {
|
||||
loge("Failed to find StatusGoService.onNativeSignal(String)");
|
||||
}
|
||||
env->DeleteLocalRef(cls);
|
||||
|
||||
// Register callback into status-go.
|
||||
SetSignalEventCallback(signalCb);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_app_status_mobile_ipc_StatusGoService_nativeCall(JNIEnv* env, jclass, jstring jMethod, jstring jArgsJson) {
|
||||
const char* method = jMethod ? env->GetStringUTFChars(jMethod, nullptr) : nullptr;
|
||||
const char* argsJson = jArgsJson ? env->GetStringUTFChars(jArgsJson, nullptr) : nullptr;
|
||||
|
||||
std::vector<std::string> args = parseArgsJson(argsJson);
|
||||
std::vector<const char*> argv;
|
||||
argv.reserve(args.size());
|
||||
for (auto& s : args) argv.push_back(s.c_str());
|
||||
|
||||
char* out = statusgo_service_dispatch(method ? method : "", argv.empty() ? nullptr : argv.data(), argv.size());
|
||||
const bool shouldFree = (out != nullptr);
|
||||
if (!out) out = (char*)"{\"error\":\"null return from dispatch\"}";
|
||||
|
||||
jstring jOut = env->NewStringUTF(out);
|
||||
|
||||
if (shouldFree) Free(out);
|
||||
|
||||
if (jMethod) env->ReleaseStringUTFChars(jMethod, method);
|
||||
if (jArgsJson) env->ReleaseStringUTFChars(jArgsJson, argsJson);
|
||||
|
||||
return jOut;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#include <jni.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <android/log.h>
|
||||
// Tiny UI-process stub for status-go's exported C API.
|
||||
// Instead of linking libstatus (real status-go) into the UI process, we export
|
||||
// the same symbols and forward them to a separate Android service process via Java.
|
||||
//
|
||||
// Note: For now, the Java side can be a placeholder. This file focuses on:
|
||||
// - providing the symbols required by the Nim glue (nim-status-go wrappers)
|
||||
// - returning heap-allocated cstrings compatible with status-go's Free()
|
||||
//
|
||||
// The service-side implementation will be added next (Binder + separate process).
|
||||
namespace {
|
||||
static JavaVM* g_vm = nullptr;
|
||||
static jclass g_bridgeClass = nullptr;
|
||||
static jmethodID g_callMethod = nullptr; // static String call(String method, String argsJson)
|
||||
static std::mutex g_lock;
|
||||
using SignalCallback = void (*)(const char* signalJson);
|
||||
static SignalCallback g_signalCb = nullptr;
|
||||
static void loge(const char* msg) {
|
||||
__android_log_write(ANDROID_LOG_ERROR, "statusgo-stub", msg);
|
||||
}
|
||||
static JNIEnv* getEnv() {
|
||||
if (!g_vm) return nullptr;
|
||||
JNIEnv* env = nullptr;
|
||||
if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) return nullptr;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
static char* dupToMalloc(const char* s) {
|
||||
if (!s) s = "";
|
||||
const size_t n = strlen(s);
|
||||
char* out = static_cast<char*>(malloc(n + 1));
|
||||
if (!out) return nullptr;
|
||||
memcpy(out, s, n);
|
||||
out[n] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
static void appendJsonEscaped(std::string& out, const char* s) {
|
||||
if (!s) return;
|
||||
for (const unsigned char* p = (const unsigned char*)s; *p; ++p) {
|
||||
const unsigned char c = *p;
|
||||
switch (c) {
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '"': out += "\\\""; break;
|
||||
case '\b': out += "\\b"; break;
|
||||
case '\f': out += "\\f"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
char buf[7];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", (unsigned)c);
|
||||
out += buf;
|
||||
} else {
|
||||
out.push_back((char)c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
static char* callJava(const char* method, const char* argsJson) {
|
||||
JNIEnv* env = getEnv();
|
||||
if (!env) {
|
||||
return dupToMalloc("{\"error\":\"status-go stub not initialized\"}");
|
||||
}
|
||||
|
||||
// Keep lock scope minimal: copy references, then perform Binder call unlocked.
|
||||
jclass bridgeClass = nullptr;
|
||||
jmethodID callMethod = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(g_lock);
|
||||
if (!g_bridgeClass || !g_callMethod) {
|
||||
return dupToMalloc("{\"error\":\"status-go stub not initialized\"}");
|
||||
}
|
||||
bridgeClass = (jclass)env->NewLocalRef(g_bridgeClass);
|
||||
callMethod = g_callMethod;
|
||||
}
|
||||
if (!bridgeClass || !callMethod) {
|
||||
if (bridgeClass) env->DeleteLocalRef(bridgeClass);
|
||||
return dupToMalloc("{\"error\":\"status-go stub not initialized\"}");
|
||||
}
|
||||
|
||||
jstring jMethod = env->NewStringUTF(method ? method : "");
|
||||
jstring jArgs = env->NewStringUTF(argsJson ? argsJson : "null");
|
||||
jstring jRet = (jstring)env->CallStaticObjectMethod(bridgeClass, callMethod, jMethod, jArgs);
|
||||
env->DeleteLocalRef(jMethod);
|
||||
env->DeleteLocalRef(jArgs);
|
||||
env->DeleteLocalRef(bridgeClass);
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
return dupToMalloc("{\"error\":\"java exception in status-go stub\"}");
|
||||
}
|
||||
if (!jRet) return dupToMalloc("");
|
||||
const char* cRet = env->GetStringUTFChars(jRet, nullptr);
|
||||
char* out = dupToMalloc(cRet);
|
||||
env->ReleaseStringUTFChars(jRet, cRet);
|
||||
env->DeleteLocalRef(jRet);
|
||||
return out;
|
||||
}
|
||||
static char* buildArgsJson(const char** argv, size_t argc) {
|
||||
std::string out;
|
||||
out.reserve(64);
|
||||
out.push_back('[');
|
||||
for (size_t i = 0; i < argc; i++) {
|
||||
if (i) out.push_back(',');
|
||||
out.push_back('"');
|
||||
appendJsonEscaped(out, argv[i] ? argv[i] : "");
|
||||
out.push_back('"');
|
||||
}
|
||||
out.push_back(']');
|
||||
return dupToMalloc(out.c_str());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
|
||||
g_vm = vm;
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
// Called from Java to provide the bridge class/method.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_app_status_mobile_StatusGoStub_nativeInit(JNIEnv* env, jclass, jclass bridgeClass) {
|
||||
std::lock_guard<std::mutex> guard(g_lock);
|
||||
if (g_bridgeClass) {
|
||||
env->DeleteGlobalRef(g_bridgeClass);
|
||||
g_bridgeClass = nullptr;
|
||||
g_callMethod = nullptr;
|
||||
}
|
||||
g_bridgeClass = (jclass)env->NewGlobalRef(bridgeClass);
|
||||
g_callMethod = env->GetStaticMethodID(g_bridgeClass, "call", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
|
||||
if (!g_callMethod) {
|
||||
loge("Failed to find StatusGoStub.call(String,String)");
|
||||
}
|
||||
}
|
||||
|
||||
// Called from Java (Binder listener) to deliver signals into the stored C callback.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_app_status_mobile_StatusGoStub_nativeDeliverSignal(JNIEnv* env, jclass, jstring jsonSignal) {
|
||||
(void)env;
|
||||
if (!jsonSignal) return;
|
||||
if (!g_signalCb) return;
|
||||
const char* c = env->GetStringUTFChars(jsonSignal, nullptr);
|
||||
g_signalCb(c);
|
||||
env->ReleaseStringUTFChars(jsonSignal, c);
|
||||
}
|
||||
|
||||
void Free(void* p) { free(p); }
|
||||
|
||||
void SetSignalEventCallback(SignalCallback cb) { g_signalCb = cb; }
|
||||
|
||||
// Called by generated exports.
|
||||
// - All arguments are passed as strings (even ints/bools) to simplify IPC.
|
||||
// - The service side will interpret them based on the called method.
|
||||
char* statusgo_stub_callv(const char* method, const char** argv, size_t argc) {
|
||||
char* argsJson = buildArgsJson(argv, argc);
|
||||
if (!argsJson) return dupToMalloc("{\"error\":\"oom\"}");
|
||||
char* out = callJava(method, argsJson);
|
||||
free(argsJson);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -48,6 +48,8 @@ android {
|
||||
$$PWD/../lib/$$LIB_PREFIX/libnim_status_client.so \
|
||||
$$PWD/../lib/$$LIB_PREFIX/libDOtherSide$$(LIB_SUFFIX)$$(LIB_EXT) \
|
||||
$$PWD/../lib/$$LIB_PREFIX/libstatus.so \
|
||||
$$PWD/../lib/$$LIB_PREFIX/libstatus_stub.so \
|
||||
$$PWD/../lib/$$LIB_PREFIX/libstatus_service.so \
|
||||
$$PWD/../lib/$$LIB_PREFIX/libsds.so \
|
||||
$$PWD/../lib/$$LIB_PREFIX/libStatusQ$$(LIB_SUFFIX)$$(LIB_EXT) \
|
||||
$$PWD/../lib/$$LIB_PREFIX/libMobileWebView$$(LIB_SUFFIX)$$(LIB_EXT)
|
||||
|
||||
@@ -285,8 +285,11 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
|
||||
result.connect()
|
||||
|
||||
proc delete*(self: AppController) =
|
||||
info "logging out..."
|
||||
self.generalService.logout()
|
||||
when defined(android):
|
||||
info "Skipping logout on AppController.delete (Android keepalive enabled)"
|
||||
else:
|
||||
info "logging out..."
|
||||
self.generalService.logout()
|
||||
|
||||
singletonInstance.delete
|
||||
self.notificationsManager.delete
|
||||
@@ -344,6 +347,10 @@ proc initializeQmlContext(self: AppController) =
|
||||
singletonInstance.engine.setRootContextProperty("localAccountSettings", self.localAccountSettingsVariant)
|
||||
singletonInstance.engine.setRootContextProperty("globalUtils", self.globalUtilsVariant)
|
||||
|
||||
# Expose a lightweight login flag that is available as soon as `main.qml` starts evaluating.
|
||||
let resumeLogin = self.accountsService.fetchLoggedInAccount().isValid()
|
||||
singletonInstance.engine.setRootContextProperty("skipOnboardingContextProperty", newQVariant(resumeLogin))
|
||||
|
||||
# Load keycard channel module (available before login for Session API)
|
||||
self.keycardChannelModule.load()
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ type
|
||||
postLoginTasks: seq[PostOnboardingTask]
|
||||
accountsService: accounts_service.Service
|
||||
generalService: general_service.Service
|
||||
resumeLogin: bool
|
||||
|
||||
proc newModule*[T](
|
||||
delegate: T,
|
||||
@@ -73,6 +74,9 @@ proc newModule*[T](
|
||||
|
||||
{.push warning[Deprecated]: off.}
|
||||
|
||||
# Forward declarations (needed because some methods call procs defined later).
|
||||
proc finishAppLoading2*[T](self: Module[T])
|
||||
|
||||
method delete*[T](self: Module[T]) =
|
||||
self.view.delete
|
||||
self.viewVariant.delete
|
||||
@@ -91,6 +95,8 @@ method onMainLoaded*[T](self: Module[T]) =
|
||||
self.viewVariant = nil
|
||||
self.controller.delete
|
||||
self.controller = nil
|
||||
if self.resumeLogin:
|
||||
self.delegate.onboardingDidLoad()
|
||||
|
||||
method onMainFailedToLoad*[T](self: Module[T]) =
|
||||
self.view.accountLoginError("Failed to load main module, please restart the app and try again.", wrongPassword = false)
|
||||
@@ -98,6 +104,13 @@ method onMainFailedToLoad*[T](self: Module[T]) =
|
||||
method load*[T](self: Module[T]) =
|
||||
singletonInstance.engine.setRootContextProperty("onboardingModule", self.viewVariant)
|
||||
self.controller.init()
|
||||
|
||||
let loggedInAccount = self.accountsService.fetchLoggedInAccount()
|
||||
self.resumeLogin = loggedInAccount.isValid()
|
||||
if (self.resumeLogin):
|
||||
self.controller.setLoggedInAccount(loggedInAccount)
|
||||
self.finishAppLoading2()
|
||||
return
|
||||
|
||||
let openedAccounts = self.controller.getOpenedAccounts()
|
||||
if openedAccounts.len > 0:
|
||||
|
||||
@@ -408,6 +408,7 @@ QtObject:
|
||||
self.events.emit(SIGNAL_DERIVED_ADDRESSES_FROM_NOT_IMPORTED_MNEMONIC_FETCHED, data)
|
||||
|
||||
proc doLogin(self: Service, account: AccountDto, passwordHash: string, chatPrivateKey: string = "", mnemonic: string = "") =
|
||||
|
||||
var request = LoginAccountRequest(
|
||||
keyUid: account.keyUid,
|
||||
kdfIterations: account.kdfIterations,
|
||||
@@ -522,3 +523,10 @@ QtObject:
|
||||
proc delete*(self: Service) =
|
||||
self.QObject.delete
|
||||
|
||||
proc fetchLoggedInAccount*(self: Service): AccountDto =
|
||||
try:
|
||||
let response = status_account.getActiveAccount()
|
||||
result = toAccountDto(response.result)
|
||||
except Exception as e:
|
||||
error "fetchLoggedInAccount failed", procName="fetchLoggedInAccount", errName = e.name, errDesription = e.msg
|
||||
result = AccountDto()
|
||||
|
||||
@@ -434,3 +434,19 @@ proc remainingKeypairCapacity*(): RpcResponse[JsonNode] =
|
||||
proc remainingWatchOnlyAccountCapacity*(): RpcResponse[JsonNode] =
|
||||
let payload = %* []
|
||||
return core.callPrivateRPC("accounts_remainingWatchOnlyAccountCapacity", payload)
|
||||
|
||||
proc getActiveAccount*(): RpcResponse[JsonNode] =
|
||||
try:
|
||||
let response = status_go.getActiveAccount()
|
||||
result.result = Json.decode(response, JsonNode)
|
||||
except RpcException as e:
|
||||
error "getActiveAccount failed", exception=e.msg
|
||||
raise newException(RpcException, e.msg)
|
||||
|
||||
proc keyUID*(): RpcResponse[JsonNode] =
|
||||
try:
|
||||
let response = status_go.keyUID()
|
||||
result.result = Json.decode(response, JsonNode)
|
||||
except RpcException as e:
|
||||
error "keyUID failed", exception=e.msg
|
||||
raise newException(RpcException, e.msg)
|
||||
+11
-1
@@ -32,6 +32,11 @@ Window {
|
||||
Theme.style: Application.styleHints.colorScheme === Qt.ColorScheme.Dark
|
||||
? Theme.Style.Dark : Theme.Style.Light
|
||||
|
||||
// Provided by Nim before `main.qml` starts (see AppController.initializeQmlContext()).
|
||||
readonly property bool skipOnboarding: typeof skipOnboardingContextProperty !== "undefined"
|
||||
? skipOnboardingContextProperty
|
||||
: false
|
||||
|
||||
property bool appIsReady: false
|
||||
|
||||
readonly property AppStores.FeatureFlagsStore featureFlagsStore: AppStores.FeatureFlagsStore {
|
||||
@@ -330,7 +335,7 @@ Window {
|
||||
close.accepted = false
|
||||
// In case of android, we need to handle moveTaskToBackground explicitly
|
||||
if (SQUtils.Utils.isAndroid)
|
||||
SystemUtils.androidMinimizeToBackground()
|
||||
close.accepted = true
|
||||
else
|
||||
applicationWindow.showMinimized()
|
||||
// In case not logged in or loading, quit app
|
||||
@@ -451,6 +456,10 @@ Window {
|
||||
safeArea.additionalMargins.left = Qt.binding(() => MobileUI.safeAreaLeft)
|
||||
|
||||
safeMarginsCleanupConnections.enabled = true
|
||||
|
||||
if (applicationWindow.skipOnboarding) {
|
||||
moveToAppMain()
|
||||
}
|
||||
}
|
||||
|
||||
signal navigateTo(string path)
|
||||
@@ -583,6 +592,7 @@ Window {
|
||||
anchors.leftMargin: parent.SafeArea.margins.left
|
||||
anchors.rightMargin: parent.SafeArea.margins.right
|
||||
anchors.bottomMargin: parent.SafeArea.margins.bottom
|
||||
active: !applicationWindow.skipOnboarding
|
||||
|
||||
sourceComponent: onboardingV2
|
||||
}
|
||||
|
||||
Vendored
+1
-1
Submodule vendor/nim-status-go updated: 0ee0a0936d...8b7deb06e4
Vendored
+1
-1
Submodule vendor/status-go updated: 87ac03209c...cc7df45f84
Reference in New Issue
Block a user