rn-bg-actions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/asterinet/react/bgactions/BackgroundActionsModule.java +91 -0
- package/android/src/main/java/com/asterinet/react/bgactions/BackgroundActionsPackage.java +26 -0
- package/android/src/main/java/com/asterinet/react/bgactions/BackgroundTaskOptions.java +104 -0
- package/android/src/main/java/com/asterinet/react/bgactions/RNBackgroundActionsTask.java +120 -0
- package/ios/RNBackgroundActions.h +6 -0
- package/ios/RNBackgroundActions.m +53 -0
- package/ios/RNBackgroundActions.xcodeproj/project.pbxproj +426 -0
- package/lib/types/RNBackgroundActionsModule.d.ts +2 -0
- package/lib/types/index.d.ts +127 -0
- package/package.json +77 -0
- package/react-native-background-actions.podspec +19 -0
- package/src/RNBackgroundActionsModule.js +7 -0
- package/src/index.js +138 -0
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 Rapsssito
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
<p align="center">
|
2
|
+
<img src="https://i.imgur.com/G8BUzdZ.png" />
|
3
|
+
<br></br>
|
4
|
+
<img src="https://github.com/Rapsssito/react-native-background-actions/workflows/Release/badge.svg" />
|
5
|
+
<img src="https://img.shields.io/npm/dw/react-native-background-actions" />
|
6
|
+
<img src="https://img.shields.io/npm/v/react-native-background-actions?color=gr&label=npm%20version" />
|
7
|
+
</p>
|
8
|
+
|
9
|
+
React Native background service library for running **background tasks forever in Android & iOS**. Schedule a background job that will run your JavaScript when your app is in the background or foreground.
|
10
|
+
|
11
|
+
### WARNING
|
12
|
+
- **Android**: This library relies on React Native's [`HeadlessJS`](https://facebook.github.io/react-native/docs/headless-js-android) for Android. Before building your JS task, make sure to read all the [documentation](https://facebook.github.io/react-native/docs/headless-js-android). The jobs will run even if the app has been closed. [In Android 12+](https://developer.android.com/guide/components/foreground-services#background-start-restrictions) you will not be able to launch background tasks from the background. A notification will be shown when the task is running, it is not possible to start the service without it. The notification will only be visible in Android.
|
13
|
+
|
14
|
+
- **iOS**: This library relies on iOS's [`UIApplication beginBackgroundTaskWithName` method](https://developer.apple.com/documentation/uikit/uiapplication/1623051-beginbackgroundtaskwithname?language=objc), which **won't keep your app in the background forever** by itself. However, you can rely on other libraries like [`react-native-track-player`](https://github.com/react-native-kit/react-native-track-player) that use audio, geolocalization, etc. to keep your app alive in the background while you excute the JS from this library.
|
15
|
+
|
16
|
+
|
17
|
+
## Table of Contents <!-- omit in toc -->
|
18
|
+
|
19
|
+
- [React Native / Android / iOS compatibility](#react-native--android--ios-compatibility)
|
20
|
+
- [Install](#install)
|
21
|
+
- [Usage](#usage)
|
22
|
+
- [Example Code](#example-code)
|
23
|
+
- [Options](#options)
|
24
|
+
- [taskIconOptions](#taskiconoptions)
|
25
|
+
- [taskProgressBarOptions](#taskprogressbaroptions)
|
26
|
+
- [Deep Linking](#deep-linking)
|
27
|
+
- [Events](#events)
|
28
|
+
- ['expiration'](#expiration)
|
29
|
+
- [Maintainers](#maintainers)
|
30
|
+
- [Acknowledgments](#acknowledgments)
|
31
|
+
- [License](#license)
|
32
|
+
|
33
|
+
## React Native / Android / iOS compatibility
|
34
|
+
To use this module you need to ensure you are using the correct version of React Native. If you are using an Android (targetSdkVersion) version lower than 31 (introduced in React Native 0.68.0) you will need to upgrade before attempting to use `react-native-background-actions`'s latest version.
|
35
|
+
|
36
|
+
| Version | React Native version | Android (targetSdkVersion) version | iOS version |
|
37
|
+
| ------- | -------------------- | ---------------------------------- | ------------ |
|
38
|
+
| `4.X.X` | `>= Unknown` | `>= 34` | `>= Unknown` |
|
39
|
+
| `3.X.X` | `>= Unknown` | `>= 31` | `>= Unknown` |
|
40
|
+
| `2.6.7` | `>= Unknown` | `>= Unknown` | `>= Unknown` |
|
41
|
+
|
42
|
+
## Install
|
43
|
+
|
44
|
+
Go to [INSTALL.md](./INSTALL.md) to see the how to install, compatibility with RN and Linking process.
|
45
|
+
|
46
|
+
## Usage
|
47
|
+
|
48
|
+
### Example Code
|
49
|
+
|
50
|
+
```js
|
51
|
+
import BackgroundService from 'react-native-background-actions';
|
52
|
+
|
53
|
+
const sleep = (time) => new Promise((resolve) => setTimeout(() => resolve(), time));
|
54
|
+
|
55
|
+
// You can do anything in your task such as network requests, timers and so on,
|
56
|
+
// as long as it doesn't touch UI. Once your task completes (i.e. the promise is resolved),
|
57
|
+
// React Native will go into "paused" mode (unless there are other tasks running,
|
58
|
+
// or there is a foreground app).
|
59
|
+
const veryIntensiveTask = async (taskDataArguments) => {
|
60
|
+
// Example of an infinite loop task
|
61
|
+
const { delay } = taskDataArguments;
|
62
|
+
await new Promise( async (resolve) => {
|
63
|
+
for (let i = 0; BackgroundService.isRunning(); i++) {
|
64
|
+
console.log(i);
|
65
|
+
await sleep(delay);
|
66
|
+
}
|
67
|
+
});
|
68
|
+
};
|
69
|
+
|
70
|
+
const options = {
|
71
|
+
taskName: 'Example',
|
72
|
+
taskTitle: 'ExampleTask title',
|
73
|
+
taskDesc: 'ExampleTask description',
|
74
|
+
taskIcon: {
|
75
|
+
name: 'ic_launcher',
|
76
|
+
type: 'mipmap',
|
77
|
+
},
|
78
|
+
color: '#ff00ff',
|
79
|
+
linkingURI: 'yourSchemeHere://chat/jane', // See Deep Linking for more info
|
80
|
+
parameters: {
|
81
|
+
delay: 1000,
|
82
|
+
},
|
83
|
+
};
|
84
|
+
|
85
|
+
|
86
|
+
await BackgroundService.start(veryIntensiveTask, options);
|
87
|
+
await BackgroundService.updateNotification({taskDesc: 'New ExampleTask description'}); // Only Android, iOS will ignore this call
|
88
|
+
// iOS will also run everything here in the background until .stop() is called
|
89
|
+
await BackgroundService.stop();
|
90
|
+
```
|
91
|
+
> If you call stop() on background no new tasks will be able to be started!
|
92
|
+
> Don't call .start() twice, as it will stop performing previous background tasks and start a new one.
|
93
|
+
> If .start() is called on the backgound, it will not have any effect.
|
94
|
+
|
95
|
+
### Options
|
96
|
+
| Property | Type | Description |
|
97
|
+
| ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
98
|
+
| `taskName` | `<string>` | Task name for identification. |
|
99
|
+
| `taskTitle` | `<string>` | **Android Required**. Notification title. |
|
100
|
+
| `taskDesc` | `<string>` | **Android Required**. Notification description. |
|
101
|
+
| `taskIcon` | [`<taskIconOptions>`](#taskIconOptions) | **Android Required**. Notification icon. |
|
102
|
+
| `color` | `<string>` | Notification color. **Default**: `"#ffffff"`. |
|
103
|
+
| `linkingURI` | `<string>` | Link that will be called when the notification is clicked. Example: `"yourSchemeHere://chat/jane"`. See [Deep Linking](#deep-linking) for more info. **Default**: `undefined`. |
|
104
|
+
| `progressBar` | [`<taskProgressBarOptions>`](#taskProgressBarOptions) | Notification progress bar. |
|
105
|
+
| `parameters` | `<any>` | Parameters to pass to the task. |
|
106
|
+
|
107
|
+
#### taskIconOptions
|
108
|
+
**Android only**
|
109
|
+
| Property | Type | Description |
|
110
|
+
| --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
111
|
+
| `name` | `<string>` | **Required**. Icon name in res/ folder. Ex: `ic_launcher`. |
|
112
|
+
| `type` | `<string>` | **Required**. Icon type in res/ folder. Ex: `mipmap`. |
|
113
|
+
| `package` | `<string>` | Icon package where to search the icon. Ex: `com.example.package`. **It defaults to the app's package. It is highly recommended to leave like that.** |
|
114
|
+
|
115
|
+
Example:
|
116
|
+
|
117
|
+

|
118
|
+
|
119
|
+
#### taskProgressBarOptions
|
120
|
+
**Android only**
|
121
|
+
| Property | Type | Description |
|
122
|
+
| --------------- | ----------- | --------------------------------------------- |
|
123
|
+
| `max` | `<number>` | **Required**. Maximum value. |
|
124
|
+
| `value` | `<number>` | **Required**. Current value. |
|
125
|
+
| `indeterminate` | `<boolean>` | Display the progress status as indeterminate. |
|
126
|
+
|
127
|
+
Example:
|
128
|
+
|
129
|
+

|
130
|
+
|
131
|
+
### Deep Linking
|
132
|
+
**Android only**
|
133
|
+
|
134
|
+
To handle incoming links when the notification is clicked by the user, first you need to modify your **`android/app/src/main/AndroidManifest.xml`** and add an `<intent-filter>` (fill `yourSchemeHere` with the name you prefer):
|
135
|
+
```xml
|
136
|
+
<manifest ... >
|
137
|
+
...
|
138
|
+
<application ... >
|
139
|
+
<activity
|
140
|
+
...
|
141
|
+
android:launchMode="singleTask"> // Add this if not present
|
142
|
+
...
|
143
|
+
<intent-filter android:label="filter_react_native">
|
144
|
+
<action android:name="android.intent.action.VIEW" />
|
145
|
+
<category android:name="android.intent.category.DEFAULT" />
|
146
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
147
|
+
<data android:scheme="yourSchemeHere" />
|
148
|
+
</intent-filter>
|
149
|
+
</application>
|
150
|
+
</manifest>
|
151
|
+
```
|
152
|
+
|
153
|
+
You must provide a `linkingURI` in the BackgroundService's [options](#options) that matches the scheme you just added to **`android/app/src/main/AndroidManifest.xml`**:
|
154
|
+
```js
|
155
|
+
const options = {
|
156
|
+
taskName: 'Example',
|
157
|
+
taskTitle: 'ExampleTask title',
|
158
|
+
taskDesc: 'ExampleTask description',
|
159
|
+
taskIcon: {
|
160
|
+
name: 'ic_launcher',
|
161
|
+
type: 'mipmap',
|
162
|
+
},
|
163
|
+
color: '#ff00ff',
|
164
|
+
linkingURI: 'yourSchemeHere://chat/jane', // Add this
|
165
|
+
parameters: {
|
166
|
+
delay: 1000,
|
167
|
+
},
|
168
|
+
};
|
169
|
+
|
170
|
+
|
171
|
+
await BackgroundService.start(veryIntensiveTask, options);
|
172
|
+
```
|
173
|
+
|
174
|
+
React Native provides a `Linking` class to get notified of incoming links. Your JavaScript code must then listen to the url using React Native `Linking` class:
|
175
|
+
```js
|
176
|
+
import { Linking } from 'react-native';
|
177
|
+
|
178
|
+
Linking.addEventListener('url', handleOpenURL);
|
179
|
+
|
180
|
+
function handleOpenURL(evt) {
|
181
|
+
// Will be called when the notification is pressed
|
182
|
+
console.log(evt.url);
|
183
|
+
// do something
|
184
|
+
}
|
185
|
+
```
|
186
|
+
|
187
|
+
### Events
|
188
|
+
#### 'expiration'
|
189
|
+
**iOS only**
|
190
|
+
Listen for the iOS-only expiration handler that allows you to 'clean up' shortly before the app’s remaining background time reaches 0. Check the iOS [documentation](https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtask) for more info.
|
191
|
+
|
192
|
+
```js
|
193
|
+
BackgroundService.on('expiration', () => {
|
194
|
+
console.log('I am being closed :(');
|
195
|
+
});
|
196
|
+
|
197
|
+
await BackgroundService.start(veryIntensiveTask, options);
|
198
|
+
|
199
|
+
```
|
200
|
+
|
201
|
+
## Maintainers
|
202
|
+
|
203
|
+
* [Rapsssito](https://github.com/rapsssito) [[Support me :heart:](https://github.com/sponsors/Rapsssito)]
|
204
|
+
|
205
|
+
## Acknowledgments
|
206
|
+
|
207
|
+
* iOS part originally forked from [react-native-background-timer](https://github.com/ocetnik/react-native-background-timer)
|
208
|
+
|
209
|
+
## License
|
210
|
+
|
211
|
+
The library is released under the MIT license. For more information see [`LICENSE`](/LICENSE).
|
@@ -0,0 +1,24 @@
|
|
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', 34)
|
9
|
+
|
10
|
+
defaultConfig {
|
11
|
+
minSdkVersion safeExtGet('minSdkVersion', 16)
|
12
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 34)
|
13
|
+
}
|
14
|
+
|
15
|
+
// Conditional for compatibility with AGP <4.2.
|
16
|
+
if (project.android.hasProperty("namespace")) {
|
17
|
+
namespace = "com.asterinet.react.bgactions"
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
dependencies {
|
22
|
+
//noinspection GradleDynamicVersion
|
23
|
+
implementation 'com.facebook.react:react-native:+' // From node_modules
|
24
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.asterinet.react.bgactions">
|
2
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
3
|
+
<application>
|
4
|
+
<service android:name=".RNBackgroundActionsTask"/>
|
5
|
+
</application>
|
6
|
+
</manifest>
|
@@ -0,0 +1,91 @@
|
|
1
|
+
package com.asterinet.react.bgactions;
|
2
|
+
|
3
|
+
import android.app.Notification;
|
4
|
+
import android.app.NotificationManager;
|
5
|
+
import android.content.Context;
|
6
|
+
import android.content.Intent;
|
7
|
+
|
8
|
+
import androidx.annotation.NonNull;
|
9
|
+
|
10
|
+
import com.facebook.react.bridge.Promise;
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
12
|
+
import com.facebook.react.bridge.ReactContext;
|
13
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
14
|
+
import com.facebook.react.bridge.ReactMethod;
|
15
|
+
import com.facebook.react.bridge.ReadableMap;
|
16
|
+
|
17
|
+
@SuppressWarnings("WeakerAccess")
|
18
|
+
public class BackgroundActionsModule extends ReactContextBaseJavaModule {
|
19
|
+
|
20
|
+
private static final String TAG = "RNBackgroundActions";
|
21
|
+
|
22
|
+
private final ReactContext reactContext;
|
23
|
+
|
24
|
+
private Intent currentServiceIntent;
|
25
|
+
|
26
|
+
public BackgroundActionsModule(ReactApplicationContext reactContext) {
|
27
|
+
super(reactContext);
|
28
|
+
this.reactContext = reactContext;
|
29
|
+
}
|
30
|
+
|
31
|
+
@NonNull
|
32
|
+
@Override
|
33
|
+
public String getName() {
|
34
|
+
return TAG;
|
35
|
+
}
|
36
|
+
|
37
|
+
@SuppressWarnings("unused")
|
38
|
+
@ReactMethod
|
39
|
+
public void start(@NonNull final ReadableMap options, @NonNull final Promise promise) {
|
40
|
+
try {
|
41
|
+
// Stop any other intent
|
42
|
+
if (currentServiceIntent != null) reactContext.stopService(currentServiceIntent);
|
43
|
+
// Create the service
|
44
|
+
currentServiceIntent = new Intent(reactContext, RNBackgroundActionsTask.class);
|
45
|
+
// Get the task info from the options
|
46
|
+
final BackgroundTaskOptions bgOptions = new BackgroundTaskOptions(reactContext, options);
|
47
|
+
currentServiceIntent.putExtras(bgOptions.getExtras());
|
48
|
+
// Start the task
|
49
|
+
reactContext.startService(currentServiceIntent);
|
50
|
+
promise.resolve(null);
|
51
|
+
} catch (Exception e) {
|
52
|
+
promise.reject(e);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
@SuppressWarnings("unused")
|
57
|
+
@ReactMethod
|
58
|
+
public void stop(@NonNull final Promise promise) {
|
59
|
+
if (currentServiceIntent != null)
|
60
|
+
reactContext.stopService(currentServiceIntent);
|
61
|
+
promise.resolve(null);
|
62
|
+
}
|
63
|
+
|
64
|
+
@SuppressWarnings("unused")
|
65
|
+
@ReactMethod
|
66
|
+
public void updateNotification(@NonNull final ReadableMap options, @NonNull final Promise promise) {
|
67
|
+
// Get the task info from the options
|
68
|
+
try {
|
69
|
+
final BackgroundTaskOptions bgOptions = new BackgroundTaskOptions(reactContext, options);
|
70
|
+
final Notification notification = RNBackgroundActionsTask.buildNotification(reactContext, bgOptions);
|
71
|
+
final NotificationManager notificationManager = (NotificationManager) reactContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
72
|
+
notificationManager.notify(RNBackgroundActionsTask.SERVICE_NOTIFICATION_ID, notification);
|
73
|
+
} catch (Exception e) {
|
74
|
+
promise.reject(e);
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
promise.resolve(null);
|
78
|
+
}
|
79
|
+
|
80
|
+
@SuppressWarnings("unused")
|
81
|
+
@ReactMethod
|
82
|
+
public void addListener(String eventName) {
|
83
|
+
// Keep: Required for RN built in Event Emitter Calls.
|
84
|
+
}
|
85
|
+
|
86
|
+
@SuppressWarnings("unused")
|
87
|
+
@ReactMethod
|
88
|
+
public void removeListeners(Integer count) {
|
89
|
+
// Keep: Required for RN built in Event Emitter Calls.
|
90
|
+
}
|
91
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
package com.asterinet.react.bgactions;
|
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
|
+
|
12
|
+
import androidx.annotation.NonNull;
|
13
|
+
|
14
|
+
@SuppressWarnings("unused")
|
15
|
+
public class BackgroundActionsPackage implements ReactPackage {
|
16
|
+
@Override
|
17
|
+
public @NonNull List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
18
|
+
//noinspection ArraysAsListWithZeroOrOneArgument
|
19
|
+
return Arrays.<NativeModule>asList(new BackgroundActionsModule(reactContext));
|
20
|
+
}
|
21
|
+
|
22
|
+
@Override
|
23
|
+
public @NonNull List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
24
|
+
return Collections.emptyList();
|
25
|
+
}
|
26
|
+
}
|
@@ -0,0 +1,104 @@
|
|
1
|
+
package com.asterinet.react.bgactions;
|
2
|
+
|
3
|
+
import android.graphics.Color;
|
4
|
+
import android.os.Bundle;
|
5
|
+
|
6
|
+
import androidx.annotation.ColorInt;
|
7
|
+
import androidx.annotation.IdRes;
|
8
|
+
import androidx.annotation.NonNull;
|
9
|
+
import androidx.annotation.Nullable;
|
10
|
+
|
11
|
+
import com.facebook.react.bridge.Arguments;
|
12
|
+
import com.facebook.react.bridge.ReactContext;
|
13
|
+
import com.facebook.react.bridge.ReadableMap;
|
14
|
+
|
15
|
+
public final class BackgroundTaskOptions {
|
16
|
+
private final Bundle extras;
|
17
|
+
|
18
|
+
public BackgroundTaskOptions(@NonNull final Bundle extras) {
|
19
|
+
this.extras = extras;
|
20
|
+
}
|
21
|
+
|
22
|
+
public BackgroundTaskOptions(@NonNull final ReactContext reactContext, @NonNull final ReadableMap options) {
|
23
|
+
// Create extras
|
24
|
+
extras = Arguments.toBundle(options);
|
25
|
+
if (extras == null)
|
26
|
+
throw new IllegalArgumentException("Could not convert arguments to bundle");
|
27
|
+
// Get taskTitle
|
28
|
+
try {
|
29
|
+
if (options.getString("taskTitle") == null)
|
30
|
+
throw new IllegalArgumentException();
|
31
|
+
} catch (Exception e) {
|
32
|
+
throw new IllegalArgumentException("Task title cannot be null");
|
33
|
+
}
|
34
|
+
// Get taskDesc
|
35
|
+
try {
|
36
|
+
if (options.getString("taskDesc") == null)
|
37
|
+
throw new IllegalArgumentException();
|
38
|
+
} catch (Exception e) {
|
39
|
+
throw new IllegalArgumentException("Task description cannot be null");
|
40
|
+
}
|
41
|
+
// Get iconInt
|
42
|
+
try {
|
43
|
+
final ReadableMap iconMap = options.getMap("taskIcon");
|
44
|
+
if (iconMap == null)
|
45
|
+
throw new IllegalArgumentException();
|
46
|
+
final String iconName = iconMap.getString("name");
|
47
|
+
final String iconType = iconMap.getString("type");
|
48
|
+
String iconPackage;
|
49
|
+
try {
|
50
|
+
iconPackage = iconMap.getString("package");
|
51
|
+
if (iconPackage == null)
|
52
|
+
throw new IllegalArgumentException();
|
53
|
+
} catch (Exception e) {
|
54
|
+
// Get the current package as default
|
55
|
+
iconPackage = reactContext.getPackageName();
|
56
|
+
}
|
57
|
+
final int iconInt = reactContext.getResources().getIdentifier(iconName, iconType, iconPackage);
|
58
|
+
extras.putInt("iconInt", iconInt);
|
59
|
+
if (iconInt == 0)
|
60
|
+
throw new IllegalArgumentException();
|
61
|
+
} catch (Exception e) {
|
62
|
+
throw new IllegalArgumentException("Task icon not found");
|
63
|
+
}
|
64
|
+
// Get color
|
65
|
+
try {
|
66
|
+
final String color = options.getString("color");
|
67
|
+
extras.putInt("color", Color.parseColor(color));
|
68
|
+
} catch (Exception e) {
|
69
|
+
extras.putInt("color", Color.parseColor("#ffffff"));
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
public Bundle getExtras() {
|
74
|
+
return extras;
|
75
|
+
}
|
76
|
+
|
77
|
+
public String getTaskTitle() {
|
78
|
+
return extras.getString("taskTitle", "");
|
79
|
+
}
|
80
|
+
|
81
|
+
public String getTaskDesc() {
|
82
|
+
return extras.getString("taskDesc", "");
|
83
|
+
}
|
84
|
+
|
85
|
+
@IdRes
|
86
|
+
public int getIconInt() {
|
87
|
+
return extras.getInt("iconInt");
|
88
|
+
}
|
89
|
+
|
90
|
+
@ColorInt
|
91
|
+
public int getColor() {
|
92
|
+
return extras.getInt("color");
|
93
|
+
}
|
94
|
+
|
95
|
+
@Nullable
|
96
|
+
public String getLinkingURI() {
|
97
|
+
return extras.getString("linkingURI");
|
98
|
+
}
|
99
|
+
|
100
|
+
@Nullable
|
101
|
+
public Bundle getProgressBar() {
|
102
|
+
return extras.getBundle("progressBar");
|
103
|
+
}
|
104
|
+
}
|
@@ -0,0 +1,120 @@
|
|
1
|
+
package com.asterinet.react.bgactions;
|
2
|
+
|
3
|
+
import android.annotation.SuppressLint;
|
4
|
+
import android.app.Notification;
|
5
|
+
import android.app.NotificationChannel;
|
6
|
+
import android.app.NotificationManager;
|
7
|
+
import android.app.PendingIntent;
|
8
|
+
import android.content.Context;
|
9
|
+
import android.content.Intent;
|
10
|
+
import android.content.pm.PackageManager;
|
11
|
+
import android.content.pm.ServiceInfo;
|
12
|
+
import android.net.Uri;
|
13
|
+
import android.os.Build;
|
14
|
+
import android.os.Bundle;
|
15
|
+
import android.Manifest;
|
16
|
+
|
17
|
+
import androidx.annotation.NonNull;
|
18
|
+
import androidx.annotation.Nullable;
|
19
|
+
import androidx.core.app.NotificationCompat;
|
20
|
+
import androidx.core.content.ContextCompat;
|
21
|
+
|
22
|
+
import com.facebook.react.HeadlessJsTaskService;
|
23
|
+
import com.facebook.react.bridge.Arguments;
|
24
|
+
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
25
|
+
|
26
|
+
final public class RNBackgroundActionsTask extends HeadlessJsTaskService {
|
27
|
+
|
28
|
+
public static final int SERVICE_NOTIFICATION_ID = 92901;
|
29
|
+
private static final String CHANNEL_ID = "RN_BACKGROUND_ACTIONS_CHANNEL";
|
30
|
+
|
31
|
+
@SuppressLint("UnspecifiedImmutableFlag")
|
32
|
+
@NonNull
|
33
|
+
public static Notification buildNotification(@NonNull Context context, @NonNull final BackgroundTaskOptions bgOptions) {
|
34
|
+
// Get info
|
35
|
+
final String taskTitle = bgOptions.getTaskTitle();
|
36
|
+
final String taskDesc = bgOptions.getTaskDesc();
|
37
|
+
final int iconInt = bgOptions.getIconInt();
|
38
|
+
final int color = bgOptions.getColor();
|
39
|
+
final String linkingURI = bgOptions.getLinkingURI();
|
40
|
+
Intent notificationIntent;
|
41
|
+
if (linkingURI != null) {
|
42
|
+
notificationIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(linkingURI));
|
43
|
+
} else {
|
44
|
+
//as RN works on single activity architecture - we don't need to find current activity on behalf of react context
|
45
|
+
notificationIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
|
46
|
+
}
|
47
|
+
final PendingIntent contentIntent;
|
48
|
+
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
49
|
+
contentIntent = PendingIntent.getActivity(context,0, notificationIntent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT);
|
50
|
+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
51
|
+
contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_MUTABLE);
|
52
|
+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
53
|
+
contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
54
|
+
} else {
|
55
|
+
contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
56
|
+
}
|
57
|
+
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
58
|
+
.setContentTitle(taskTitle)
|
59
|
+
.setContentText(taskDesc)
|
60
|
+
.setSmallIcon(iconInt)
|
61
|
+
.setContentIntent(contentIntent)
|
62
|
+
.setOngoing(true)
|
63
|
+
.setPriority(NotificationCompat.PRIORITY_MIN)
|
64
|
+
.setColor(color);
|
65
|
+
|
66
|
+
final Bundle progressBarBundle = bgOptions.getProgressBar();
|
67
|
+
if (progressBarBundle != null) {
|
68
|
+
final int progressMax = (int) Math.floor(progressBarBundle.getDouble("max"));
|
69
|
+
final int progressCurrent = (int) Math.floor(progressBarBundle.getDouble("value"));
|
70
|
+
final boolean progressIndeterminate = progressBarBundle.getBoolean("indeterminate");
|
71
|
+
builder.setProgress(progressMax, progressCurrent, progressIndeterminate);
|
72
|
+
}
|
73
|
+
return builder.build();
|
74
|
+
}
|
75
|
+
|
76
|
+
@Override
|
77
|
+
protected @Nullable
|
78
|
+
HeadlessJsTaskConfig getTaskConfig(Intent intent) {
|
79
|
+
final Bundle extras = intent.getExtras();
|
80
|
+
if (extras != null) {
|
81
|
+
return new HeadlessJsTaskConfig(extras.getString("taskName"), Arguments.fromBundle(extras), 0, true);
|
82
|
+
}
|
83
|
+
return null;
|
84
|
+
}
|
85
|
+
|
86
|
+
@Override
|
87
|
+
public int onStartCommand(Intent intent, int flags, int startId) {
|
88
|
+
final Bundle extras = intent.getExtras();
|
89
|
+
if (extras == null) {
|
90
|
+
throw new IllegalArgumentException("Extras cannot be null");
|
91
|
+
}
|
92
|
+
final BackgroundTaskOptions bgOptions = new BackgroundTaskOptions(extras);
|
93
|
+
createNotificationChannel(bgOptions.getTaskTitle(), bgOptions.getTaskDesc()); // Necessary creating channel for API 26+
|
94
|
+
// Create the notification
|
95
|
+
final Notification notification = buildNotification(this, bgOptions);
|
96
|
+
|
97
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
98
|
+
startForeground(SERVICE_NOTIFICATION_ID, notification);
|
99
|
+
} else {
|
100
|
+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
|
101
|
+
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
102
|
+
) {
|
103
|
+
startForeground(SERVICE_NOTIFICATION_ID, notification,
|
104
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
return super.onStartCommand(intent, flags, startId);
|
109
|
+
}
|
110
|
+
|
111
|
+
private void createNotificationChannel(@NonNull final String taskTitle, @NonNull final String taskDesc) {
|
112
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
113
|
+
final int importance = NotificationManager.IMPORTANCE_LOW;
|
114
|
+
final NotificationChannel channel = new NotificationChannel(CHANNEL_ID, taskTitle, importance);
|
115
|
+
channel.setDescription(taskDesc);
|
116
|
+
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
117
|
+
notificationManager.createNotificationChannel(channel);
|
118
|
+
}
|
119
|
+
}
|
120
|
+
}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
@import UIKit;
|
2
|
+
#import "RNBackgroundActions.h"
|
3
|
+
|
4
|
+
@implementation RNBackgroundActions {
|
5
|
+
UIBackgroundTaskIdentifier bgTask;
|
6
|
+
}
|
7
|
+
|
8
|
+
RCT_EXPORT_MODULE()
|
9
|
+
- (NSArray<NSString *> *)supportedEvents
|
10
|
+
{
|
11
|
+
return @[@"expiration"];
|
12
|
+
}
|
13
|
+
|
14
|
+
- (void) _start
|
15
|
+
{
|
16
|
+
[self _stop];
|
17
|
+
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"RNBackgroundActions" expirationHandler:^{
|
18
|
+
[self onExpiration];
|
19
|
+
[[UIApplication sharedApplication] endBackgroundTask: self->bgTask];
|
20
|
+
self->bgTask = UIBackgroundTaskInvalid;
|
21
|
+
}];
|
22
|
+
}
|
23
|
+
|
24
|
+
- (void) _stop
|
25
|
+
{
|
26
|
+
if (bgTask != UIBackgroundTaskInvalid) {
|
27
|
+
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
|
28
|
+
bgTask = UIBackgroundTaskInvalid;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
- (void)onExpiration
|
33
|
+
{
|
34
|
+
[self sendEventWithName:@"expiration"
|
35
|
+
body:@{}];
|
36
|
+
}
|
37
|
+
|
38
|
+
RCT_EXPORT_METHOD(start:(NSDictionary *)options
|
39
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
40
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
41
|
+
{
|
42
|
+
[self _start];
|
43
|
+
resolve(nil);
|
44
|
+
}
|
45
|
+
|
46
|
+
RCT_EXPORT_METHOD(stop:(RCTPromiseResolveBlock)resolve
|
47
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
48
|
+
{
|
49
|
+
[self _stop];
|
50
|
+
resolve(nil);
|
51
|
+
}
|
52
|
+
|
53
|
+
@end
|