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
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
11 changes: 9 additions & 2 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ concurrency:

jobs:
linting:
name: ESLint
name: Lint
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
Expand All @@ -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: |
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ ext {
}

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

<receiver
android:name=".RebootBroadcastReceiver"
Expand Down
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
Expand Up @@ -17,6 +17,7 @@
*
*/

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.Service;
import android.content.Intent;
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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));
}
}
}
}
Expand Down
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
Expand Up @@ -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;

Expand Down Expand Up @@ -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:"
Expand All @@ -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
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
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
Expand Up @@ -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
Expand Up @@ -20,7 +20,7 @@
#import "NotifeeCoreDelegateHolder.h"
#import "NotifeeCoreUtil.h"

@implementation NotifeeCoreUNUserNotificationCenter

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

View workflow job for this annotation

GitHub Actions / iOS

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

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

}
}

Expand Down
Loading
Loading