Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow adding foreground service types dynamically #1050

Merged
merged 6 commits into from
Sep 11, 2024
Merged
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
11 changes: 9 additions & 2 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ concurrency:

jobs:
linting:
name: ESLint
name: Lint
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
@@ -24,6 +24,11 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Configure JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Set workflow variables
id: workflow-variables
run: |
@@ -43,8 +48,10 @@ jobs:
retry_wait_seconds: 30
max_attempts: 3
command: yarn --no-audit --prefer-offline
- name: Lint
- name: Lint javascript
run: yarn validate:all:js
- name: Lint Native - validate:all:check
run: yarn format:all:check

typescript:
name: TypeScript Build Validation
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ ext {
}

android {
compileSdkVersion 33
compileSdkVersion 34
testOptions {
unitTests.returnDefaultValues = true
}
3 changes: 2 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -30,7 +30,8 @@
<!-- Foreground Service -->
<service
android:name=".ForegroundService"
android:exported="false" />
android:exported="false"
android:foregroundServiceType="shortService" />

<receiver
android:name=".RebootBroadcastReceiver"
41 changes: 34 additions & 7 deletions android/src/main/java/app/notifee/core/ForegroundService.java
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
*
*/

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.Service;
import android.content.Intent;
@@ -39,6 +40,8 @@ public class ForegroundService extends Service {

public static String mCurrentNotificationId = null;

public static int mCurrentForegroundServiceType = -1;

static void start(int hashCode, Notification notification, Bundle notificationBundle) {
Intent intent = new Intent(ContextHolder.getApplicationContext(), ForegroundService.class);
intent.setAction(START_FOREGROUND_SERVICE_ACTION);
@@ -69,13 +72,15 @@ static void stop() {
}
}

@SuppressLint({"ForegroundServiceType", "MissingPermission"})
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Check if action is to stop the foreground service
if (intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())) {
stopSelf();
mCurrentNotificationId = null;
return 0;
mCurrentForegroundServiceType = -1;
return Service.START_STICKY_COMPATIBILITY;
}

Bundle extras = intent.getExtras();
@@ -91,25 +96,47 @@ public int onStartCommand(Intent intent, int flags, int startId) {

if (mCurrentNotificationId == null) {
mCurrentNotificationId = notificationModel.getId();
startForeground(hashCode, notification);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
int foregroundServiceType = notificationModel.getAndroid().getForegroundServiceType();
startForeground(hashCode, notification, foregroundServiceType);
mCurrentForegroundServiceType = foregroundServiceType;
} else {
startForeground(hashCode, notification);
}

// On headless task complete
final MethodCallResult<Void> methodCallResult =
(e, aVoid) -> {
stopForeground(true);
mCurrentNotificationId = null;
mCurrentForegroundServiceType = -1;
};

ForegroundServiceEvent foregroundServiceEvent =
new ForegroundServiceEvent(notificationModel, methodCallResult);

EventBus.post(foregroundServiceEvent);
} else if (mCurrentNotificationId.equals(notificationModel.getId())) {
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
.notify(hashCode, notification);
} else {
EventBus.post(
new NotificationEvent(NotificationEvent.TYPE_FG_ALREADY_EXIST, notificationModel));
if (mCurrentNotificationId.equals(notificationModel.getId())) {
boolean shouldPostNotificationAgain = true;
// find if we need to start the service again if the type was changed
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
int foregroundServiceType = notificationModel.getAndroid().getForegroundServiceType();
if (foregroundServiceType != mCurrentForegroundServiceType) {
startForeground(hashCode, notification, foregroundServiceType);
mCurrentForegroundServiceType = foregroundServiceType;
shouldPostNotificationAgain = false;
}
}
if (shouldPostNotificationAgain) {
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
.notify(hashCode, notification);
}
} else {
EventBus.post(
new NotificationEvent(NotificationEvent.TYPE_FG_ALREADY_EXIST, notificationModel));
}
}
}
}
34 changes: 17 additions & 17 deletions android/src/main/java/app/notifee/core/NotifeeAlarmManager.java
Original file line number Diff line number Diff line change
@@ -34,9 +34,8 @@
import app.notifee.core.utility.ObjectUtils;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.Task;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@@ -154,11 +153,12 @@ static void scheduleTimestampTriggerNotification(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

// Check whether the alarmType is the exact alarm
boolean isExactAlarm = Arrays
.asList(TimestampTriggerModel.AlarmType.SET_EXACT,
TimestampTriggerModel.AlarmType.SET_EXACT_AND_ALLOW_WHILE_IDLE,
TimestampTriggerModel.AlarmType.SET_ALARM_CLOCK)
.contains(alarmType);
boolean isExactAlarm =
Arrays.asList(
TimestampTriggerModel.AlarmType.SET_EXACT,
TimestampTriggerModel.AlarmType.SET_EXACT_AND_ALLOW_WHILE_IDLE,
TimestampTriggerModel.AlarmType.SET_ALARM_CLOCK)
.contains(alarmType);
if (isExactAlarm && !alarmManager.canScheduleExactAlarms()) {
System.err.println(
"Missing SCHEDULE_EXACT_ALARM permission. Trigger not scheduled. See:"
@@ -176,15 +176,15 @@ static void scheduleTimestampTriggerNotification(
break;
case SET_AND_ALLOW_WHILE_IDLE:
AlarmManagerCompat.setAndAllowWhileIdle(
alarmManager, AlarmManager.RTC_WAKEUP, timestampTrigger.getTimestamp(), pendingIntent);
alarmManager, AlarmManager.RTC_WAKEUP, timestampTrigger.getTimestamp(), pendingIntent);
break;
case SET_EXACT:
AlarmManagerCompat.setExact(
alarmManager, AlarmManager.RTC_WAKEUP, timestampTrigger.getTimestamp(), pendingIntent);
alarmManager, AlarmManager.RTC_WAKEUP, timestampTrigger.getTimestamp(), pendingIntent);
break;
case SET_EXACT_AND_ALLOW_WHILE_IDLE:
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, AlarmManager.RTC_WAKEUP, timestampTrigger.getTimestamp(), pendingIntent);
alarmManager, AlarmManager.RTC_WAKEUP, timestampTrigger.getTimestamp(), pendingIntent);
break;
case SET_ALARM_CLOCK:
// probably a good default behavior for setAlarmClock's
@@ -196,16 +196,16 @@ static void scheduleTimestampTriggerNotification(

Context context = getApplicationContext();
Intent launchActivityIntent =
context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());

