diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index 72dffde33f..2b79fc306a 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -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', diff --git a/templates/android/README.md.twig b/templates/android/README.md.twig index 1ad125493e..706a09671a 100644 --- a/templates/android/README.md.twig +++ b/templates/android/README.md.twig @@ -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. diff --git a/templates/android/library/src/main/java/io/package/services/Push.kt.twig b/templates/android/library/src/main/java/io/package/services/Push.kt.twig index e2dc82857a..f0818a1494 100644 --- a/templates/android/library/src/main/java/io/package/services/Push.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Push.kt.twig @@ -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) diff --git a/templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig b/templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig index 80e6dd88ae..e471940011 100644 --- a/templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/PushBackground.kt.twig @@ -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, ) } diff --git a/templates/android/library/src/main/java/io/package/services/PushBridge.kt.twig b/templates/android/library/src/main/java/io/package/services/PushBridge.kt.twig index 6465c91393..614fa55b0f 100644 --- a/templates/android/library/src/main/java/io/package/services/PushBridge.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/PushBridge.kt.twig @@ -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() } diff --git a/templates/android/library/src/main/java/io/package/services/PushCore.kt.twig b/templates/android/library/src/main/java/io/package/services/PushCore.kt.twig index 0467f531f4..aa737441ec 100644 --- a/templates/android/library/src/main/java/io/package/services/PushCore.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/PushCore.kt.twig @@ -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. */ diff --git a/templates/flutter/README.md.twig b/templates/flutter/README.md.twig index 5ffa20358e..f96f58e375 100644 --- a/templates/flutter/README.md.twig +++ b/templates/flutter/README.md.twig @@ -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. diff --git a/templates/flutter/lib/src/mqtt.dart.twig b/templates/flutter/lib/src/mqtt.dart.twig index 4382c967e7..2f90323bbb 100644 --- a/templates/flutter/lib/src/mqtt.dart.twig +++ b/templates/flutter/lib/src/mqtt.dart.twig @@ -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 map) => @@ -57,6 +67,7 @@ class PushBackgroundStatus { map['ignoringBatteryOptimizations'] == true, foregroundService: map['foregroundService'] == true, bestEffort: map['bestEffort'] == true, + backgroundRestricted: map['backgroundRestricted'] == true, ); } @@ -189,9 +200,12 @@ abstract class Push extends Service { /// background. It only affects [background] subscriptions. A no-op elsewhere. Future 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 backgroundStatus(); diff --git a/templates/flutter/test/src/push_background_status_test.dart.twig b/templates/flutter/test/src/push_background_status_test.dart.twig new file mode 100644 index 0000000000..aee5e69b3b --- /dev/null +++ b/templates/flutter/test/src/push_background_status_test.dart.twig @@ -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); + }); + }); +} diff --git a/templates/react-native/README.md.twig b/templates/react-native/README.md.twig index 24834b4c53..a1a6dad290 100644 --- a/templates/react-native/README.md.twig +++ b/templates/react-native/README.md.twig @@ -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. @@ -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(); } diff --git a/templates/react-native/src/services/push.ts.twig b/templates/react-native/src/services/push.ts.twig index 7a411a9936..c3cbf27400 100644 --- a/templates/react-native/src/services/push.ts.twig +++ b/templates/react-native/src/services/push.ts.twig @@ -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. */ @@ -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 { diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index b5d6e21738..2b880bdaeb 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -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', @@ -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 = [ diff --git a/tests/e2e/languages/android/Tests.kt b/tests/e2e/languages/android/Tests.kt index 2fe467bb05..54c8067cf1 100644 --- a/tests/e2e/languages/android/Tests.kt +++ b/tests/e2e/languages/android/Tests.kt @@ -25,6 +25,7 @@ import io.appwrite.services.Foo import io.appwrite.services.General import io.appwrite.services.Plaintext import io.appwrite.services.Push +import io.appwrite.services.PushBackgroundStatus import io.appwrite.services.Realtime import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -981,6 +982,26 @@ class ServiceTest { ) refusedPush.close() + // Restricted background usage (the app's battery settings) makes delivery best effort. Android + // reports it from 9 on; PushBackgroundRestrictedTest covers it there. + val shadow = org.robolectric.Shadows.shadowOf(context.getSystemService(android.app.ActivityManager::class.java)) + val unrestrictedStatus = Push.backgroundStatus(context) + shadow.setBackgroundRestricted(true) + val restrictedStatus = Push.backgroundStatus(context) + shadow.setBackgroundRestricted(false) + val resetStatus = Push.backgroundStatus(context) + val restrictionReported = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P + writeToFile( + if (restrictedStatus == unrestrictedStatus.copy(backgroundRestricted = restrictionReported) && + !unrestrictedStatus.backgroundRestricted && + resetStatus == unrestrictedStatus + ) { + "Push background restricted:passed" + } else { + "Push background restricted:failed (sdk: ${android.os.Build.VERSION.SDK_INT}, restricted: $restrictedStatus, unrestricted: $unrestrictedStatus, reset: $resetStatus)" + }, + ) + // A message from createPush: the notification shows the server's title and body, not the // raw JSON, and still posts when its image cannot be fetched. One with neither a title nor // a body (an empty notification block, a data-only push, a badge or an image alone) posts @@ -1201,6 +1222,56 @@ class ServiceTest { } } +@Config(manifest=Config.NONE, sdk=[android.os.Build.VERSION_CODES.P]) +@RunWith(AndroidJUnit4::class) +class PushBackgroundRestrictedTest { + private val context = ApplicationProvider.getApplicationContext() + private val shadow = org.robolectric.Shadows.shadowOf(context.getSystemService(android.app.ActivityManager::class.java)) + private val power = org.robolectric.Shadows.shadowOf(context.getSystemService(android.os.PowerManager::class.java)) + + @Test + fun testRestrictedBackgroundUsageMakesDeliveryBestEffort() { + val unrestricted = Push.backgroundStatus(context) + shadow.setBackgroundRestricted(true) + val restricted = Push.backgroundStatus(context) + shadow.setBackgroundRestricted(false) + val reset = Push.backgroundStatus(context) + + assertFalse("unrestricted: $unrestricted", unrestricted.backgroundRestricted) + assertFalse("unrestricted: $unrestricted", unrestricted.bestEffort) + assertTrue("restricted: $restricted", restricted.backgroundRestricted) + assertTrue("restricted: $restricted", restricted.bestEffort) + assertTrue("reset: $reset, unrestricted: $unrestricted", reset == unrestricted) + } + + @Test + fun testBatteryOptimisationExemptionLiftsRestrictedBackgroundUsage() { + shadow.setBackgroundRestricted(true) + power.setIgnoringBatteryOptimizations(context.packageName, true) + val exempt = Push.backgroundStatus(context) + power.setIgnoringBatteryOptimizations(context.packageName, false) + shadow.setBackgroundRestricted(false) + + assertTrue("exempt: $exempt", exempt.exactAlarms) + assertTrue("exempt: $exempt", exempt.ignoringBatteryOptimizations) + assertTrue("exempt: $exempt", exempt.backgroundRestricted) + assertFalse("exempt: $exempt", exempt.bestEffort) + } + + @Test + fun testForegroundModeDoesNotLiftRestrictedBackgroundUsage() { + assertTrue( + PushBackgroundStatus(exactAlarms = false, ignoringBatteryOptimizations = true, foregroundService = true, backgroundRestricted = true).bestEffort, + ) + assertTrue( + PushBackgroundStatus(exactAlarms = false, ignoringBatteryOptimizations = false, foregroundService = true, backgroundRestricted = true).bestEffort, + ) + assertFalse( + PushBackgroundStatus(exactAlarms = true, ignoringBatteryOptimizations = true, foregroundService = true, backgroundRestricted = true).bestEffort, + ) + } +} + @Config(manifest=Config.NONE) @RunWith(AndroidJUnit4::class) class RequestNullEncodingTest { diff --git a/tests/e2e/languages/react-native/android-test/Tests.kt b/tests/e2e/languages/react-native/android-test/Tests.kt index 40abb05c06..6a7d32e8d7 100644 --- a/tests/e2e/languages/react-native/android-test/Tests.kt +++ b/tests/e2e/languages/react-native/android-test/Tests.kt @@ -12,6 +12,7 @@ import android.content.pm.ResolveInfo import android.os.Build import android.net.Uri import android.os.Looper +import android.os.PowerManager import android.util.Base64 import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -22,12 +23,15 @@ import com.facebook.react.modules.network.ForwardingCookieHandler import io.appwrite.reactnative.AppwriteCookiesModule import io.appwrite.reactnative.AppwritePushModule import io.appwrite.services.PushBackground +import io.appwrite.services.PushBridge import io.appwrite.services.PushMessage import io.appwrite.services.PushOpenActivity import io.appwrite.services.PushReceiver import io.appwrite.services.PushStore import org.json.JSONArray import org.json.JSONObject +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Robolectric @@ -439,3 +443,59 @@ class Tests { const val SESSION = "eyJpZCI6ImUyZS1zZXNzaW9uLXVzZXIiLCJzZWNyZXQiOiJlMmUtc2VjcmV0In0=" } } + +/** + * The status the SDKs read through the bridge on Android 9, where restricted background usage is + * reported: best effort unless the app is also exempt from battery optimisation. + */ +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +@RunWith(AndroidJUnit4::class) +class PushBridgeBackgroundStatusTest { + private val context = ApplicationProvider.getApplicationContext() + private val shadow = shadowOf(context.getSystemService(android.app.ActivityManager::class.java)) + private val power = shadowOf(context.getSystemService(PowerManager::class.java)) + private val bridge = PushBridge( + context, + object : PushBridge.Events { + override fun onMessage(subscriptionId: String, message: PushMessage, ackToken: String) = Unit + + override fun onError(message: String) = Unit + + override fun onConnection(connected: Boolean) = Unit + }, + ) + + @Test + fun testRestrictedBackgroundUsageIsBestEffort() { + val unrestricted = JSONObject(bridge.backgroundStatus()) + shadow.setBackgroundRestricted(true) + val restricted = JSONObject(bridge.backgroundStatus()) + shadow.setBackgroundRestricted(false) + + assertFalse("unrestricted: $unrestricted", unrestricted.getBoolean(BACKGROUND_RESTRICTED)) + assertFalse("unrestricted: $unrestricted", unrestricted.getBoolean(BEST_EFFORT)) + assertTrue("restricted: $restricted", restricted.getBoolean(BACKGROUND_RESTRICTED)) + assertTrue("restricted: $restricted", restricted.getBoolean(BEST_EFFORT)) + } + + @Test + fun testBatteryOptimisationExemptionLiftsRestrictedBackgroundUsage() { + shadow.setBackgroundRestricted(true) + power.setIgnoringBatteryOptimizations(context.packageName, true) + val exempt = JSONObject(bridge.backgroundStatus()) + power.setIgnoringBatteryOptimizations(context.packageName, false) + shadow.setBackgroundRestricted(false) + + assertTrue("exempt: $exempt", exempt.getBoolean(EXACT_ALARMS)) + assertTrue("exempt: $exempt", exempt.getBoolean(IGNORING_BATTERY_OPTIMIZATIONS)) + assertTrue("exempt: $exempt", exempt.getBoolean(BACKGROUND_RESTRICTED)) + assertFalse("exempt: $exempt", exempt.getBoolean(BEST_EFFORT)) + } + + private companion object { + const val EXACT_ALARMS = "exactAlarms" + const val IGNORING_BATTERY_OPTIMIZATIONS = "ignoringBatteryOptimizations" + const val BACKGROUND_RESTRICTED = "backgroundRestricted" + const val BEST_EFFORT = "bestEffort" + } +} diff --git a/tests/e2e/languages/react-native/push.node.js b/tests/e2e/languages/react-native/push.node.js index ca588d014b..0ee7eb91bb 100644 --- a/tests/e2e/languages/react-native/push.node.js +++ b/tests/e2e/languages/react-native/push.node.js @@ -197,7 +197,7 @@ async function main() { NativeModules.AppwritePush.resumed.push(args); }, backgroundStatus: async () => - JSON.stringify({ exactAlarms: false, ignoringBatteryOptimizations: false, foregroundService: false, bestEffort: true }), + JSON.stringify({ exactAlarms: false, ignoringBatteryOptimizations: false, foregroundService: false, bestEffort: true, backgroundRestricted: true }), requestExactAlarms: async () => true, requestIgnoreBatteryOptimizations: async () => false, setErrorCallback: async () => null, @@ -260,7 +260,7 @@ async function main() { const askedBattery = await firstUser.requestIgnoreBatteryOptimizations(); const resumed = NativeModules.AppwritePush.resumed.find((args) => args[0] === 'appwrite-session'); console.log(nativeConnection); - console.log(status?.bestEffort === true && askedExact === true && askedBattery === false && resumed?.[1] === e2eSession && resumed?.[2] === false ? 'Push background status JS:passed' : 'Push background status JS:failed'); + console.log(status?.bestEffort === true && status?.backgroundRestricted === true && askedExact === true && askedBattery === false && resumed?.[1] === e2eSession && resumed?.[2] === false ? 'Push background status JS:passed' : 'Push background status JS:failed'); console.log(launched?.topic === 'news' && launched.data.saleId === '42' && tapped.length === 1 && tapped[0].data.saleId === '7' && NativeModules.AppwritePush.listening.join() === 'true,false' ? 'Push notification opened JS:passed' : 'Push notification opened JS:failed'); // The same taps outside Android, where expo-notifications posted the notification: the launching