├── .npmrc
├── yarn.lock
├── README.md
├── react-native-config.js
├── .gitignore
├── package.json
├── android
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── supersami
│ └── foregroundservice
│ ├── ForegroundServicePackage.java
│ ├── Constants.java
│ ├── ForegroundServiceTask.java
│ ├── MainActivity.java
│ ├── NotificationConfig.java
│ ├── ForegroundServiceModule.java
│ ├── NotificationHelper.java
│ └── ForegroundService.java
├── .github
└── workflows
│ └── npm-publish.yml
├── index.d.ts
├── postinstall.js
└── index.js
/.npmrc:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@supersami/rn-foreground-service@https://github.com/Raja0sama/rn-foreground-service#v2":
6 | version "1.0.4"
7 | resolved "https://github.com/Raja0sama/rn-foreground-service#2da73db49422198de587e6720e01e25219ea93b2"
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @supersami/rn-foreground-service v2 🤟
2 |
3 | Looking for contributors to help maintain this repository, as I am currently busy and unable to keep it up to date.
4 |
5 | >> Documentation has been moved to https://rn-foreground.vercel.app/
6 |
7 | ## License
8 |
9 | MIT © [rajaosama](https://github.com/raja0sama)
10 |
--------------------------------------------------------------------------------
/react-native-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | dependency: {
3 | platforms: {
4 | android: {
5 | packageImportPath: 'import com.supersami.foregroundservice.ForegroundServicePackage;',
6 | packageInstance: 'new ForegroundServicePackage()',
7 | },
8 | },
9 | },
10 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # OSX
3 | #
4 | .DS_Store
5 |
6 | # node.js
7 | #
8 | node_modules/
9 | npm-debug.log
10 | yarn-error.log
11 |
12 |
13 | # Xcode
14 | #
15 | build/
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 | xcuserdata
25 | *.xccheckout
26 | *.moved-aside
27 | DerivedData
28 | *.hmap
29 | *.ipa
30 | *.xcuserstate
31 | project.xcworkspace
32 |
33 |
34 | # Android/IntelliJ
35 | #
36 | build/
37 | .idea
38 | .gradle
39 | local.properties
40 | *.iml
41 |
42 | # BUCK
43 | buck-out/
44 | \.buckd/
45 | *.keystore
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@supersami/rn-foreground-service",
3 | "version": "2.2.5",
4 | "description": "A Foreground Service for React Native",
5 | "main": "index.js",
6 | "repository": "Raja0sama/rn-foreground-service",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "postinstall": "node postinstall.js"
10 | },
11 | "license": "MIT",
12 | "keywords": [
13 | "react",
14 | "react-native",
15 | "android",
16 | "foreground",
17 | "service",
18 | "background"
19 | ],
20 | "author": {
21 | "name": "Raja Osama (supersami)"
22 | },
23 | "peerDependencies": {
24 | "react-native": ">=0.59.0"
25 | },
26 | "dependencies": {}
27 | }
28 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | def safeExtGet(prop, fallback) {
4 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
5 | }
6 |
7 | android {
8 | compileSdkVersion safeExtGet('compileSdkVersion', '31')
9 | buildToolsVersion safeExtGet('buildToolsVersion', '30.0.2')
10 |
11 | defaultConfig {
12 | minSdkVersion safeExtGet('minSdkVersion', '21')
13 | targetSdkVersion safeExtGet('targetSdkVersion', '31')
14 | versionCode 1
15 | versionName "1.0"
16 | }
17 | lintOptions {
18 | abortOnError false
19 | }
20 | }
21 |
22 | repositories {
23 | mavenCentral()
24 | }
25 |
26 | dependencies {
27 | implementation 'com.facebook.react:react-native:+'
28 | }
29 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will publish a package to npm registry when manually triggered
2 | name: Publish to npm
3 |
4 | on:
5 | workflow_dispatch: # Manual trigger
6 |
7 | jobs:
8 | publish-npm:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 |
13 | - name: Setup Node.js
14 | uses: actions/setup-node@v3
15 | with:
16 | node-version: 16
17 | registry-url: https://registry.npmjs.org/
18 |
19 | - name: Install dependencies
20 | run: npm ci || npm install
21 |
22 | # Optional: Add a build step if your package needs to be built
23 | # - name: Build
24 | # run: npm run build
25 |
26 | - name: Publish to npm
27 | run: npm publish
28 | env:
29 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN }}
30 |
--------------------------------------------------------------------------------
/android/src/main/java/com/supersami/foregroundservice/ForegroundServicePackage.java:
--------------------------------------------------------------------------------
1 | package com.supersami.foregroundservice;
2 |
3 | import java.util.Arrays;
4 | import java.util.Collections;
5 | import java.util.List;
6 |
7 | import com.facebook.react.ReactPackage;
8 | import com.facebook.react.bridge.NativeModule;
9 | import com.facebook.react.bridge.ReactApplicationContext;
10 | import com.facebook.react.uimanager.ViewManager;
11 | import com.facebook.react.bridge.JavaScriptModule;
12 |
13 | public class ForegroundServicePackage implements ReactPackage {
14 | @Override
15 | public List createNativeModules(ReactApplicationContext reactContext) {
16 | return Arrays.asList(new ForegroundServiceModule(reactContext));
17 | }
18 |
19 | @Override
20 | public List createViewManagers(ReactApplicationContext reactContext) {
21 | return Collections.emptyList();
22 | }
23 | }
--------------------------------------------------------------------------------
/android/src/main/java/com/supersami/foregroundservice/Constants.java:
--------------------------------------------------------------------------------
1 | package com.supersami.foregroundservice;
2 |
3 | class Constants {
4 | static final String NOTIFICATION_CONFIG = "com.supersami.foregroundservice.notif_config";
5 | static final String TASK_CONFIG = "com.supersami.foregroundservice.task_config";
6 |
7 | static final String ACTION_FOREGROUND_SERVICE_START = "com.supersami.foregroundservice.service_start";
8 | static final String ACTION_FOREGROUND_SERVICE_STOP = "com.supersami.foregroundservice.service_stop";
9 | static final String ACTION_FOREGROUND_SERVICE_STOP_ALL = "com.supersami.foregroundservice.service_all";
10 | static final String ACTION_FOREGROUND_RUN_TASK = "com.supersami.foregroundservice.service_run_task";
11 | static final String ACTION_UPDATE_NOTIFICATION = "com.supersami.foregroundservice.service_update_notification";
12 |
13 | static final String ERROR_INVALID_CONFIG = "ERROR_INVALID_CONFIG";
14 | static final String ERROR_SERVICE_ERROR = "ERROR_SERVICE_ERROR";
15 | static final String ERROR_ANDROID_VERSION = "ERROR_ANDROID_VERSION";
16 |
17 |
18 |
19 |
20 |
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/android/src/main/java/com/supersami/foregroundservice/ForegroundServiceTask.java:
--------------------------------------------------------------------------------
1 | package com.supersami.foregroundservice;
2 |
3 | import android.app.Notification;
4 | import android.app.Service;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.os.IBinder;
8 | import android.util.Log;
9 |
10 | import com.facebook.react.HeadlessJsTaskService;
11 | import com.facebook.react.bridge.Arguments;
12 | import com.facebook.react.jstasks.HeadlessJsTaskConfig;
13 | import javax.annotation.Nullable;
14 |
15 | import static com.supersami.foregroundservice.Constants.NOTIFICATION_CONFIG;
16 |
17 |
18 | // https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java
19 |
20 | public class ForegroundServiceTask extends HeadlessJsTaskService {
21 |
22 | @Nullable
23 | protected HeadlessJsTaskConfig getTaskConfig(Intent intent) {
24 | Bundle extras = intent.getExtras();
25 | if (extras != null) {
26 | return new HeadlessJsTaskConfig(
27 | extras.getString("taskName"),
28 | Arguments.fromBundle(extras),
29 | 5000, // timeout for the task
30 | true // optional: defines whether or not the task is allowed in foreground. Default is false
31 | );
32 | }
33 | return null;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/android/src/main/java/com/supersami/foregroundservice/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.supersami.foregroundservice;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.util.Log;
6 |
7 | import com.facebook.react.ReactActivity;
8 | import com.facebook.react.ReactActivityDelegate;
9 | import com.facebook.react.ReactRootView;
10 | import com.facebook.react.bridge.Arguments;
11 | import com.facebook.react.bridge.WritableMap;
12 | import com.facebook.react.modules.core.DeviceEventManagerModule;
13 |
14 | public class MainActivity extends ReactActivity {
15 |
16 | public boolean isOnNewIntent = false;
17 |
18 | @Override
19 | public void onNewIntent(Intent intent) {
20 | super.onNewIntent(intent);
21 | isOnNewIntent = true;
22 | ForegroundEmitter();
23 | }
24 |
25 | @Override
26 | protected void onStart() {
27 | super.onStart();
28 | if(isOnNewIntent == true){}else {
29 | ForegroundEmitter();
30 | }
31 | }
32 |
33 | public void ForegroundEmitter(){
34 | // this method is to send back data from java to javascript so one can easily
35 | // know which button from notification or the notification button is clicked
36 | String main = getIntent().getStringExtra("mainOnPress");
37 | String btn = getIntent().getStringExtra("buttonOnPress");
38 | String btn2 = getIntent().getStringExtra("button2OnPress");
39 | WritableMap map = Arguments.createMap();
40 | if (main != null) {
41 | map.putString("main", main);
42 | }
43 | if (btn != null) {
44 | map.putString("button", btn);
45 | }
46 | if (btn2 != null) {
47 | map.putString("button", btn);
48 | }
49 | try {
50 | getReactInstanceManager().getCurrentReactContext()
51 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
52 | .emit("notificationClickHandle", map);
53 | } catch (Exception e) {
54 | Log.e("SuperLog", "Caught Exception: " + e.getMessage());
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/android/src/main/java/com/supersami/foregroundservice/NotificationConfig.java:
--------------------------------------------------------------------------------
1 | package com.supersami.foregroundservice;
2 |
3 | import android.content.Context;
4 | import android.content.pm.ApplicationInfo;
5 | import android.content.pm.PackageManager;
6 | import androidx.core.content.res.ResourcesCompat;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 |
10 |
11 | // took ideas from: https://github.com/zo0r/react-native-push-notification/blob/master/android/src/main/java/com/dieam/reactnativepushnotification/modules/RNPushNotificationConfig.java
12 |
13 |
14 | class NotificationConfig {
15 |
16 | private static final String KEY_CHANNEL_NAME = "com.supersami.foregroundservice.notification_channel_name";
17 | private static final String KEY_CHANNEL_DESCRIPTION = "com.supersami.foregroundservice.notification_channel_description";
18 | private static final String KEY_NOTIFICATION_COLOR = "com.supersami.foregroundservice.notification_color";
19 |
20 | private static Bundle metadata;
21 | private Context context;
22 |
23 | public NotificationConfig(Context context) {
24 | this.context = context;
25 | if (metadata == null) {
26 | try {
27 | ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
28 | metadata = applicationInfo.metaData;
29 | } catch (PackageManager.NameNotFoundException e) {
30 | e.printStackTrace();
31 | Log.e("NotificationConfig", "Error reading application meta, falling back to defaults");
32 | metadata = new Bundle();
33 | }
34 | }
35 | }
36 |
37 | public String getChannelName() {
38 | try {
39 | return metadata.getString(KEY_CHANNEL_NAME);
40 | } catch (Exception e) {
41 | Log.w("NotificationConfig", "Unable to find " + KEY_CHANNEL_NAME + " in manifest. Falling back to default");
42 | }
43 | // Default
44 | return "com.supersami.foregroundservice";
45 | }
46 |
47 | public String getChannelDescription() {
48 | try {
49 | return metadata.getString(KEY_CHANNEL_DESCRIPTION);
50 | } catch (Exception e) {
51 | Log.w("NotificationConfig", "Unable to find " + KEY_CHANNEL_DESCRIPTION + " in manifest. Falling back to default");
52 | }
53 | // Default
54 | return "com.supersami.foregroundservice";
55 | }
56 |
57 | public int getNotificationColor() {
58 | try {
59 | int resourceId = metadata.getInt(KEY_NOTIFICATION_COLOR);
60 | return ResourcesCompat.getColor(context.getResources(), resourceId, null);
61 | } catch (Exception e) {
62 | Log.w("NotificationConfig", "Unable to find " + KEY_NOTIFICATION_COLOR + " in manifest. Falling back to default");
63 | }
64 | // Default
65 | return -1;
66 | }
67 | }
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | declare const ReactNativeForegroundService: {
2 | register: ({
3 | config,
4 | }: {
5 | config: {
6 | alert: boolean;
7 | onServiceErrorCallBack: () => void;
8 | };
9 | }) => void;
10 | start: ({
11 | id,
12 | title,
13 | message,
14 | vibration,
15 | visibility,
16 | icon,
17 | largeIcon,
18 | importance,
19 | number,
20 | button,
21 | buttonText,
22 | buttonOnPress,
23 | button2,
24 | button2Text,
25 | button2OnPress,
26 | mainOnPress,
27 | progress,
28 | color,
29 | setOnlyAlertOnce,
30 | }: {
31 | id: any;
32 | title?: any;
33 | message?: string | undefined;
34 | vibration?: boolean | undefined;
35 | visibility?: string | undefined;
36 | icon?: string | undefined;
37 | largeIcon?: string | undefined;
38 | importance?: string | undefined;
39 | number?: string | undefined;
40 | button?: boolean | undefined;
41 | buttonText?: string | undefined;
42 | buttonOnPress?: string | undefined;
43 | button2?: boolean | undefined;
44 | button2Text?: string | undefined;
45 | button2OnPress?: string | undefined;
46 | mainOnPress?: string | undefined;
47 | progress?: {
48 | max: number;
49 | curr: number;
50 | };
51 | color?: string;
52 | setOnlyAlertOnce?: string;
53 | }) => Promise;
54 | update: ({
55 | id,
56 | title,
57 | message,
58 | vibration,
59 | visibility,
60 | largeIcon,
61 | icon,
62 | importance,
63 | number,
64 | button,
65 | buttonText,
66 | buttonOnPress,
67 | button2,
68 | button2Text,
69 | button2OnPress,
70 | mainOnPress,
71 | progress,
72 | color,
73 | setOnlyAlertOnce,
74 | }: {
75 | id: any;
76 | title?: any;
77 | message?: string | undefined;
78 | vibration?: boolean | undefined;
79 | visibility?: string | undefined;
80 | largeIcon?: string | undefined;
81 | icon?: string | undefined;
82 | importance?: string | undefined;
83 | number?: string | undefined;
84 | button?: boolean | undefined;
85 | buttonText?: string | undefined;
86 | buttonOnPress?: string | undefined;
87 | button2?: boolean | undefined;
88 | button2Text?: string | undefined;
89 | button2OnPress?: string | undefined;
90 | mainOnPress?: string | undefined;
91 | progress?: {
92 | max: number;
93 | curr: number;
94 | };
95 | color?: string;
96 | setOnlyAlertOnce?: string;
97 | }) => Promise;
98 | stop: () => Promise;
99 | stopAll: () => Promise;
100 | is_running: () => boolean;
101 | add_task: (
102 | task: any,
103 | {
104 | delay,
105 | onLoop,
106 | taskId,
107 | onSuccess,
108 | onError,
109 | }: {
110 | delay?: number | undefined;
111 | onLoop?: boolean | undefined;
112 | taskId?: string | undefined;
113 | onSuccess?: (() => void) | undefined;
114 | onError?: ((e) => void) | undefined;
115 | },
116 | ) => string;
117 | update_task: (
118 | task: any,
119 | {
120 | delay,
121 | onLoop,
122 | taskId,
123 | onSuccess,
124 | onError,
125 | }: {
126 | delay?: number | undefined;
127 | onLoop?: boolean | undefined;
128 | taskId?: string | undefined;
129 | onSuccess?: (() => void) | undefined;
130 | onError?: (() => void) | undefined;
131 | },
132 | ) => string;
133 | remove_task: (taskId: any) => void;
134 | is_task_running: (taskId: any) => boolean;
135 | remove_all_tasks: () => {};
136 | get_task: (taskId: any) => any;
137 | get_all_tasks: () => {};
138 | eventListener: (callBack: any) => () => void;
139 | };
140 | export default ReactNativeForegroundService;
141 |
--------------------------------------------------------------------------------
/postinstall.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | const foregroundServicePermTemplate = `
5 |
6 |
7 | `;
8 | const metadataTemplate = `
9 |
13 |
17 |
21 |
22 | // also define android:foregroundServiceType="" according to your use case
23 | // also define android:foregroundServiceType="" according to your use case
24 | `;
25 |
26 | const androidManifestPath = `${process.cwd()}/android/app/src/main/AndroidManifest.xml`;
27 |
28 | fs.readFile(androidManifestPath, "utf8", function (err, data) {
29 | if (err) {
30 | return console.log(err);
31 | }
32 |
33 | if (!data.includes(foregroundServicePermTemplate)) {
34 | const reg = /]*>/;
35 | const content = reg.exec(data)[0];
36 |
37 | const result = data.replace(
38 | reg,
39 | `${content}\n${foregroundServicePermTemplate}`
40 | );
41 | fs.writeFile(androidManifestPath, result, "utf8", function (err) {
42 | if (err) return console.log(err);
43 | });
44 | }
45 |
46 | if (!data.includes(metadataTemplate)) {
47 | const reg = /]*>/;
48 | const content = reg.exec(data)[0];
49 |
50 | const result = data.replace(reg, `${content}${metadataTemplate}`);
51 | console.log({ result });
52 |
53 | fs.writeFile(androidManifestPath, result, "utf8", function (err) {
54 | if (err) return console.log(err);
55 | });
56 | }
57 | });
58 |
59 | const colorTemplate = `
60 | - #00C4D1
61 |
62 | - @color/blue
63 |
64 | `;
65 |
66 | const colorFilePath = `${process.cwd()}/android/app/src/main/res/values/colors.xml`;
67 |
68 | // Ensure the directory exists
69 | const dirPath = path.dirname(colorFilePath);
70 | fs.mkdirSync(dirPath, { recursive: true });
71 |
72 | // Check if the file exists
73 | if (!fs.existsSync(colorFilePath)) {
74 | // Create the file with initial content if it doesn't exist
75 | fs.writeFileSync(colorFilePath, `${colorTemplate}`, "utf8");
76 | console.log(`Successfully created color file at ${colorFilePath}`);
77 | } else {
78 | // Read and update the file if it exists
79 | fs.readFile(colorFilePath, "utf8", function (err, data) {
80 | if (err) {
81 | return console.error(`Error reading file: ${err}`);
82 | }
83 |
84 | const reg = /]*>/;
85 | const content = reg.exec(data)?.[0];
86 |
87 | let result;
88 | if (!content) {
89 | result = `${colorTemplate}`;
90 | } else {
91 | result = data.replace(reg, `${content}${colorTemplate}`);
92 | }
93 |
94 | fs.writeFile(colorFilePath, result, "utf8", function (err) {
95 | if (err) {
96 | return console.error(`Error writing file: ${err}`);
97 | }
98 | console.log(`Successfully updated color file at ${colorFilePath}`);
99 | });
100 | });
101 | }
--------------------------------------------------------------------------------
/android/src/main/java/com/supersami/foregroundservice/ForegroundServiceModule.java:
--------------------------------------------------------------------------------
1 | package com.supersami.foregroundservice;
2 |
3 | import android.content.ComponentName;
4 | import android.content.Intent;
5 | import android.app.NotificationManager;
6 | import android.os.Build;
7 | import android.util.Log;
8 |
9 | import com.facebook.react.bridge.Arguments;
10 | import com.facebook.react.bridge.Promise;
11 | import com.facebook.react.bridge.ReactApplicationContext;
12 | import com.facebook.react.bridge.ReactContextBaseJavaModule;
13 | import com.facebook.react.bridge.ReactMethod;
14 | import com.facebook.react.bridge.ReadableMap;
15 |
16 | import static com.supersami.foregroundservice.Constants.ERROR_INVALID_CONFIG;
17 | import static com.supersami.foregroundservice.Constants.ERROR_SERVICE_ERROR;
18 | import static com.supersami.foregroundservice.Constants.NOTIFICATION_CONFIG;
19 | import static com.supersami.foregroundservice.Constants.TASK_CONFIG;
20 |
21 | public class ForegroundServiceModule extends ReactContextBaseJavaModule {
22 |
23 | private final ReactApplicationContext reactContext;
24 |
25 | public ForegroundServiceModule(ReactApplicationContext reactContext) {
26 | super(reactContext);
27 | this.reactContext = reactContext;
28 | }
29 |
30 | @Override
31 | public String getName() {
32 | return "ForegroundService";
33 | }
34 |
35 | private boolean isRunning() {
36 | // Get the ForegroundService running value
37 | ForegroundService instance = ForegroundService.getInstance();
38 | int res = 0;
39 | if (instance != null) {
40 | res = instance.isRunning();
41 | }
42 | return res > 0;
43 | }
44 |
45 | @ReactMethod
46 | public void startService(ReadableMap notificationConfig, Promise promise) {
47 | if (notificationConfig == null) {
48 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: Notification config is invalid");
49 | return;
50 | }
51 |
52 | if (!notificationConfig.hasKey("id")) {
53 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: id is required");
54 | return;
55 | }
56 |
57 | if (!notificationConfig.hasKey("title")) {
58 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: title is reqired");
59 | return;
60 | }
61 |
62 | if (!notificationConfig.hasKey("message")) {
63 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: message is required");
64 | return;
65 | }
66 |
67 | if (!notificationConfig.hasKey("ServiceType")) {
68 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: ServiceType is required");
69 | return;
70 | }
71 |
72 | try {
73 | Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
74 | intent.setAction(Constants.ACTION_FOREGROUND_SERVICE_START);
75 | intent.putExtra(NOTIFICATION_CONFIG, Arguments.toBundle(notificationConfig));
76 | ForegroundService.setReactContext(getReactApplicationContext());
77 | ComponentName componentName = getReactApplicationContext().startService(intent);
78 |
79 | if (componentName != null) {
80 | promise.resolve(null);
81 | } else {
82 | promise.reject(ERROR_SERVICE_ERROR, "ForegroundService: Foreground service failed to start.");
83 | }
84 | } catch (IllegalStateException e) {
85 | promise.reject(ERROR_SERVICE_ERROR, "ForegroundService: Foreground service failed to start.");
86 | }
87 | }
88 |
89 | @ReactMethod
90 | public void updateNotification(ReadableMap notificationConfig, Promise promise) {
91 | if (notificationConfig == null) {
92 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: Notification config is invalid");
93 | return;
94 | }
95 |
96 | if (!notificationConfig.hasKey("id")) {
97 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: id is required");
98 | return;
99 | }
100 |
101 | if (!notificationConfig.hasKey("title")) {
102 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: title is reqired");
103 | return;
104 | }
105 |
106 | if (!notificationConfig.hasKey("message")) {
107 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: message is required");
108 | return;
109 | }
110 |
111 | try {
112 |
113 | Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
114 | intent.setAction(Constants.ACTION_UPDATE_NOTIFICATION);
115 | intent.putExtra(NOTIFICATION_CONFIG, Arguments.toBundle(notificationConfig));
116 | ForegroundService.setReactContext(getReactApplicationContext());
117 | ComponentName componentName = getReactApplicationContext().startService(intent);
118 |
119 | if (componentName != null) {
120 | promise.resolve(null);
121 | } else {
122 | promise.reject(ERROR_SERVICE_ERROR, "Update notification failed.");
123 | }
124 | } catch (IllegalStateException e) {
125 | promise.reject(ERROR_SERVICE_ERROR, "Update notification failed, service failed to start.");
126 | }
127 | }
128 |
129 | // helper to dismiss a notification. Useful if we used multiple notifications
130 | // for our service since stopping the foreground service will only dismiss one notification
131 | @ReactMethod
132 | public void cancelNotification(ReadableMap notificationConfig, Promise promise) {
133 | if (notificationConfig == null) {
134 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: Notification config is invalid");
135 | return;
136 | }
137 |
138 | if (!notificationConfig.hasKey("id")) {
139 | promise.reject(ERROR_INVALID_CONFIG, "ForegroundService: id is required");
140 | return;
141 | }
142 |
143 | try {
144 | int id = (int) notificationConfig.getDouble("id");
145 |
146 | NotificationManager mNotificationManager = (NotificationManager) this.reactContext.getSystemService(this.reactContext.NOTIFICATION_SERVICE);
147 | mNotificationManager.cancel(id);
148 |
149 | promise.resolve(null);
150 | } catch (Exception e) {
151 | promise.reject(ERROR_SERVICE_ERROR, "Failed to cancel notification.");
152 | }
153 | }
154 |
155 | @ReactMethod
156 | public void stopService(Promise promise) {
157 |
158 | // stop main service
159 | Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
160 | intent.setAction(Constants.ACTION_FOREGROUND_SERVICE_STOP);
161 |
162 | //getReactApplicationContext().stopService(intent);
163 | // Looks odd, but we do indeed send the stop flag with a start command
164 | // if it fails, use the violent stop service instead
165 | try {
166 | getReactApplicationContext().startService(intent);
167 | } catch (IllegalStateException e) {
168 | try {
169 | getReactApplicationContext().stopService(intent);
170 | } catch (Exception e2) {
171 | promise.reject(ERROR_SERVICE_ERROR, "Service stop failed: " + e2.getMessage());
172 | return;
173 | }
174 | }
175 |
176 | // Also stop headless tasks, should be noop if it's not running.
177 | // TODO: Not working, headless task must finish regardless. We have to rely on JS code being well done.
178 | // intent = new Intent(getReactApplicationContext(), ForegroundServiceTask.class);
179 | // getReactApplicationContext().stopService(intent);
180 | promise.resolve(null);
181 | }
182 |
183 | @ReactMethod
184 | public void stopServiceAll(Promise promise) {
185 |
186 | // stop main service with all action
187 | Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
188 | intent.setAction(Constants.ACTION_FOREGROUND_SERVICE_STOP_ALL);
189 |
190 | try {
191 | getReactApplicationContext().startService(intent);
192 | } catch (IllegalStateException e) {
193 | try {
194 | getReactApplicationContext().stopService(intent);
195 | } catch (Exception e2) {
196 | promise.reject(ERROR_SERVICE_ERROR, "Service stop all failed: " + e2.getMessage());
197 | return;
198 | }
199 | }
200 |
201 | promise.resolve(null);
202 | }
203 |
204 | @ReactMethod
205 | public void runTask(ReadableMap taskConfig, Promise promise) {
206 |
207 | if (!taskConfig.hasKey("taskName")) {
208 | promise.reject(ERROR_INVALID_CONFIG, "taskName is required");
209 | return;
210 | }
211 |
212 | if (!taskConfig.hasKey("delay")) {
213 | promise.reject(ERROR_INVALID_CONFIG, "delay is required");
214 | return;
215 | }
216 |
217 | try {
218 |
219 | Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
220 | intent.setAction(Constants.ACTION_FOREGROUND_RUN_TASK);
221 | intent.putExtra(TASK_CONFIG, Arguments.toBundle(taskConfig));
222 |
223 | ComponentName componentName = getReactApplicationContext().startService(intent);
224 |
225 | if (componentName != null) {
226 | promise.resolve(null);
227 | } else {
228 | promise.reject(ERROR_SERVICE_ERROR, "Failed to run task: Service did not start");
229 | }
230 | } catch (IllegalStateException e) {
231 | promise.reject(ERROR_SERVICE_ERROR, "Failed to run task: Service did not start");
232 | }
233 | }
234 |
235 | @ReactMethod
236 | public void isRunning(Promise promise) {
237 |
238 | // Get the ForegroundService running value
239 | ForegroundService instance = ForegroundService.getInstance();
240 | int res = 0;
241 | if (instance != null) {
242 | res = instance.isRunning();
243 | }
244 |
245 | promise.resolve(res);
246 | }
247 |
248 | }
249 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | NativeModules,
3 | AppRegistry,
4 | DeviceEventEmitter,
5 | NativeEventEmitter,
6 | Alert
7 | } from 'react-native';
8 |
9 | // ANDROID ONLY
10 | // Copied and adapted from https://github.com/voximplant/react-native-foreground-service
11 | // and https://github.com/zo0r/react-native-push-notification/
12 |
13 | const ForegroundServiceModule = NativeModules.ForegroundService;
14 |
15 | /**
16 | * @property {number} id - Unique notification id
17 | * @property {string} title - Notification title
18 | * @property {string} message - Notification message
19 | * @property {string} ServiceType - Foreground service types are Mandatory in Android 14
20 | * @property {string} number - int specified as string > 0, for devices that support it, this might be used to set the badge counter
21 | * @property {string} icon - Small icon name | ic_notification
22 | * @property {string} largeIcon - Large icon name | ic_launcher
23 | * @property {string} visibility - private | public | secret
24 | * @property {boolean} ongoing - true/false if the notification is ongoing. The notification the service was started with will always be ongoing
25 | * @property {number} [importance] - Importance (and priority for older devices) of this notification. This might affect notification sound One of:
26 | * none - IMPORTANCE_NONE (by default),
27 | * min - IMPORTANCE_MIN,
28 | * low - IMPORTANCE_LOW,
29 | * default - IMPORTANCE_DEFAULT
30 | * high - IMPORTANCE_HIGH,
31 | * max - IMPORTANCE_MAX
32 | */
33 | const NotificationConfig = {};
34 |
35 | /**
36 | * @property {string} taskName - name of the js task configured with registerForegroundTask
37 | * @property {number} delay - start task in delay miliseconds, use 0 to start immediately
38 | * ... any other values passed to the task as well
39 | */
40 | const TaskConfig = {};
41 |
42 | class ForegroundService {
43 | /**
44 | * Registers a piece of JS code to be ran on the service
45 | * NOTE: This must be called before anything else, or the service will fail.
46 | * NOTE2: Registration must also happen at module level (not at mount)
47 | * task will receive all parameters from runTask
48 | * @param {task} async function to be called
49 | */
50 | static registerForegroundTask(taskName, task) {
51 | AppRegistry.registerHeadlessTask(taskName, () => task);
52 | }
53 |
54 | /**
55 | * Start foreground service
56 | * Multiple calls won't start multiple instances of the service, but will increase its internal counter
57 | * so calls to stop won't stop until it reaches 0.
58 | * Note: notificationConfig can't be re-used (becomes immutable)
59 | * @param {NotificationConfig} notificationConfig - Notification config
60 | * @return Promise
61 | */
62 | static async startService(notificationConfig) {
63 | console.log('Start Service Triggered');
64 | return await ForegroundServiceModule.startService(notificationConfig);
65 | }
66 |
67 | /**
68 | * Updates a notification of a running service. Make sure to use the same ID
69 | * or it will trigger a separate notification.
70 | * Note: this method might fail if called right after starting the service
71 | * since the service might not be yet ready.
72 | * If service is not running, it will be started automatically like calling startService.
73 | * @param {NotificationConfig} notificationConfig - Notification config
74 | * @return Promise
75 | */
76 | static async updateNotification(notificationConfig) {
77 | console.log(' Update Service Triggered');
78 | return await ForegroundServiceModule.updateNotification(notificationConfig);
79 | }
80 |
81 | /**
82 | * Cancels/dimisses a notification given its id. Useful if the service used
83 | * more than one notification
84 | * @param {number} id - Notification id to cancel
85 | * @return Promise
86 | */
87 | static async cancelNotification(id) {
88 | console.log('Cancel Service Triggered');
89 | return await ForegroundServiceModule.cancelNotification({id: id});
90 | }
91 |
92 | /**
93 | * Stop foreground service. Note: Pending tasks might still complete.
94 | * If startService will called multiple times, this needs to be called as many times.
95 | * @return Promise
96 | */
97 | static async stopService() {
98 | console.log('Stop Service Triggered');
99 | return await ForegroundServiceModule.stopService();
100 | }
101 |
102 | /**
103 | * Stop foreground service. Note: Pending tasks might still complete.
104 | * This will stop the service regardless of how many times start was called
105 | * @return Promise
106 | */
107 | static async stopServiceAll() {
108 | return await ForegroundServiceModule.stopServiceAll();
109 | }
110 |
111 | /**
112 | * Runs a previously configured headless task.
113 | * Task must be able to self stop if the service is stopped, since it can't be force killed once started.
114 | * Note: This method might silently fail if the service is not running, but will run successfully
115 | * if the service is still spinning up.
116 | * If the service is not running because it was killed, it will be attempted to be started again
117 | * using the last notification available.
118 | * @param {TaskConfig} taskConfig - Notification config
119 | * @return Promise
120 | */
121 | static async runTask(taskConfig) {
122 | return await ForegroundServiceModule.runTask(taskConfig);
123 | }
124 |
125 | /**
126 | * Returns an integer indicating if the service is running or not.
127 | * The integer represents the internal counter of how many startService
128 | * calls were done without calling stopService
129 | * @return Promise
130 | */
131 | static async isRunning() {
132 | return await ForegroundServiceModule.isRunning();
133 | }
134 | }
135 |
136 | const randHashString = len => {
137 | return 'x'.repeat(len).replace(/[xy]/g, c => {
138 | let r = (Math.random() * 16) | 0,
139 | v = c == 'x' ? r : (r & 0x3) | 0x8;
140 | return v.toString(16);
141 | });
142 | };
143 |
144 | //initial state
145 | let tasks = {};
146 | const samplingInterval = 500; //ms
147 | let serviceRunning = false;
148 |
149 | const deleteTask = taskId => {
150 | delete tasks[taskId];
151 | };
152 |
153 | const taskRunner = async () => {
154 | try {
155 | if (!serviceRunning) return;
156 |
157 | const now = Date.now();
158 | let promises = [];
159 |
160 | //iterate over all tasks
161 | Object.entries(tasks).forEach(([taskId, task]) => {
162 | //check if this task's execution time has arrived
163 | if (now >= task.nextExecutionTime) {
164 | //push this task's promise for later execution
165 | promises.push(
166 | Promise.resolve(task.task()).then(task.onSuccess, task.onError),
167 | );
168 | //if this is a looped task then increment its nextExecutionTime by delay for the next interval
169 | if (task.onLoop) task.nextExecutionTime = now + task.delay;
170 | //else delete the one-off task
171 | else deleteTask(taskId);
172 | }
173 | });
174 |
175 | //execute all tasks promises in parallel
176 | await Promise.all(promises);
177 | } catch (error) {
178 | console.log('Error in FgService taskRunner:', error);
179 | }
180 | };
181 |
182 | const register = ({config: {alert, onServiceErrorCallBack}}) => {
183 | if (!serviceRunning) {
184 | setupServiceErrorListener({
185 | alert,
186 | onServiceFailToStart: onServiceErrorCallBack,
187 | });
188 | return ForegroundService.registerForegroundTask('myTaskName', taskRunner);
189 | }
190 | };
191 |
192 | const start = async ({
193 | id,
194 | title = id,
195 | message = 'Foreground Service Running...',
196 | ServiceType,
197 | vibration = false,
198 | visibility = 'public',
199 | icon = 'ic_notification',
200 | largeIcon = 'ic_launcher',
201 | importance = 'max',
202 | number = '1',
203 | button = false,
204 | buttonText = '',
205 | buttonOnPress = 'buttonOnPress',
206 | button2 = false,
207 | button2Text = '',
208 | button2OnPress = 'button2OnPress',
209 | mainOnPress = 'mainOnPress',
210 | progress,
211 | color,
212 | setOnlyAlertOnce,
213 | }) => {
214 | try {
215 | if (!serviceRunning) {
216 | await ForegroundService.startService({
217 | id,
218 | title,
219 | message,
220 | ServiceType,
221 | vibration,
222 | visibility,
223 | icon,
224 | largeIcon,
225 | importance,
226 | number,
227 | button,
228 | buttonText,
229 | buttonOnPress,
230 | button2,
231 | button2Text,
232 | button2OnPress,
233 | mainOnPress,
234 | progressBar: !!progress,
235 | progressBarMax: progress?.max,
236 | progressBarCurr: progress?.curr,
237 | color,
238 | setOnlyAlertOnce,
239 | });
240 | serviceRunning = true;
241 | await ForegroundService.runTask({
242 | taskName: 'myTaskName',
243 | delay: samplingInterval,
244 | loopDelay: samplingInterval,
245 | onLoop: true,
246 | });
247 | } else console.log('Foreground service is already running.');
248 | } catch (error) {
249 | throw error;
250 | }
251 | };
252 |
253 | const update = async ({
254 | id,
255 | title = id,
256 | message = 'Foreground Service Running...',
257 | ServiceType,
258 | vibration = false,
259 | visibility = 'public',
260 | largeIcon = 'ic_launcher',
261 | icon = 'ic_launcher',
262 | importance = 'max',
263 | number = '0',
264 | button = false,
265 | buttonText = '',
266 | buttonOnPress = 'buttonOnPress',
267 | button2 = false,
268 | button2Text = '',
269 | button2OnPress = 'button2OnPress',
270 | mainOnPress = 'mainOnPress',
271 | progress,
272 | color,
273 | setOnlyAlertOnce,
274 | }) => {
275 | try {
276 | await ForegroundService.updateNotification({
277 | id,
278 | title,
279 | message,
280 | ServiceType,
281 | vibration,
282 | visibility,
283 | largeIcon,
284 | icon,
285 | importance,
286 | number,
287 | button,
288 | buttonText,
289 | buttonOnPress,
290 | button2,
291 | button2Text,
292 | button2OnPress,
293 | mainOnPress,
294 | progressBar: !!progress,
295 | progressBarMax: progress?.max,
296 | progressBarCurr: progress?.curr,
297 | setOnlyAlertOnce,
298 | color,
299 | });
300 | if (!serviceRunning) {
301 | serviceRunning = true;
302 | await ForegroundService.runTask({
303 | taskName: 'myTaskName',
304 | delay: samplingInterval,
305 | loopDelay: samplingInterval,
306 | onLoop: true,
307 | });
308 | }
309 | } catch (error) {
310 | throw error;
311 | }
312 | };
313 |
314 | const stop = () => {
315 | serviceRunning = false;
316 | return ForegroundService.stopService();
317 | };
318 | const stopAll = () => {
319 | serviceRunning = false;
320 | return ForegroundService.stopServiceAll();
321 | };
322 | const is_running = () => serviceRunning;
323 |
324 | const add_task = (
325 | task,
326 | {
327 | delay = 5000,
328 | onLoop = true,
329 | taskId = randHashString(12),
330 | onSuccess = () => {},
331 | onError = () => {},
332 | },
333 | ) => {
334 | const _type = typeof task;
335 | if (_type !== 'function')
336 | throw `invalid task of type ${_type}, expected a function or a Promise`;
337 |
338 | if (!tasks[taskId])
339 | tasks[taskId] = {
340 | task,
341 | nextExecutionTime: Date.now(),
342 | delay: Math.ceil(delay / samplingInterval) * samplingInterval,
343 | onLoop: onLoop,
344 | taskId,
345 | onSuccess,
346 | onError,
347 | };
348 |
349 | return taskId;
350 | };
351 |
352 | const update_task = (
353 | task,
354 | {
355 | delay = 5000,
356 | onLoop = true,
357 | taskId = randHashString(12),
358 | onSuccess = () => {},
359 | onError = () => {},
360 | },
361 | ) => {
362 | const _type = typeof task;
363 | if (_type !== 'function')
364 | throw `invalid task of type ${_type}, expected a function or a Promise`;
365 |
366 | tasks[taskId] = {
367 | task,
368 | nextExecutionTime: Date.now(),
369 | delay: Math.ceil(delay / samplingInterval) * samplingInterval,
370 | onLoop: onLoop,
371 | taskId,
372 | onSuccess,
373 | onError,
374 | };
375 |
376 | return taskId;
377 | };
378 |
379 | const remove_task = taskId => deleteTask(taskId);
380 |
381 | const is_task_running = taskId => (tasks[taskId] ? true : false);
382 |
383 | const remove_all_tasks = () => (tasks = {});
384 |
385 | const get_task = taskId => tasks[taskId];
386 |
387 | const get_all_tasks = () => tasks;
388 |
389 | const eventListener = callBack => {
390 | let subscription = DeviceEventEmitter.addListener(
391 | 'notificationClickHandle',
392 | callBack,
393 | );
394 |
395 | return function cleanup() {
396 | subscription.remove();
397 | };
398 | };
399 |
400 | const eventEmitter = new NativeEventEmitter(ForegroundServiceModule);
401 | export function setupServiceErrorListener({onServiceFailToStart, alert}) {
402 | const listener = eventEmitter.addListener('onServiceError', message => {
403 | alert && Alert.alert('Service Error', message);
404 | if (onServiceFailToStart) {
405 | onServiceFailToStart();
406 | }
407 | stop();
408 | });
409 |
410 | return () => {
411 | listener.remove();
412 | };
413 | }
414 |
415 | const ReactNativeForegroundService = {
416 | register,
417 | start,
418 | update,
419 | stop,
420 | stopAll,
421 | is_running,
422 | add_task,
423 | update_task,
424 | remove_task,
425 | is_task_running,
426 | remove_all_tasks,
427 | get_task,
428 | get_all_tasks,
429 | eventListener,
430 | };
431 |
432 | export default ReactNativeForegroundService;
433 |
--------------------------------------------------------------------------------
/android/src/main/java/com/supersami/foregroundservice/NotificationHelper.java:
--------------------------------------------------------------------------------
1 | package com.supersami.foregroundservice;
2 |
3 | import android.app.Notification;
4 | import android.app.NotificationChannel;
5 | import android.app.NotificationManager;
6 | import android.app.PendingIntent;
7 | import android.content.Context;
8 | import android.content.Intent;
9 | import android.graphics.Bitmap;
10 | import android.graphics.BitmapFactory;
11 | import android.graphics.Color;
12 | import android.os.Build;
13 | import android.os.Bundle;
14 | import androidx.core.app.NotificationCompat;
15 | import android.util.Log;
16 |
17 | import com.facebook.react.R;
18 |
19 | class NotificationHelper {
20 | private static final String TAG = "ForegroundService";
21 | private static final String NOTIFICATION_CHANNEL_ID = "com.supersami.foregroundservice.channel";
22 |
23 | private static NotificationHelper instance = null;
24 | private NotificationManager mNotificationManager;
25 |
26 | PendingIntent pendingBtnIntent;
27 | PendingIntent pendingBtn2Intent;
28 | private Context context;
29 | private NotificationConfig config;
30 |
31 | public static synchronized NotificationHelper getInstance(Context context) {
32 | if (instance == null) {
33 | instance = new NotificationHelper(context);
34 | }
35 | return instance;
36 | }
37 |
38 | private NotificationHelper(Context context) {
39 | mNotificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
40 | this.context = context;
41 | this.config = new NotificationConfig(context);
42 | }
43 |
44 | // Get the appropriate PendingIntent flags based on Android version
45 | private int getPendingIntentFlags(boolean isMutable) {
46 | // For Android 12+, we need to explicitly specify mutability
47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
48 | return isMutable ? PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_IMMUTABLE;
49 | }
50 | // For Android 6.0+, use FLAG_UPDATE_CURRENT for compatibility
51 | else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
52 | return PendingIntent.FLAG_UPDATE_CURRENT;
53 | }
54 | // Fallback for older versions
55 | else {
56 | return 0;
57 | }
58 | }
59 |
60 | Notification buildNotification(Context context, Bundle bundle) {
61 | if (bundle == null) {
62 | Log.e(TAG, "buildNotification: invalid config");
63 | return null;
64 | }
65 | Class mainActivityClass = getMainActivityClass(context);
66 | if (mainActivityClass == null) {
67 | return null;
68 | }
69 |
70 | // Main notification intent
71 | Intent notificationIntent = new Intent(context, mainActivityClass);
72 | notificationIntent.putExtra("mainOnPress", bundle.getString("mainOnPress"));
73 | int uniqueInt1 = (int) (System.currentTimeMillis() & 0xfffffff);
74 |
75 | // For the main intent we might need it to be mutable depending on the use case
76 | boolean mainIntentMutable = bundle.getBoolean("mainIntentMutable", false);
77 | PendingIntent pendingIntent = PendingIntent.getActivity(
78 | context,
79 | uniqueInt1,
80 | notificationIntent,
81 | getPendingIntentFlags(mainIntentMutable)
82 | );
83 |
84 | // First button intent (if enabled)
85 | if (bundle.getBoolean("button", false)) {
86 | Intent notificationBtnIntent = new Intent(context, mainActivityClass);
87 | notificationBtnIntent.putExtra("buttonOnPress", bundle.getString("buttonOnPress"));
88 | int uniqueInt = (int) (System.currentTimeMillis() & 0xfffffff);
89 |
90 | // Button intents are mutable if specified, immutable by default
91 | boolean buttonMutable = bundle.getBoolean("buttonMutable", false);
92 | pendingBtnIntent = PendingIntent.getActivity(
93 | context,
94 | uniqueInt,
95 | notificationBtnIntent,
96 | getPendingIntentFlags(buttonMutable)
97 | );
98 | }
99 |
100 | // Second button intent (if enabled)
101 | if (bundle.getBoolean("button2", false)) {
102 | Intent notificationBtn2Intent = new Intent(context, mainActivityClass);
103 | notificationBtn2Intent.putExtra("button2OnPress", bundle.getString("button2OnPress"));
104 | int uniqueInt2 = (int) (System.currentTimeMillis() & 0xfffffff);
105 |
106 | // Button intents are mutable if specified, immutable by default
107 | boolean button2Mutable = bundle.getBoolean("button2Mutable", false);
108 | pendingBtn2Intent = PendingIntent.getActivity(
109 | context,
110 | uniqueInt2,
111 | notificationBtn2Intent,
112 | getPendingIntentFlags(button2Mutable)
113 | );
114 | }
115 |
116 | String title = bundle.getString("title");
117 |
118 | int priority = NotificationCompat.PRIORITY_HIGH;
119 | final String priorityString = bundle.getString("importance");
120 |
121 | if (priorityString != null) {
122 | switch(priorityString.toLowerCase()) {
123 | case "max":
124 | priority = NotificationCompat.PRIORITY_MAX;
125 | break;
126 | case "high":
127 | priority = NotificationCompat.PRIORITY_HIGH;
128 | break;
129 | case "low":
130 | priority = NotificationCompat.PRIORITY_LOW;
131 | break;
132 | case "min":
133 | priority = NotificationCompat.PRIORITY_MIN;
134 | break;
135 | case "default":
136 | priority = NotificationCompat.PRIORITY_DEFAULT;
137 | break;
138 | default:
139 | priority = NotificationCompat.PRIORITY_HIGH;
140 | }
141 | }
142 |
143 | int visibility = NotificationCompat.VISIBILITY_PRIVATE;
144 | String visibilityString = bundle.getString("visibility");
145 |
146 | if (visibilityString != null) {
147 | switch(visibilityString.toLowerCase()) {
148 | case "private":
149 | visibility = NotificationCompat.VISIBILITY_PRIVATE;
150 | break;
151 | case "public":
152 | visibility = NotificationCompat.VISIBILITY_PUBLIC;
153 | break;
154 | case "secret":
155 | visibility = NotificationCompat.VISIBILITY_SECRET;
156 | break;
157 | default:
158 | visibility = NotificationCompat.VISIBILITY_PRIVATE;
159 | }
160 | }
161 |
162 | // Create notification channel for Android 8.0+
163 | checkOrCreateChannel(mNotificationManager, bundle);
164 |
165 | NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
166 | .setContentTitle(title)
167 | .setVisibility(visibility)
168 | .setPriority(priority)
169 | .setContentIntent(pendingIntent)
170 | .setOngoing(bundle.getBoolean("ongoing", false))
171 | .setContentText(bundle.getString("message"));
172 |
173 | // Add action buttons if configured
174 | if (bundle.getBoolean("button", false)) {
175 | notificationBuilder.addAction(
176 | R.drawable.redbox_top_border_background,
177 | bundle.getString("buttonText", "Button"),
178 | pendingBtnIntent
179 | );
180 | }
181 |
182 | if (bundle.getBoolean("button2", false)) {
183 | notificationBuilder.addAction(
184 | R.drawable.redbox_top_border_background,
185 | bundle.getString("button2Text", "Button"),
186 | pendingBtn2Intent
187 | );
188 | }
189 |
190 | // Set notification color
191 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
192 | notificationBuilder.setColor(this.config.getNotificationColor());
193 | }
194 |
195 | String color = bundle.getString("color");
196 | if (color != null) {
197 | try {
198 | notificationBuilder.setColor(Color.parseColor(color));
199 | } catch (IllegalArgumentException e) {
200 | Log.e(TAG, "Invalid color format: " + color);
201 | }
202 | }
203 |
204 | // Use big text style for better readability
205 | notificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bundle.getString("message")));
206 |
207 | // Set small icon
208 | String iconName = bundle.getString("icon");
209 | if (iconName == null) {
210 | iconName = "ic_launcher";
211 | }
212 | notificationBuilder.setSmallIcon(getResourceIdForResourceName(context, iconName));
213 |
214 | // Set large icon
215 | String largeIconName = bundle.getString("largeIcon");
216 | if (largeIconName == null) {
217 | largeIconName = "ic_launcher";
218 | }
219 |
220 | int largeIconResId = getResourceIdForResourceName(context, largeIconName);
221 | if (largeIconResId != 0) {
222 | try {
223 | Bitmap largeIconBitmap = BitmapFactory.decodeResource(context.getResources(), largeIconResId);
224 | notificationBuilder.setLargeIcon(largeIconBitmap);
225 | } catch (Exception e) {
226 | Log.e(TAG, "Failed to set large icon: " + e.getMessage());
227 | }
228 | }
229 |
230 | // Set number badge if provided
231 | String numberString = bundle.getString("number");
232 | if (numberString != null) {
233 | try {
234 | int numberInt = Integer.parseInt(numberString);
235 | if (numberInt > 0) {
236 | notificationBuilder.setNumber(numberInt);
237 | }
238 | } catch (NumberFormatException e) {
239 | Log.e(TAG, "Invalid number format: " + numberString);
240 | }
241 | }
242 |
243 | // Set progress bar if enabled
244 | Boolean progress = bundle.getBoolean("progressBar");
245 | if (progress) {
246 | double max = bundle.getDouble("progressBarMax");
247 | double curr = bundle.getDouble("progressBarCurr");
248 | notificationBuilder.setProgress((int)max, (int)curr, false);
249 | }
250 |
251 | // Prevent duplicate sound/vibration when updating
252 | notificationBuilder.setOnlyAlertOnce(true);
253 |
254 | return notificationBuilder.build();
255 | }
256 |
257 | private Class getMainActivityClass(Context context) {
258 | String packageName = context.getPackageName();
259 | Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
260 | if (launchIntent == null || launchIntent.getComponent() == null) {
261 | Log.e(TAG, "Failed to get launch intent or component");
262 | return null;
263 | }
264 | try {
265 | return Class.forName(launchIntent.getComponent().getClassName());
266 | } catch (ClassNotFoundException e) {
267 | Log.e(TAG, "Failed to get main activity class");
268 | return null;
269 | }
270 | }
271 |
272 | private int getResourceIdForResourceName(Context context, String resourceName) {
273 | int resourceId = context.getResources().getIdentifier(resourceName, "drawable", context.getPackageName());
274 | if (resourceId == 0) {
275 | resourceId = context.getResources().getIdentifier(resourceName, "mipmap", context.getPackageName());
276 | }
277 | return resourceId;
278 | }
279 |
280 | private static boolean channelCreated = false;
281 | private void checkOrCreateChannel(NotificationManager manager, Bundle bundle) {
282 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
283 | return;
284 | if (channelCreated)
285 | return;
286 | if (manager == null)
287 | return;
288 |
289 | int importance = NotificationManager.IMPORTANCE_HIGH;
290 | final String importanceString = bundle.getString("importance");
291 |
292 | if (importanceString != null) {
293 | switch(importanceString.toLowerCase()) {
294 | case "default":
295 | importance = NotificationManager.IMPORTANCE_DEFAULT;
296 | break;
297 | case "max":
298 | importance = NotificationManager.IMPORTANCE_MAX;
299 | break;
300 | case "high":
301 | importance = NotificationManager.IMPORTANCE_HIGH;
302 | break;
303 | case "low":
304 | importance = NotificationManager.IMPORTANCE_LOW;
305 | break;
306 | case "min":
307 | importance = NotificationManager.IMPORTANCE_MIN;
308 | break;
309 | case "none":
310 | importance = NotificationManager.IMPORTANCE_NONE;
311 | break;
312 | case "unspecified":
313 | importance = NotificationManager.IMPORTANCE_UNSPECIFIED;
314 | break;
315 | default:
316 | importance = NotificationManager.IMPORTANCE_HIGH;
317 | }
318 | }
319 |
320 | NotificationChannel channel = new NotificationChannel(
321 | NOTIFICATION_CHANNEL_ID,
322 | this.config.getChannelName(),
323 | importance
324 | );
325 |
326 | channel.setDescription(this.config.getChannelDescription());
327 | channel.enableLights(true);
328 | channel.enableVibration(bundle.getBoolean("vibration"));
329 | channel.setShowBadge(true);
330 |
331 | manager.createNotificationChannel(channel);
332 | channelCreated = true;
333 | }
334 | }
--------------------------------------------------------------------------------
/android/src/main/java/com/supersami/foregroundservice/ForegroundService.java:
--------------------------------------------------------------------------------
1 | package com.supersami.foregroundservice;
2 |
3 | import java.io.Console;
4 |
5 | import android.app.Notification;
6 | import android.app.NotificationManager;
7 | import android.app.Service;
8 | import android.content.Intent;
9 | import android.os.Build;
10 | import android.os.Bundle;
11 | import android.os.IBinder;
12 | import android.os.Handler;
13 | import android.util.Log;
14 |
15 | import com.facebook.react.bridge.ReactContext;
16 | import com.facebook.react.modules.core.DeviceEventManagerModule;
17 | import com.facebook.react.HeadlessJsTaskService;
18 |
19 | import static com.supersami.foregroundservice.Constants.NOTIFICATION_CONFIG;
20 | import static com.supersami.foregroundservice.Constants.TASK_CONFIG;
21 |
22 | // NOTE: headless task will still block the UI so don't do heavy work, but this is also good
23 | // since they will share the JS environment
24 | // Service will also be a singleton in order to quickly find out if it is running
25 | public class ForegroundService extends Service {
26 |
27 | private static ForegroundService mInstance = null;
28 | private static Bundle lastNotificationConfig = null;
29 | private int running = 0;
30 |
31 | private static ReactContext reactContext;
32 |
33 | public static void setReactContext(ReactContext context) {
34 | reactContext = context;
35 | }
36 |
37 | public static boolean isServiceCreated() {
38 | try {
39 | return mInstance != null && mInstance.ping();
40 | } catch (NullPointerException e) {
41 | return false;
42 | }
43 | }
44 |
45 | public static ForegroundService getInstance() {
46 | if (isServiceCreated()) {
47 | return mInstance;
48 | }
49 | return null;
50 | }
51 |
52 | public int isRunning() {
53 | return running;
54 | }
55 |
56 | private boolean ping() {
57 | return true;
58 | }
59 |
60 | @Override
61 | public void onCreate() {
62 | //Log.e("ForegroundService", "destroy called");
63 | running = 0;
64 | mInstance = this;
65 | }
66 |
67 | @Override
68 | public void onDestroy() {
69 | //Log.e("ForegroundService", "destroy called");
70 | this.handler.removeCallbacks(this.runnableCode);
71 | running = 0;
72 | mInstance = null;
73 | }
74 |
75 | @Override
76 | public IBinder onBind(Intent intent) {
77 | return null;
78 | }
79 |
80 | private boolean startService(Bundle notificationConfig) {
81 | try {
82 | int id = (int) notificationConfig.getDouble("id");
83 | String foregroundServiceType = notificationConfig.getString("ServiceType");
84 |
85 | Notification notification = NotificationHelper
86 | .getInstance(getApplicationContext())
87 | .buildNotification(getApplicationContext(), notificationConfig);
88 |
89 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
90 | // For Android 10 (API 29) and above
91 | startForeground(id, notification, getServiceTypeForAndroid10(foregroundServiceType));
92 | } else {
93 | // For older Android versions
94 | startForeground(id, notification);
95 | }
96 |
97 | running += 1;
98 | lastNotificationConfig = notificationConfig;
99 | return true;
100 |
101 | } catch (Exception e) {
102 | if (reactContext != null) {
103 | Log.e("ForegroundService", "Failed to start service: " + e.getMessage());
104 | reactContext
105 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
106 | .emit("onServiceError", e.getMessage());
107 | }
108 | return false;
109 | }
110 | }
111 |
112 | private int getServiceTypeForAndroid10(String customServiceType) {
113 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
114 | switch (customServiceType) {
115 | case "camera":
116 | return 8; // ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
117 | case "connectedDevice":
118 | return 32; // ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
119 | case "dataSync":
120 | return 16; // ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
121 | case "health":
122 | return 64; // ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH
123 | case "location":
124 | return 1; // ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
125 | case "mediaPlayback":
126 | return 2; // ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
127 | case "mediaProjection":
128 | return 4; // ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
129 | case "microphone":
130 | return 128; // ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
131 | case "phoneCall":
132 | return 256; // ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
133 | case "remoteMessaging":
134 | return 1024; // ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
135 | case "shortService":
136 | return 2048; // ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
137 | case "specialUse":
138 | return 4096; // ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
139 | case "systemExempted":
140 | return 8192; // ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
141 | default:
142 | return 1; // Default to location
143 | }
144 | }
145 | return 0; // This won't be used for Android < 10
146 | }
147 |
148 | private int mapServiceType(String customServiceType) {
149 | // Use direct integer constants instead of ServiceInfo constants
150 | switch (customServiceType) {
151 | case "camera":
152 | return 8; // ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
153 | case "connectedDevice":
154 | return 32; // ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
155 | case "dataSync":
156 | return 16; // ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
157 | case "health":
158 | return 64; // ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH
159 | case "location":
160 | return 1; // ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
161 | case "mediaPlayback":
162 | return 2; // ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
163 | case "mediaProjection":
164 | return 4; // ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
165 | case "microphone":
166 | return 128; // ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
167 | case "phoneCall":
168 | return 256; // ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
169 | case "remoteMessaging":
170 | return 1024; // ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
171 | case "shortService":
172 | return 2048; // ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
173 | case "specialUse":
174 | return 4096; // ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
175 | case "systemExempted":
176 | return 8192; // ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
177 | default:
178 | throw new IllegalArgumentException("Unknown foreground service type: " + customServiceType);
179 | }
180 | }
181 |
182 | public Bundle taskConfig;
183 | private Handler handler = new Handler();
184 | private Runnable runnableCode = new Runnable() {
185 | @Override
186 | public void run() {
187 | final Intent service = new Intent(getApplicationContext(), ForegroundServiceTask.class);
188 | service.putExtras(taskConfig);
189 | try {
190 | getApplicationContext().startService(service);
191 | } catch (Exception e) {
192 | Log.e("ForegroundService", "Failed to start foreground service in loop: " + e.getMessage());
193 | }
194 |
195 | int delay = (int) taskConfig.getDouble("delay");
196 |
197 | int loopDelay = (int) taskConfig.getDouble("loopDelay");
198 | Log.d("SuperLog", "" + loopDelay);
199 | handler.postDelayed(this, loopDelay);
200 | }
201 | };
202 |
203 | @Override
204 | public int onStartCommand(Intent intent, int flags, int startId) {
205 | String action = intent.getAction();
206 |
207 | /**
208 | * From the docs: Every call to this method will result in a
209 | * corresponding call to the target service's
210 | * Service.onStartCommand(Intent, int, int) method, with the intent
211 | * given here. This provides a convenient way to submit jobs to a
212 | * service without having to bind and call on to its interface.
213 | */
214 | //Log.d("ForegroundService", "onStartCommand flags: " + String.valueOf(flags) + " " + String.valueOf(startId));
215 | if (action != null) {
216 | if (action.equals(Constants.ACTION_FOREGROUND_SERVICE_START)) {
217 | if (intent.getExtras() != null && intent.getExtras().containsKey(NOTIFICATION_CONFIG)) {
218 | Bundle notificationConfig = intent.getExtras().getBundle(NOTIFICATION_CONFIG);
219 | startService(notificationConfig);
220 | }
221 | }
222 |
223 | if (action.equals(Constants.ACTION_UPDATE_NOTIFICATION)) {
224 | if (intent.getExtras() != null && intent.getExtras().containsKey(NOTIFICATION_CONFIG)) {
225 | Bundle notificationConfig = intent.getExtras().getBundle(NOTIFICATION_CONFIG);
226 |
227 | if (running <= 0) {
228 | Log.d("ForegroundService", "Update Notification called without a running service, trying to restart service.");
229 | startService(notificationConfig);
230 | } else {
231 | try {
232 | int id = (int) notificationConfig.getDouble("id");
233 |
234 | Notification notification = NotificationHelper
235 | .getInstance(getApplicationContext())
236 | .buildNotification(getApplicationContext(), notificationConfig);
237 |
238 | NotificationManager mNotificationManager = (NotificationManager) getSystemService(getApplicationContext().NOTIFICATION_SERVICE);
239 | mNotificationManager.notify(id, notification);
240 |
241 | lastNotificationConfig = notificationConfig;
242 | } catch (Exception e) {
243 | Log.e("ForegroundService", "Failed to update notification: " + e.getMessage());
244 | }
245 | }
246 | }
247 | } else if (action.equals(Constants.ACTION_FOREGROUND_RUN_TASK)) {
248 | if (running <= 0 && lastNotificationConfig == null) {
249 | Log.e("ForegroundService", "Service is not running to run tasks.");
250 | stopSelf();
251 | return START_NOT_STICKY;
252 | } else {
253 | // try to re-start service if it was killed
254 | if (running <= 0) {
255 | Log.d("ForegroundService", "Run Task called without a running service, trying to restart service.");
256 | if (!startService(lastNotificationConfig)) {
257 | Log.e("ForegroundService", "Service is not running to run tasks.");
258 | return START_REDELIVER_INTENT;
259 | }
260 | }
261 |
262 | if (intent.getExtras() != null && intent.getExtras().containsKey(TASK_CONFIG)) {
263 | taskConfig = intent.getExtras().getBundle(TASK_CONFIG);
264 |
265 | try {
266 | if (taskConfig.getBoolean("onLoop") == true) {
267 | this.handler.post(this.runnableCode);
268 | } else {
269 | this.runHeadlessTask(taskConfig);
270 | }
271 | } catch (Exception e) {
272 | Log.e("ForegroundService", "Failed to start task: " + e.getMessage());
273 | }
274 | }
275 | }
276 | } else if (action.equals(Constants.ACTION_FOREGROUND_SERVICE_STOP)) {
277 | if (running > 0) {
278 | running -= 1;
279 |
280 | if (running == 0) {
281 | stopSelf();
282 | lastNotificationConfig = null;
283 | }
284 | } else {
285 | Log.d("ForegroundService", "Service is not running to stop.");
286 | stopSelf();
287 | lastNotificationConfig = null;
288 | }
289 | return START_NOT_STICKY;
290 | } else if (action.equals(Constants.ACTION_FOREGROUND_SERVICE_STOP_ALL)) {
291 | running = 0;
292 | mInstance = null;
293 | lastNotificationConfig = null;
294 | stopSelf();
295 | return START_NOT_STICKY;
296 | }
297 | }
298 |
299 | // service to restart automatically if it's killed
300 | return START_REDELIVER_INTENT;
301 | }
302 |
303 | public void runHeadlessTask(Bundle bundle) {
304 | final Intent service = new Intent(getApplicationContext(), ForegroundServiceTask.class);
305 | service.putExtras(bundle);
306 |
307 | int delay = (int) bundle.getDouble("delay");
308 |
309 | if (delay <= 0) {
310 | try {
311 | getApplicationContext().startService(service);
312 | } catch (Exception e) {
313 | Log.e("ForegroundService", "Failed to start delayed headless task: " + e.getMessage());
314 | }
315 | // wakelock should be released automatically by the task
316 | // Shouldn't be needed, it's called automatically by headless
317 | //HeadlessJsTaskService.acquireWakeLockNow(getApplicationContext());
318 | } else {
319 | new Handler().postDelayed(new Runnable() {
320 | @Override
321 | public void run() {
322 | if (running <= 0) {
323 | return;
324 | }
325 | try {
326 | getApplicationContext().startService(service);
327 | } catch (Exception e) {
328 | Log.e("ForegroundService", "Failed to start delayed headless task: " + e.getMessage());
329 | }
330 | }
331 | }, delay);
332 | }
333 | }
334 | }
--------------------------------------------------------------------------------