├── .eslintrc.js ├── .gitignore ├── .npmignore ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── expo │ └── modules │ └── foregroundactions │ ├── ExpoForegroundActionsModule.kt │ ├── ExpoForegroundActionsService.kt │ └── ExpoForegroundOptions.kt ├── assets ├── App_tsx.png └── logo.png ├── example ├── .gitignore ├── App.tsx ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── expo │ │ │ │ └── modules │ │ │ │ └── foregroundactions │ │ │ │ └── example │ │ │ │ └── ReactNativeFlipper.java │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── expo │ │ │ │ │ └── modules │ │ │ │ │ └── foregroundactions │ │ │ │ │ └── example │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ │ ├── drawable-hdpi │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-mdpi │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable │ │ │ │ ├── rn_edit_text_material.xml │ │ │ │ └── splashscreen.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── values-night │ │ │ │ └── colors.xml │ │ │ │ └── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── release │ │ │ └── java │ │ │ └── expo │ │ │ └── modules │ │ │ └── foregroundactions │ │ │ └── example │ │ │ └── ReactNativeFlipper.java │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── ios │ ├── .gitignore │ ├── .xcode.env │ ├── Podfile │ ├── Podfile.lock │ ├── Podfile.properties.json │ ├── expoforegroundactionsexample.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── expoforegroundactionsexample.xcscheme │ ├── expoforegroundactionsexample.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── expoforegroundactionsexample │ │ ├── AppDelegate.h │ │ ├── AppDelegate.mm │ │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── App-Icon-1024x1024@1x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── SplashScreen.imageset │ │ │ ├── Contents.json │ │ │ └── image.png │ │ └── SplashScreenBackground.imageset │ │ │ ├── Contents.json │ │ │ └── image.png │ │ ├── Info.plist │ │ ├── SplashScreen.storyboard │ │ ├── Supporting │ │ └── Expo.plist │ │ ├── expoforegroundactionsexample-Bridging-Header.h │ │ ├── expoforegroundactionsexample.entitlements │ │ ├── main.m │ │ └── noop-file.swift ├── metro.config.js ├── package-lock.json ├── package.json ├── plugins │ └── expo-foreground-actions.js ├── readme.md ├── tsconfig.json ├── webpack.config.js └── yarn.lock ├── expo-module.config.json ├── ios ├── ExpoForegroundActions.podspec └── ExpoForegroundActionsModule.swift ├── package-lock.json ├── package.json ├── plugins └── expo-foreground-actions.js ├── src ├── ExpoForegroundActions.types.ts ├── ExpoForegroundActionsModule.ts └── index.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['universe/native', 'universe/web'], 4 | ignorePatterns: ['build'], 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # VSCode 6 | .vscode/ 7 | jsconfig.json 8 | 9 | # Xcode 10 | # 11 | build/ 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | *.xccheckout 22 | *.moved-aside 23 | DerivedData 24 | *.hmap 25 | *.ipa 26 | *.xcuserstate 27 | project.xcworkspace 28 | 29 | # Android/IJ 30 | # 31 | .classpath 32 | .cxx 33 | .gradle 34 | .idea 35 | .project 36 | .settings 37 | local.properties 38 | android.iml 39 | android/app/libs 40 | android/keystores/debug.keystore 41 | 42 | # Cocoapods 43 | # 44 | example/ios/Pods 45 | 46 | # Ruby 47 | example/vendor/ 48 | 49 | # node.js 50 | # 51 | node_modules/ 52 | npm-debug.log 53 | yarn-debug.log 54 | yarn-error.log 55 | 56 | # Expo 57 | .expo/* 58 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude all top-level hidden directories by convention 2 | /.*/ 3 | 4 | __mocks__ 5 | __tests__ 6 | 7 | /babel.config.js 8 | /android/src/androidTest/ 9 | /android/src/test/ 10 | /android/build/ 11 | /example/ 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
EXPO-FOREGROUND-ACTIONS

5 |
Running Foreground actions for Android/IOS
6 |
Developed with the software and tools below.
7 | 8 |

9 | JavaScript 10 | TypeScript 11 | React 12 | React 13 | Swift 14 | Kotlin 15 | Expo 16 | 17 |

