Skip to content

Commit

Permalink
switched to JobScheduler to fix crash on older versions of Android (#465
Browse files Browse the repository at this point in the history
)

* fixed crash on older versions of Android

Signed-off-by: Jonas Kalderstam <[email protected]>

* switched to JobScheduler to fix crash on older versions of Android

---------

Signed-off-by: Jonas Kalderstam <[email protected]>
  • Loading branch information
spacecowboy authored Dec 20, 2024
1 parent 49f3008 commit cc8c9f7
Show file tree
Hide file tree
Showing 35 changed files with 715 additions and 543 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.content.SharedPreferences
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.work.WorkManager
import com.nononsenseapps.feeder.ApplicationCoroutineScope
import com.nononsenseapps.feeder.FeederApplication
import com.nononsenseapps.feeder.archmodel.FeedItemStore
Expand Down Expand Up @@ -87,7 +86,6 @@ class RssLocalSyncKtTest : DIAware {
bind<SyncRemoteStore>() with singleton { SyncRemoteStore(di) }
bind<OkHttpClient>() with singleton { cachingHttpClient() }
import(networkModule)
bind<WorkManager>() with singleton { WorkManager.getInstance(getApplicationContext()) }
bind<SharedPreferences>() with singleton { getApplicationContext<FeederApplication>().getSharedPreferences("test", Context.MODE_PRIVATE) }
bind<ApplicationCoroutineScope>() with singleton { ApplicationCoroutineScope() }
bind<Repository>() with singleton { Repository(di) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.work.WorkManager
import com.nononsenseapps.feeder.archmodel.FeedStore
import com.nononsenseapps.feeder.archmodel.SettingsStore
import com.nononsenseapps.feeder.db.room.AppDatabase
Expand Down Expand Up @@ -60,7 +59,6 @@ class ExportSavedTest : DIAware {
bind<SettingsStore>() with singleton { SettingsStore(di) }
bind<FeedStore>() with singleton { FeedStore(di) }
bind<OPMLParserHandler>() with singleton { OPMLImporter(di) }
bind<WorkManager>() with singleton { WorkManager.getInstance(this@ExportSavedTest.context) }
bind<ToastMaker>() with
instance(
object : ToastMaker {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import androidx.work.WorkManager
import com.nononsenseapps.feeder.archmodel.FeedStore
import com.nononsenseapps.feeder.archmodel.PREF_VAL_OPEN_WITH_CUSTOM_TAB
import com.nononsenseapps.feeder.archmodel.SettingsStore
Expand Down Expand Up @@ -61,7 +60,6 @@ class OPMLTest : DIAware {
bind<SettingsStore>() with singleton { SettingsStore(di) }
bind<FeedStore>() with singleton { FeedStore(di) }
bind<OPMLParserHandler>() with singleton { OPMLImporter(di) }
bind<WorkManager>() with singleton { WorkManager.getInstance(this@OPMLTest.context) }
bind<ToastMaker>() with
instance(
object : ToastMaker {
Expand Down
18 changes: 6 additions & 12 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<!-- To limit syncing to only WiFi -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Remove foreground permission which is merged in by workmanager -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" tools:node="remove"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" tools:node="remove"/>
<!-- For background jobs -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" />

<permission
android:name="${applicationId}.permission.read"
Expand Down Expand Up @@ -194,16 +194,10 @@
android:launchMode="singleInstance"
android:taskAffinity="${applicationId}.OpenLinkTask" />

<provider
android:name=".contentprovider.RSSContentProvider"
android:authorities="${applicationId}.rssprovider"
android:exported="true"
android:readPermission="${applicationId}.permission.read" />

<!-- Remove foreground service which is merged in by workmanager -->
<!-- Service for background jobs -->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
tools:node="remove" />
android:name=".background.FeederJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />

</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.GifDecoder
Expand Down Expand Up @@ -82,7 +81,6 @@ class FeederApplication : Application(), DIAware, ImageLoaderFactory {

import(archModelModule)

bind<WorkManager>() with singleton { WorkManager.getInstance(this@FeederApplication) }
bind<ContentResolver>() with singleton { contentResolver }
bind<ToastMaker>() with
singleton {
Expand Down
84 changes: 18 additions & 66 deletions app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import android.app.Application
import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.paging.PagingData
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.nononsenseapps.feeder.ApplicationCoroutineScope
import com.nononsenseapps.feeder.background.runOnceBlocklistUpdate
import com.nononsenseapps.feeder.background.runOnceRssSync
import com.nononsenseapps.feeder.background.runOnceSyncChainSendRead
import com.nononsenseapps.feeder.background.schedulePeriodicRssSync
import com.nononsenseapps.feeder.db.room.Feed
import com.nononsenseapps.feeder.db.room.FeedForSettings
import com.nononsenseapps.feeder.db.room.FeedItem
Expand All @@ -26,9 +25,6 @@ import com.nononsenseapps.feeder.db.room.SyncDevice
import com.nononsenseapps.feeder.db.room.SyncRemote
import com.nononsenseapps.feeder.model.FeedUnreadCount
import com.nononsenseapps.feeder.model.ThumbnailImage
import com.nononsenseapps.feeder.model.workmanager.BlockListWorker
import com.nononsenseapps.feeder.model.workmanager.SyncServiceSendReadWorker
import com.nononsenseapps.feeder.model.workmanager.requestFeedSync
import com.nononsenseapps.feeder.sync.DeviceListResponse
import com.nononsenseapps.feeder.sync.ErrorResponse
import com.nononsenseapps.feeder.sync.SyncRestClient
Expand All @@ -37,7 +33,6 @@ import com.nononsenseapps.feeder.ui.compose.feedarticle.FeedListFilter
import com.nononsenseapps.feeder.ui.compose.feedarticle.emptyFeedListFilter
import com.nononsenseapps.feeder.util.Either
import com.nononsenseapps.feeder.util.addDynamicShortcutToFeed
import com.nononsenseapps.feeder.util.logDebug
import com.nononsenseapps.feeder.util.reportShortcutToFeedUsed
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
Expand All @@ -54,7 +49,6 @@ import org.kodein.di.instance
import java.net.URL
import java.time.Instant
import java.time.ZonedDateTime
import java.util.concurrent.TimeUnit

@OptIn(ExperimentalCoroutinesApi::class)
class Repository(override val di: DI) : DIAware {
Expand All @@ -67,7 +61,6 @@ class Repository(override val di: DI) : DIAware {
private val application: Application by instance()
private val syncRemoteStore: SyncRemoteStore by instance()
private val syncClient: SyncRestClient by instance()
private val workManager: WorkManager by instance()

init {
addFeederNewsIfInitialStart()
Expand All @@ -84,9 +77,10 @@ class Repository(override val di: DI) : DIAware {
),
)
settingsStore.setAddedFeederNews(true)
requestFeedSync(
runOnceRssSync(
di = di,
feedId = feedId,
triggeredByUser = false,
)
}
}
Expand Down Expand Up @@ -198,12 +192,12 @@ class Repository(override val di: DI) : DIAware {

suspend fun addBlocklistPattern(pattern: String) {
settingsStore.addBlocklistPattern(pattern)
scheduleBlockListUpdate(0)
runOnceBlocklistUpdate(di)
}

suspend fun removeBlocklistPattern(pattern: String) {
settingsStore.removeBlocklistPattern(pattern)
scheduleBlockListUpdate(0)
runOnceBlocklistUpdate(di)
}

suspend fun setBlockStatusForNewInFeed(
Expand Down Expand Up @@ -409,7 +403,7 @@ class Repository(override val di: DI) : DIAware {
feedItemStore.markAsReadAndNotified(itemId)
}
}
scheduleSendRead()
runOnceSyncChainSendRead(di)
}

suspend fun markAsUnread(itemId: Long) {
Expand Down Expand Up @@ -491,7 +485,7 @@ class Repository(override val di: DI) : DIAware {
tag.isNotBlank() -> feedItemStore.markAllAsReadInTag(tag)
else -> feedItemStore.markAllAsRead()
}
scheduleSendRead()
runOnceSyncChainSendRead(di)
setMinReadTime(Instant.now())
}

Expand All @@ -508,7 +502,7 @@ class Repository(override val di: DI) : DIAware {
descending = SortingOptions.NEWEST_FIRST != currentSorting.value,
cursor = cursor,
)
scheduleSendRead()
runOnceSyncChainSendRead(di)
}

suspend fun markAfterAsRead(
Expand All @@ -524,7 +518,7 @@ class Repository(override val di: DI) : DIAware {
descending = SortingOptions.NEWEST_FIRST == currentSorting.value,
cursor = cursor,
)
scheduleSendRead()
runOnceSyncChainSendRead(di)
}

val allTags: Flow<List<String>> = feedStore.allTags
Expand Down Expand Up @@ -553,7 +547,9 @@ class Repository(override val di: DI) : DIAware {

fun toggleTagExpansion(tag: String) = sessionStore.toggleTagExpansion(tag)

fun ensurePeriodicSyncConfigured() = settingsStore.configurePeriodicSync(replace = false)
fun ensurePeriodicSyncConfigured() {
schedulePeriodicRssSync(di = di, replace = false)
}

fun getFeedsItemsWithDefaultFullTextNeedingDownload(): Flow<List<FeedItemIdWithLink>> = feedItemStore.getFeedsItemsWithDefaultFullTextNeedingDownload()

Expand Down Expand Up @@ -705,53 +701,6 @@ class Repository(override val di: DI) : DIAware {
}
}

private fun scheduleSendRead() {
logDebug(LOG_TAG, "Scheduling work")

val constraints =
Constraints.Builder()
.setRequiresCharging(syncOnlyWhenCharging.value)

if (syncOnlyOnWifi.value) {
constraints.setRequiredNetworkType(NetworkType.UNMETERED)
} else {
constraints.setRequiredNetworkType(NetworkType.CONNECTED)
}

val workRequest =
OneTimeWorkRequestBuilder<SyncServiceSendReadWorker>()
.addTag("feeder")
.keepResultsForAtLeast(5, TimeUnit.MINUTES)
.setConstraints(constraints.build())
.setInitialDelay(10, TimeUnit.SECONDS)

workManager.enqueueUniqueWork(
SyncServiceSendReadWorker.UNIQUE_SENDREAD_NAME,
ExistingWorkPolicy.REPLACE,
workRequest.build(),
)
}

fun scheduleBlockListUpdate(delaySeconds: Long) {
logDebug(LOG_TAG, "Scheduling work")

val constraints =
Constraints.Builder()

val workRequest =
OneTimeWorkRequestBuilder<BlockListWorker>()
.addTag("feeder")
.keepResultsForAtLeast(5, TimeUnit.MINUTES)
.setConstraints(constraints.build())
.setInitialDelay(delaySeconds, TimeUnit.SECONDS)

workManager.enqueueUniqueWork(
BlockListWorker.UNIQUE_BLOCKLIST_NAME,
ExistingWorkPolicy.REPLACE,
workRequest.build(),
)
}

suspend fun syncLoadFeedIfStale(
feedId: Long,
staleTime: Long,
Expand Down Expand Up @@ -820,6 +769,9 @@ class Repository(override val di: DI) : DIAware {
sessionStore.setSyncWorkerRunning(running)
}

val isSyncChainConfigured: Boolean
get() = syncClient.isConfigured

/**
* Set the retry after time for feeds with the given base URL.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,10 @@ import android.content.SharedPreferences
import android.database.sqlite.SQLiteConstraintException
import android.os.Build
import androidx.annotation.StringRes
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.nononsenseapps.feeder.R
import com.nononsenseapps.feeder.background.schedulePeriodicRssSync
import com.nononsenseapps.feeder.db.room.BlocklistDao
import com.nononsenseapps.feeder.db.room.ID_UNSET
import com.nononsenseapps.feeder.model.workmanager.FeedSyncer
import com.nononsenseapps.feeder.model.workmanager.UNIQUE_PERIODIC_NAME
import com.nononsenseapps.feeder.model.workmanager.oldPeriodics
import com.nononsenseapps.feeder.ui.compose.feedarticle.FeedListFilter
import com.nononsenseapps.feeder.util.PREF_MAX_ITEM_COUNT_PER_FEED
import com.nononsenseapps.feeder.util.getStringNonNull
Expand All @@ -31,7 +24,6 @@ import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.instance
import java.time.Instant
import java.util.concurrent.TimeUnit

@OptIn(ExperimentalCoroutinesApi::class)
class SettingsStore(override val di: DI) : DIAware {
Expand Down Expand Up @@ -227,7 +219,7 @@ class SettingsStore(override val di: DI) : DIAware {
fun setSyncOnlyOnWifi(value: Boolean) {
_syncOnlyOnWifi.value = value
sp.edit().putBoolean(PREF_SYNC_ONLY_WIFI, value).apply()
configurePeriodicSync(replace = true)
schedulePeriodicRssSync(di = di, replace = true)
}

private val _syncOnlyWhenCharging =
Expand All @@ -237,7 +229,7 @@ class SettingsStore(override val di: DI) : DIAware {
fun setSyncOnlyWhenCharging(value: Boolean) {
_syncOnlyWhenCharging.value = value
sp.edit().putBoolean(PREF_SYNC_ONLY_CHARGING, value).apply()
configurePeriodicSync(replace = true)
schedulePeriodicRssSync(di = di, replace = true)
}

private val _loadImageOnlyOnWifi = MutableStateFlow(sp.getBoolean(PREF_IMG_ONLY_WIFI, false))
Expand Down Expand Up @@ -426,54 +418,7 @@ class SettingsStore(override val di: DI) : DIAware {
fun setSyncFrequency(value: SyncFrequency) {
_syncFrequency.value = value
sp.edit().putString(PREF_SYNC_FREQ, "${value.minutes}").apply()
configurePeriodicSync(replace = true)
}

fun configurePeriodicSync(replace: Boolean) {
val workManager: WorkManager by instance()
val shouldSync = syncFrequency.value.minutes > 0

// Clear old job always to replace with new one
for (oldPeriodic in oldPeriodics) {
workManager.cancelUniqueWork(oldPeriodic)
}

if (shouldSync) {
val constraints =
Constraints.Builder()
.setRequiresCharging(syncOnlyWhenCharging.value)

if (syncOnlyOnWifi.value) {
constraints.setRequiredNetworkType(NetworkType.UNMETERED)
} else {
constraints.setRequiredNetworkType(NetworkType.CONNECTED)
}

val timeInterval = syncFrequency.value.minutes

val workRequestBuilder =
PeriodicWorkRequestBuilder<FeedSyncer>(
timeInterval,
TimeUnit.MINUTES,
)

val syncWork =
workRequestBuilder
.setConstraints(constraints.build())
.addTag("feeder")
.build()

workManager.enqueueUniquePeriodicWork(
UNIQUE_PERIODIC_NAME,
when (replace) {
true -> ExistingPeriodicWorkPolicy.UPDATE
false -> ExistingPeriodicWorkPolicy.KEEP
},
syncWork,
)
} else {
workManager.cancelUniqueWork(UNIQUE_PERIODIC_NAME)
}
schedulePeriodicRssSync(di = di, replace = true)
}

private val _openAiSettings =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.nononsenseapps.feeder.background

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob

class BackgroundCoroutineScope : CoroutineScope {
override val coroutineContext = Dispatchers.Default + SupervisorJob()
}
Loading

0 comments on commit cc8c9f7

Please sign in to comment.