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
5 changes: 5 additions & 0 deletions src/SDK/Language/Flutter.php
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,11 @@ public function getFiles(): array
'destination' => '/test/src/interceptor_test.dart',
'template' => 'flutter/test/src/interceptor_test.dart.twig',
],
[
'scope' => 'default',
'destination' => '/test/src/push_background_status_test.dart',
'template' => 'flutter/test/src/push_background_status_test.dart.twig',
],
[
'scope' => 'default',
'destination' => '/test/src/realtime_response_test.dart',
Expand Down
7 changes: 7 additions & 0 deletions templates/android/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ The SDK uses exact alarms on its own whenever the app may schedule them. To allo
nothing to ask, including when the permission is not declared. The SDK never opens these
screens on its own.

If `backgroundRestricted` is true, the user restricted the app's background usage and messages can
wait until the app is opened. Exact alarms don't lift it, and neither does foreground mode: Android
stops a restricted app's foreground service once it leaves the screen. Ask the user to choose
Unrestricted in the app's battery settings (Settings > Apps > your app > Battery); granting the
battery optimisation exemption also lets the SDK's alarms and jobs run, and `bestEffort` then
reflects only the exact-alarm state.

If the user force-stops the app (Settings > Force stop, and on some devices swiping it away from
recents), Android cancels its alarms and jobs: nothing is delivered until the app is opened
again, and the broker then replays what was sent meanwhile.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -828,10 +828,14 @@ class Push(
const val EXTRA_PAYLOAD = PushBackground.EXTRA_PAYLOAD

/**
* What background delivery can rely on: exact alarms, battery-optimisation exemption and
* foreground mode. When [PushBackgroundStatus.bestEffort] is true, wake-ups are inexact and
* messages can arrive late while the app is closed; explain why before asking with
* [requestExactAlarms] or [requestIgnoreBatteryOptimizations].
* What background delivery can rely on: exact alarms, battery-optimisation exemption,
* foreground mode and restricted background usage. When [PushBackgroundStatus.bestEffort]
* is true, messages can arrive late while the app is closed; explain why before asking with
* [requestExactAlarms] or [requestIgnoreBatteryOptimizations]. When
* [PushBackgroundStatus.backgroundRestricted] is true, neither exact alarms nor foreground
* mode lift it: ask the user to choose Unrestricted in the app's battery settings
* (Settings > Apps > your app > Battery), or for the battery-optimisation exemption, which
* also lets the SDK's alarms and jobs run.
*/
@JvmStatic
fun backgroundStatus(context: Context): PushBackgroundStatus = PushBackground.backgroundStatus(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,8 @@ internal object PushBackground {
app.getSystemService(AlarmManager::class.java)?.canScheduleExactAlarms() == true,
ignoringBatteryOptimizations = app.getSystemService(PowerManager::class.java)?.isIgnoringBatteryOptimizations(app.packageName) == true,
foregroundService = PushStore.foreground(app),
backgroundRestricted = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
app.getSystemService(ActivityManager::class.java)?.isBackgroundRestricted == true,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,13 @@ class PushBridge(context: Context, private val events: Events) {
return if (registered) PushStore.takeStoppedError(appContext) else null
}

/** What background delivery can rely on, as `{"exactAlarms", "ignoringBatteryOptimizations", "foregroundService", "bestEffort"}`. */
/** What background delivery can rely on, as `{"exactAlarms", "ignoringBatteryOptimizations", "foregroundService", "backgroundRestricted", "bestEffort"}`. */
fun backgroundStatus(): String = PushBackground.backgroundStatus(appContext).let {
JSONObject()
.put("exactAlarms", it.exactAlarms)
.put("ignoringBatteryOptimizations", it.ignoringBatteryOptimizations)
.put("foregroundService", it.foregroundService)
.put("backgroundRestricted", it.backgroundRestricted)
.put("bestEffort", it.bestEffort)
.toString()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,36 @@ data class PushMessage(
/**
* What background delivery can rely on. Without exact alarms, the scheduled wake-ups are inexact
* and Doze can defer them, so delivery is [bestEffort] unless foreground mode keeps the
* connection open.
* connection open. When the user restricted background usage, delivery is [bestEffort] unless the
* app is also exempt from battery optimisation, and foreground mode no longer counts: Android stops
* a restricted app's foreground service once it leaves the screen, exemption or not.
*/
data class PushBackgroundStatus(
data class PushBackgroundStatus @JvmOverloads constructor(
/** The app may schedule exact alarms (`SCHEDULE_EXACT_ALARM`, granted). */
val exactAlarms: Boolean,
/** The app is exempt from battery optimisation. */
val ignoringBatteryOptimizations: Boolean,
/** Foreground mode (`setForeground(true)`) keeps the connection open in a service. */
val foregroundService: Boolean,
/**
* The user restricted the app's background usage in its battery settings (Android 9+). Android
* may then hold its alarms, jobs and foreground service while it is closed, so messages can
* wait until the app is opened. The battery-optimisation exemption lets the alarms and jobs
* run again, but foreground mode still stops once the app leaves the screen.
*/
val backgroundRestricted: Boolean = false,
) {
/** Wake-ups may be deferred by Doze, so messages can arrive late while the app is closed. */
/**
* Messages can arrive late, or not until the app is opened, while the app is closed: background
* usage is restricted without the battery-optimisation exemption, or wake-ups may be deferred
* by Doze.
*/
val bestEffort: Boolean
get() = !exactAlarms && !foregroundService
get() {
val held = backgroundRestricted && !ignoringBatteryOptimizations
val foreground = foregroundService && !backgroundRestricted
return held || (!exactAlarms && !foreground)
}
}

/** Connection parameters shared between the in-process client and the foreground service. */
Expand Down
7 changes: 7 additions & 0 deletions templates/flutter/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ The SDK uses exact alarms on its own whenever the app may schedule them. To allo
nothing to ask, including when the permission is not declared. The SDK never opens these
screens on its own.

If `backgroundRestricted` is true, the user restricted the app's background usage and messages can
wait until the app is opened. Exact alarms don't lift it, and neither does foreground mode: Android
stops a restricted app's foreground service once it leaves the screen. Ask the user to choose
Unrestricted in the app's battery settings (Settings > Apps > your app > Battery); granting the
battery optimisation exemption also lets the SDK's alarms and jobs run, and `bestEffort` then
reflects only the exact-alarm state. It is always false below Android 9.

If the user force-stops the app (Settings > Force stop, and on some devices swiping it away from
recents), Android cancels its alarms and jobs: nothing is delivered until the app is opened
again, and the broker then replays what was sent meanwhile.
Expand Down
22 changes: 18 additions & 4 deletions templates/flutter/lib/src/mqtt.dart.twig
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,24 @@ class PushBackgroundStatus {
/// Foreground mode ([Push.setForeground]) keeps the connection open in a service.
final bool foregroundService;

/// Wake-ups may be deferred by Doze, so messages can arrive late while the app is closed.
/// Messages can arrive late, or not until the app is opened, while the app is closed: background
/// usage is restricted without a battery optimisation exemption, or wake-ups may be deferred by
/// Doze without exact alarms or foreground mode.
final bool bestEffort;

/// The user restricted the app's background usage in the app's battery settings (Settings >
/// Apps > your app > Battery), Android 9+. Android may then hold its alarms, jobs and foreground
/// service while it is closed, so messages can wait until the app is opened. The battery
/// optimisation exemption lets the alarms and jobs run again, but foreground mode still stops
/// once the app leaves the screen. False below Android 9.
final bool backgroundRestricted;

const PushBackgroundStatus({
required this.exactAlarms,
required this.ignoringBatteryOptimizations,
required this.foregroundService,
required this.bestEffort,
this.backgroundRestricted = false,
});

factory PushBackgroundStatus.fromMap(Map<String, dynamic> map) =>
Expand All @@ -57,6 +67,7 @@ class PushBackgroundStatus {
map['ignoringBatteryOptimizations'] == true,
foregroundService: map['foregroundService'] == true,
bestEffort: map['bestEffort'] == true,
backgroundRestricted: map['backgroundRestricted'] == true,
);
}

Expand Down Expand Up @@ -189,9 +200,12 @@ abstract class Push extends Service {
/// background. It only affects [background] subscriptions. A no-op elsewhere.
Future<void> setForeground(bool enabled);

/// Android: what background delivery can rely on. When `bestEffort` is true the scheduled
/// wake-ups are inexact and Doze can defer them, so messages may arrive late while the app is
/// closed; explain why, then ask with [requestExactAlarms] or
/// Android: what background delivery can rely on. When `bestEffort` is true messages may arrive
/// late, or not until the app is opened, while the app is closed, so explain why, then ask. If
/// `backgroundRestricted` is true and the app is not `ignoringBatteryOptimizations`, ask the user
/// to choose Unrestricted in the app's battery settings (Settings > Apps > your app > Battery),
/// or ask with [requestIgnoreBatteryOptimizations]; otherwise the scheduled wake-ups are inexact
/// and Doze can defer them, so ask with [requestExactAlarms] or
/// [requestIgnoreBatteryOptimizations]. Null elsewhere.
Future<PushBackgroundStatus?> backgroundStatus();

Expand Down
45 changes: 45 additions & 0 deletions templates/flutter/test/src/push_background_status_test.dart.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:{{language.params.packageName}}/src/mqtt.dart';

void main() {
group('PushBackgroundStatus', () {
test('fromMap should read backgroundRestricted', () {
final status = PushBackgroundStatus.fromMap({
'exactAlarms': true,
'ignoringBatteryOptimizations': true,
'foregroundService': true,
'bestEffort': true,
'backgroundRestricted': true,
});

expect(status.backgroundRestricted, isTrue);
expect(status.bestEffort, isTrue);
expect(status.exactAlarms, isTrue);
expect(status.ignoringBatteryOptimizations, isTrue);
expect(status.foregroundService, isTrue);
});

test('fromMap should default backgroundRestricted to false', () {
final status = PushBackgroundStatus.fromMap({
'exactAlarms': false,
'ignoringBatteryOptimizations': false,
'foregroundService': false,
'bestEffort': true,
});

expect(status.backgroundRestricted, isFalse);
expect(status.bestEffort, isTrue);
});

test('constructor should default backgroundRestricted to false', () {
const status = PushBackgroundStatus(
exactAlarms: true,
ignoringBatteryOptimizations: false,
foregroundService: false,
bestEffort: false,
);

expect(status.backgroundRestricted, isFalse);
});
});
}
11 changes: 10 additions & 1 deletion templates/react-native/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ With Expo, list them under `android.permissions` in `app.json` instead.
nothing to ask, including when the permission is not declared. The SDK never opens these
screens on its own.

If `backgroundRestricted` is true, the user restricted the app's background usage and messages can
wait until the app is opened. Exact alarms don't lift it, and neither does foreground mode: Android
stops a restricted app's foreground service once it leaves the screen. Ask the user to choose
Unrestricted in the app's battery settings (Settings > Apps > your app > Battery); granting the
battery optimisation exemption also lets the SDK's alarms and jobs run, and `bestEffort` then
reflects only the exact-alarm state. It is always false below Android 9.

If the user force-stops the app (Settings > Force stop, and on some devices swiping it away from
recents), Android cancels its alarms and jobs: nothing is delivered until the app is opened
again, and the broker then replays what was sent meanwhile.
Expand All @@ -113,7 +120,9 @@ Set the notification icon with

```js
const status = await push.backgroundStatus(); // null outside Android
if (status?.bestEffort) {
if (status?.backgroundRestricted && !status.ignoringBatteryOptimizations) {
// Ask the user to choose Unrestricted in the app's battery settings.
} else if (status?.bestEffort) {
// Explain why, then from a button press:
await push.requestExactAlarms();
}
Expand Down
23 changes: 19 additions & 4 deletions templates/react-native/src/services/push.ts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,20 @@ export interface PushBackgroundStatus {
ignoringBatteryOptimizations: boolean;
/** Foreground mode ({@link Push.setForeground}) keeps the connection open in a service. */
foregroundService: boolean;
/** Wake-ups may be deferred by Doze, so messages can arrive late while the app is closed. */
/**
* Messages can arrive late, or not until the app is opened, while the app is closed: background
* usage is restricted without a battery optimisation exemption, or wake-ups may be deferred by
* Doze without exact alarms or foreground mode.
*/
bestEffort: boolean;
/**
* The user restricted the app's background usage in the app's battery settings (Settings >
* Apps > your app > Battery), Android 9+. Android may then hold its alarms, jobs and foreground
* service while it is closed, so messages can wait until the app is opened. The battery
* optimisation exemption lets the alarms and jobs run again, but foreground mode still stops
* once the app leaves the screen. False below Android 9.
*/
backgroundRestricted: boolean;
}

/** A background notification the user tapped. */
Expand Down Expand Up @@ -293,9 +305,12 @@ export class Push extends Service {
}

/**
* Android: what background delivery can rely on. When `bestEffort` is true the scheduled
* wake-ups are inexact and Doze can defer them, so messages may arrive late while the app is
* closed; explain why, then ask with {@link requestExactAlarms} or
* Android: what background delivery can rely on. When `bestEffort` is true messages may arrive
* late, or not until the app is opened, while the app is closed, so explain why, then ask. If
* `backgroundRestricted` is true and the app is not `ignoringBatteryOptimizations`, ask the user
* to choose Unrestricted in the app's battery settings (Settings > Apps > your app > Battery),
* or ask with {@link requestIgnoreBatteryOptimizations}; otherwise the scheduled wake-ups are
* inexact and Doze can defer them, so ask with {@link requestExactAlarms} or
* {@link requestIgnoreBatteryOptimizations}. Null elsewhere.
*/
async backgroundStatus(): Promise<PushBackgroundStatus | null> {
Expand Down
5 changes: 3 additions & 2 deletions tests/e2e/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,8 @@ abstract class Base extends TestCase
'Push background drain:passed',
];

// Android only: no notification while the app is on screen, the watchdog job, and resume with
// the current credential.
// Android only: no notification while the app is on screen, the watchdog job, resume with the
// current credential, and restricted background usage.
protected const ANDROID_PUSH_BACKGROUND_RESPONSES = [
'Push background message:passed',
'Push foreground no notification:passed',
Expand All @@ -333,6 +333,7 @@ abstract class Base extends TestCase
'Push background resume credential:passed',
'Push background close:passed',
'Push background refused:passed',
'Push background restricted:passed',
];

protected const PUSH_BACKGROUND_RESPONSES = [
Expand Down
Loading
Loading