18 | 19 | npm version 20 | 21 | GitHub license 22 | git-last-commit 23 | GitHub commit activity 24 | GitHub top language 25 |
26 | 27 | --- 28 | 29 | ## 📖 Table of Contents 30 | 31 | - [📖 Table of Contents](#-table-of-contents) 32 | - [📍 Overview](#-overview) 33 | - [📦 Features](#-features) 34 | - [📂 repository Structure](#-repository-structure) 35 | - [🚀 Getting Started](#-getting-started) 36 | - [🔧 Installation](#-installation) 37 | - [🤖 How to use](#-how-to-use) 38 | - [🤖 Functions](#-functions) 39 | - [🤖 Interfaces](#-interfaces) 40 | - [🛣 Roadmap](#-roadmap) 41 | - [🤝 Contributing](#-contributing) 42 | - [📄 License](#-license) 43 | - [👏 Acknowledgments](#-acknowledgments) 44 | 45 | --- 46 | 47 | ## 📍 Overview 48 | 49 | Start actions that continue to run in the grace period after the user switches apps. This library facilitates the 50 | execution of **ios**'s `beginBackgroundTaskWithName` and **android**'s `startForegroundService` methods. The primary 51 | objective is to emulate the behavior of `beginBackgroundTaskWithName`, allowing actions to persist even when the user 52 | switches to another app. Examples include sending chat messages, creating tasks, or running synchronizations. 53 | 54 | On iOS, the grace period typically lasts around 30 seconds, while on Android, foreground tasks can run for a longer 55 | duration, subject to device models and background policies. In general, a foreground task can safely run for about 30 56 | seconds on both platforms. However, it's important to note that this library is not intended for background location 57 | tracking. iOS's limited 30-second window makes it impractical for such purposes. For background location tracking, 58 | alternatives like WorkManager or GTaskScheduler are more suitable. 59 | 60 | For usage instructions, please refer to 61 | the [Example](https://github.com/Acetyld/expo-foreground-actions/tree/main/example) provided. 62 | 63 | --- 64 | 65 | ## 📦 Features 66 | 67 | ### For IOS & Android: 68 | 69 | - Execute JavaScript while the app is in the background. 70 | - Run multiple foreground actions simultaneously. 71 | - Forcefully terminate all foreground actions. 72 | 73 | ### For Android: 74 | 75 | - Display notifications with customizable titles, descriptions, and optional progress bars, along with support for deep 76 | linking. 77 | - Comply with the latest Android 34+ background policy, ensuring that foreground services continue to run without 78 | displaying a visible notification. Users can still access these services from the notification drawer. 79 | 80 | ### For IOS: 81 | 82 | - Receive notifications when the background execution time is about to expire. This feature allows users to save their 83 | data and terminate tasks gracefully. 84 | 85 | ### Web Support: 86 | 87 | - Limited support for web platforms. We recommend using the `runInJS` method due to potential errors when attempting to 88 | run foreground actions on web browsers. 89 | 90 | --- 91 | 92 | ## 📂 Repository Structure 93 | 94 | ```sh 95 | └── expo-foreground-actions/ 96 | ├── .eslintrc.js 97 | ├── android/ 98 | ├── example/ 99 | │ ├── App.tsx 100 | │ ├── android/ 101 | │ ├── app.json 102 | │ ├── babel.config.js 103 | │ ├── metro.config.js 104 | │ ├── package-lock.json 105 | │ ├── package.json 106 | │ ├── plugins/ 107 | │ │ └── expo-foreground-actions.js 108 | │ ├── tsconfig.json 109 | │ ├── webpack.config.js 110 | │ └── yarn.lock 111 | ├── ios/ 112 | ├── package-lock.json 113 | ├── package.json 114 | ├── plugins/ 115 | │ └── expo-foreground-actions.js 116 | ├── src/ 117 | │ ├── ExpoForegroundActions.types.ts 118 | │ ├── ExpoForegroundActionsModule.ts 119 | │ └── index.ts 120 | ├── tsconfig.json 121 | └── yarn.lock 122 | 123 | ``` 124 | 125 | ## 🚀 Getting Started 126 | 127 | ***Dependencies*** 128 | 129 | Please ensure you have the following dependencies installed on your system: 130 | 131 | `- ℹ️ Expo v49+` 132 | 133 | `- ℹ️ Bare/Manage workflow, we do not support Expo GO` 134 | 135 | ### 🔧 Installation 136 | 137 | 1. Clone the expo-foreground-actions repository: 138 | 139 | **NPM** 140 | ```sh 141 | npm install expo-foreground-actions 142 | ``` 143 | 144 | **Yarn** 145 | ```sh 146 | yarn add expo-foreground-actions 147 | ``` 148 | 149 | 2. Install the plugin, for now download the repo and copy 150 | the [plugins](https://github.com/Acetyld/expo-foreground-actions/tree/main/plugins) folder to your project root. 151 | 3. Then update your app.json to include the plugin and a scheme if u wanna use the plugin with a 152 | deeplink.
https://docs.expo.dev/guides/linking/ 153 | ```sh 154 | "expo": { 155 | "scheme": "myapp", 156 | "plugins": [ 157 | [ 158 | "./plugins/expo-foreground-actions" 159 | ] 160 | ], 161 | } 162 | ``` 163 | 164 | 3. Make sure the plugin is loaded in your app.json, you can do this by running **prebuild on managed** or by running * 165 | *pod install/gradle build** on bare. 166 | 167 | ### 🤖 How to use? 168 | 169 | For the time being, dedicated documentation is not available. However, you can explore the usage of current methods in 170 | the provided example app. Refer to the [Example](https://github.com/Acetyld/expo-foreground-actions/tree/main/example) 171 | folder to understand how to utilize this package effectively. 172 | 173 | ![Example](assets/App_tsx.png) 174 | 175 | ### 🤖 Functions 176 | 177 | #### `runForegroundedAction` 178 | 179 | ```typescript 180 | export const runForegroundedAction = async ( 181 | act: (api: ForegroundApi) => Promise, 182 | androidSettings: AndroidSettings, 183 | settings: Settings = { runInJS: false } 184 | ): Promise; 185 | ``` 186 | 187 | - `act`: The foreground action function to be executed. 188 | - `androidSettings`: Android-specific settings for the foreground action. 189 | - `settings`: Additional settings for the foreground action. 190 | 191 | #### `startForegroundAction` 192 | 193 | ```typescript 194 | export const startForegroundAction = async ( 195 | options?: AndroidSettings 196 | ): Promise; 197 | ``` 198 | 199 | - `options`: Android-specific settings for the foreground action. 200 | 201 | #### `stopForegroundAction` 202 | 203 | ```typescript 204 | export const stopForegroundAction = async (id: number): Promise; 205 | ``` 206 | 207 | - `id`: The unique identifier of the foreground action to stop. 208 | 209 | #### `updateForegroundedAction` 210 | 211 | ```typescript 212 | export const updateForegroundedAction = async ( 213 | id: number, 214 | options: AndroidSettings 215 | ): Promise; 216 | ``` 217 | 218 | - `id`: The unique identifier of the foreground action to update. 219 | - `options`: Updated Android-specific settings for the foreground action. 220 | 221 | #### `forceStopAllForegroundActions` 222 | 223 | ```typescript 224 | export const forceStopAllForegroundActions = async (): Promise; 225 | ``` 226 | 227 | - Forcefully stops all running foreground actions. 228 | 229 | #### `getForegroundIdentifiers` 230 | 231 | ```typescript 232 | export const getForegroundIdentifiers = async (): Promise; 233 | ``` 234 | 235 | - Retrieves the identifiers of all currently running foreground actions. 236 | 237 | #### `getRanTaskCount` 238 | 239 | ```typescript 240 | export const getRanTaskCount = () => ranTaskCount; 241 | ``` 242 | 243 | - Retrieves the count of tasks that have run. 244 | 245 | #### `getBackgroundTimeRemaining` 246 | 247 | ```typescript 248 | export const getBackgroundTimeRemaining = async (): Promise; 249 | ``` 250 | 251 | - Retrieves the remaining background execution time on iOS. 252 | 253 | ### 🤖 Interfaces 254 | 255 | #### `ExpireEventPayload` 256 | 257 | ```typescript 258 | export type ExpireEventPayload = { 259 | remaining: number; 260 | identifier: number; 261 | }; 262 | ``` 263 | 264 | - `remaining`: The remaining time in seconds before the foreground action expires. 265 | - `identifier`: The unique identifier of the foreground action. 266 | 267 | #### `AndroidSettings` 268 | 269 | ```typescript 270 | export interface AndroidSettings { 271 | headlessTaskName: string; 272 | notificationTitle: string; 273 | notificationDesc: string; 274 | notificationColor: string; 275 | notificationIconName: string; 276 | notificationIconType: string; 277 | notificationProgress: number; 278 | notificationMaxProgress: number; 279 | notificationIndeterminate: boolean; 280 | linkingURI: string; 281 | } 282 | ``` 283 | 284 | - `headlessTaskName`: Name of the headless task associated with the foreground action. 285 | - `notificationTitle`: Title of the notification shown during the foreground action. 286 | - `notificationDesc`: Description of the notification. 287 | - `notificationColor`: Color of the notification. 288 | - `notificationIconName`: Name of the notification icon. 289 | - `notificationIconType`: Type of the notification icon. 290 | - `notificationProgress`: Current progress value for the notification. 291 | - `notificationMaxProgress`: Maximum progress value for the notification. 292 | - `notificationIndeterminate`: Indicates if the notification progress is indeterminate. 293 | - `linkingURI`: URI to link to when the notification is pressed. 294 | 295 | #### `Settings` 296 | 297 | ```typescript 298 | export interface Settings { 299 | events?: { 300 | onIdentifier?: (identifier: number) => void; 301 | } 302 | runInJS?: boolean, 303 | } 304 | ``` 305 | 306 | - `events`: Event handlers for foreground actions. 307 | - `onIdentifier`: A callback function called when an identifier is generated. 308 | - `runInJS`: Indicates whether to run the foreground action without using a headless task or ios background task. 309 | 310 | --- 311 | 312 | ## 🛣 Project Roadmap 313 | 314 | > - [X] `ℹ️ Task 1: Initial launch` 315 | > - [X] `ℹ️ Task 2: Possiblity to run multiple foreground tasks` 316 | > - [ ] `ℹ️ Any idea's are welcome =)` 317 | 318 | --- 319 | 320 | ## 🤝 Contributing 321 | 322 | Contributions are welcome! Here are several ways you can contribute: 323 | 324 | - **[Submit Pull Requests](https://github.com/Acetyld/expo-foreground-actions/blob/main/CONTRIBUTING.md)**: Review open 325 | PRs, and submit your own PRs. 326 | - **[Join the Discussions](https://github.com/Acetyld/expo-foreground-actions/discussions)**: Share your insights, 327 | provide feedback, or ask questions. 328 | - **[Report Issues](https://github.com/Acetyld/expo-foreground-actions/issues)**: Submit bugs found or log feature 329 | requests for ACETYLD. 330 | 331 | #### *Contributing Guidelines* 332 | 333 |
334 | Click to expand 335 | 336 | 1. **Fork the Repository**: Start by forking the project repository to your GitHub account. 337 | 2. **Clone Locally**: Clone the forked repository to your local machine using a Git client. 338 | ```sh 339 | git clone 340 | ``` 341 | 3. **Create a New Branch**: Always work on a new branch, giving it a descriptive name. 342 | ```sh 343 | git checkout -b new-feature-x 344 | ``` 345 | 4. **Make Your Changes**: Develop and test your changes locally. 346 | 5. **Commit Your Changes**: Commit with a clear and concise message describing your updates. 347 | ```sh 348 | git commit -m 'Implemented new feature x.' 349 | ``` 350 | 6. **Push to GitHub**: Push the changes to your forked repository. 351 | ```sh 352 | git push origin new-feature-x 353 | ``` 354 | 7. **Submit a Pull Request**: Create a PR against the original project repository. Clearly describe the changes and 355 | their motivations. 356 | 357 | Once your PR is reviewed and approved, it will be merged into the main branch. 358 | 359 |
360 | 361 | --- 362 | 363 | ## 📄 License 364 | 365 | This project is protected under the [MIT](https://choosealicense.com/licenses) License. For more details, refer to 366 | the [LICENSE](https://choosealicense.com/licenses/) file. 367 | 368 | --- 369 | 370 | ## 👏 Acknowledgments 371 | 372 | - Idea/inspiration from https://github.com/Rapsssito/react-native-background-actions 373 | - [Expo](https://expo.dev) for providing a platform to build universal apps using React Native. 374 | - [Benedikt](https://twitter.com/bndkt) for mentioning this package in the "thisweekinreact" 375 | newsletter: [Week 176](https://thisweekinreact.com/newsletter/176) 376 | 377 | [**Return**](#Top) 378 | 379 | --- 380 | 381 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'maven-publish' 4 | 5 | group = 'expo.modules.foregroundactions' 6 | version = '0.1.0' 7 | 8 | buildscript { 9 | def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") 10 | if (expoModulesCorePlugin.exists()) { 11 | apply from: expoModulesCorePlugin 12 | applyKotlinExpoModulesCorePlugin() 13 | } 14 | 15 | // Simple helper that allows the root project to override versions declared by this library. 16 | ext.safeExtGet = { prop, fallback -> 17 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 18 | } 19 | 20 | // Ensures backward compatibility 21 | ext.getKotlinVersion = { 22 | if (ext.has("kotlinVersion")) { 23 | ext.kotlinVersion() 24 | } else { 25 | ext.safeExtGet("kotlinVersion", "1.8.10") 26 | } 27 | } 28 | 29 | repositories { 30 | mavenCentral() 31 | } 32 | 33 | dependencies { 34 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}") 35 | } 36 | } 37 | 38 | afterEvaluate { 39 | publishing { 40 | publications { 41 | release(MavenPublication) { 42 | from components.release 43 | } 44 | } 45 | repositories { 46 | maven { 47 | url = mavenLocal().url 48 | } 49 | } 50 | } 51 | } 52 | 53 | android { 54 | compileSdkVersion safeExtGet("compileSdkVersion", 33) 55 | 56 | compileOptions { 57 | sourceCompatibility JavaVersion.VERSION_11 58 | targetCompatibility JavaVersion.VERSION_11 59 | } 60 | 61 | kotlinOptions { 62 | jvmTarget = JavaVersion.VERSION_11.majorVersion 63 | } 64 | 65 | namespace "expo.modules.foregroundactions" 66 | defaultConfig { 67 | minSdkVersion safeExtGet("minSdkVersion", 21) 68 | targetSdkVersion safeExtGet("targetSdkVersion", 33) 69 | versionCode 1 70 | versionName "0.1.0" 71 | } 72 | lintOptions { 73 | abortOnError false 74 | } 75 | publishing { 76 | singleVariant("release") { 77 | withSourcesJar() 78 | } 79 | } 80 | } 81 | 82 | repositories { 83 | mavenCentral() 84 | } 85 | 86 | dependencies { 87 | implementation project(':expo-modules-core') 88 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" 89 | //noinspection GradleDynamicVersion 90 | implementation 'com.facebook.react:react-native:+' 91 | } 92 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/src/main/java/expo/modules/foregroundactions/ExpoForegroundActionsModule.kt: -------------------------------------------------------------------------------- 1 | package expo.modules.foregroundactions 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Notification 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.graphics.Color 9 | import android.os.Build 10 | import expo.modules.kotlin.Promise 11 | import expo.modules.kotlin.exception.CodedException 12 | import expo.modules.kotlin.exception.toCodedException 13 | import expo.modules.kotlin.modules.Module 14 | import expo.modules.kotlin.modules.ModuleDefinition 15 | 16 | 17 | const val ON_EXPIRATION_EVENT = "onExpirationEvent" 18 | 19 | class ExpoForegroundActionsModule : Module() { 20 | private val intentMap: MutableMap = mutableMapOf() 21 | private var currentReferenceId: Int = 0 22 | 23 | 24 | // Each module class must implement the definition function. The definition consists of components 25 | // that describes the module's functionality and behavior. 26 | // See https://docs.expo.dev/modules/module-api for more details about available components. 27 | @SuppressLint("DiscouragedApi") 28 | override fun definition() = ModuleDefinition { 29 | Events(ON_EXPIRATION_EVENT) 30 | 31 | // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument. 32 | // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity. 33 | // The module will be accessible from `requireNativeModule('ExpoForegroundActions')` in JavaScript. 34 | Name("ExpoForegroundActions") 35 | 36 | 37 | AsyncFunction("startForegroundAction") { options: ExpoForegroundOptions, promise: Promise -> 38 | try { 39 | val intent = Intent(context, ExpoForegroundActionsService::class.java) 40 | intent.putExtra("headlessTaskName", options.headlessTaskName) 41 | intent.putExtra("notificationTitle", options.notificationTitle) 42 | intent.putExtra("notificationDesc", options.notificationDesc) 43 | intent.putExtra("notificationColor", options.notificationColor) 44 | val notificationIconInt: Int = context.resources.getIdentifier(options.notificationIconName, options.notificationIconType, context.packageName) 45 | intent.putExtra("notificationIconInt", notificationIconInt) 46 | intent.putExtra("notificationProgress", options.notificationProgress) 47 | intent.putExtra("notificationMaxProgress", options.notificationMaxProgress) 48 | intent.putExtra("notificationIndeterminate", options.notificationIndeterminate) 49 | intent.putExtra("linkingURI", options.linkingURI) 50 | currentReferenceId++ 51 | 52 | intentMap[currentReferenceId] = intent 53 | intent.putExtra("notificationId", currentReferenceId) 54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 55 | context.startForegroundService(intent) 56 | } else { 57 | context.startService(intent) 58 | } 59 | promise.resolve(currentReferenceId) 60 | 61 | } catch (e: Exception) { 62 | println(e.message); 63 | 64 | // Handle other exceptions 65 | promise.reject(e.toCodedException()) 66 | } 67 | } 68 | 69 | AsyncFunction("stopForegroundAction") { identifier: Int, promise: Promise -> 70 | try { 71 | println(identifier); 72 | println(intentMap); 73 | val intent = intentMap[identifier] 74 | if (intent !== null) { 75 | context.stopService(intent) 76 | intentMap.remove(identifier) 77 | } else { 78 | println("Background task with identifier $identifier does not exist or has already been ended"); 79 | 80 | } 81 | } catch (e: Exception) { 82 | println(e.message); 83 | // Handle other exceptions 84 | promise.reject(e.toCodedException()) 85 | } 86 | promise.resolve(null) 87 | } 88 | 89 | AsyncFunction("updateForegroundedAction") { identifier: Int, options: ExpoForegroundOptions, promise: Promise -> 90 | try { 91 | val notificationIconInt: Int = context.resources.getIdentifier(options.notificationIconName, options.notificationIconType, context.packageName) 92 | val notification: Notification = ExpoForegroundActionsService.buildNotification( 93 | context, 94 | options.notificationTitle, 95 | options.notificationDesc, 96 | Color.parseColor(options.notificationColor), 97 | notificationIconInt, 98 | options.notificationProgress, 99 | options.notificationMaxProgress, 100 | options.notificationIndeterminate, 101 | options.linkingURI, 102 | ); 103 | val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 104 | notificationManager.notify(identifier, notification) 105 | promise.resolve(null) 106 | } catch (e: Exception) { 107 | println(e.message); 108 | // Handle other exceptions 109 | promise.reject(e.toCodedException()) 110 | } 111 | } 112 | 113 | AsyncFunction("forceStopAllForegroundActions") { promise: Promise -> 114 | try { 115 | if (intentMap.isEmpty()) { 116 | println("No intents to stop.") 117 | } else { 118 | for ((_, intent) in intentMap) { 119 | context.stopService(intent) 120 | } 121 | intentMap.clear() 122 | } 123 | promise.resolve(null) 124 | } catch (e: Exception) { 125 | println(e.message) 126 | // Handle other exceptions 127 | promise.reject(e.toCodedException()) 128 | } 129 | } 130 | AsyncFunction("getForegroundIdentifiers") { promise: Promise -> 131 | val identifiers = intentMap.keys.toTypedArray() 132 | promise.resolve(identifiers) 133 | } 134 | } 135 | 136 | private val context 137 | get() = requireNotNull(appContext.reactContext) { 138 | "React Application Context is null" 139 | } 140 | 141 | private val applicationContext 142 | get() = requireNotNull(this.context.applicationContext) { 143 | "React Application Context is null" 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /android/src/main/java/expo/modules/foregroundactions/ExpoForegroundActionsService.kt: -------------------------------------------------------------------------------- 1 | package expo.modules.foregroundactions 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.Color 10 | import android.net.Uri 11 | import android.os.Build 12 | import android.os.Bundle 13 | import android.os.IBinder 14 | import androidx.core.app.NotificationCompat 15 | import com.facebook.react.HeadlessJsTaskService 16 | import com.facebook.react.bridge.Arguments 17 | import com.facebook.react.jstasks.HeadlessJsTaskConfig 18 | 19 | 20 | class ExpoForegroundActionsService : HeadlessJsTaskService() { 21 | companion object { 22 | private const val CHANNEL_ID = "ExpoForegroundActionChannel" 23 | fun buildNotification( 24 | context: Context, 25 | notificationTitle: String, 26 | notificationDesc: String, 27 | notificationColor: Int, 28 | notificationIconInt: Int, 29 | notificationProgress: Int, 30 | notificationMaxProgress: Int, 31 | notificationIndeterminate: Boolean, 32 | linkingURI: String 33 | ): Notification { 34 | 35 | val notificationIntent: Intent = if (linkingURI.isNotEmpty()) { 36 | Intent(Intent.ACTION_VIEW, Uri.parse(linkingURI)) 37 | } else { 38 | Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER) 39 | } 40 | val contentIntent: PendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); 41 | val builder = NotificationCompat.Builder(context, CHANNEL_ID) 42 | .setContentTitle(notificationTitle) 43 | .setContentText(notificationDesc) 44 | .setSmallIcon(notificationIconInt) 45 | .setContentIntent(contentIntent) 46 | .setOngoing(true) 47 | .setSilent(true) 48 | .setProgress(notificationMaxProgress, notificationProgress, notificationIndeterminate) 49 | .setPriority(NotificationCompat.PRIORITY_MIN) 50 | .setColor(notificationColor) 51 | return builder.build() 52 | } 53 | } 54 | 55 | 56 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 57 | val extras: Bundle? = intent?.extras 58 | requireNotNull(extras) { "Extras cannot be null" } 59 | 60 | 61 | val notificationTitle: String = extras.getString("notificationTitle")!!; 62 | val notificationDesc: String = extras.getString("notificationDesc")!!; 63 | val notificationColor: Int = Color.parseColor(extras.getString("notificationColor")) 64 | val notificationIconInt: Int = extras.getInt("notificationIconInt"); 65 | val notificationProgress: Int = extras.getInt("notificationProgress"); 66 | val notificationMaxProgress: Int = extras.getInt("notificationMaxProgress"); 67 | val notificationIndeterminate: Boolean = extras.getBoolean("notificationIndeterminate"); 68 | val notificationId: Int = extras.getInt("notificationId"); 69 | val linkingURI: String = extras.getString("linkingURI")!!; 70 | 71 | 72 | println("notificationIconInt"); 73 | println(notificationIconInt); 74 | println("On create door dion") 75 | println("onStartCommand") 76 | createNotificationChannel() // Necessary creating channel for API 26+ 77 | println("After createNotificationChannel") 78 | 79 | println("buildNotification") 80 | val notification: Notification = buildNotification( 81 | this, 82 | notificationTitle, 83 | notificationDesc, 84 | notificationColor, 85 | notificationIconInt, 86 | notificationProgress, 87 | notificationMaxProgress, 88 | notificationIndeterminate, 89 | linkingURI 90 | ) 91 | println("Starting foreground") 92 | 93 | startForeground(notificationId, notification) 94 | println("After foreground") 95 | return super.onStartCommand(intent, flags, startId) 96 | } 97 | 98 | override fun onBind(intent: Intent): IBinder? { 99 | return null 100 | } 101 | 102 | private fun createNotificationChannel() { 103 | println("createNotificationChannel") 104 | 105 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 106 | val serviceChannel = NotificationChannel(CHANNEL_ID, "Foreground Service Channel", 107 | NotificationManager.IMPORTANCE_DEFAULT) 108 | val manager = getSystemService(NotificationManager::class.java) 109 | manager!!.createNotificationChannel(serviceChannel) 110 | } 111 | } 112 | 113 | override fun getTaskConfig(intent: Intent): HeadlessJsTaskConfig? { 114 | return intent.extras?.let { 115 | HeadlessJsTaskConfig( 116 | intent.extras?.getString("headlessTaskName")!!, 117 | Arguments.fromBundle(it), 118 | 0, // timeout for the task 119 | true // optional: defines whether or not the task is allowed in foreground. 120 | // Default is false 121 | ) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /android/src/main/java/expo/modules/foregroundactions/ExpoForegroundOptions.kt: -------------------------------------------------------------------------------- 1 | package expo.modules.foregroundactions 2 | 3 | import expo.modules.kotlin.records.Field 4 | import expo.modules.kotlin.records.Record 5 | 6 | class ExpoForegroundOptions : Record { 7 | @Field 8 | val headlessTaskName: String = "default" 9 | 10 | @Field 11 | val notificationTitle: String = "Notification Title" 12 | 13 | @Field 14 | val notificationDesc: String = "Notification Description" 15 | 16 | @Field 17 | val notificationColor: String = "#FFC107" 18 | 19 | @Field 20 | val notificationIconName: String = "ic_launcher" 21 | 22 | @Field 23 | val notificationIconType: String = "mipmap" 24 | 25 | @Field 26 | val notificationProgress: Int = 0 27 | 28 | @Field 29 | val notificationMaxProgress: Int = 100 30 | 31 | @Field 32 | val notificationIndeterminate: Boolean = false 33 | 34 | @Field 35 | val linkingURI: String = "" 36 | } 37 | -------------------------------------------------------------------------------- /assets/App_tsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acetyld/expo-foreground-actions/9c1f35716aadcbc5f13a3605c37b767f55c4d8c0/assets/App_tsx.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acetyld/expo-foreground-actions/9c1f35716aadcbc5f13a3605c37b767f55c4d8c0/assets/logo.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | addExpirationListener, 3 | forceStopAllForegroundActions, 4 | getBackgroundTimeRemaining, 5 | getForegroundIdentifiers, 6 | runForegroundedAction, 7 | stopForegroundAction, 8 | updateForegroundedAction 9 | } from "expo-foreground-actions"; 10 | import { useEffect, useRef } from "react"; 11 | import { Button, Linking, Platform, StatusBar, StyleSheet, View } from "react-native"; 12 | import { ForegroundApi } from "expo-foreground-actions/ExpoForegroundActions.types"; 13 | 14 | Linking.addEventListener("url", onUrl); 15 | 16 | function onUrl(evt: any) { 17 | /*Here we check the deeplink URL*/ 18 | console.log(evt.url); 19 | } 20 | 21 | /*Quick await*/ 22 | function wait(ms: number) { 23 | return new Promise((resolve) => setTimeout(resolve, ms)); 24 | } 25 | 26 | 27 | const MyHelperFunction = async ({ 28 | headlessTaskName, 29 | identifier 30 | }: ForegroundApi) => { 31 | let time = Date.now(); 32 | let duration = 0; 33 | while (duration < 50) { 34 | console.log("Logging every 1 second... from foreground!", time); 35 | await wait(1000); // Wait for 1 second 36 | duration += 1; 37 | await updateForegroundedAction(identifier, { 38 | headlessTaskName: headlessTaskName, 39 | notificationTitle: "Notification Title", 40 | notificationDesc: "Notification Description", 41 | notificationColor: "#FFC107", 42 | notificationIconName: "ic_launcher", 43 | notificationIconType: "mipmap", 44 | notificationProgress: duration * 10, 45 | notificationMaxProgress: 100, 46 | notificationIndeterminate: false, 47 | linkingURI: "myapp://" 48 | }); 49 | 50 | if (Platform.OS === "ios") { 51 | await getBackgroundTimeRemaining().then((r) => { 52 | console.log("Remaining time:", r); 53 | }); 54 | } 55 | } 56 | console.log("Logging interval ended."); 57 | }; 58 | 59 | 60 | export default function App() { 61 | const intervalRef = useRef(null); 62 | const currentRunningId = useRef(null); 63 | useEffect(() => { 64 | /*Triggered on IOS expiration*/ 65 | const sub = addExpirationListener((event) => { 66 | console.log(event); 67 | }); 68 | 69 | return () => { 70 | sub && sub.remove(); 71 | }; 72 | }, []); 73 | 74 | useEffect(() => { 75 | /*We do this so we can see when app is backgrounded*/ 76 | intervalRef.current = setInterval(() => { 77 | console.log("Logging every 1 second, so we can see when the app gets \"paused\""); 78 | }, 1000); 79 | 80 | return () => { 81 | intervalRef.current && clearInterval(intervalRef.current); 82 | }; 83 | }, []); 84 | return ( 85 | 86 | 87 |