diff --git a/README.md b/README.md
index a43aa2f..5c4317e 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ Here **_> 1 minute tasks' scheduler_** and **_Immediate tasks' scheduler_** take
### Android
-Plugin supports devices running Android 5.1 Lollipop (SDK 22) to Android 10 Q (SDK 29). Given that this plugin supports last Android 10 changes in foreground services, **Android Build Tools 29.x.x+ is required**.
+Plugin supports devices running Android 5.1 Lollipop (SDK 22) to Android 12 S (SDK 31). Given that this plugin relies on exact alarms to schedule tasks in time and supports last Android 12 changes regarding exact alarm scheduling, **Android Build Tools 31.x.x+ are required**.
### iOS
diff --git a/demo/app/tests/internal/tasks/schedulers/time-based/android/exact-alarm-perms-manager.android.spec.ts b/demo/app/tests/internal/tasks/schedulers/time-based/android/exact-alarm-perms-manager.android.spec.ts
new file mode 100644
index 0000000..d6a1719
--- /dev/null
+++ b/demo/app/tests/internal/tasks/schedulers/time-based/android/exact-alarm-perms-manager.android.spec.ts
@@ -0,0 +1,65 @@
+import { ExactAlarmPermsManager } from "nativescript-task-dispatcher/internal/tasks/schedulers/time-based/android/alarms/exact-alarm-perms-manager.android";
+import {
+ createOsAlarmManagerMock,
+ createOsForegroundActivityMock,
+ isSdkBelow,
+} from "~/tests/internal/tasks/schedulers/time-based/android/index";
+import { createScheduleExactAlarmPermRequestIntent } from "nativescript-task-dispatcher/internal/tasks/schedulers/time-based/android/intents.android";
+import { Utils } from "@nativescript/core";
+
+describe("Exact alarm perms manager", () => {
+ if (typeof android === "undefined") {
+ return;
+ }
+ const alarmManagerMock = createOsAlarmManagerMock();
+ const foregroundActivityMock = createOsForegroundActivityMock();
+ beforeEach(() => {
+ spyOn(alarmManagerMock, "canScheduleExactAlarms").and.returnValue(
+ false
+ );
+ spyOn(foregroundActivityMock, "startActivity");
+ });
+
+ it("scheduling exact alarms is allowed by default when api level is lower than 31", () => {
+ const exactAlarmPermsManager = new ExactAlarmPermsManager(
+ alarmManagerMock,
+ 30
+ );
+
+ const isGranted = exactAlarmPermsManager.isGranted();
+
+ expect(isGranted).toBeTruthy();
+ expect(alarmManagerMock.canScheduleExactAlarms).not.toHaveBeenCalled();
+ });
+
+ it("checks if schedule exact alarm permission is granted", () => {
+ if (isSdkBelow(31)) return;
+
+ const exactAlarmPermsManager = new ExactAlarmPermsManager(
+ alarmManagerMock
+ );
+
+ const isGranted = exactAlarmPermsManager.isGranted();
+
+ expect(isGranted).toBeFalse();
+ expect(alarmManagerMock.canScheduleExactAlarms).toHaveBeenCalled();
+ });
+
+ it("requests the permission when it has to", () => {
+ if (isSdkBelow(31)) return;
+
+ const exactAlarmPermsManager = new ExactAlarmPermsManager(
+ alarmManagerMock,
+ android.os.Build.VERSION.SDK_INT,
+ () => foregroundActivityMock
+ );
+
+ exactAlarmPermsManager.request();
+
+ expect(foregroundActivityMock.startActivity).toHaveBeenCalledOnceWith(
+ createScheduleExactAlarmPermRequestIntent(
+ Utils.android.getApplicationContext().getPackageName()
+ )
+ );
+ });
+});
diff --git a/demo/app/tests/internal/tasks/schedulers/time-based/android/index.ts b/demo/app/tests/internal/tasks/schedulers/time-based/android/index.ts
index 132d1d3..eedafb9 100644
--- a/demo/app/tests/internal/tasks/schedulers/time-based/android/index.ts
+++ b/demo/app/tests/internal/tasks/schedulers/time-based/android/index.ts
@@ -24,7 +24,22 @@ export function createOsAlarmManagerMock(): android.app.AlarmManager {
cancel(p0: android.app.PendingIntent): void {
return;
},
+ canScheduleExactAlarms(): boolean {
+ return true;
+ },
};
return alarmManager as android.app.AlarmManager;
}
+
+export function createOsForegroundActivityMock(): android.app.Activity {
+ const foregroundActivity = {
+ startActivity(param: android.content.Intent) {},
+ };
+
+ return foregroundActivity as android.app.Activity;
+}
+
+export function isSdkBelow(version: number) {
+ return android && android.os.Build.VERSION.SDK_INT < version;
+}
diff --git a/demo/app/tests/internal/tasks/schedulers/time-based/android/power-savings-manager.android.spec.ts b/demo/app/tests/internal/tasks/schedulers/time-based/android/power-savings-manager.android.spec.ts
index 2f82f19..ef3c53b 100644
--- a/demo/app/tests/internal/tasks/schedulers/time-based/android/power-savings-manager.android.spec.ts
+++ b/demo/app/tests/internal/tasks/schedulers/time-based/android/power-savings-manager.android.spec.ts
@@ -1,39 +1,67 @@
import { PowerSavingsManager } from "nativescript-task-dispatcher/internal/tasks/schedulers/time-based/android/alarms/power-savings-manager.android";
+import { createOsForegroundActivityMock, isSdkBelow } from "./index";
+import { Utils } from "@nativescript/core";
+import { createSavingsDeactivationIntent } from "nativescript-task-dispatcher/internal/tasks/schedulers/time-based/android/intents.android";
describe("Power savings manager", () => {
if (typeof android === "undefined") {
return;
}
- const osPowerManager = createOsPowerManagerMock();
+ const powerManagerMock = createOsPowerManagerMock();
+ const foregroundActivityMock = createOsForegroundActivityMock();
beforeEach(() => {
spyOn(
- osPowerManager,
+ powerManagerMock,
"isIgnoringBatteryOptimizations"
- ).and.callThrough();
+ ).and.returnValue(false);
+ spyOn(foregroundActivityMock, "startActivity");
});
- it("checks if power savings are enabled", () => {
- const powerSavingsManager = new PowerSavingsManager(osPowerManager);
+ it("savings are disabled by default when api level is lower than 23", () => {
+ const powerSavingsManager = new PowerSavingsManager(
+ powerManagerMock,
+ 22
+ );
const areDisabled = powerSavingsManager.areDisabled();
- expect(areDisabled === true || areDisabled === false).toBeTruthy();
+ expect(areDisabled).toBeTruthy();
expect(
- osPowerManager.isIgnoringBatteryOptimizations
- ).toHaveBeenCalled();
+ powerManagerMock.isIgnoringBatteryOptimizations
+ ).not.toHaveBeenCalled();
});
- it("returns true by default when api level is lower than 23", () => {
- const powerSavingsManager = new PowerSavingsManager(osPowerManager, 22);
+ it("checks if power savings are enabled", () => {
+ if (isSdkBelow(23)) return;
+
+ const powerSavingsManager = new PowerSavingsManager(powerManagerMock);
const areDisabled = powerSavingsManager.areDisabled();
- expect(areDisabled).toBeTruthy();
+ expect(areDisabled).toBeFalse();
expect(
- osPowerManager.isIgnoringBatteryOptimizations
- ).not.toHaveBeenCalled();
+ powerManagerMock.isIgnoringBatteryOptimizations
+ ).toHaveBeenCalled();
+ });
+
+ it("requests to disable savings when it has to", () => {
+ if (isSdkBelow(23)) return;
+
+ const powerSavingsManager = new PowerSavingsManager(
+ powerManagerMock,
+ android.os.Build.VERSION.SDK_INT,
+ () => foregroundActivityMock
+ );
+
+ powerSavingsManager.requestDeactivation();
+
+ expect(foregroundActivityMock.startActivity).toHaveBeenCalledOnceWith(
+ createSavingsDeactivationIntent(
+ Utils.android.getApplicationContext().getPackageName()
+ )
+ );
});
});
diff --git a/src-native/android/tdproject/taskdispatcher/src/main/AndroidManifest.xml b/src-native/android/tdproject/taskdispatcher/src/main/AndroidManifest.xml
index 70ae673..cfbcc07 100644
--- a/src-native/android/tdproject/taskdispatcher/src/main/AndroidManifest.xml
+++ b/src-native/android/tdproject/taskdispatcher/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
android:exported="true">
+
diff --git a/src/internal/tasks/schedulers/time-based/android/alarms/exact-alarm-perms-manager.android.ts b/src/internal/tasks/schedulers/time-based/android/alarms/exact-alarm-perms-manager.android.ts
new file mode 100644
index 0000000..514b560
--- /dev/null
+++ b/src/internal/tasks/schedulers/time-based/android/alarms/exact-alarm-perms-manager.android.ts
@@ -0,0 +1,64 @@
+import { getLogger, Logger } from "../../../../../utils/logger";
+import { Application, Utils } from "@nativescript/core";
+import { createScheduleExactAlarmPermRequestIntent } from "../intents.android";
+import { waitForActivityResume } from "./perm-request-common";
+
+export class ExactAlarmPermsManager {
+ private logger: Logger;
+ private readonly appPackage: string;
+ private askedOnce: boolean;
+
+ constructor(
+ private alarmManager: android.app.AlarmManager = Utils.android
+ .getApplicationContext()
+ .getSystemService(android.content.Context.ALARM_SERVICE),
+ private skdVersion = android.os.Build.VERSION.SDK_INT,
+ private activityGetter = () => Application.android.foregroundActivity
+ ) {
+ this.logger = getLogger("ExactAlarmPermsManager");
+ this.appPackage = Utils.android.getApplicationContext().getPackageName();
+ }
+
+ isGranted(): boolean {
+ if (this.skdVersion < 31) {
+ return true;
+ }
+
+ return this.alarmManager.canScheduleExactAlarms();
+ }
+
+ async request(): Promise {
+ if (this.askedOnce || this.isGranted()) {
+ return;
+ }
+
+ const visibleActivity = this.activityGetter();
+ if (!visibleActivity) {
+ this.logger.warn("Schedule exact alarms can not be asked in background");
+
+ return;
+ }
+ this.askedOnce = true;
+
+ const activityResume = waitForActivityResume();
+
+ const intent = createScheduleExactAlarmPermRequestIntent(this.appPackage);
+ visibleActivity.startActivity(intent);
+
+ await activityResume;
+
+ if (!this.isGranted()) {
+ throw new Error(
+ "Schedule exact alarms permission is required for the app to work as expected!"
+ );
+ }
+ }
+}
+
+let exactAlarmPermsManager: ExactAlarmPermsManager;
+export function getExactAlarmPermsManager(): ExactAlarmPermsManager {
+ if (!exactAlarmPermsManager) {
+ exactAlarmPermsManager = new ExactAlarmPermsManager();
+ }
+ return exactAlarmPermsManager;
+}
diff --git a/src/internal/tasks/schedulers/time-based/android/alarms/perm-request-common.ts b/src/internal/tasks/schedulers/time-based/android/alarms/perm-request-common.ts
new file mode 100644
index 0000000..660bac4
--- /dev/null
+++ b/src/internal/tasks/schedulers/time-based/android/alarms/perm-request-common.ts
@@ -0,0 +1,19 @@
+import { Application } from "@nativescript/core";
+import { AndroidApplication } from "@nativescript/core";
+
+export function waitForActivityResume(): Promise {
+ return new Promise((resolve) => {
+ const resumeHandler = () => {
+ Application.android.off(
+ AndroidApplication.activityResumedEvent,
+ resumeHandler
+ );
+ resolve();
+ };
+
+ Application.android.on(
+ AndroidApplication.activityResumedEvent,
+ resumeHandler
+ );
+ });
+}
diff --git a/src/internal/tasks/schedulers/time-based/android/alarms/power-savings-manager.android.ts b/src/internal/tasks/schedulers/time-based/android/alarms/power-savings-manager.android.ts
index 2759d8d..3bb36c5 100644
--- a/src/internal/tasks/schedulers/time-based/android/alarms/power-savings-manager.android.ts
+++ b/src/internal/tasks/schedulers/time-based/android/alarms/power-savings-manager.android.ts
@@ -1,6 +1,7 @@
import { Application, Utils } from "@nativescript/core";
import { createSavingsDeactivationIntent } from "../intents.android";
import { Logger, getLogger } from "../../../../../utils/logger";
+import { waitForActivityResume } from "./perm-request-common";
export class PowerSavingsManager {
private logger: Logger;
@@ -11,35 +12,47 @@ export class PowerSavingsManager {
private powerManager: android.os.PowerManager = Utils.android
.getApplicationContext()
.getSystemService(android.content.Context.POWER_SERVICE),
- private skdVersion = android.os.Build.VERSION.SDK_INT
+ private skdVersion = android.os.Build.VERSION.SDK_INT,
+ private activityGetter = () => Application.android.foregroundActivity
) {
this.logger = getLogger("PowerSavingsManager");
this.appPackage = Utils.android.getApplicationContext().getPackageName();
}
+ areDisabled(): boolean {
+ if (this.skdVersion < 23) {
+ return true;
+ }
+
+ return this.powerManager.isIgnoringBatteryOptimizations(this.appPackage);
+ }
+
// TODO: Evaluate what to do with devices not running Android Stock layer
- requestDeactivation(): void {
+ async requestDeactivation(): Promise {
if (this.askedOnce || this.areDisabled()) {
return;
}
- if (!Application.android.foregroundActivity) {
+ const visibleActivity = this.activityGetter();
+ if (!visibleActivity) {
this.logger.warn("Battery savings can not be enabled in background");
return;
}
-
this.askedOnce = true;
+
+ const activityResume = waitForActivityResume();
+
const intent = createSavingsDeactivationIntent(this.appPackage);
- Application.android.foregroundActivity.startActivity(intent);
- }
+ visibleActivity.startActivity(intent);
- areDisabled(): boolean {
- if (this.skdVersion < 23) {
- return true;
- }
+ await activityResume;
- return this.powerManager.isIgnoringBatteryOptimizations(this.appPackage);
+ if (!this.areDisabled()) {
+ throw new Error(
+ "Disabling battery optimizations is required for the app to work as expected!"
+ );
+ }
}
}
diff --git a/src/internal/tasks/schedulers/time-based/android/boot-receiver.android.ts b/src/internal/tasks/schedulers/time-based/android/boot-receiver.android.ts
index 3c11808..0a09f72 100644
--- a/src/internal/tasks/schedulers/time-based/android/boot-receiver.android.ts
+++ b/src/internal/tasks/schedulers/time-based/android/boot-receiver.android.ts
@@ -1,5 +1,6 @@
import { AndroidAlarmScheduler } from "./alarms/alarm/scheduler.android";
import { getLogger } from "../../../../utils/logger";
+import { getExactAlarmPermsManager } from "./alarms/exact-alarm-perms-manager.android";
export class BootReceiver
implements es.uji.geotec.taskdispatcher.BootReceiverDelegate {
@@ -7,6 +8,13 @@ export class BootReceiver
const logger = getLogger("BootReceiver");
logger.info("Performing boot initializations");
+ if (!getExactAlarmPermsManager().isGranted()) {
+ logger.error(
+ "Exact alarm permission not granted! Cancelling alarm setup"
+ );
+ return;
+ }
+
const alarmScheduler = new AndroidAlarmScheduler();
alarmScheduler
.setup()
diff --git a/src/internal/tasks/schedulers/time-based/android/intents.android.ts b/src/internal/tasks/schedulers/time-based/android/intents.android.ts
index 77d52b0..ca55334 100644
--- a/src/internal/tasks/schedulers/time-based/android/intents.android.ts
+++ b/src/internal/tasks/schedulers/time-based/android/intents.android.ts
@@ -31,6 +31,13 @@ export function createSavingsDeactivationIntent(appPackage: string) {
);
}
+export function createScheduleExactAlarmPermRequestIntent(appPackage: string) {
+ return new android.content.Intent(
+ android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
+ android.net.Uri.parse(`package:${appPackage}`)
+ );
+}
+
const ARS_RUN_IN_FOREGROUND = "foreground";
const ARS_TIME_OFFSET = "time-offset";
const ARS_INVOCATION_TIME = "invocation-time";
diff --git a/src/platforms/android/taskdispatcher-release.aar b/src/platforms/android/taskdispatcher-release.aar
index d067d70..e09c5d2 100644
Binary files a/src/platforms/android/taskdispatcher-release.aar and b/src/platforms/android/taskdispatcher-release.aar differ
diff --git a/src/references.d.ts b/src/references.d.ts
index c345a95..2d2a2d6 100644
--- a/src/references.d.ts
+++ b/src/references.d.ts
@@ -1,4 +1,4 @@
-///
+///
///
///
diff --git a/src/task-dispatcher.android.ts b/src/task-dispatcher.android.ts
index 8b4a4fd..d53d4e8 100644
--- a/src/task-dispatcher.android.ts
+++ b/src/task-dispatcher.android.ts
@@ -2,7 +2,6 @@ import { Common, ConfigParams } from "./task-dispatcher.common";
import { Task } from "./tasks";
import { TaskGraph } from "./tasks/graph";
-import { taskGraph } from "./internal/tasks/graph/loader";
import { taskGraphBrowser } from "./internal/tasks/graph/browser";
import { setTaskSchedulerCreator } from "./internal/tasks/schedulers/time-based/common";
@@ -15,6 +14,7 @@ import { getAlarmRunnerService } from "./internal/tasks/schedulers/time-based/an
import { getWatchDogReceiver } from "./internal/tasks/schedulers/time-based/android/alarms/watchdog/receiver.android";
import { getTaskChainRunnerService } from "./internal/tasks/schedulers/event-driven/android/runner-service.android";
import { getPowerSavingsManager } from "./internal/tasks/schedulers/time-based/android/alarms/power-savings-manager.android";
+import { getExactAlarmPermsManager } from "./internal/tasks/schedulers/time-based/android/alarms/exact-alarm-perms-manager.android";
const BATTERY_OPTIMIZATIONS_THRESHOLD = 15 * 60;
@@ -35,12 +35,19 @@ class TaskDispatcher extends Common {
if (this.requiresDisablingBatteryOptimizations()) {
return Promise.resolve(false);
}
+ if (!getExactAlarmPermsManager().isGranted()) {
+ return Promise.resolve(false);
+ }
return super.isReady();
}
- prepare(): Promise {
+ async prepare(): Promise {
if (this.requiresDisablingBatteryOptimizations()) {
- this.requestToDisableBatteryOptimizations();
+ await this.requestToDisableBatteryOptimizations();
+ }
+ const exactAlarmPermsManager = getExactAlarmPermsManager();
+ if (!exactAlarmPermsManager.isGranted()) {
+ await exactAlarmPermsManager.request();
}
return super.prepare();
}
@@ -58,12 +65,12 @@ class TaskDispatcher extends Common {
return false;
}
- private requestToDisableBatteryOptimizations() {
+ private async requestToDisableBatteryOptimizations() {
const manager = getPowerSavingsManager();
if (manager.areDisabled()) {
return;
}
- manager.requestDeactivation();
+ await manager.requestDeactivation();
}
private wireUpNativeComponents() {