Skip to content

Commit 6595a6f

Browse files
committed
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
1 parent 36e11d6 commit 6595a6f

File tree

26 files changed

+1545
-11
lines changed

26 files changed

+1545
-11
lines changed

docs/adr/0001-android-status-go-as-a-service.md

Lines changed: 437 additions & 0 deletions
Large diffs are not rendered by default.

docs/architecture.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This shows the flow from the UI all the way to the backend.
88

99
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.
1010

11+
## Architecture Decision Records (ADRs)
12+
13+
- `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.
14+
1115
```mermaid
1216
flowchart LR
1317
qml["Frontend (QML)"] --> statusq(["StatusQ"])

mobile/Makefile

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
-include ./scripts/EnvVariables.mk
22
-include ./scripts/Common.mk
33

4-
STATUS_GO_LIB := $(LIB_PATH)/libstatus$(LIB_EXT)
5-
64
# FLAG_KEYCARD_ENABLED: Controls NFC/Keycard support
75
# - iOS: Default 0 (disabled) - Build without NFC, works with free Apple Developer account
86
# - Android: Default 1 (enabled) - NFC support doesn't require paid account on Android
@@ -41,6 +39,7 @@ ifeq ($(OS),android)
4139
ANDROID_NDK_ROOT="$(ANDROID_NDK_ROOT)" \
4240
ANDROID_API="$(ANDROID_API)" \
4341
HOST_OS="$(HOST_OS)" \
42+
NIM_SDS_SOURCE_DIR="$(NIM_SDS_SOURCE_DIR)" \
4443
USE_SYSTEM_NIM=$(USE_SYSTEM_NIM) \
4544
GO_GENERATE_CMD="go generate" \
4645
SHELL=/bin/sh
@@ -56,6 +55,29 @@ endif
5655
@cp $(NIM_SDS_SOURCE_DIR)/build/libsds$(LIB_EXT) $(LIB_PATH)/libsds$(LIB_EXT)
5756
@cp ../vendor/status-go/build/bin/libstatus$(LIB_EXT) $(LIB_PATH)/libstatus$(LIB_EXT)
5857

58+
$(STATUS_GO_STUB_LIB): $(STATUS_GO_LIB)
59+
@echo "Building status-go stub library (UI process)"
60+
@mkdir -p $(LIB_PATH)
61+
@$(MAKE) $(STATUS_GO_STUB_GEN)
62+
@$(CXX) -shared -fPIC -O2 \
63+
-o $(STATUS_GO_STUB_LIB) \
64+
$(STATUS_DESKTOP)/mobile/statusgo_stub/statusgo_stub.cpp \
65+
$(STATUS_GO_STUB_GEN) \
66+
-llog -lc++_shared
67+
68+
$(STATUS_GO_SERVICE_LIB): $(STATUS_GO_LIB)
69+
@echo "Building status-go service JNI library (service process)"
70+
@mkdir -p $(LIB_PATH)
71+
@$(MAKE) $(STATUS_GO_STUB_GEN)
72+
@$(CXX) -shared -fPIC -O2 \
73+
-o $(STATUS_GO_SERVICE_LIB) \
74+
$(STATUS_DESKTOP)/mobile/statusgo_service/statusgo_service_jni.cpp \
75+
$(STATUS_GO_SERVICE_GEN) \
76+
-L$(LIB_PATH) -lstatus -llog -lc++_shared
77+
78+
$(STATUS_GO_STUB_GEN):
79+
@$(MAKE) -C $(STATUS_DESKTOP)/vendor/status-go statusgo-stub-bindings
80+
5981
$(STATUS_Q_LIB): $(STATUS_Q_FILES) $(STATUS_Q_SCRIPT) $(STATUS_Q_UI_FILES)
6082
@echo "Building StatusQ"
6183
@STATUSQ=$(STATUSQ) QT_MAJOR=$(QT_MAJOR) LIB_SUFFIX=$(LIB_SUFFIX) LIB_EXT=$(LIB_EXT) $(STATUS_Q_SCRIPT) $(HANDLE_OUTPUT)
@@ -85,7 +107,12 @@ $(STATUS_DESKTOP_RCC): $(STATUS_DESKTOP_UI_FILES) compile-translations
85107
@make -C $(STATUS_DESKTOP) rcc $(HANDLE_OUTPUT)
86108
@echo "Status Desktop rcc built"
87109

