-
Notifications
You must be signed in to change notification settings - Fork 5
Fix synchronization issue with android notification #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a15c885
8d3d074
710a9ca
a3ac03b
e3c135c
67b6944
0467b42
6f402f8
99c0f34
976621f
28382b4
ca7748e
c55c82c
1e6d803
d4bef0b
7063263
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.vydia.RNUploader | ||
|
|
||
| import android.content.Context | ||
| import android.net.ConnectivityManager | ||
| import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED | ||
| import android.net.NetworkCapabilities.TRANSPORT_WIFI | ||
|
|
||
| enum class Connectivity { | ||
| NoWifi, NoInternet, Ok; | ||
|
|
||
| companion object { | ||
| fun fetch(context: Context, wifiOnly: Boolean): Connectivity { | ||
| val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | ||
| val network = manager.activeNetwork | ||
| val capabilities = manager.getNetworkCapabilities(network) | ||
|
|
||
| val hasInternet = capabilities?.hasCapability(NET_CAPABILITY_VALIDATED) == true | ||
|
|
||
| // not wifiOnly, return early | ||
| if (!wifiOnly) return if (hasInternet) Ok else NoInternet | ||
|
|
||
| // handle wifiOnly | ||
| return if (hasInternet && capabilities?.hasTransport(TRANSPORT_WIFI) == true) | ||
| Ok | ||
| else | ||
| NoWifi // don't return NoInternet here, more direct to request to join wifi | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.vydia.RNUploader | ||
|
|
||
| class MissingOptionException(optionName: String) : | ||
| IllegalArgumentException("Missing '$optionName'") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package com.vydia.RNUploader | ||
|
|
||
| import android.app.Notification | ||
| import android.app.NotificationManager | ||
| import android.app.PendingIntent | ||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.os.Build | ||
| import android.widget.RemoteViews | ||
| import androidx.core.app.NotificationCompat | ||
| import com.facebook.react.bridge.ReadableMap | ||
|
|
||
|
|
||
| object UploadNotification { | ||
| var id: Int = 0 | ||
| private set | ||
| var title: String = "Uploading files" | ||
| private set | ||
| var titleNoInternet: String = "Waiting for internet connection" | ||
| private set | ||
| var titleNoWifi: String = "Waiting for WiFi connection" | ||
| private set | ||
| var channel: String = "File Uploads" | ||
| private set | ||
|
|
||
| private var activeUpload: Upload? = null | ||
|
|
||
| @Synchronized | ||
| fun setActiveUpload(upload: Upload?) { | ||
| this.activeUpload = upload | ||
| } | ||
|
|
||
| @Synchronized | ||
| fun getActiveUpload(): Upload? { | ||
| return this.activeUpload | ||
| } | ||
|
|
||
| @Synchronized | ||
| fun releaseActiveUpload(upload: Upload) { | ||
| if (this.activeUpload?.id == upload.id) this.activeUpload = null | ||
| } | ||
|
|
||
| fun setOptions(opts: ReadableMap) { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these options are moved over from the Upload class, so they are now global options instead of per-Upload options |
||
| id = opts.getString("notificationId")?.hashCode() | ||
| ?: throw MissingOptionException("notificationId") | ||
| title = opts.getString("notificationTitle") | ||
| ?: throw MissingOptionException("notificationTitle") | ||
| titleNoInternet = opts.getString("notificationTitleNoInternet") | ||
| ?: throw MissingOptionException("notificationTitleNoInternet") | ||
| titleNoWifi = opts.getString("notificationTitleNoWifi") | ||
| ?: throw MissingOptionException("notificationTitleNoWifi") | ||
| channel = opts.getString("notificationChannel") | ||
| ?: throw MissingOptionException("notificationChannel") | ||
| } | ||
|
|
||
| // builds the notification required to enable Foreground mode | ||
| fun build(context: Context): Notification { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moved from UploadWorker.ts. Not much has changed except for |
||
| // since all workers share the same notification ID, | ||
| // get the active upload so we don't overwrite the notification when multiple uploads are running | ||
| val wifiOnly = getActiveUpload()?.wifiOnly ?: false | ||
| val channel = channel | ||
| val progress = UploadProgress.total() | ||
| val progress2Decimals = "%.2f".format(progress) | ||
| val title = when (Connectivity.fetch(context, wifiOnly)) { | ||
| Connectivity.NoWifi -> titleNoWifi | ||
| Connectivity.NoInternet -> titleNoInternet | ||
| Connectivity.Ok -> title | ||
| } | ||
|
|
||
| // Custom layout for progress notification. | ||
| // The default hides the % text. This one shows it on the right, | ||
| // like most examples in various docs. | ||
| val content = RemoteViews(context.packageName, R.layout.notification) | ||
| content.setTextViewText(R.id.notification_title, title) | ||
| content.setTextViewText(R.id.notification_progress, "${progress2Decimals}%") | ||
| content.setProgressBar(R.id.notification_progress_bar, 100, progress.toInt(), false) | ||
|
|
||
| return NotificationCompat.Builder(context, channel).run { | ||
| // Starting Android 12, the notification shows up with a confusing delay of 10s. | ||
| // This fixes that delay. | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) | ||
| foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE | ||
|
|
||
| // Required by android. Here we use the system's default upload icon | ||
| setSmallIcon(android.R.drawable.stat_sys_upload) | ||
| // These prevent the notification from being force-dismissed or dismissed when pressed | ||
| setOngoing(true) | ||
| setAutoCancel(false) | ||
| // These help show the same custom content when the notification collapses and expands | ||
| setCustomContentView(content) | ||
| setCustomBigContentView(content) | ||
| // opens the app when the notification is pressed | ||
| setContentIntent(openAppIntent(context)) | ||
| build() | ||
| } | ||
| } | ||
|
|
||
| fun update(context: Context) { | ||
| val notification = build(context) | ||
| val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||
| manager.notify(id, notification) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| private fun openAppIntent(context: Context): PendingIntent? { | ||
| val intent = Intent(context, NotificationReceiver::class.java) | ||
| val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE | ||
| return PendingIntent.getBroadcast(context, "RNFileUpload-notification".hashCode(), intent, flags) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,8 @@ | ||
| package com.vydia.RNUploader | ||
|
|
||
| import android.app.Notification | ||
| import android.app.NotificationManager | ||
| import android.app.PendingIntent | ||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.content.pm.ServiceInfo | ||
| import android.net.ConnectivityManager | ||
| import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED | ||
| import android.net.NetworkCapabilities.TRANSPORT_WIFI | ||
| import android.os.Build | ||
| import android.widget.RemoteViews | ||
| import androidx.core.app.NotificationCompat | ||
| import androidx.work.CoroutineWorker | ||
| import androidx.work.ForegroundInfo | ||
| import androidx.work.WorkerParameters | ||
|
|
@@ -48,18 +39,13 @@ private val client = OkHttpClient.Builder() | |
| .callTimeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT) | ||
| .build() | ||
|
|
||
| private enum class Connectivity { NoWifi, NoInternet, Ok } | ||
|
|
||
| class UploadWorker(private val context: Context, params: WorkerParameters) : | ||
| CoroutineWorker(context, params) { | ||
|
|
||
| enum class Input { Params } | ||
|
|
||
| private lateinit var upload: Upload | ||
| private var retries = 0 | ||
| private var connectivity = Connectivity.Ok | ||
| private val notificationManager = | ||
| context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||
|
|
||
| override suspend fun doWork(): Result = withContext(Dispatchers.IO) { | ||
| // Retrieve the upload. If this throws errors, error reporting won't work. | ||
|
|
@@ -125,11 +111,15 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : | |
| UploadProgress.add(upload.id, size) | ||
|
|
||
| // Don't bother to run on an invalid network | ||
| if (!validateAndReportConnectivity()) return null | ||
| if (Connectivity.fetch(context, upload.wifiOnly) != Connectivity.Ok) return null | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notification not updated when connectivity check failsMedium Severity When the connectivity check fails in |
||
|
|
||
| // wait for its turn to run | ||
| semaphore.acquire() | ||
|
|
||
| // mark as active upload for notification | ||
| UploadNotification.setActiveUpload(upload) | ||
| UploadNotification.update(context) | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Semaphore leak if notification update throws exceptionHigh Severity If |
||
| try { | ||
| return okhttpUpload(client, upload, file) { progress -> | ||
| handleProgress(progress, size) | ||
|
|
@@ -141,23 +131,28 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : | |
| throw error | ||
| } finally { | ||
| semaphore.release() | ||
| // by this point, another worker might have started and set itself as active upload | ||
| // so only clear if it's still this upload | ||
| UploadNotification.releaseActiveUpload(upload) | ||
| } | ||
| } | ||
|
|
||
| private fun handleProgress(bytesSentTotal: Long, fileSize: Long) { | ||
| UploadProgress.set(upload.id, bytesSentTotal) | ||
| EventReporter.progress(upload.id, bytesSentTotal, fileSize) | ||
| notificationManager.notify(upload.notificationId, buildNotification()) | ||
| UploadNotification.update(context) | ||
| } | ||
|
|
||
| private fun handleSuccess(response: UploadResponse) { | ||
| UploadProgress.complete(upload.id) | ||
| EventReporter.success(upload.id, response) | ||
| UploadNotification.update(context) | ||
| } | ||
|
|
||
| private fun handleError(error: Throwable) { | ||
| UploadProgress.remove(upload.id) | ||
| EventReporter.error(upload.id, error) | ||
| UploadNotification.update(context) | ||
| } | ||
|
|
||
| // Check if cancelled by user or new worker with same ID | ||
|
|
@@ -167,6 +162,7 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : | |
|
|
||
| UploadProgress.remove(upload.id) | ||
| EventReporter.cancelled(upload.id) | ||
| UploadNotification.update(context) | ||
| return true | ||
| } | ||
|
|
||
|
|
@@ -176,7 +172,7 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : | |
|
|
||
| // Error was thrown due to unmet network preferences. | ||
| // Also happens every time you switch from one network to any other | ||
| if (!validateAndReportConnectivity()) unlimitedRetry = true | ||
| if (Connectivity.fetch(context, upload.wifiOnly) != Connectivity.Ok) unlimitedRetry = true | ||
| // Due to the flaky nature of networking, sometimes the network is | ||
| // valid but the URL is still inaccessible, so keep waiting until | ||
| // the URL is accessible | ||
|
|
@@ -200,56 +196,10 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : | |
| return retries <= upload.maxRetries | ||
| } | ||
|
|
||
| // Checks connection and alerts connection issues | ||
| private fun validateAndReportConnectivity(): Boolean { | ||
| this.connectivity = validateConnectivity(context, upload.wifiOnly) | ||
| // alert connectivity mode | ||
| notificationManager.notify(upload.notificationId, buildNotification()) | ||
| return this.connectivity == Connectivity.Ok | ||
| } | ||
|
|
||
| // builds the notification required to enable Foreground mode | ||
| fun buildNotification(): Notification { | ||
| val channel = upload.notificationChannel | ||
| val progress = UploadProgress.total() | ||
| val progress2Decimals = "%.2f".format(progress) | ||
| val title = when (connectivity) { | ||
| Connectivity.NoWifi -> upload.notificationTitleNoWifi | ||
| Connectivity.NoInternet -> upload.notificationTitleNoInternet | ||
| Connectivity.Ok -> upload.notificationTitle | ||
| } | ||
|
|
||
| // Custom layout for progress notification. | ||
| // The default hides the % text. This one shows it on the right, | ||
| // like most examples in various docs. | ||
| val content = RemoteViews(context.packageName, R.layout.notification) | ||
| content.setTextViewText(R.id.notification_title, title) | ||
| content.setTextViewText(R.id.notification_progress, "${progress2Decimals}%") | ||
| content.setProgressBar(R.id.notification_progress_bar, 100, progress.toInt(), false) | ||
|
|
||
| return NotificationCompat.Builder(context, channel).run { | ||
| // Starting Android 12, the notification shows up with a confusing delay of 10s. | ||
| // This fixes that delay. | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) | ||
| foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE | ||
|
|
||
| // Required by android. Here we use the system's default upload icon | ||
| setSmallIcon(android.R.drawable.stat_sys_upload) | ||
| // These prevent the notification from being force-dismissed or dismissed when pressed | ||
| setOngoing(true) | ||
| setAutoCancel(false) | ||
| // These help show the same custom content when the notification collapses and expands | ||
| setCustomContentView(content) | ||
| setCustomBigContentView(content) | ||
| // opens the app when the notification is pressed | ||
| setContentIntent(openAppIntent(context)) | ||
| build() | ||
| } | ||
| } | ||
|
|
||
| override suspend fun getForegroundInfo(): ForegroundInfo { | ||
| val notification = buildNotification() | ||
| val id = upload.notificationId | ||
| val notification = UploadNotification.build(context) | ||
| val id = UploadNotification.id | ||
| // Starting Android 14, FOREGROUND_SERVICE_TYPE_DATA_SYNC is mandatory, otherwise app will crash | ||
| return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) | ||
| ForegroundInfo(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) | ||
|
|
@@ -258,28 +208,4 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : | |
| } | ||
| } | ||
|
|
||
| // This is outside and synchronized to ensure consistent status across workers | ||
| @Synchronized | ||
| private fun validateConnectivity(context: Context, wifiOnly: Boolean): Connectivity { | ||
| val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | ||
| val network = manager.activeNetwork | ||
| val capabilities = manager.getNetworkCapabilities(network) | ||
|
|
||
| val hasInternet = capabilities?.hasCapability(NET_CAPABILITY_VALIDATED) == true | ||
|
|
||
| // not wifiOnly, return early | ||
| if (!wifiOnly) return if (hasInternet) Connectivity.Ok else Connectivity.NoInternet | ||
|
|
||
| // handle wifiOnly | ||
| return if (hasInternet && capabilities?.hasTransport(TRANSPORT_WIFI) == true) | ||
| Connectivity.Ok | ||
| else | ||
| Connectivity.NoWifi // don't return NoInternet here, more direct to request to join wifi | ||
| } | ||
|
|
||
|
|
||
| private fun openAppIntent(context: Context): PendingIntent? { | ||
| val intent = Intent(context, NotificationReceiver::class.java) | ||
| val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE | ||
| return PendingIntent.getBroadcast(context, "RNFileUpload-notification".hashCode(), intent, flags) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved from
UploadWorker.tsno logical change