Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
2 changes: 2 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.terrartreact">
<!-- Required for requestIgnoreBatteryOptimizations(); merged into consumers. -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
</manifest>
80 changes: 78 additions & 2 deletions android/src/main/java/com/terrartreact/TerraRtReactModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
}
Comment on lines +406 to +415

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls an API 23-only method unguarded, so older supported Android devices can crash here.

Suggested change
/** Whether the app is exempt from battery optimizations (aggressive OEMs kill streams otherwise). */
@ReactMethod
public void isIgnoringBatteryOptimizations(Promise promise){
PowerManager pm = (PowerManager) this.reactContext.getSystemService(Context.POWER_SERVICE);
promise.resolve(pm != null && pm.isIgnoringBatteryOptimizations(this.reactContext.getPackageName()));
}
/** 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);
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);
}
}
}

5 changes: 5 additions & 0 deletions ios/TerraRtReact.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion react-native-terra-rt-react.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 73 additions & 1 deletion src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
52 changes: 52 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,55 @@ export function disconnect(connections: Connections): Promise<SuccessMessage> {
export function connectWithWatchOS(): Promise<SuccessMessage> {
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<SuccessMessage> {
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<SuccessMessage> {
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<boolean> {
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<boolean> {
if (Platform.OS !== 'android') {
return Promise.resolve(true);
}
return TerraRtReact.requestIgnoreBatteryOptimizations();
}
Loading