From 0b80d9a37daf357ac5dc69cf5152d5d342b79e03 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 4 Jul 2024 12:12:45 +0800 Subject: [PATCH] Fix crash on Android when saving items --- android/Omnivore/.gitignore | 1 + ...otlin-compiler-11820217245799401229.salive | 0 .../omnivore/core/network/SearchQuery.kt | 1 - .../feature/library/LibrarySyncWorker.kt | 77 +++++++++++++++++++ .../feature/save/SaveSheetActivity.kt | 34 ++++---- .../omnivore/feature/save/SaveURLWorker.kt | 71 ++++++++++++++++- 6 files changed, 166 insertions(+), 18 deletions(-) delete mode 100644 android/Omnivore/.kotlin/sessions/kotlin-compiler-11820217245799401229.salive diff --git a/android/Omnivore/.gitignore b/android/Omnivore/.gitignore index aa724b770..04f46af22 100644 --- a/android/Omnivore/.gitignore +++ b/android/Omnivore/.gitignore @@ -1,5 +1,6 @@ *.iml .gradle +.kotlin /local.properties /.idea/caches /.idea/libraries diff --git a/android/Omnivore/.kotlin/sessions/kotlin-compiler-11820217245799401229.salive b/android/Omnivore/.kotlin/sessions/kotlin-compiler-11820217245799401229.salive deleted file mode 100644 index e69de29bb..000000000 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt index fbd5ff4b2..ff80b86f3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt @@ -184,7 +184,6 @@ fun loadLibraryItemContent(context: Context, libraryItemId: String): String? { return try { return readFromInternalStorage(context = context, fileName = "${libraryItemId}.html") } catch (e: Exception) { - e.printStackTrace() null } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibrarySyncWorker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibrarySyncWorker.kt index 85ba6e69b..04fe65c34 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibrarySyncWorker.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibrarySyncWorker.kt @@ -1,18 +1,36 @@ package app.omnivore.omnivore.feature.library +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context +import android.os.Build +import android.util.Log +import androidx.compose.ui.text.intl.Locale +import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo import androidx.work.WorkerParameters +import app.omnivore.omnivore.R import app.omnivore.omnivore.core.data.repository.LibraryRepository import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.core.datastore.libraryLastSyncTimestamp +import app.omnivore.omnivore.core.datastore.omnivoreAuthToken +import app.omnivore.omnivore.graphql.generated.SaveUrlMutation +import app.omnivore.omnivore.graphql.generated.type.SaveUrlInput +import app.omnivore.omnivore.utils.Constants +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.Optional import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.time.Instant +import java.util.TimeZone +import java.util.UUID +import java.util.regex.Pattern @HiltWorker class LibrarySyncWorker @AssistedInject constructor( @@ -21,8 +39,27 @@ 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" + const val NOTIFICATION_CHANNEL_NAME = "Sync library" + const val NOTIFICATION_ID = 2 + } override suspend fun doWork(): Result { + try { + setForeground(createForegroundInfo()) + } catch (e: Exception) { + e.printStackTrace() + return Result.failure() + } + return withContext(Dispatchers.IO) { try { performItemSync() @@ -35,6 +72,46 @@ class LibrarySyncWorker @AssistedInject constructor( } } + 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 { + "" + } + + return NotificationCompat.Builder(applicationContext, channelId) + .setContentTitle("Syncing library items") + .setContentText("Your library is being synced") + .setSmallIcon(R.drawable.ic_notification) + .setPriority(NotificationCompat.PRIORITY_LOW) + .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 "" + } + } + private suspend fun performItemSync( cursor: String? = null, since: String = getLastSyncTime()?.toString() ?: Instant.MIN.toString(), diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveSheetActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveSheetActivity.kt index 9a2c99ae1..3dcaa2c1b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveSheetActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveSheetActivity.kt @@ -1,9 +1,11 @@ package app.omnivore.omnivore.feature.save import android.content.ContentValues +import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log +import android.view.WindowManager import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background @@ -16,9 +18,12 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.dp import androidx.work.Constraints +import androidx.work.Data import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OutOfQuotaPolicy import androidx.work.WorkInfo @@ -49,7 +54,7 @@ class SaveSheetActivity : AppCompatActivity() { if (intent.type?.startsWith("text/plain") == true) { intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extractedText = it - workManager.enqueueSaveWorker(it) + workManager.enqueueSaveWorker(this, it) Log.d(ContentValues.TAG, "Extracted text: $extractedText") } } @@ -80,8 +85,6 @@ class SaveSheetActivity : AppCompatActivity() { } val scaffoldState: ScaffoldState = rememberScaffoldState() - - val message = when (saveState) { SaveState.DEFAULT -> "" SaveState.SAVING -> "Saved to Omnivore" @@ -129,25 +132,28 @@ class SaveSheetActivity : AppCompatActivity() { } } - private fun WorkManager.enqueueSaveWorker(url: String) { + private fun WorkManager.enqueueSaveWorker(context: Context, url: String) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() - val saveWorkerRequest = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .setInputData(workDataOf("url" to url)) - .addTag(url) - // Can add other configs like setBackoffCriteria to retry sync if failed - .build() - val syncWorkerRequest = OneTimeWorkRequestBuilder() + val syncWorkerRequest = OneTimeWorkRequest.Builder(LibrarySyncWorker::class.java) .setConstraints(constraints) .addTag(url) - .setInitialDelay(5.seconds.toJavaDuration()) .build() - beginWith(saveWorkerRequest) + val inputData = Data.Builder() + .putString("url", url) + .build() + + val saveURLWorkRequest = OneTimeWorkRequest.Builder(SaveURLWorker::class.java) + .setConstraints(constraints) + .setInputData(inputData) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .addTag(url) + .build() + + beginWith(saveURLWorkRequest) .then(syncWorkerRequest) .enqueue() } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveURLWorker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveURLWorker.kt index b305dd8a9..192839b1a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveURLWorker.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/save/SaveURLWorker.kt @@ -1,10 +1,18 @@ package app.omnivore.omnivore.feature.save +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context +import android.os.Build +import android.util.Log import androidx.compose.ui.text.intl.Locale +import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo import androidx.work.WorkerParameters +import app.omnivore.omnivore.R import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.core.datastore.omnivoreAuthToken import app.omnivore.omnivore.graphql.generated.SaveUrlMutation @@ -26,16 +34,34 @@ 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" + const val NOTIFICATION_CHANNEL_NAME = "URL Saver" + const val NOTIFICATION_ID = 1 + } override suspend fun doWork(): Result { - return withContext(Dispatchers.IO){ + try { + setForeground(createForegroundInfo()) + } catch (e: Exception) { + e.printStackTrace() + return Result.failure() + } + + return withContext(Dispatchers.IO) { val url = inputData.getString("url") ?: return@withContext Result.failure() if (saveURL(url)) Result.success() else Result.failure() } } private suspend fun saveURL(url: String): Boolean { - val authToken = datastoreRepository.getString(omnivoreAuthToken) ?: return false val apolloClient = ApolloClient.Builder() @@ -60,9 +86,10 @@ class SaveURLWorker @AssistedInject constructor( ) ) ).execute() - return (response.data?.saveUrl?.onSaveSuccess?.url != null) } catch (e: Exception) { + Log.d("omnivore", "FAILED TO SAVE ITEM") + e.printStackTrace() return false } } @@ -77,5 +104,43 @@ 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() + } else { + "" + } + + return NotificationCompat.Builder(applicationContext, channelId) + .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) + .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 URL saving" + } + + val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + + return NOTIFICATION_CHANNEL_ID + } else { + return "" + } + } }