88-
$(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)
110+
111+
ifeq ($(OS),android)
112+
$(NIM_STATUS_CLIENT_LIB): $(STATUS_GO_STUB_LIB) $(STATUS_GO_SERVICE_LIB)
113+
endif
114+
115+
$(NIM_STATUS_CLIENT_LIB): $(STATUS_DESKTOP_NIM_FILES) $(NIM_STATUS_CLIENT_SCRIPT) $(STATUS_DESKTOP_RCC) $(DOTHERSIDE_LIB) $(OPENSSL_LIB) $(STATUS_Q_LIB) $(QRCODEGEN_LIB)
89116
@echo "Building Status Desktop Lib"
90117
@STATUS_DESKTOP=$(STATUS_DESKTOP) \
91118
LIB_SUFFIX=$(LIB_SUFFIX) \

mobile/android/qt6/AndroidManifest.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
package="app.status.mobile"
44
android:installLocation="auto">
5+
<!--
6+
Signature-level permission used to harden Binder IPC between the UI process and the
7+
separate-process StatusGoService. This is defense-in-depth on top of android:exported="false".
8+
-->
9+
<permission
10+
android:name="app.status.mobile.permission.STATUSGO_SERVICE"
11+
android:label="Status status-go service access"
12+
android:protectionLevel="signature" />
13+
14+
<uses-permission android:name="app.status.mobile.permission.STATUSGO_SERVICE" />
515
<!-- non-dangerous permissions -->
616
<uses-permission android:name="android.permission.INTERNET"/>
717
<uses-permission android:name="android.permission.NFC"/>
@@ -10,6 +20,8 @@
1020
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
1121
<uses-permission android:name="android.permission.VIBRATE" />
1222
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
23+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
24+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
1325

1426
<!-- features -->
1527
<uses-feature android:name="android.hardware.camera" android:required="false" />
@@ -83,5 +95,17 @@
8395
android:name="android.support.FILE_PROVIDER_PATHS"
8496
android:resource="@xml/qtprovider_paths"/>
8597
</provider>
98+
99+
<!-- Separate-process status-go host service -->
100+
<service
101+
android:name=".ipc.StatusGoService"
102+
android:exported="false"
103+
android:permission="app.status.mobile.permission.STATUSGO_SERVICE"
104+
android:process=":statusgo"
105+
android:foregroundServiceType="specialUse">
106+
<property
107+
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
108+
android:value="status-go background" />
109+
</service>
86110
</application>
87111
</manifest>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package app.status.mobile.ipc;
2+
3+
import app.status.mobile.ipc.IStatusGoSignalListener;
4+
5+
interface IStatusGoService {
6+
/** Generic call into status-go exports (method name is the C export name). */
7+
String call(String method, String argsJson);
8+
9+
/**
10+
* Same as call(), but writes the response to a file in the service's cache dir
11+
* and returns the absolute file path. This avoids Binder size limits for large JSON.
12+
*/
13+
String callToFile(String method, String argsJson);
14+
15+
/** Register a signal listener. */
16+
void registerSignalListener(IStatusGoSignalListener listener);
17+
18+
/** Unregister a signal listener. */
19+
void unregisterSignalListener(IStatusGoSignalListener listener);
20+
21+
/**
22+
* UI visibility hint used for notification suppression.
23+
*
24+
* If {@code visible=true}, the UI is in foreground; the service should not post OS
25+
* message notifications (to avoid duplicates / to match “only when background” behavior).
26+
*/
27+
void setUiVisible(boolean visible);
28+
}
29+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package app.status.mobile.ipc;
2+
3+
/** One-way signal stream from status-go service to UI process. */
4+
oneway interface IStatusGoSignalListener {
5+
void onSignal(String jsonSignal);
6+
}
7+

