diff --git a/android/build.gradle b/android/build.gradle
index fb05585..86895d4 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -71,7 +71,7 @@ dependencies {
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
- implementation 'co.tryterra:terra-rtandroid:0.4.11'
+ implementation 'co.tryterra:terra-rtandroid:0.4.12'
}
if (isNewArchitectureEnabled()) {
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index c0cb69a..61a413c 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,3 +1,5 @@
+
+
diff --git a/android/src/main/java/com/terrartreact/TerraRtReactModule.java b/android/src/main/java/com/terrartreact/TerraRtReactModule.java
index 6ef4497..58331d5 100644
--- a/android/src/main/java/com/terrartreact/TerraRtReactModule.java
+++ b/android/src/main/java/com/terrartreact/TerraRtReactModule.java
@@ -13,6 +13,11 @@
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.bridge.ReadableArray;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.provider.Settings;
import co.tryterra.terrartandroid.enums.Connections;
import co.tryterra.terrartandroid.enums.DataTypes;
import com.facebook.react.modules.core.DeviceEventManagerModule;
@@ -350,11 +355,82 @@ public void connectDevice(String deviceId, Promise promise){
return;
}
+ // The SDK reuses this callback for later connection updates
+ // (e.g. disconnect) — a promise must resolve exactly once.
+ final java.util.concurrent.atomic.AtomicBoolean resolved =
+ new java.util.concurrent.atomic.AtomicBoolean(false);
this.terraRt.connectDevice(devices.get(deviceId), (success) -> {
- map.putBoolean("success", success);
- promise.resolve(map);
+ if (resolved.compareAndSet(false, true)) {
+ map.putBoolean("success", success);
+ promise.resolve(map);
+ }
return Unit.INSTANCE;
});
}
+
+ /**
+ * Customizes the persistent notification and (re)starts the streaming
+ * foreground service. As of terra-rtandroid 0.4.12 the service lifecycle
+ * is managed by the SDK (startRealtime starts it, stopRealtime/disconnect
+ * stop it) — calling this is only needed to override the notification.
+ */
+ @ReactMethod
+ public void startForegroundService(String title, String text, Promise promise){
+ WritableMap map = new WritableNativeMap();
+ if (this.terraRt == null){
+ map.putBoolean("success", false);
+ map.putString("error", "Please initialise a terra class by using `initTerra` first");
+ promise.resolve(map);
+ return;
+ }
+ this.terraRt.startForegroundService(title, text, null);
+ map.putBoolean("success", true);
+ promise.resolve(map);
+ }
+
+ /** Stops the streaming foreground service (managed automatically by the SDK). */
+ @ReactMethod
+ public void stopForegroundService(Promise promise){
+ WritableMap map = new WritableNativeMap();
+ if (this.terraRt == null){
+ map.putBoolean("success", false);
+ map.putString("error", "Please initialise a terra class by using `initTerra` first");
+ promise.resolve(map);
+ return;
+ }
+ this.terraRt.stopForegroundService();
+ map.putBoolean("success", true);
+ promise.resolve(map);
+ }
+
+ /** Whether the app is exempt from battery optimizations (aggressive OEMs kill streams otherwise). */
+ @ReactMethod
+ public void isIgnoringBatteryOptimizations(Promise promise){
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
+ promise.resolve(true); // no battery optimizations before API 23
+ return;
+ }
+ PowerManager pm = (PowerManager) this.reactContext.getSystemService(Context.POWER_SERVICE);
+ promise.resolve(pm != null && pm.isIgnoringBatteryOptimizations(this.reactContext.getPackageName()));
+ }
+
+ /** Opens the system dialog requesting a battery-optimization exemption. */
+ @ReactMethod
+ public void requestIgnoreBatteryOptimizations(Promise promise){
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
+ promise.resolve(true); // no battery optimizations before API 23
+ return;
+ }
+ try {
+ Intent intent = new Intent(
+ Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
+ Uri.parse("package:" + this.reactContext.getPackageName()));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ this.reactContext.startActivity(intent);
+ promise.resolve(true);
+ } catch (Exception e) {
+ promise.resolve(false);
+ }
+ }
}
diff --git a/ios/TerraRtReact.swift b/ios/TerraRtReact.swift
index c179f53..107b757 100644
--- a/ios/TerraRtReact.swift
+++ b/ios/TerraRtReact.swift
@@ -165,7 +165,12 @@ class TerraRtReact: NSObject {
}
if let device_ = TerraRtReact.scannedDevices[device]{
+ // The SDK reuses this callback for later connection updates
+ // (e.g. disconnect) — a promise must resolve exactly once.
+ var resolved = false
terraRT.connectDevice(device_){success in
+ guard !resolved else { return }
+ resolved = true
resolve(["success": success])
}
}
diff --git a/package-lock.json b/package-lock.json
index eb7c7ed..b98b20f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "react-native-terra-rt-react",
- "version": "0.2.3",
+ "name": "terra-rt",
+ "version": "0.2.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "react-native-terra-rt-react",
- "version": "0.2.3",
+ "name": "terra-rt",
+ "version": "0.2.7",
"license": "MIT",
"dependencies": {
"terra-react": "^1.8.6"
diff --git a/package.json b/package.json
index 7e07fa6..9750739 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "terra-rt",
- "version": "0.2.6",
+ "version": "0.2.7",
"description": "React Native SDK for Terra's realtime (websocket) streaming \u2014 live data from BLE devices and Apple Watch.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
diff --git a/react-native-terra-rt-react.podspec b/react-native-terra-rt-react.podspec
index 6b40fd1..1cfb39c 100644
--- a/react-native-terra-rt-react.podspec
+++ b/react-native-terra-rt-react.podspec
@@ -17,7 +17,7 @@ Pod::Spec.new do |s|
s.source_files = "ios/**/*.{h,m,mm,swift}"
s.frameworks = ['HealthKit']
s.dependency "React-Core"
- s.dependency "TerraRTiOS", "=0.3.13"
+ s.dependency "TerraRTiOS", "=0.3.14"
# Don't install the dependencies when we run `pod install` in the old architecture.
diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx
index bf84291..b77f791 100644
--- a/src/__tests__/index.test.tsx
+++ b/src/__tests__/index.test.tsx
@@ -1 +1,73 @@
-it.todo('write a test');
+import { NativeModules, Platform } from 'react-native';
+
+// Must exist before src/index is imported, or the linking-error proxy trips.
+NativeModules.TerraRtReact = {
+ connectDevice: jest.fn(async () => ({ success: true, error: null })),
+ startForegroundService: jest.fn(async () => ({ success: true, error: null })),
+ stopForegroundService: jest.fn(async () => ({ success: true, error: null })),
+ isIgnoringBatteryOptimizations: jest.fn(async () => false),
+ requestIgnoreBatteryOptimizations: jest.fn(async () => true),
+};
+
+// require, not import: imports hoist above the NativeModules assignment,
+// which would capture the linking-error proxy instead of the mock.
+const {
+ isIgnoringBatteryOptimizations,
+ requestIgnoreBatteryOptimizations,
+ startForegroundService,
+ stopForegroundService,
+} = require('../index');
+
+const native = NativeModules.TerraRtReact;
+
+describe('background-streaming API (android)', () => {
+ beforeEach(() => {
+ Platform.OS = 'android';
+ jest.clearAllMocks();
+ });
+
+ it('startForegroundService forwards the notification content', async () => {
+ const res = await startForegroundService('Terra', 'Streaming live data');
+ expect(native.startForegroundService).toHaveBeenCalledWith(
+ 'Terra',
+ 'Streaming live data'
+ );
+ expect(res).toEqual({ success: true, error: null });
+ });
+
+ it('stopForegroundService calls through', async () => {
+ await stopForegroundService();
+ expect(native.stopForegroundService).toHaveBeenCalled();
+ });
+
+ it('battery-optimization helpers call through', async () => {
+ await expect(isIgnoringBatteryOptimizations()).resolves.toBe(false);
+ await expect(requestIgnoreBatteryOptimizations()).resolves.toBe(true);
+ expect(native.isIgnoringBatteryOptimizations).toHaveBeenCalled();
+ expect(native.requestIgnoreBatteryOptimizations).toHaveBeenCalled();
+ });
+});
+
+describe('background-streaming API (ios no-ops)', () => {
+ beforeEach(() => {
+ Platform.OS = 'ios';
+ jest.clearAllMocks();
+ });
+
+ it('resolves successfully without touching the native module', async () => {
+ await expect(startForegroundService('t', 'x')).resolves.toEqual({
+ success: true,
+ error: null,
+ });
+ await expect(stopForegroundService()).resolves.toEqual({
+ success: true,
+ error: null,
+ });
+ await expect(isIgnoringBatteryOptimizations()).resolves.toBe(true);
+ await expect(requestIgnoreBatteryOptimizations()).resolves.toBe(true);
+ expect(native.startForegroundService).not.toHaveBeenCalled();
+ expect(native.stopForegroundService).not.toHaveBeenCalled();
+ expect(native.isIgnoringBatteryOptimizations).not.toHaveBeenCalled();
+ expect(native.requestIgnoreBatteryOptimizations).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/index.tsx b/src/index.tsx
index 3fd5123..b8ed95d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -84,3 +84,55 @@ export function disconnect(connections: Connections): Promise {
export function connectWithWatchOS(): Promise {
return TerraRtReact.connectWithWatchOS();
}
+
+/**
+ * ANDROID ONLY. Customizes the persistent notification shown while
+ * streaming and (re)starts the streaming foreground service. As of
+ * terra-rtandroid 0.4.12 the service lifecycle is managed by the SDK
+ * (startRealtime starts it; stopRealtime/disconnect stop it) — call this
+ * only to override the default notification content. No-op on iOS.
+ */
+export function startForegroundService(
+ notificationTitle: string,
+ notificationText: string
+): Promise {
+ if (Platform.OS !== 'android') {
+ return Promise.resolve({ success: true, error: null });
+ }
+ return TerraRtReact.startForegroundService(
+ notificationTitle,
+ notificationText
+ );
+}
+
+/** ANDROID ONLY. Stops the streaming foreground service. No-op on iOS. */
+export function stopForegroundService(): Promise {
+ if (Platform.OS !== 'android') {
+ return Promise.resolve({ success: true, error: null });
+ }
+ return TerraRtReact.stopForegroundService();
+}
+
+/**
+ * ANDROID ONLY. Whether the app is exempt from battery optimizations —
+ * aggressive OEM battery managers can kill background streaming without
+ * the exemption. Always true on iOS.
+ */
+export function isIgnoringBatteryOptimizations(): Promise {
+ if (Platform.OS !== 'android') {
+ return Promise.resolve(true);
+ }
+ return TerraRtReact.isIgnoringBatteryOptimizations();
+}
+
+/**
+ * ANDROID ONLY. Opens the system dialog asking the user to exempt the app
+ * from battery optimizations. Resolves false if the dialog can't open.
+ * No-op (true) on iOS.
+ */
+export function requestIgnoreBatteryOptimizations(): Promise {
+ if (Platform.OS !== 'android') {
+ return Promise.resolve(true);
+ }
+ return TerraRtReact.requestIgnoreBatteryOptimizations();
+}