├── .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 | } --------------------------------------------------------------------------------