Fixed auto sync

This commit is contained in:
Ash 2022-03-21 22:40:40 +08:00
parent 582e0f8148
commit c7c708d92a
11 changed files with 106 additions and 107 deletions

View File

@ -54,6 +54,7 @@ dependencies {
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha") implementation("com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha")
implementation "androidx.datastore:datastore-preferences:1.0.0" implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "com.airbnb.android:lottie-compose:4.2.2" implementation "com.airbnb.android:lottie-compose:4.2.2"
implementation "androidx.hilt:hilt-work:1.0.0"
implementation "androidx.work:work-runtime-ktx:2.8.0-alpha01" implementation "androidx.work:work-runtime-ktx:2.8.0-alpha01"
implementation "net.dankito.readability4j:readability4j:1.0.8" implementation "net.dankito.readability4j:readability4j:1.0.8"
implementation "androidx.navigation:navigation-compose:2.5.0-alpha01" implementation "androidx.navigation:navigation-compose:2.5.0-alpha01"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="me.ash.reader"> package="me.ash.reader">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -23,6 +24,11 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
</application> </application>
</manifest> </manifest>

View File

@ -1,22 +1,32 @@
package me.ash.reader package me.ash.reader
import android.app.Application import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.*
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.repository.* import me.ash.reader.data.repository.*
import me.ash.reader.data.source.OpmlLocalDataSource import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@DelicateCoroutinesApi @DelicateCoroutinesApi
@HiltAndroidApp @HiltAndroidApp
class App : Application() { class App : Application(), Configuration.Provider {
@Inject @Inject
lateinit var readerDatabase: ReaderDatabase lateinit var readerDatabase: ReaderDatabase
@Inject
lateinit var workerFactory: HiltWorkerFactory
@Inject
lateinit var workManager: WorkManager
@Inject @Inject
lateinit var opmlLocalDataSource: OpmlLocalDataSource lateinit var opmlLocalDataSource: OpmlLocalDataSource
@ -44,15 +54,43 @@ class App : Application() {
@Inject @Inject
lateinit var rssRepository: RssRepository lateinit var rssRepository: RssRepository
private val applicationScope = CoroutineScope(Dispatchers.IO)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
GlobalScope.launch { applicationScope.launch {
accountInit()
workerInit()
}
}
private suspend fun accountInit() {
if (accountRepository.isNoAccount()) { if (accountRepository.isNoAccount()) {
val account = accountRepository.addDefaultAccount() val account = accountRepository.addDefaultAccount()
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!) applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!)
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountType, account.type) applicationContext.dataStore.put(DataStoreKeys.CurrentAccountType, account.type)
} }
rssRepository.get().doSync(true)
} }
private fun workerInit() {
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
).addTag(SyncWorker.WORK_NAME).build()
workManager.enqueueUniquePeriodicWork(
SyncWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
repeatingRequest
)
} }
override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
} }

View File

@ -2,11 +2,18 @@ package me.ash.reader.data.repository
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.work.* import androidx.work.CoroutineWorker
import kotlinx.coroutines.DelicateCoroutinesApi import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Mutex
import me.ash.reader.DataStoreKeys import me.ash.reader.DataStoreKeys
import me.ash.reader.data.account.AccountDao import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article import me.ash.reader.data.article.Article
@ -18,12 +25,9 @@ import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.Group import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.group.GroupWithFeed import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.dataStore import me.ash.reader.dataStore
import me.ash.reader.get import me.ash.reader.get
import java.util.concurrent.TimeUnit
import javax.inject.Inject
abstract class AbstractRssRepository constructor( abstract class AbstractRssRepository constructor(
private val context: Context, private val context: Context,
@ -43,19 +47,11 @@ abstract class AbstractRssRepository constructor(
val isNotSyncing: Boolean = !isSyncing val isNotSyncing: Boolean = !isSyncing
} }
abstract fun getSyncState(): StateFlow<SyncState>
abstract suspend fun updateArticleInfo(article: Article) abstract suspend fun updateArticleInfo(article: Article)
abstract suspend fun subscribe(feed: Feed, articles: List<Article>) abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
abstract suspend fun sync( abstract suspend fun sync()
context: Context,
accountDao: AccountDao,
articleDao: ArticleDao,
feedDao: FeedDao,
rssNetworkDataSource: RssNetworkDataSource
)
fun pullGroups(): Flow<MutableList<Group>> { fun pullGroups(): Flow<MutableList<Group>> {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
@ -135,46 +131,32 @@ abstract class AbstractRssRepository constructor(
return workManager.getWorkInfosByTag("sync").get().size.toString() return workManager.getWorkInfosByTag("sync").get().size.toString()
} }
suspend fun doSync(isWork: Boolean? = false) { companion object {
if (isWork == true) { val mutex = Mutex()
workManager.cancelAllWork()
val syncWorkerRequest: WorkRequest = private val _syncState = MutableStateFlow(SyncState())
PeriodicWorkRequestBuilder<SyncWorker>( val syncState = _syncState.asStateFlow()
15, TimeUnit.MINUTES
).setConstraints( fun updateSyncState(function: (SyncState) -> SyncState) {
Constraints.Builder() _syncState.update(function)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
).addTag("sync").build()
workManager.enqueue(syncWorkerRequest)
} else {
sync(context, accountDao, articleDao, feedDao, rssNetworkDataSource)
} }
} }
} }
@DelicateCoroutinesApi @HiltWorker
class SyncWorker( class SyncWorker @AssistedInject constructor(
context: Context, @Assisted context: Context,
workerParams: WorkerParameters, @Assisted workerParams: WorkerParameters,
private val rssRepository: RssRepository,
) : CoroutineWorker(context, workerParams) { ) : CoroutineWorker(context, workerParams) {
@Inject
lateinit var rssRepository: RssRepository
@Inject
lateinit var rssNetworkDataSource: RssNetworkDataSource
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ") Log.i("RLog", "doWork: ")
val db = ReaderDatabase.getInstance(applicationContext) rssRepository.get().sync()
rssRepository.get().sync(
applicationContext,
db.accountDao(),
db.articleDao(),
db.feedDao(),
rssNetworkDataSource
)
return Result.success() return Result.success()
} }
companion object {
const val WORK_NAME = "article.sync"
}
} }

