@ammarahmed/react-native-upload 6.32.0 → 7.0.0-alpha.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/.eslintignore +1 -2
- package/README.md +166 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/appfolio/uploader/MultipartCompletionService.kt +33 -0
- package/android/src/main/java/com/appfolio/uploader/MultipartUploadService.kt +200 -0
- package/android/src/main/java/com/appfolio/uploader/MultipartUploader.kt +787 -0
- package/android/src/main/java/com/appfolio/uploader/UploaderModule.kt +76 -2
- package/dist/index.d.ts +161 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +318 -9
- package/ios/RNFileUploader.m +80 -24
- package/ios/RNMultipartUploader.h +28 -0
- package/ios/RNMultipartUploader.m +748 -0
- package/package.json +3 -3
- package/src/index.ts +404 -2
package/.eslintignore
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
example/
|
|
2
|
-
example/RNBackgroundExample/e2e
|
|
1
|
+
example/UploadExample/node_modules
|
package/README.md
CHANGED
|
@@ -140,6 +140,171 @@ const options = {
|
|
|
140
140
|
|
|
141
141
|
Note the `field` property is required for multipart uploads.
|
|
142
142
|
|
|
143
|
+
## S3 / Cloud Multipart Upload (Chunked)
|
|
144
|
+
|
|
145
|
+
For large files (e.g. > 25MB) that need to be uploaded in chunked parts to AWS S3, Google Cloud Storage, Cloudflare R2, or MinIO using presigned URLs:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import Upload from "@ammarahmed/react-native-upload";
|
|
149
|
+
|
|
150
|
+
const upload = Upload.createMultipartS3({
|
|
151
|
+
uploadId: "unique-upload-id-123",
|
|
152
|
+
path: "/path/to/large-video.mp4",
|
|
153
|
+
parts: [
|
|
154
|
+
"https://s3.amazonaws.com/bucket/file?partNumber=1&uploadId=...",
|
|
155
|
+
"https://s3.amazonaws.com/bucket/file?partNumber=2&uploadId=...",
|
|
156
|
+
// ... presigned URLs for each part
|
|
157
|
+
],
|
|
158
|
+
maxParallel: 3, // Optional: maximum parallel chunk uploads (default: 3)
|
|
159
|
+
notification: {
|
|
160
|
+
enabled: true,
|
|
161
|
+
filename: "large-video.mp4", // Optional: human-readable name in notification
|
|
162
|
+
},
|
|
163
|
+
appGroup: "group.com.myapp", // Optional: iOS App Group for Share Extensions
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Track progress
|
|
167
|
+
upload.onProgress((event) => {
|
|
168
|
+
console.log(`Uploaded ${event.bytesUploaded}/${event.totalBytes} bytes (${(event.progress * 100).toFixed(1)}%)`);
|
|
169
|
+
console.log(`Current chunk index: ${event.chunkIndex}`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const result = await upload.start();
|
|
174
|
+
if (result.success) {
|
|
175
|
+
// Array of ETags matching the order of the parts (for S3 CompleteMultipartUpload)
|
|
176
|
+
console.log("Upload completed with ETags:", result.etags);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error.code === "CANCELLED") {
|
|
180
|
+
console.log("Upload was cancelled");
|
|
181
|
+
} else {
|
|
182
|
+
console.error("Upload failed:", error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// To cancel mid-upload:
|
|
187
|
+
// await upload.cancel();
|
|
188
|
+
// or statically:
|
|
189
|
+
// await Upload.cancelMultipartS3("unique-upload-id-123");
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Resumption on Network Failure
|
|
193
|
+
|
|
194
|
+
If an upload is interrupted or network drops, you can resume by passing `existingEtags`. `onProgress` emits an `etags` array matching the parts list (with empty strings for pending parts):
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
const upload = Upload.createMultipartS3({
|
|
198
|
+
uploadId: savedUploadId, // Re-use the existing S3 upload ID
|
|
199
|
+
path: "/path/to/large-video.mp4",
|
|
200
|
+
parts: presignedUrls,
|
|
201
|
+
existingEtags: savedEtags, // Fixed-length array with ETags for completed parts, "" for pending parts
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Disabling resumability
|
|
206
|
+
|
|
207
|
+
Uploads are resumable by default: their state is persisted so that they survive the app process being killed. Pass `resumable: false` to an individual upload to turn that off:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
Upload.createMultipartS3({ uploadId, path, parts, resumable: false });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A non-resumable upload still keeps running while the app is in the background, but nothing is written to disk. If the app process dies and the app is launched again, the upload is not resumed and does not show up in `getMultipartS3Uploads()`.
|
|
214
|
+
|
|
215
|
+
- Android: the foreground service is not restarted for it and no headless completion runs after a process death.
|
|
216
|
+
- iOS: iOS may keep sending parts that were already queued while the app is dead. On the next launch the library cancels them and deletes their chunk files. The multipart upload is never completed, so abort it on your server if needed.
|
|
217
|
+
|
|
218
|
+
### Finishing the upload in the background
|
|
219
|
+
|
|
220
|
+
An S3 multipart upload is only complete once you send `CompleteMultipartUpload` with the part ETags. To make that happen even when the app is suspended, or was killed and restarted for the upload, register a completion handler. Do it at the top level of your entry file (`index.js`), outside of any component, so it exists when the app is started in the background:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import Upload from "@ammarahmed/react-native-upload";
|
|
224
|
+
|
|
225
|
+
Upload.setMultipartCompletionHandler(async ({ uploadId, status, etags, error }) => {
|
|
226
|
+
if (status !== "completed") return;
|
|
227
|
+
// Load whatever you saved when you started the upload (bucket, key, ...)
|
|
228
|
+
await completeMultipartUpload(uploadId, etags);
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- The handler is called when every part has been uploaded (or the upload failed). When it resolves, the upload is acknowledged and its stored state is removed. If it throws, the upload is not acknowledged and stays pending.
|
|
233
|
+
- It is called while the app is running and, on Android, from a headless JS task when the app process was not running. Save what you need to finish the upload (for example the S3 key) to storage before starting it; the headless task starts your JS bundle from scratch.
|
|
234
|
+
- If you use the handler, don't also finish the upload from the `start()` promise.
|
|
235
|
+
- Fallback: completed uploads are persisted natively. If the handler could not run, check on every app launch and finish them without uploading again:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const pending = await Upload.getMultipartS3Uploads();
|
|
239
|
+
for (const upload of pending) {
|
|
240
|
+
if (upload.status === "completed") {
|
|
241
|
+
await completeMultipartUpload(upload.uploadId, upload.etags);
|
|
242
|
+
await Upload.acknowledgeMultipartS3(upload.uploadId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`status` is `running`, `completed` or `failed`. A failed upload keeps its `etags`, so you can start it again with `existingEtags`.
|
|
248
|
+
|
|
249
|
+
### iOS Background Behavior
|
|
250
|
+
|
|
251
|
+
All multipart uploads share one background `NSURLSession`. Every part is queued when the upload starts, so iOS keeps uploading while the app is suspended and wakes the app when parts finish. Upload state is persisted and reattached when the app is relaunched.
|
|
252
|
+
|
|
253
|
+
Forward the system's background session event to the library in your `AppDelegate`:
|
|
254
|
+
|
|
255
|
+
```swift
|
|
256
|
+
func application(
|
|
257
|
+
_ application: UIApplication,
|
|
258
|
+
handleEventsForBackgroundURLSession identifier: String,
|
|
259
|
+
completionHandler: @escaping () -> Void
|
|
260
|
+
) {
|
|
261
|
+
RNFileUploader.setCompletionHandlerWithIdentifier(identifier, completionHandler: completionHandler)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
(With Swift, import the library header in your bridging header: `#import <react-native-upload/RNFileUploader.h>`.)
|
|
266
|
+
|
|
267
|
+
Notes:
|
|
268
|
+
- The system completion handler is released when your JS calls `canSuspendIfBackground()`, which the library calls after the handler runs. Keep the handler short, iOS gives the app limited time when it wakes it.
|
|
269
|
+
- Chunk files are written to a copy in Application Support (or the App Group container when `appGroup` is set) until the upload ends, so you need free space for a second copy of the file.
|
|
270
|
+
- If the user force-quits the app, iOS cancels the background transfers. Resume with `existingEtags`.
|
|
271
|
+
|
|
272
|
+
### Android Foreground Service and Headless Task
|
|
273
|
+
|
|
274
|
+
Background support on Android is opt-in. The library does not add any permissions or services to your app. Add the following to your app's `AndroidManifest.xml`:
|
|
275
|
+
|
|
276
|
+
```xml
|
|
277
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
278
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
279
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
280
|
+
|
|
281
|
+
<application>
|
|
282
|
+
<service
|
|
283
|
+
android:name="com.appfolio.uploader.MultipartUploadService"
|
|
284
|
+
android:exported="false"
|
|
285
|
+
android:foregroundServiceType="dataSync" />
|
|
286
|
+
<service
|
|
287
|
+
android:name="com.appfolio.uploader.MultipartCompletionService"
|
|
288
|
+
android:exported="false" />
|
|
289
|
+
</application>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
How it works:
|
|
293
|
+
- Uploads run in a native engine that is independent of your Activity and of the React instance. State (part URLs, ETags, status) is saved to disk after every part.
|
|
294
|
+
- With `notification.enabled` the engine runs a foreground service, which also keeps your JS timers running while the app is in the background.
|
|
295
|
+
- If the system restarts the service after killing the app's process, the engine resumes the remaining parts on its own.
|
|
296
|
+
- When an upload finishes and no React instance is running, `MultipartCompletionService` starts a headless JS task that runs your completion handler.
|
|
297
|
+
- After an upload ends, the service stays alive until the handler resolves (or `Upload.acknowledgeMultipartS3(uploadId)` is called), or for 30 seconds, or 2 minutes if the headless task was started.
|
|
298
|
+
|
|
299
|
+
Notes:
|
|
300
|
+
- Google Play requires a declaration for `FOREGROUND_SERVICE_DATA_SYNC`.
|
|
301
|
+
- `POST_NOTIFICATIONS` must be requested at runtime on Android 13+ for the notification to appear.
|
|
302
|
+
- The foreground service is only started while your app is in the foreground. Without it, the process can still be killed by the system, and uploads are then resumed the next time the app starts.
|
|
303
|
+
- A force stop or an OEM that kills the app when it is removed from recents does not restart the service. The persisted uploads resume the next time the app is opened, and completed ones can be finished with `getMultipartS3Uploads()` as shown above.
|
|
304
|
+
- On Android 15+ `dataSync` services have a time limit; the service stops itself when the system times it out.
|
|
305
|
+
- The library compiles against `compileSdkVersion` 35 or higher.
|
|
306
|
+
- Notification texts can be customized with `onProgressTitle`, `onStartMessage`, `onResumeMessage`, `onProgressMessage` (use `{progress}` for the percentage) and `notificationChannelName`.
|
|
307
|
+
|
|
143
308
|
# API
|
|
144
309
|
|
|
145
310
|
## Top Level Functions
|
|
@@ -326,7 +491,7 @@ Is there an example/sandbox app to test out this package?
|
|
|
326
491
|
|
|
327
492
|
> Yes, there is a simple react native app that comes with an [express](https://github.com/expressjs/express) server where you can see react-native-background-upload in action and try things out in an isolated local environment.
|
|
328
493
|
|
|
329
|
-
[
|
|
494
|
+
[UploadExample](example/UploadExample)
|
|
330
495
|
|
|
331
496
|
Does it support iOS camera roll assets?
|
|
332
497
|
|
package/android/build.gradle
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.appfolio.uploader
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import com.facebook.react.HeadlessJsTaskService
|
|
7
|
+
import com.facebook.react.jstasks.HeadlessJsTaskConfig
|
|
8
|
+
|
|
9
|
+
class MultipartCompletionService : HeadlessJsTaskService() {
|
|
10
|
+
companion object {
|
|
11
|
+
private const val TAG = "MultipartCompletion"
|
|
12
|
+
private const val TASK_KEY = "RNMultipartUploadCompletion"
|
|
13
|
+
private const val TASK_TIMEOUT_MS = 120_000L
|
|
14
|
+
private const val EXTRA_UPLOAD_ID = "extra_upload_id"
|
|
15
|
+
|
|
16
|
+
fun start(context: Context, uploadId: String) {
|
|
17
|
+
try {
|
|
18
|
+
val intent = Intent(context, MultipartCompletionService::class.java).apply {
|
|
19
|
+
putExtra(EXTRA_UPLOAD_ID, uploadId)
|
|
20
|
+
}
|
|
21
|
+
context.startService(intent)
|
|
22
|
+
} catch (e: Exception) {
|
|
23
|
+
Log.w(TAG, "Could not start MultipartCompletionService: ${e.message}")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
|
|
29
|
+
val uploadId = intent?.getStringExtra(EXTRA_UPLOAD_ID) ?: return null
|
|
30
|
+
val payload = MultipartUploader.getInstance(this).completionPayload(uploadId) ?: return null
|
|
31
|
+
return HeadlessJsTaskConfig(TASK_KEY, payload, TASK_TIMEOUT_MS, true)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
package com.appfolio.uploader
|
|
2
|
+
|
|
3
|
+
import android.app.ActivityManager
|
|
4
|
+
import android.app.Notification
|
|
5
|
+
import android.app.NotificationChannel
|
|
6
|
+
import android.app.NotificationManager
|
|
7
|
+
import android.app.Service
|
|
8
|
+
import android.content.Context
|
|
9
|
+
import android.content.Intent
|
|
10
|
+
import android.content.pm.ServiceInfo
|
|
11
|
+
import android.os.Binder
|
|
12
|
+
import android.os.Build
|
|
13
|
+
import android.os.IBinder
|
|
14
|
+
import android.util.Log
|
|
15
|
+
import androidx.core.app.NotificationCompat
|
|
16
|
+
|
|
17
|
+
class MultipartUploadService : Service() {
|
|
18
|
+
companion object {
|
|
19
|
+
private const val TAG = "MultipartUploadService"
|
|
20
|
+
private const val CHANNEL_ID = "BackgroundUploadChannel"
|
|
21
|
+
private const val NOTIFICATION_ID = 1002
|
|
22
|
+
|
|
23
|
+
@Volatile
|
|
24
|
+
var instance: MultipartUploadService? = null
|
|
25
|
+
|
|
26
|
+
const val ACTION_START = "com.appfolio.uploader.START_FOREGROUND_UPLOAD"
|
|
27
|
+
const val ACTION_STOP = "com.appfolio.uploader.STOP_FOREGROUND_UPLOAD"
|
|
28
|
+
|
|
29
|
+
const val EXTRA_TITLE = "extra_title"
|
|
30
|
+
const val EXTRA_MESSAGE = "extra_message"
|
|
31
|
+
|
|
32
|
+
private fun isAppInForeground(context: Context): Boolean {
|
|
33
|
+
val state = ActivityManager.RunningAppProcessInfo()
|
|
34
|
+
ActivityManager.getMyMemoryState(state)
|
|
35
|
+
return state.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun ensureChannel(context: Context, channelName: String) {
|
|
39
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
40
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
41
|
+
val channel = NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW)
|
|
42
|
+
manager?.createNotificationChannel(channel)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun start(context: Context, title: String, message: String, channelName: String) {
|
|
47
|
+
if (!isAppInForeground(context)) {
|
|
48
|
+
Log.w(TAG, "App is not in the foreground, uploading without a foreground service")
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
ensureChannel(context, channelName)
|
|
53
|
+
val intent = Intent(context, MultipartUploadService::class.java).apply {
|
|
54
|
+
action = ACTION_START
|
|
55
|
+
putExtra(EXTRA_TITLE, title)
|
|
56
|
+
putExtra(EXTRA_MESSAGE, message)
|
|
57
|
+
}
|
|
58
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
59
|
+
context.startForegroundService(intent)
|
|
60
|
+
} else {
|
|
61
|
+
context.startService(intent)
|
|
62
|
+
}
|
|
63
|
+
} catch (e: Exception) {
|
|
64
|
+
// If Android OS blocks startForegroundService (e.g. ForegroundServiceStartNotAllowedException),
|
|
65
|
+
// catch it so the app process does not crash. The coroutine will still run in process.
|
|
66
|
+
Log.w(TAG, "Could not start MultipartUploadService: ${e.message}")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun updateProgress(context: Context, title: String, progress: Int, message: String) {
|
|
71
|
+
if (instance == null) return
|
|
72
|
+
try {
|
|
73
|
+
val notification = buildNotification(context, title, progress, message, ongoing = true)
|
|
74
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
75
|
+
manager?.notify(NOTIFICATION_ID, notification)
|
|
76
|
+
} catch (e: Exception) {
|
|
77
|
+
Log.w(TAG, "Could not update progress: ${e.message}")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun stop(context: Context) {
|
|
82
|
+
try {
|
|
83
|
+
instance?.stopForegroundAndSelf()
|
|
84
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
85
|
+
notificationManager?.cancel(NOTIFICATION_ID)
|
|
86
|
+
} catch (e: Exception) {
|
|
87
|
+
Log.w(TAG, "Could not stop MultipartUploadService: ${e.message}")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private fun buildNotification(context: Context, title: String, progress: Int, message: String, ongoing: Boolean): Notification {
|
|
92
|
+
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
93
|
+
.setContentTitle(title)
|
|
94
|
+
.setContentText(message)
|
|
95
|
+
.setSmallIcon(android.R.drawable.stat_sys_upload)
|
|
96
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
97
|
+
.setOngoing(ongoing)
|
|
98
|
+
.setAutoCancel(!ongoing)
|
|
99
|
+
|
|
100
|
+
if (progress in 0..100) {
|
|
101
|
+
builder.setProgress(100, progress, false)
|
|
102
|
+
} else {
|
|
103
|
+
builder.setProgress(0, 0, true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return builder.build()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private val binder = LocalBinder()
|
|
111
|
+
private var isStarted = false
|
|
112
|
+
private var notificationManager: NotificationManager? = null
|
|
113
|
+
|
|
114
|
+
inner class LocalBinder : Binder() {
|
|
115
|
+
fun getService(): MultipartUploadService = this@MultipartUploadService
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override fun onCreate() {
|
|
119
|
+
super.onCreate()
|
|
120
|
+
instance = this
|
|
121
|
+
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override fun onBind(intent: Intent?): IBinder = binder
|
|
125
|
+
|
|
126
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
127
|
+
if (intent == null) {
|
|
128
|
+
val resumed = MultipartUploader.getInstance(this).resumeInterrupted().firstOrNull { it.notificationEnabled }
|
|
129
|
+
if (resumed == null) {
|
|
130
|
+
stopSelf()
|
|
131
|
+
return START_NOT_STICKY
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
ensureChannel(this, resumed.channelName)
|
|
135
|
+
startForegroundWithNotification(resumed.title, 0, resumed.resumeMessage)
|
|
136
|
+
} catch (e: Exception) {
|
|
137
|
+
Log.e(TAG, "Failed startForeground on restart: ${e.message}")
|
|
138
|
+
stopSelf()
|
|
139
|
+
return START_NOT_STICKY
|
|
140
|
+
}
|
|
141
|
+
return START_STICKY
|
|
142
|
+
}
|
|
143
|
+
when (intent.action) {
|
|
144
|
+
ACTION_START -> {
|
|
145
|
+
val title = intent.getStringExtra(EXTRA_TITLE) ?: ""
|
|
146
|
+
val message = intent.getStringExtra(EXTRA_MESSAGE) ?: ""
|
|
147
|
+
try {
|
|
148
|
+
startForegroundWithNotification(title, 0, message)
|
|
149
|
+
} catch (e: Exception) {
|
|
150
|
+
Log.e(TAG, "Failed startForeground in onStartCommand: ${e.message}")
|
|
151
|
+
// Stop service if foreground transition was denied to prevent ANR / crash
|
|
152
|
+
stopSelf()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
ACTION_STOP -> {
|
|
156
|
+
stopForegroundAndSelf()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return if (intent.action == ACTION_START) START_STICKY else START_NOT_STICKY
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private fun startForegroundWithNotification(title: String, progress: Int, message: String) {
|
|
163
|
+
val notification = buildNotification(this, title, progress, message, ongoing = true)
|
|
164
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
165
|
+
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
|
166
|
+
} else {
|
|
167
|
+
startForeground(NOTIFICATION_ID, notification)
|
|
168
|
+
}
|
|
169
|
+
isStarted = true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fun stopForegroundAndSelf() {
|
|
173
|
+
isStarted = false
|
|
174
|
+
try {
|
|
175
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
176
|
+
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
177
|
+
} else {
|
|
178
|
+
@Suppress("DEPRECATION")
|
|
179
|
+
stopForeground(true)
|
|
180
|
+
}
|
|
181
|
+
notificationManager?.cancel(NOTIFICATION_ID)
|
|
182
|
+
} catch (e: Exception) {
|
|
183
|
+
Log.w(TAG, "Error in stopForeground: ${e.message}")
|
|
184
|
+
} finally {
|
|
185
|
+
stopSelf()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
override fun onTimeout(startId: Int, fgsType: Int) {
|
|
190
|
+
stopForegroundAndSelf()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override fun onDestroy() {
|
|
194
|
+
if (instance === this) {
|
|
195
|
+
instance = null
|
|
196
|
+
}
|
|
197
|
+
stopForegroundAndSelf()
|
|
198
|
+
super.onDestroy()
|
|
199
|
+
}
|
|
200
|
+
}
|