mobile/android/qt6/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ android {
6161
buildToolsVersion androidBuildToolsVersion
6262
ndkVersion = androidNdkVersion
6363

64+
buildFeatures {
65+
aidl true
66+
}
67+
6468
sourceSets {
6569
main {
6670
manifest.srcFile 'AndroidManifest.xml'

mobile/android/qt6/src/app/status/mobile/SecureAndroidAuthentication.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@ public void setNegativeButton(String negativeButton) {
8787
}
8888

8989
/** Cancel current biometric request, if any. */
90-
public void cancel() {
90+
public int cancel() {
9191
if (mCancel != null && !mCancel.isCanceled()) mCancel.cancel();
9292
mCancel = null;
93+
return 0;
9394
}
9495

9596
/** Capability check — returns your BIOMETRIC_* codes. */
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package app.status.mobile;
2+
3+
import android.content.Context;
4+
import app.status.mobile.ipc.StatusGoServiceClient;
5+
6+
/**
7+
* UI-process bridge used by the native status-go stub library (libstatus_stub.so).
8+
*
9+
* For now this is a placeholder: it provides a stable Java surface for the native stub
10+
* to call into. Next step is to back this by a Binder client that talks to the
11+
* separate status-go service process.
12+
*/
13+
public final class StatusGoStub {
14+
static {
15+
// Loads libstatus_stub.so (needed for JNI_OnLoad + nativeInit).
16+
System.loadLibrary("status_stub");
17+
}
18+
19+
private StatusGoStub() {}
20+
21+
// Native: supplies the Java class that implements call().
22+
private static native void nativeInit(Class<?> bridgeClass);
23+
24+
// Native: used later to deliver signals from Binder listener to Nim callback.
25+
public static native void nativeDeliverSignal(String jsonSignal);
26+
27+
/** Must be called early (e.g. Activity.onCreate) to bind the Java bridge. */
28+
public static void ensureInitialized(Context context) {
29+
nativeInit(StatusGoStub.class);
30+
// Start/bind the status-go service early so first RPC doesn't block too long.
31+
StatusGoServiceClient.get().ensureStartedAndBound(context);
32+
}
33+
34+
/**
35+
* Called from native status-go stub.
36+
* @param method status-go exported method name (e.g. "CallPrivateRPC")
37+
* @param argsJson JSON array of string args (placeholder encoding for now)
38+
*/
39+
public static String call(String method, String argsJson) {
40+
// Called from native stub exports; forward to separate-process service.
41+
if (sContext == null) {
42+
return "{\"error\":\"StatusGoStub not initialized\"}";
43+
}
44+
return StatusGoServiceClient.get().call(sContext, method, argsJson);
45+
}
46+
47+
/** Hint to the service whether the UI is currently visible (foreground). */
48+
public static void setUiVisible(boolean visible) {
49+
if (sContext == null) return;
50+
StatusGoServiceClient.get().setUiVisible(sContext, visible);
51+
}
52+
53+
private static volatile Context sContext;
54+
55+
/** Called by Activity. Keep application context for later native calls. */
56+
public static void setContext(Context context) {
57+
sContext = context.getApplicationContext();
58+
}
59+
}
60+

mobile/android/qt6/src/app/status/mobile/StatusQtActivity.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,29 @@
33
import org.qtproject.qt.android.bindings.QtActivity;
44
import android.os.Build;
55
import android.os.Bundle;
6+
import android.content.pm.PackageManager;
67
import androidx.core.splashscreen.SplashScreen;
78
import java.util.concurrent.atomic.AtomicBoolean;
9+
import im.status.mobileui.PushNotificationHelper;
810

911
public class StatusQtActivity extends QtActivity {
1012
private static final AtomicBoolean splashShouldHide = new AtomicBoolean(false);
13+
private static final int REQUEST_CODE_POST_NOTIFICATIONS = 1001;
1114

1215
// QTBUG-140897: Android 16 keyboard workaround
1316
// Remove this line when Qt 6.10+ fixes the issue, and delete Android16KeyboardWorkaround.java
1417
private Android16KeyboardWorkaround mKeyboardWorkaround;
1518

1619
@Override
1720
public void onCreate(Bundle savedInstanceState) {
21+
// Initialize the status-go UI stub bridge early.
22+
// (In the service-based architecture this forwards to the separate status-go process.)
23+
StatusGoStub.setContext(this);
24+
StatusGoStub.ensureInitialized(this);
25+
26+
// IMPORTANT: call super.onCreate() after starting/binding the service.
27+
// QtActivity may start the Qt (Nim) side during super.onCreate(), and the Nim
28+
// onboarding resume check queries the service immediately on startup.
1829
super.onCreate(savedInstanceState);
1930

2031
if (Build.VERSION.SDK_INT >= 31) { // Android 12+
@@ -27,6 +38,20 @@ public void onCreate(Bundle savedInstanceState) {
2738
mKeyboardWorkaround = Android16KeyboardWorkaround.install(this);
2839
}
2940

41+
@Override
42+
protected void onResume() {
43+
super.onResume();
44+
// Inform the status-go service that UI is visible so it can suppress OS notifications.
45+
StatusGoStub.setUiVisible(true);
46+
}
47+
48+
@Override
49+
protected void onPause() {
50+
// Inform the status-go service that UI is no longer in foreground.
51+
StatusGoStub.setUiVisible(false);
52+
super.onPause();
53+
}
54+
3055
@Override
3156
protected void onDestroy() {
3257
// QTBUG-140897: Cleanup workaround resources
@@ -35,7 +60,7 @@ protected void onDestroy() {
3560
mKeyboardWorkaround.cleanup();
3661
mKeyboardWorkaround = null;
3762
}
38-
63+
3964
super.onDestroy();
4065
}
4166

0 commit comments

Comments
 (0)