PendingIntent pendingLaunchIntent =
PendingIntent.getActivity(
context,
notificationModel.getId().hashCode(),
launchActivityIntent,
mutabilityFlag);
PendingIntent.getActivity(
context,
notificationModel.getId().hashCode(),
launchActivityIntent,
mutabilityFlag);
AlarmManagerCompat.setAlarmClock(
alarmManager, timestampTrigger.getTimestamp(), pendingLaunchIntent, pendingIntent);
alarmManager, timestampTrigger.getTimestamp(), pendingLaunchIntent, pendingIntent);
break;
}
}
Original file line number Diff line number Diff line change
@@ -634,7 +634,8 @@ static void createIntervalTriggerNotification(

PeriodicWorkRequest.Builder workRequestBuilder;
workRequestBuilder =
new PeriodicWorkRequest.Builder(Worker.class, interval, trigger.getTimeUnit()).setInitialDelay(interval, trigger.getTimeUnit());
new PeriodicWorkRequest.Builder(Worker.class, interval, trigger.getTimeUnit())
.setInitialDelay(interval, trigger.getTimeUnit());

workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
workRequestBuilder.addTag(uniqueWorkName);
Original file line number Diff line number Diff line change
@@ -24,9 +24,7 @@
import app.notifee.core.event.MainComponentEvent;
import app.notifee.core.model.NotificationAndroidPressActionModel;
import app.notifee.core.utility.IntentUtils;

import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

public class NotificationPendingIntent {
public static final String EVENT_TYPE_INTENT_KEY = "notifee_event_type";
@@ -152,8 +150,11 @@ static Intent createLaunchActivityIntent(
// to handle a custom launchActivity
Boolean shouldOverwriteDefaultLaunchActivityIntent = launchActivityIntent == null;
if (launchActivityIntent != null) {
// overwrite if custom launch activity set (launch activity in payload does not equal current activity)
shouldOverwriteDefaultLaunchActivityIntent = launchActivity != "default" && launchActivityIntent.getComponent().getClassName() != launchActivity;
// overwrite if custom launch activity set (launch activity in payload does not equal
// current activity)
shouldOverwriteDefaultLaunchActivityIntent =
launchActivity != "default"
&& launchActivityIntent.getComponent().getClassName() != launchActivity;
}

// Set new launch activity intent
Original file line number Diff line number Diff line change
@@ -94,7 +94,7 @@ private static void handleNotificationActionIntent(Context context, Intent inten

if (notificationModel.getAndroid().getAutoCancel()) {
NotificationManagerCompat.from(context)
.cancel(intent.getIntExtra(NOTIFICATION_ID_INTENT_KEY, 0));
.cancel(intent.getIntExtra(NOTIFICATION_ID_INTENT_KEY, 0));
}

InitialNotificationEvent initialNotificationEvent =
Original file line number Diff line number Diff line change
@@ -18,11 +18,14 @@
*/

import android.app.Notification;
import android.content.pm.ServiceInfo;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import app.notifee.core.Logger;
@@ -60,6 +63,31 @@ public static NotificationAndroidModel fromBundle(Bundle bundle) {
return null;
}

@RequiresApi(api = Build.VERSION_CODES.Q)
public int getForegroundServiceType() {
if (!mNotificationAndroidBundle.containsKey("foregroundServiceTypes")) {
// no foreground service types provided, so we default to manifest
return ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
}

ArrayList<?> foregroundServiceTypesArrayList =
Objects.requireNonNull(
mNotificationAndroidBundle.getParcelableArrayList("foregroundServiceTypes"));

int foregroundServiceType = 0;
for (int i = 0; i < foregroundServiceTypesArrayList.size(); i++) {
foregroundServiceType |= ObjectUtils.getInt(foregroundServiceTypesArrayList.get(i));
}

// from Android 14, it is disallowed to use NONE type, so we default to manifest
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
&& foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
return ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
}

return foregroundServiceType;
}

/**
* Gets whether the notification is for a foreground service
*
@@ -288,7 +316,7 @@ public Boolean getCircularLargeIcon() {
if (mNotificationAndroidBundle.containsKey("lights")) {
try {
ArrayList<?> lightList =
Objects.requireNonNull(mNotificationAndroidBundle.getParcelableArrayList("lights"));
Objects.requireNonNull(mNotificationAndroidBundle.getParcelableArrayList("lights"));
String rawColor = (String) lightList.get(0);

ArrayList<Integer> lights = new ArrayList<>(3);
@@ -298,9 +326,7 @@ public Boolean getCircularLargeIcon() {

return lights;
} catch (Exception e) {
Logger.e(
TAG,
"getLights -> Failed to parse lights");
Logger.e(TAG, "getLights -> Failed to parse lights");
return null;
}
}
Original file line number Diff line number Diff line change
@@ -87,21 +87,20 @@ private TimestampTriggerModel(Bundle bundle) {
type = 2;
}


// this is for the deprecated `alarmManager.allowWhileIdle` option
if (alarmManagerBundle.containsKey("allowWhileIdle") &&
alarmManagerBundle.getBoolean("allowWhileIdle")) {
if (alarmManagerBundle.containsKey("allowWhileIdle")
&& alarmManagerBundle.getBoolean("allowWhileIdle")) {
type = 3;
}

switch (type){
switch (type) {
case 0:
mAlarmType = AlarmType.SET;
break;
case 1:
mAlarmType = AlarmType.SET_AND_ALLOW_WHILE_IDLE;
break;
// default behavior when alarmManager is true:
// default behavior when alarmManager is true:
default:
case 2:
mAlarmType = AlarmType.SET_EXACT;
24 changes: 22 additions & 2 deletions docs-react-native/react-native/docs/android/foreground-service.md
Original file line number Diff line number Diff line change
@@ -186,9 +186,29 @@ To specify which service types you require, add `notifee`'s foreground service t
<manifest>
...
<!-- For example, with one service type -->
<service android:name="app.notifee.core.ForegroundService" android:foregroundServiceType="location" />
<service android:name="app.notifee.core.ForegroundService" android:foregroundServiceType="location" tools:replace="android:foregroundServiceType" />

<!-- Or, with multiple service types -->
<service android:name="app.notifee.core.ForegroundService" android:foregroundServiceType="location|camera|microphone" />
<service android:name="app.notifee.core.ForegroundService" android:foregroundServiceType="location|camera|microphone" tools:replace="android:foregroundServiceType" />
</manifest>
```

From Android 14 (API level 34) or higher, the operating system checks when you create a foreground service to make sure your app has all the appropriate permissions for that service type. For example, when you create a foreground service of type `microphone`, the operating system verifies that your app currently has the `RECORD_AUDIO` permission. If you don't have that permission, the system throws a `SecurityException`. To support this, a notification can be created with `foregroundServiceTypes` property which specifies the types to be used while creating the service:

```js
import notifee, { AndroidForegroundServiceType } from '@notifee/react-native';

notifee.displayNotification({
title: 'Foreground service',
body: 'This notification will exist for the lifetime of the service runner',
android: {
channelId,
asForegroundService: true,
foregroundServiceTypes: [AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_CAMERA, AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_MICROPHONE],
},
});
```

If no [foregroundServiceTypes](/react-native/docs/android/interaction#quick-actions) property is provided, the types are taken from the manifest.

If any permission was granted while running the service, the same notification (with the same notification ID and same channel ID) can be posted again with the new `foregroundServiceTypes` property array with the same notification ID and the current running service will be updated with the new types.
4 changes: 2 additions & 2 deletions ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@
#import "NotifeeCoreDelegateHolder.h"
#import "NotifeeCoreUtil.h"

@implementation NotifeeCoreUNUserNotificationCenter

Check warning on line 23 in ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m

GitHub Actions / iOS

method definition for 'buildNotificationContent:' not found [-Wincomplete-implementation]
struct {
unsigned int willPresentNotification : 1;
unsigned int didReceiveNotificationResponse : 1;
@@ -186,7 +186,8 @@
didReceiveNotificationResponse:response
withCompletionHandler:completionHandler];
} else {
notifeeNotification = [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request];
notifeeNotification =
[NotifeeCoreUtil parseUNNotificationRequest:response.notification.request];
}
}

@@ -251,7 +252,6 @@
dispatch_get_main_queue(), ^{
completionHandler();
});

}
}

Loading

Unchanged files with check annotations Beta

- (void)application_onDidFinishLaunchingNotification:(nonnull NSNotification *)notification {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSDictionary *notifUserInfo =

Check warning on line 63 in ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m

GitHub Actions / iOS

unused variable 'notifUserInfo' [-Wunused-variable]
notification.userInfo[UIApplicationLaunchOptionsLocalNotificationKey];
UILocalNotification *launchNotification =
(UILocalNotification *)notification.userInfo[UIApplicationLaunchOptionsLocalNotificationKey];
RCTAppSetupPrepareApp(application);
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

Check warning on line 62 in tests_react_native/ios/testing/AppDelegate.mm

GitHub Actions / iOS

sending 'AppDelegate *const __strong' to parameter of incompatible type 'id<RCTBridgeDelegate>'
#if RCT_NEW_ARCH_ENABLED
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
private readonly jsStack: string;
// TODO native error type
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types

Check warning on line 14 in packages/react-native/src/NotifeeNativeError.ts

GitHub Actions / Lint

'@typescript-eslint/explicit-module-boundary-types' rule is disabled but never reported
constructor(nativeError: any, jsStack = '') {
super();
const { userInfo } = nativeError;
}
// todo errorEvent type
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types

Check warning on line 57 in packages/react-native/src/NotifeeNativeError.ts

GitHub Actions / Lint

'@typescript-eslint/explicit-module-boundary-types' rule is disabled but never reported
static fromEvent(errorEvent: any, stack?: string): NotifeeNativeError {
return new NotifeeNativeError({ userInfo: errorEvent }, stack || new Error().stack);
}
export * from './id';
export * from './validate';
/* eslint-disable-next-line @typescript-eslint/ban-types */

Check warning on line 10 in packages/react-native/src/utils/index.ts

GitHub Actions / Lint

'@typescript-eslint/ban-types' rule is disabled but never reported
export function isError(value: object): boolean {
if (Object.prototype.toString.call(value) === '[object Error]') {
return true;
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */

Check warning on line 1 in packages/react-native/src/utils/validate.ts

GitHub Actions / Lint

'@typescript-eslint/explicit-module-boundary-types' rule is disabled but never reported
/* eslint-disable @typescript-eslint/ban-types */
/*
* Copyright (c) 2016-present Invertase Limited
firebase.messaging().setBackgroundMessageHandler(onBackgroundMessage);
function Root(): any {
// @ts-ignore
const [id, setId] = React.useState<string | null>(null);

Check warning on line 96 in tests_react_native/example/app.tsx

GitHub Actions / Lint

'setId' is assigned a value but never used. Allowed unused elements of array destructuring patterns must match /^_/u
async function init(): Promise<void> {
const fcmToken = await firebase.messaging().getToken();
const date = new Date(Date.now());
date.setSeconds(date.getSeconds() + 15);
// @ts-ignore
const trigger: TimestampTrigger = {

Check warning on line 178 in tests_react_native/example/app.tsx

GitHub Actions / Lint

'trigger' is assigned a value but never used
type: 0,
timestamp: date.getTime(),
alarmManager: true,