View File

@ -4,9 +4,6 @@ import android.content.Context
import android.util.Log import android.util.Log
import androidx.work.WorkManager import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import me.ash.reader.DataStoreKeys import me.ash.reader.DataStoreKeys
import me.ash.reader.data.account.AccountDao import me.ash.reader.data.account.AccountDao
@ -33,18 +30,13 @@ class FeverRssRepository @Inject constructor(
private val groupDao: GroupDao, private val groupDao: GroupDao,
private val rssHelper: RssHelper, private val rssHelper: RssHelper,
private val feverApiDataSource: FeverApiDataSource, private val feverApiDataSource: FeverApiDataSource,
private val accountDao: AccountDao,
rssNetworkDataSource: RssNetworkDataSource, rssNetworkDataSource: RssNetworkDataSource,
accountDao: AccountDao,
workManager: WorkManager, workManager: WorkManager,
) : AbstractRssRepository( ) : AbstractRssRepository(
context, accountDao, articleDao, groupDao, context, accountDao, articleDao, groupDao,
feedDao, rssNetworkDataSource, workManager, feedDao, rssNetworkDataSource, workManager,
) { ) {
private val mutex = Mutex()
private val syncState = MutableStateFlow(SyncState())
override fun getSyncState() = syncState
override suspend fun updateArticleInfo(article: Article) { override suspend fun updateArticleInfo(article: Article) {
articleDao.update(article) articleDao.update(article)
} }
@ -56,18 +48,12 @@ class FeverRssRepository @Inject constructor(
}) })
} }
override suspend fun sync( override suspend fun sync() {
context: Context,
accountDao: AccountDao,
articleDao: ArticleDao,
feedDao: FeedDao,
rssNetworkDataSource: RssNetworkDataSource
) {
mutex.withLock { mutex.withLock {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
?: return ?: return
syncState.update { updateSyncState {
it.copy( it.copy(
feedCount = 1, feedCount = 1,
syncedCount = 1, syncedCount = 1,
@ -140,7 +126,7 @@ class FeverRssRepository @Inject constructor(
accountDao.update(accountDao.queryById(accountId)!!.apply { accountDao.update(accountDao.queryById(accountId)!!.apply {
updateAt = Date() updateAt = Date()
}) })
syncState.update { updateSyncState {
it.copy( it.copy(
feedCount = 0, feedCount = 0,
syncedCount = 0, syncedCount = 0,

View File

@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat.getSystemService
import androidx.work.WorkManager import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import me.ash.reader.* import me.ash.reader.*
import me.ash.reader.data.account.AccountDao import me.ash.reader.data.account.AccountDao
@ -34,19 +33,14 @@ class LocalRssRepository @Inject constructor(
private val articleDao: ArticleDao, private val articleDao: ArticleDao,
private val feedDao: FeedDao, private val feedDao: FeedDao,
private val rssHelper: RssHelper, private val rssHelper: RssHelper,
rssNetworkDataSource: RssNetworkDataSource, private val rssNetworkDataSource: RssNetworkDataSource,
private val accountDao: AccountDao,
groupDao: GroupDao, groupDao: GroupDao,
accountDao: AccountDao,
workManager: WorkManager, workManager: WorkManager,
) : AbstractRssRepository( ) : AbstractRssRepository(
context, accountDao, articleDao, groupDao, context, accountDao, articleDao, groupDao,
feedDao, rssNetworkDataSource, workManager, feedDao, rssNetworkDataSource, workManager,
) { ) {
private val mutex = Mutex()
private val syncState = MutableStateFlow(SyncState())
override fun getSyncState() = syncState
override suspend fun updateArticleInfo(article: Article) { override suspend fun updateArticleInfo(article: Article) {
articleDao.update(article) articleDao.update(article)
} }
@ -58,13 +52,7 @@ class LocalRssRepository @Inject constructor(
}) })
} }
override suspend fun sync( override suspend fun sync() {
context: Context,
accountDao: AccountDao,
articleDao: ArticleDao,
feedDao: FeedDao,
rssNetworkDataSource: RssNetworkDataSource
) {
mutex.withLock { mutex.withLock {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
?: return ?: return
@ -98,10 +86,10 @@ class LocalRssRepository @Inject constructor(
} }
} }
) )
syncState.update { updateSyncState {
it.copy( it.copy(
feedCount = feeds.size, feedCount = feeds.size,
syncedCount = syncState.value.syncedCount + 1, syncedCount = it.syncedCount + 1,
currentFeedName = feed.name currentFeedName = feed.name
) )
} }
@ -173,7 +161,7 @@ class LocalRssRepository @Inject constructor(
} }
) )
} }
syncState.update { updateSyncState {
it.copy( it.copy(
feedCount = 0, feedCount = 0,
syncedCount = 0, syncedCount = 0,

View File

@ -16,7 +16,6 @@ import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.ui.page.common.NotificationGroupName
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.findActivity import me.ash.reader.ui.extension.findActivity
import me.ash.reader.ui.page.common.ExtraName import me.ash.reader.ui.page.common.ExtraName
@ -46,9 +45,9 @@ fun HomePage(
intent.extras?.get(ExtraName.ARTICLE_ID)?.let { intent.extras?.get(ExtraName.ARTICLE_ID)?.let {
readViewModel.dispatch(ReadViewAction.ScrollToItem(2)) readViewModel.dispatch(ReadViewAction.ScrollToItem(2))
scope.launch { scope.launch {
val article = val article = readViewModel
readViewModel.rssRepository.get().findArticleById(it.toString().toInt()) .rssRepository.get()
?: return@launch .findArticleById(it.toString().toInt()) ?: return@launch
readViewModel.dispatch(ReadViewAction.InitData(article)) readViewModel.dispatch(ReadViewAction.InitData(article))
if (article.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent) if (article.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)

View File

@ -15,6 +15,7 @@ import kotlinx.coroutines.launch
import me.ash.reader.data.constant.Filter import me.ash.reader.data.constant.Filter
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group import me.ash.reader.data.group.Group
import me.ash.reader.data.repository.AbstractRssRepository
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.ui.extension.animateScrollToPage import me.ash.reader.ui.extension.animateScrollToPage
import javax.inject.Inject import javax.inject.Inject
@ -31,7 +32,7 @@ class HomeViewModel @Inject constructor(
private val _filterState = MutableStateFlow(FilterState()) private val _filterState = MutableStateFlow(FilterState())
val filterState = _filterState.asStateFlow() val filterState = _filterState.asStateFlow()
val syncState = rssRepository.get().getSyncState() val syncState = AbstractRssRepository.syncState
fun dispatch(action: HomeViewAction) { fun dispatch(action: HomeViewAction) {
when (action) { when (action) {
@ -47,7 +48,7 @@ class HomeViewModel @Inject constructor(
private fun sync(callback: () -> Unit = {}) { private fun sync(callback: () -> Unit = {}) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
rssRepository.get().doSync() rssRepository.get().sync()
callback() callback()
} }
} }

View File

@ -50,7 +50,7 @@ class FeedsViewModel @Inject constructor(
private fun addFromFile(inputStream: InputStream) { private fun addFromFile(inputStream: InputStream) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
opmlRepository.saveToDatabase(inputStream) opmlRepository.saveToDatabase(inputStream)
pullFeeds(isStarred = false, isUnread = false) rssRepository.get().sync()
} }
} }
@ -60,7 +60,6 @@ class FeedsViewModel @Inject constructor(
isStarred = filterState.filter.isStarred(), isStarred = filterState.filter.isStarred(),
isUnread = filterState.filter.isUnread(), isUnread = filterState.filter.isUnread(),
) )
_viewState
} }
} }

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.flow package me.ash.reader.ui.page.home.flow
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -77,7 +78,11 @@ fun FlowPage(
} }
}, },
actions = { actions = {
IconButton(onClick = {}) { IconButton(onClick = {
viewModel.dispatch(FlowViewAction.PeekSyncWork)
Toast.makeText(context, viewState.syncWorkInfo.length.toString(), Toast.LENGTH_SHORT)
.show()
}) {
Icon( Icon(
imageVector = Icons.Rounded.DoneAll, imageVector = Icons.Rounded.DoneAll,
contentDescription = stringResource(R.string.mark_all_as_read), contentDescription = stringResource(R.string.mark_all_as_read),

View File

@ -21,9 +21,3 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# Custom
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching = true