Handle foreground/background sync on older + newer Android versions

This commit is contained in:
Jackson Harper
2024-07-06 10:00:42 +08:00
parent f2fc0c49d0
commit 978c17c7fb
4 changed files with 106 additions and 77 deletions

View File

@ -7,6 +7,8 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:remove="android:maxSdkVersion" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" />
<application
@ -63,5 +65,9 @@
tools:node="remove">
</provider>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false" />
</application>
</manifest>

View File

@ -4,6 +4,7 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import androidx.compose.ui.text.intl.Locale
@ -31,7 +32,6 @@ import java.time.Instant
import java.util.TimeZone
import java.util.UUID
import java.util.regex.Pattern
@HiltWorker
class LibrarySyncWorker @AssistedInject constructor(
@Assisted appContext: Context,
@ -39,12 +39,6 @@ class LibrarySyncWorker @AssistedInject constructor(
private val libraryRepository: LibraryRepository,
private val datastoreRepository: DatastoreRepository,
) : CoroutineWorker(appContext, workerParams) {
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
NOTIFICATION_ID,
createNotification()
)
}
companion object {
const val NOTIFICATION_CHANNEL_ID = "LIBRARY_SYNC_WORKER_CHANNEL"
@ -52,64 +46,73 @@ class LibrarySyncWorker @AssistedInject constructor(
const val NOTIFICATION_ID = 2
}
override suspend fun doWork(): Result {
try {
setForeground(createForegroundInfo())
} catch (e: Exception) {
e.printStackTrace()
return Result.failure()
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = createNotification()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ForegroundInfo(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
ForegroundInfo(NOTIFICATION_ID, notification)
}
}
return withContext(Dispatchers.IO) {
override suspend fun doWork(): Result {
return try {
// Start foreground service immediately
setForeground(getForegroundInfo())
withContext(Dispatchers.IO) {
performItemSync()
loadUsingSearchAPI()
Log.d("LibrarySyncWorker", "Library sync completed successfully")
Result.success()
}
} catch (e: IllegalStateException) {
Log.w("LibrarySyncWorker", "Couldn't start foreground service", e)
// Continue with the work without the foreground service
try {
performItemSync()
loadUsingSearchAPI()
Log.d("LibrarySyncWorker", "Library sync completed without foreground service")
Result.success()
} catch (e: Exception) {
e.printStackTrace()
Log.e("LibrarySyncWorker", "Failed to sync library without foreground service", e)
Result.failure()
}
} catch (e: Exception) {
Log.e("LibrarySyncWorker", "Unexpected error in LibrarySyncWorker", e)
Result.failure()
}
}
private fun createForegroundInfo(): ForegroundInfo {
val notification = createNotification()
return ForegroundInfo(NOTIFICATION_ID, notification)
}
private fun createNotification(): Notification {
val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
} else {
""
}
val channelId = createNotificationChannel()
return NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle("Syncing library items")
.setContentText("Your library is being synced")
.setSmallIcon(R.drawable.ic_notification)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
}
private fun createNotificationChannel(): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelName = NOTIFICATION_CHANNEL_NAME
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
channelName,
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Notification channel for library syncing"
}
val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
return NOTIFICATION_CHANNEL_ID
} else {
return ""
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notification channel for library syncing"
}
val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
return NOTIFICATION_CHANNEL_ID
}
private suspend fun performItemSync(

View File

@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
@ -137,11 +138,6 @@ class SaveSheetActivity : AppCompatActivity() {
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val syncWorkerRequest = OneTimeWorkRequest.Builder(LibrarySyncWorker::class.java)
.setConstraints(constraints)
.addTag(url)
.build()
val inputData = Data.Builder()
.putString("url", url)
.build()
@ -153,9 +149,8 @@ class SaveSheetActivity : AppCompatActivity() {
.addTag(url)
.build()
beginWith(saveURLWorkRequest)
.then(syncWorkerRequest)
.enqueue()
WorkManager.getInstance(context)
.enqueueUniqueWork("saveUrl", ExistingWorkPolicy.REPLACE, saveURLWorkRequest)
}
@Composable

View File

@ -4,6 +4,7 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import androidx.compose.ui.text.intl.Locale
@ -34,12 +35,6 @@ class SaveURLWorker @AssistedInject constructor(
@Assisted workerParams: WorkerParameters,
private val datastoreRepository: DatastoreRepository,
) : CoroutineWorker(appContext, workerParams) {
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
NOTIFICATION_ID,
createNotification()
)
}
companion object {
const val NOTIFICATION_CHANNEL_ID = "SAVE_URL_WORKER_CHANNEL"
@ -47,17 +42,53 @@ class SaveURLWorker @AssistedInject constructor(
const val NOTIFICATION_ID = 1
}
override suspend fun doWork(): Result {
try {
setForeground(createForegroundInfo())
} catch (e: Exception) {
e.printStackTrace()
return Result.failure()
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = createNotification()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ForegroundInfo(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
ForegroundInfo(NOTIFICATION_ID, notification)
}
}
return withContext(Dispatchers.IO) {
val url = inputData.getString("url") ?: return@withContext Result.failure()
if (saveURL(url)) Result.success() else Result.failure()
override suspend fun doWork(): Result {
return try {
// Start foreground service immediately
setForeground(getForegroundInfo())
withContext(Dispatchers.IO) {
val url = inputData.getString("url")
if (url == null) {
Log.e("SaveURLWorker", "No URL provided")
return@withContext Result.failure()
}
if (saveURL(url)) {
Log.d("SaveURLWorker", "URL saved successfully")
Result.success()
} else {
Log.e("SaveURLWorker", "Failed to save URL")
Result.failure()
}
}
} catch (e: IllegalStateException) {
Log.w("SaveURLWorker", "Couldn't start foreground service", e)
// Continue with the work without the foreground service
val url = inputData.getString("url")
if (url != null && saveURL(url)) {
Log.d("SaveURLWorker", "URL saved successfully without foreground service")
Result.success()
} else {
Log.e("SaveURLWorker", "Failed to save URL without foreground service")
Result.failure()
}
} catch (e: Exception) {
Log.e("SaveURLWorker", "Unexpected error in SaveURLWorker", e)
Result.failure()
}
}
@ -104,11 +135,6 @@ class SaveURLWorker @AssistedInject constructor(
return null
}
private fun createForegroundInfo(): ForegroundInfo {
val notification = createNotification()
return ForegroundInfo(NOTIFICATION_ID, notification)
}
private fun createNotification(): Notification {
val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
@ -120,17 +146,16 @@ class SaveURLWorker @AssistedInject constructor(
.setContentTitle("Saving URL")
.setContentText("Your URL is being saved in the background.")
.setSmallIcon(R.drawable.ic_notification) // Ensure this icon is valid
.setPriority(NotificationCompat.PRIORITY_LOW)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
}
private fun createNotificationChannel(): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelName = NOTIFICATION_CHANNEL_NAME
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
channelName,
NotificationManager.IMPORTANCE_LOW
NOTIFICATION_CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH // Changed from LOW to HIGH
).apply {
description = "Notification channel for URL saving"
}
@ -138,9 +163,9 @@ class SaveURLWorker @AssistedInject constructor(
val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
return NOTIFICATION_CHANNEL_ID
NOTIFICATION_CHANNEL_ID
} else {
return ""
""
}
}
}