-
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
b94890f
874935a
488717f
c2c9922
7bc5b7c
e2cd58e
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,187 @@ | ||
| 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 androidx.core.content.edit | ||
| import com.facebook.react.bridge.ReadableMap | ||
|
|
||
|
|
||
| object UploadNotification { | ||
| private var activeUpload: Upload? = null | ||
|
|
||
| @Synchronized | ||
| fun setOptions(opts: ReadableMap, context: Context) { | ||
| NotificationOptions.set(opts, context) | ||
| } | ||
|
|
||
| @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 | ||
| } | ||
|
|
||
| // builds the notification required to enable Foreground mode | ||
| @Synchronized | ||
| fun build(context: Context): Pair<Int, Notification> { | ||
| val opts = NotificationOptions.get(context) | ||
|
|
||
| // Determine wifiOnly preference for connectivity check: | ||
| // - If an upload is actively running, use its preference | ||
| // - Otherwise, check the queue: if ANY upload can proceed with just mobile data (wifiOnly=false), | ||
| // we only need internet to make progress. If ALL uploads need WiFi, we need WiFi. | ||
| // This ensures the notification ("Waiting for internet" vs "Waiting for WiFi") reflects | ||
| // the minimum connectivity required to make progress. | ||
| val wifiOnly = getActiveUpload()?.wifiOnly ?: !UploadProgress.hasNonWifiOnlyUploads() | ||
| val progress = UploadProgress.total() | ||
| val progress2Decimals = "%.2f".format(progress) | ||
| val title = when (Connectivity.fetch(context, wifiOnly)) { | ||
| Connectivity.NoWifi -> opts.titleNoWifi | ||
| Connectivity.NoInternet -> opts.titleNoInternet | ||
| Connectivity.Ok -> opts.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) | ||
|
|
||
| val notification = NotificationCompat.Builder(context, opts.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() | ||
| } | ||
|
|
||
| return Pair(opts.id, notification) | ||
| } | ||
|
|
||
| @Synchronized | ||
| fun update(context: Context) { | ||
| val (id, 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) | ||
| } | ||
|
|
||
|
|
||
| private data class Options( | ||
| val id: Int, | ||
| val title: String, | ||
| val titleNoInternet: String, | ||
| val titleNoWifi: String, | ||
| val channel: String | ||
| ) | ||
|
|
||
| /** | ||
| * Manages notification options with persistence to SharedPreferences. | ||
| * Options are loaded lazily: from memory if set, otherwise from SharedPreferences, | ||
| * otherwise falls back to defaults. | ||
| */ | ||
| private object NotificationOptions { | ||
| private const val PREFS_NAME = "UploadNotificationPrefs" | ||
| private const val PREF_ID = "id" | ||
| private const val PREF_TITLE = "title" | ||
| private const val PREF_TITLE_NO_INTERNET = "titleNoInternet" | ||
| private const val PREF_TITLE_NO_WIFI = "titleNoWifi" | ||
| private const val PREF_CHANNEL = "channel" | ||
|
|
||
| private var cached: Options? = null | ||
|
|
||
| @Synchronized | ||
| fun get(context: Context): Options { | ||
| cached?.let { return it } | ||
| loadFromPrefs(context)?.let { cached = it; return it } | ||
| return Options( | ||
| id = "default-upload-notification".hashCode(), | ||
| title = "Uploading files", | ||
| titleNoInternet = "Waiting for internet connection", | ||
| titleNoWifi = "Waiting for WiFi connection", | ||
| channel = "File Uploads" | ||
| ) | ||
| } | ||
|
|
||
| @Synchronized | ||
| fun set(opts: ReadableMap, context: Context) { | ||
| val options = 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") | ||
| ) | ||
| cached = options | ||
| // In rare cases, the Worker might be killed and restarted by the operating system, | ||
| // so we need to persist the options | ||
| saveToPrefs(context, options) | ||
| } | ||
|
|
||
| private fun loadFromPrefs(context: Context): Options? { | ||
| val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) | ||
| val id = prefs.getInt(PREF_ID, 0) | ||
| if (id == 0) return null | ||
|
|
||
| return Options( | ||
| id = id, | ||
| title = prefs.getString(PREF_TITLE, "Uploading files") ?: "Uploading files", | ||
| titleNoInternet = prefs.getString(PREF_TITLE_NO_INTERNET, "Waiting for internet connection") | ||
| ?: "Waiting for internet connection", | ||
| titleNoWifi = prefs.getString(PREF_TITLE_NO_WIFI, "Waiting for WiFi connection") | ||
| ?: "Waiting for WiFi connection", | ||
| channel = prefs.getString(PREF_CHANNEL, "File Uploads") ?: "File Uploads" | ||
| ) | ||
| } | ||
|
|
||
| private fun saveToPrefs(context: Context, options: Options) { | ||
| context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) | ||
| .edit { | ||
| putInt(PREF_ID, options.id) | ||
| .putString(PREF_TITLE, options.title) | ||
| .putString(PREF_TITLE_NO_INTERNET, options.titleNoInternet) | ||
| .putString(PREF_TITLE_NO_WIFI, options.titleNoWifi) | ||
| .putString(PREF_CHANNEL, options.channel) | ||
| } | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,26 +11,18 @@ data class Upload( | |
| val url: String, | ||
| val path: String, | ||
| val method: String, | ||
| val maxRetries: Int, | ||
|
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. Resumed workers use uninitialized notification settings after app restartMedium Severity The notification settings ( Additional Locations (1) |
||
| val wifiOnly: Boolean, | ||
| val maxRetries: Int, | ||
| val headers: Map<String, String>, | ||
| val notificationId: Int, | ||
| val notificationTitle: String, | ||
| val notificationTitleNoInternet: String, | ||
| val notificationTitleNoWifi: String, | ||
| val notificationChannel: String, | ||
| ) { | ||
| class MissingOptionException(optionName: String) : | ||
| IllegalArgumentException("Missing '$optionName'") | ||
|
|
||
| companion object { | ||
| fun fromReadableMap(map: ReadableMap) = Upload( | ||
| id = map.getString("customUploadId") ?: UUID.randomUUID().toString(), | ||
| url = map.getString(Upload::url.name) ?: throw MissingOptionException(Upload::url.name), | ||
| path = map.getString(Upload::path.name) ?: throw MissingOptionException(Upload::path.name), | ||
| method = map.getString(Upload::method.name) ?: "POST", | ||
| maxRetries = if (map.hasKey(Upload::maxRetries.name)) map.getInt(Upload::maxRetries.name) else 5, | ||
| wifiOnly = if (map.hasKey(Upload::wifiOnly.name)) map.getBoolean(Upload::wifiOnly.name) else false, | ||
| maxRetries = if (map.hasKey(Upload::maxRetries.name)) map.getInt(Upload::maxRetries.name) else 5, | ||
| headers = map.getMap(Upload::headers.name).let { headers -> | ||
| if (headers == null) return@let mapOf() | ||
| val map = mutableMapOf<String, String>() | ||
|
|
@@ -39,16 +31,6 @@ data class Upload( | |
| } | ||
| return@let map | ||
| }, | ||
| notificationId = map.getString(Upload::notificationId.name)?.hashCode() | ||
| ?: throw MissingOptionException(Upload::notificationId.name), | ||
| notificationTitle = map.getString(Upload::notificationTitle.name) | ||
| ?: throw MissingOptionException(Upload::notificationTitle.name), | ||
| notificationTitleNoInternet = map.getString(Upload::notificationTitleNoInternet.name) | ||
| ?: throw MissingOptionException(Upload::notificationTitleNoInternet.name), | ||
| notificationTitleNoWifi = map.getString(Upload::notificationTitleNoWifi.name) | ||
| ?: throw MissingOptionException(Upload::notificationTitleNoWifi.name), | ||
| notificationChannel = map.getString(Upload::notificationChannel.name) | ||
| ?: throw MissingOptionException(Upload::notificationChannel.name), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
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