Skip to content

Commit

Permalink
Merge pull request #43 from GeoTecINIT/exact-alarm-perm
Browse files Browse the repository at this point in the history
Android 12 exact alarm permissions + fixes to battery savings permission request
  • Loading branch information
agonper authored Apr 4, 2022
2 parents 52b6d0f + eace50d commit 7db5271
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 31 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
)
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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()
)
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Application } from "@nativescript/core";
import { AndroidApplication } from "@nativescript/core";

export function waitForActivityResume(): Promise<void> {
return new Promise<void>((resolve) => {
const resumeHandler = () => {
Application.android.off(
AndroidApplication.activityResumedEvent,
resumeHandler
);
resolve();
};

Application.android.on(
AndroidApplication.activityResumedEvent,
resumeHandler
);
});
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<void> {
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!"
);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
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 {
onReceive(context: android.content.Context, intent: android.content.Intent) {
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Binary file modified src/platforms/android/taskdispatcher-release.aar
Binary file not shown.
2 changes: 1 addition & 1 deletion src/references.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <reference path="./node_modules/@nativescript/types-android/lib/android-26.d.ts" />
/// <reference path="./node_modules/@nativescript/types-android/lib/android-31.d.ts" />
/// <reference path="./node_modules/@nativescript/types-ios/index.d.ts" />

/// <reference path="./typings/android/native-lib.d.ts" />
Loading

0 comments on commit 7db5271

Please sign in to comment.