Refactor repository coroutine

This commit is contained in:
Ash 2022-04-01 02:06:59 +08:00
parent 004005d8be
commit e1e43019f5
14 changed files with 389 additions and 341 deletions

View File

@ -5,10 +5,11 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.repository.*
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase
@ -57,9 +58,13 @@ class App : Application(), Configuration.Provider {
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
@DispatcherDefault
lateinit var dispatcherDefault: CoroutineDispatcher
override fun onCreate() {
super.onCreate()
applicationScope.launch(Dispatchers.IO) {
applicationScope.launch(dispatcherDefault) {
accountInit()
workerInit()
}

View File

@ -17,6 +17,8 @@ import java.io.IOException
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val Context.currentAccountId: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
val Context.currentAccountType: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!!
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
this.edit {

View File

@ -7,7 +7,7 @@ import androidx.paging.PagingSource
import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import me.ash.reader.currentAccountId
@ -32,6 +32,7 @@ abstract class AbstractRssRepository constructor(
private val feedDao: FeedDao,
private val rssNetworkDataSource: RssNetworkDataSource,
private val workManager: WorkManager,
private val dispatcherIO: CoroutineDispatcher,
) {
data class SyncState(
val feedCount: Int = 0,
@ -59,11 +60,11 @@ abstract class AbstractRssRepository constructor(
}
fun pullGroups(): Flow<MutableList<Group>> {
return groupDao.queryAllGroup(context.currentAccountId).flowOn(Dispatchers.IO)
return groupDao.queryAllGroup(context.currentAccountId).flowOn(dispatcherIO)
}
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
return groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(Dispatchers.IO)
return groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(dispatcherIO)
}
fun pullArticles(
@ -72,7 +73,6 @@ abstract class AbstractRssRepository constructor(
isStarred: Boolean = false,
isUnread: Boolean = false,
): PagingSource<Int, ArticleWithFeed> {
Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}")
val accountId = context.currentAccountId
Log.i(
"RLog",
@ -107,7 +107,6 @@ abstract class AbstractRssRepository constructor(
isStarred: Boolean = false,
isUnread: Boolean = false,
): Flow<List<ImportantCount>> {
Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}")
val accountId = context.currentAccountId
Log.i(
"RLog",
@ -119,7 +118,7 @@ abstract class AbstractRssRepository constructor(
isUnread -> articleDao
.queryImportantCountWhenIsUnread(accountId, isUnread)
else -> articleDao.queryImportantCountWhenIsAll(accountId)
}.flowOn(Dispatchers.IO)
}.flowOn(dispatcherIO)
}
suspend fun findFeedById(id: String): Feed? {
@ -130,7 +129,7 @@ abstract class AbstractRssRepository constructor(
return articleDao.queryById(id)
}
suspend fun isExist(url: String): Boolean {
suspend fun isFeedExist(url: String): Boolean {
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
}

View File

@ -4,8 +4,11 @@ import android.content.Context
import android.util.Log
import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import me.ash.reader.*
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao
@ -13,11 +16,16 @@ import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.source.FeverApiDataSource
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.spacerDollar
import net.dankito.readability4j.extended.Readability4JExtended
import java.util.*
import javax.inject.Inject
import kotlin.collections.set
class FeverRssRepository @Inject constructor(
@ApplicationContext
@ -29,10 +37,17 @@ class FeverRssRepository @Inject constructor(
private val feverApiDataSource: FeverApiDataSource,
private val accountDao: AccountDao,
rssNetworkDataSource: RssNetworkDataSource,
@ApplicationScope
private val applicationScope: CoroutineScope,
@DispatcherDefault
private val dispatcherDefault: CoroutineDispatcher,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
workManager: WorkManager,
) : AbstractRssRepository(
context, accountDao, articleDao, groupDao,
feedDao, rssNetworkDataSource, workManager,
dispatcherIO
) {
override suspend fun updateArticleInfo(article: Article) {
articleDao.update(article)
@ -58,88 +73,90 @@ class FeverRssRepository @Inject constructor(
}
override suspend fun sync() {
mutex.withLock {
val accountId = context.currentAccountId
applicationScope.launch(dispatcherDefault) {
mutex.withLock {
val accountId = context.currentAccountId
updateSyncState {
it.copy(
feedCount = 1,
syncedCount = 1,
currentFeedName = "Fever"
)
}
updateSyncState {
it.copy(
feedCount = 1,
syncedCount = 1,
currentFeedName = "Fever"
)
}
if (feedDao.queryAll(accountId).isNullOrEmpty()) {
// Temporary add feeds
val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
Log.i("RLog", "Fever groups: $feverGroupsBody")
feverGroupsBody.groups.forEach {
groupDao.insert(
Group(
if (feedDao.queryAll(accountId).isNullOrEmpty()) {
// Temporary add feeds
val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
Log.i("RLog", "Fever groups: $feverGroupsBody")
feverGroupsBody.groups.forEach {
groupDao.insert(
Group(
id = accountId.spacerDollar(it.id),
name = it.title,
accountId = accountId,
)
)
}
val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
feverGroupsBody.feeds_groups.forEach { item ->
item.feed_ids
.split(",")
.map { it.toInt() }
.forEach { id ->
feverFeedsGroupsMap[id] = item.group_id
}
}
val feeds = feverFeeds.map {
Feed(
id = accountId.spacerDollar(it.id),
name = it.title,
accountId = accountId,
url = it.url,
groupId = feverFeedsGroupsMap[it.id].toString(),
accountId = accountId
)
)
}
feedDao.insertList(feeds)
}
val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
feverGroupsBody.feeds_groups.forEach { item ->
item.feed_ids
.split(",")
.map { it.toInt() }
.forEach { id ->
feverFeedsGroupsMap[id] = item.group_id
}
}
val feeds = feverFeeds.map {
Feed(
id = accountId.spacerDollar(it.id),
name = it.title,
url = it.url,
groupId = feverFeedsGroupsMap[it.id].toString(),
accountId = accountId
)
}
feedDao.insertList(feeds)
}
// Add articles
val articles = mutableListOf<Article>()
feverApiDataSource.itemsBySince(since = 1647444325925621L)
.execute().body()!!.items
.forEach {
articles.add(
Article(
id = accountId.spacerDollar(it.id),
date = Date(it.created_on_time * 1000),
title = it.title,
author = it.author,
rawDescription = it.html,
shortDescription = (
Readability4JExtended("", it.html)
.parse().textContent ?: ""
).take(100).trim(),
link = it.url,
accountId = accountId,
feedId = it.feed_id.toString(),
isUnread = it.is_read == 0,
isStarred = it.is_saved == 1,
// Add articles
val articles = mutableListOf<Article>()
feverApiDataSource.itemsBySince(since = 1647444325925621L)
.execute().body()!!.items
.forEach {
articles.add(
Article(
id = accountId.spacerDollar(it.id),
date = Date(it.created_on_time * 1000),
title = it.title,
author = it.author,
rawDescription = it.html,
shortDescription = (
Readability4JExtended("", it.html)
.parse().textContent ?: ""
).take(100).trim(),
link = it.url,
accountId = accountId,
feedId = it.feed_id.toString(),
isUnread = it.is_read == 0,
isStarred = it.is_saved == 1,
)
)
}
articleDao.insertList(articles)
// Complete sync
accountDao.update(accountDao.queryById(accountId)!!.apply {
updateAt = Date()
})
updateSyncState {
it.copy(
feedCount = 0,
syncedCount = 0,
currentFeedName = ""
)
}
articleDao.insertList(articles)
// Complete sync
accountDao.update(accountDao.queryById(accountId)!!.apply {
updateAt = Date()
})
updateSyncState {
it.copy(
feedCount = 0,
syncedCount = 0,
currentFeedName = ""
)
}
}
}

View File

@ -11,11 +11,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import me.ash.reader.MainActivity
import me.ash.reader.R
import me.ash.reader.currentAccountId
@ -26,6 +22,9 @@ import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
@ -41,10 +40,17 @@ class LocalRssRepository @Inject constructor(
private val rssNetworkDataSource: RssNetworkDataSource,
private val accountDao: AccountDao,
private val groupDao: GroupDao,
@ApplicationScope
private val applicationScope: CoroutineScope,
@DispatcherDefault
private val dispatcherDefault: CoroutineDispatcher,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
workManager: WorkManager,
) : AbstractRssRepository(
context, accountDao, articleDao, groupDao,
feedDao, rssNetworkDataSource, workManager,
dispatcherIO
) {
private val notificationManager: NotificationManager =
(getSystemService(
@ -84,40 +90,38 @@ class LocalRssRepository @Inject constructor(
}
override suspend fun sync() {
mutex.withLock {
withContext(Dispatchers.IO) {
val preTime = System.currentTimeMillis()
val accountId = context.currentAccountId
val articles = mutableListOf<Article>()
feedDao.queryAll(accountId)
.also { feed -> updateSyncState { it.copy(feedCount = feed.size) } }
.map { feed -> async { syncFeed(feed) } }
.awaitAll()
.forEach {
if (it.isNotify) {
notify(it.articles)
}
articles.addAll(it.articles)
applicationScope.launch(dispatcherDefault) {
val preTime = System.currentTimeMillis()
val accountId = context.currentAccountId
val articles = mutableListOf<Article>()
feedDao.queryAll(accountId)
.also { feed -> updateSyncState { it.copy(feedCount = feed.size) } }
.map { feed -> async { syncFeed(feed) } }
.awaitAll()
.forEach {
if (it.isNotify) {
notify(it.articles)
}
articles.addAll(it.articles)
}
articleDao.insertList(articles)
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
accountDao.queryById(accountId)?.let { account ->
accountDao.update(
account.apply {
updateAt = Date()
}
)
}
updateSyncState {
it.copy(
feedCount = 0,
syncedCount = 0,
currentFeedName = ""
)
}
articleDao.insertList(articles)
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
accountDao.queryById(accountId)?.let { account ->
accountDao.update(
account.apply {
updateAt = Date()
}
)
}
}
updateSyncState {
it.copy(
feedCount = 0,
syncedCount = 0,
currentFeedName = ""
)
}
}.join()
}
data class ArticleNotify(
@ -127,10 +131,20 @@ class LocalRssRepository @Inject constructor(
private suspend fun syncFeed(feed: Feed): ArticleNotify {
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
val articles = rssHelper.queryRssXml(feed, latest?.link).also {
if (feed.icon == null && it.isNotEmpty()) {
rssHelper.queryRssIcon(feedDao, feed, it.first().link)
var articles: List<Article>? = null
try {
articles = rssHelper.queryRssXml(feed, latest?.link)
} catch (e: Exception) {
Log.e("RLog", "queryRssXml[${feed.name}]: ${e.message}")
return ArticleNotify(listOf(), false)
}
try {
if (feed.icon == null && !articles.isNullOrEmpty()) {
rssHelper.queryRssIcon(feedDao, feed, articles.first().link)
}
} catch (e: Exception) {
Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
return ArticleNotify(listOf(), false)
}
updateSyncState {
it.copy(

View File

@ -1,19 +1,20 @@
package me.ash.reader.data.repository
import android.content.Context
import android.util.Log
import be.ceau.opml.OpmlWriter
import be.ceau.opml.entity.Body
import be.ceau.opml.entity.Head
import be.ceau.opml.entity.Opml
import be.ceau.opml.entity.Outline
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.R
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.spacerDollar
import java.io.InputStream
import java.util.*
import javax.inject.Inject
@ -25,69 +26,70 @@ class OpmlRepository @Inject constructor(
private val feedDao: FeedDao,
private val accountDao: AccountDao,
private val rssRepository: RssRepository,
private val opmlLocalDataSource: OpmlLocalDataSource
private val opmlLocalDataSource: OpmlLocalDataSource,
private val stringsRepository: StringsRepository,
) {
@Throws(Exception::class)
suspend fun saveToDatabase(inputStream: InputStream) {
try {
val defaultGroup = groupDao.queryById(opmlLocalDataSource.getDefaultGroupId())!!
val groupWithFeedList =
opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup)
groupWithFeedList.forEach { groupWithFeed ->
if (groupWithFeed.group != defaultGroup) {
groupDao.insert(groupWithFeed.group)
}
val repeatList = mutableListOf<Feed>()
groupWithFeed.feeds.forEach {
it.groupId = groupWithFeed.group.id
if (rssRepository.get().isExist(it.url)) {
repeatList.add(it)
}
}
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
val groupWithFeedList =
opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup)
groupWithFeedList.forEach { groupWithFeed ->
if (groupWithFeed.group != defaultGroup) {
groupDao.insert(groupWithFeed.group)
}
} catch (e: Exception) {
Log.e("saveToDatabase", "${e.message}")
val repeatList = mutableListOf<Feed>()
groupWithFeed.feeds.forEach {
it.groupId = groupWithFeed.group.id
if (rssRepository.get().isFeedExist(it.url)) {
repeatList.add(it)
}
}
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
}
}
suspend fun saveToString(): String? =
try {
val defaultGroup = groupDao.queryById(opmlLocalDataSource.getDefaultGroupId())!!
OpmlWriter().write(
Opml(
"2.0",
Head(
accountDao.queryById(context.currentAccountId).name,
Date().toString(), null, null, null,
null, null, null, null,
null, null, null, null,
),
Body(groupDao.queryAllGroupWithFeed(context.currentAccountId).map {
Outline(
mapOf(
"text" to it.group.name,
"title" to it.group.name,
"isDefault" to (it.group.id == defaultGroup.id).toString()
),
it.feeds.map { feed ->
Outline(
mapOf(
"text" to feed.name,
"title" to feed.name,
"xmlUrl" to feed.url,
"htmlUrl" to feed.url,
"isNotification" to feed.isNotification.toString(),
"isFullContent" to feed.isFullContent.toString(),
),
listOf()
)
}
)
})
)
@Throws(Exception::class)
suspend fun saveToString(): String {
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
return OpmlWriter().write(
Opml(
"2.0",
Head(
accountDao.queryById(context.currentAccountId).name,
Date().toString(), null, null, null,
null, null, null, null,
null, null, null, null,
),
Body(groupDao.queryAllGroupWithFeed(context.currentAccountId).map {
Outline(
mapOf(
"text" to it.group.name,
"title" to it.group.name,
"isDefault" to (it.group.id == defaultGroup.id).toString()
),
it.feeds.map { feed ->
Outline(
mapOf(
"text" to feed.name,
"title" to feed.name,
"xmlUrl" to feed.url,
"htmlUrl" to feed.url,
"isNotification" to feed.isNotification.toString(),
"isFullContent" to feed.isFullContent.toString(),
),
listOf()
)
}
)
})
)
} catch (e: Exception) {
Log.e("saveToString", "${e.message}")
null
}
)!!
}
private fun getDefaultGroupId(): String {
val readYouString = stringsRepository.getString(R.string.read_you)
val defaultString = stringsRepository.getString(R.string.defaults)
return context.currentAccountId.spacerDollar(readYouString + defaultString)
}
}

View File

@ -4,17 +4,20 @@ import android.content.Context
import android.text.Html
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.ash.reader.currentAccountId
import me.ash.reader.data.article.Article
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.feed.FeedWithArticle
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.spacerDollar
import net.dankito.readability4j.Readability4J
import net.dankito.readability4j.extended.Readability4JExtended
import okhttp3.*
import java.io.IOException
import okhttp3.OkHttpClient
import okhttp3.Request
import java.text.ParsePosition
import java.text.SimpleDateFormat
import java.util.*
@ -24,19 +27,23 @@ class RssHelper @Inject constructor(
@ApplicationContext
private val context: Context,
private val rssNetworkDataSource: RssNetworkDataSource,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
) {
@Throws(Exception::class)
suspend fun searchFeed(feedLink: String): FeedWithArticle {
val accountId = context.currentAccountId
val parseRss = rssNetworkDataSource.parseRss(feedLink)
val feed = Feed(
id = accountId.spacerDollar(UUID.randomUUID().toString()),
name = parseRss.title!!,
url = feedLink,
groupId = "",
accountId = accountId,
)
return FeedWithArticle(feed, queryRssXml(feed))
return withContext(dispatcherIO) {
val accountId = context.currentAccountId
val parseRss = rssNetworkDataSource.parseRss(feedLink)
val feed = Feed(
id = accountId.spacerDollar(UUID.randomUUID().toString()),
name = parseRss.title!!,
url = feedLink,
groupId = "",
accountId = accountId,
)
FeedWithArticle(feed, queryRssXml(feed))
}
}
fun parseDescriptionContent(link: String, content: String): String {
@ -46,42 +53,39 @@ class RssHelper @Inject constructor(
return element.toString()
}
fun parseFullContent(link: String, title: String, callback: (String) -> Unit) {
OkHttpClient()
.newCall(Request.Builder().url(link).build())
.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
callback(e.message.toString())
@Throws(Exception::class)
suspend fun parseFullContent(link: String, title: String): String {
return withContext(dispatcherIO) {
val response = OkHttpClient()
.newCall(Request.Builder().url(link).build())
.execute()
val content = response.body!!.string()
val readability4J: Readability4J =
Readability4JExtended(link, content)
val articleContent = readability4J.parse().articleContent
if (articleContent == null) {
""
} else {
val h1Element = articleContent.selectFirst("h1")
if (h1Element != null && h1Element.hasText() && h1Element.text() == title) {
h1Element.remove()
}
override fun onResponse(call: Call, response: Response) {
val content = response.body?.string()
val readability4J: Readability4J =
Readability4JExtended(link, content ?: "")
val articleContent = readability4J.parse().articleContent
if (articleContent == null) {
callback("")
} else {
val h1Element = articleContent.selectFirst("h1")
if (h1Element != null && h1Element.hasText() && h1Element.text() == title) {
h1Element.remove()
}
callback(articleContent.toString())
}
}
})
articleContent.toString()
}
}
}
@Throws(Exception::class)
suspend fun queryRssXml(
feed: Feed,
latestLink: String? = null,
): List<Article> {
val a = mutableListOf<Article>()
try {
return withContext(dispatcherIO) {
val a = mutableListOf<Article>()
val accountId = context.currentAccountId
val parseRss = rssNetworkDataSource.parseRss(feed.url)
parseRss.items.forEach {
if (latestLink != null && latestLink == it.link) return a
if (latestLink != null && latestLink == it.link) return@withContext a
Log.i("RLog", "request rss ${feed.name}: ${it.title}")
a.add(
Article(
@ -104,63 +108,57 @@ class RssHelper @Inject constructor(
)
)
}
return a
} catch (e: Exception) {
Log.e("RLog", "error ${feed.name}: ${e.message}")
return a
a
}
}
@Throws(Exception::class)
suspend fun queryRssIcon(
feedDao: FeedDao,
feed: Feed,
articleLink: String?,
articleLink: String,
) {
try {
if (articleLink == null) return
withContext(dispatcherIO) {
val execute = OkHttpClient()
.newCall(Request.Builder().url(articleLink).build())
.execute()
val content = execute.body?.string()
val content = execute.body!!.string()
val regex =
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
if (content != null) {
var iconLink = regex
.find(content)
?.groups?.get(3)
?.value
Log.i("rlog", "queryRssIcon: $iconLink")
if (iconLink != null) {
if (iconLink.startsWith("//")) {
iconLink = "http:$iconLink"
}
if (iconLink.startsWith("/")) {
val domainRegex =
Regex("""http(s)?://(([\w-]+\.)+\w+(:\d{1,5})?)""")
iconLink =
"http://${domainRegex.find(articleLink)?.groups?.get(2)?.value}$iconLink"
}
saveRssIcon(feedDao, feed, iconLink)
} else {
// saveRssIcon(feedDao, feed, "")
var iconLink = regex
.find(content)
?.groups?.get(3)
?.value
Log.i("rlog", "queryRssIcon: $iconLink")
if (iconLink != null) {
if (iconLink.startsWith("//")) {
iconLink = "http:$iconLink"
}
if (iconLink.startsWith("/")) {
val domainRegex =
Regex("""http(s)?://(([\w-]+\.)+\w+(:\d{1,5})?)""")
iconLink =
"http://${domainRegex.find(articleLink)?.groups?.get(2)?.value}$iconLink"
}
saveRssIcon(feedDao, feed, iconLink)
} else {
// saveRssIcon(feedDao, feed, "")
}
} catch (e: Exception) {
Log.e("RLog", "queryRssIcon: ${e.message}")
}
}
private suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
val execute = OkHttpClient()
.newCall(Request.Builder().url(iconLink).build())
.execute()
feedDao.update(
feed.apply {
icon = execute.body?.bytes()
}
)
@Throws(Exception::class)
suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
withContext(dispatcherIO) {
val response = OkHttpClient()
.newCall(Request.Builder().url(iconLink).build())
.execute()
feedDao.update(
feed.apply {
icon = response.body!!.bytes()
}
)
}
}
private fun parseDate(

View File

@ -2,10 +2,8 @@ package me.ash.reader.data.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.DataStoreKeys
import me.ash.reader.currentAccountType
import me.ash.reader.data.account.Account
import me.ash.reader.dataStore
import me.ash.reader.get
import javax.inject.Inject
class RssRepository @Inject constructor(
@ -15,13 +13,11 @@ class RssRepository @Inject constructor(
private val feverRssRepository: FeverRssRepository,
// private val googleReaderRssRepository: GoogleReaderRssRepository,
) {
fun get() = when (getAccountType()) {
fun get() = when (context.currentAccountType) {
Account.Type.LOCAL -> localRssRepository
// Account.Type.LOCAL -> feverRssRepository
Account.Type.FEVER -> feverRssRepository
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
else -> throw IllegalStateException("Unknown account type: ${getAccountType()}")
else -> throw IllegalStateException("Unknown account type: ${context.currentAccountType}")
}
private fun getAccountType(): Int = context.dataStore.get(DataStoreKeys.CurrentAccountType)!!
}

View File

@ -3,11 +3,13 @@ package me.ash.reader.data.source
import android.content.Context
import be.ceau.opml.OpmlParser
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.ash.reader.currentAccountId
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.data.module.DispatcherIO
import java.io.InputStream
import java.util.*
import javax.inject.Inject
@ -15,79 +17,77 @@ import javax.inject.Inject
class OpmlLocalDataSource @Inject constructor(
@ApplicationContext
private val context: Context,
private val stringsRepository: StringsRepository,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
) {
fun getDefaultGroupId(): String {
val readYouString = stringsRepository.getString(R.string.read_you)
val defaultString = stringsRepository.getString(R.string.defaults)
return context.dataStore
.get(DataStoreKeys.CurrentAccountId)!!
.spacerDollar(readYouString + defaultString)
}
@Throws(Exception::class)
suspend fun parseFileInputStream(
inputStream: InputStream,
defaultGroup: Group
): List<GroupWithFeed> {
return withContext(dispatcherIO) {
val accountId = context.currentAccountId
val opml = OpmlParser().parse(inputStream)
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {
it.addGroup(defaultGroup)
}
// @Throws(XmlPullParserException::class, IOException::class)
fun parseFileInputStream(inputStream: InputStream, defaultGroup: Group): List<GroupWithFeed> {
val accountId = context.currentAccountId
val opml = OpmlParser().parse(inputStream)
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {
it.addGroup(defaultGroup)
}
opml.body.outlines.forEach {
// Only feeds
if (it.subElements.isEmpty()) {
// It's a empty group
if (it.attributes["xmlUrl"] == null) {
opml.body.outlines.forEach {
// Only feeds
if (it.subElements.isEmpty()) {
// It's a empty group
if (it.attributes["xmlUrl"] == null) {
if (!it.attributes["isDefault"].toBoolean()) {
groupWithFeedList.addGroup(
Group(
id = UUID.randomUUID().toString(),
name = it.attributes["title"] ?: it.text!!,
accountId = accountId,
)
)
}
} else {
groupWithFeedList.addFeedToDefault(
Feed(
id = UUID.randomUUID().toString(),
name = it.attributes["title"] ?: it.text!!,
url = it.attributes["xmlUrl"]!!,
groupId = defaultGroup.id,
accountId = accountId,
isNotification = it.attributes["isNotification"].toBoolean(),
isFullContent = it.attributes["isFullContent"].toBoolean(),
)
)
}
} else {
var groupId = defaultGroup.id
if (!it.attributes["isDefault"].toBoolean()) {
groupId = UUID.randomUUID().toString()
groupWithFeedList.addGroup(
Group(
id = UUID.randomUUID().toString(),
id = groupId,
name = it.attributes["title"] ?: it.text!!,
accountId = accountId,
)
)
}
} else {
groupWithFeedList.addFeedToDefault(
Feed(
id = UUID.randomUUID().toString(),
name = it.attributes["title"] ?: it.text!!,
url = it.attributes["xmlUrl"]!!,
groupId = defaultGroup.id,
accountId = accountId,
isNotification = it.attributes["isNotification"].toBoolean(),
isFullContent = it.attributes["isFullContent"].toBoolean(),
it.subElements.forEach { outline ->
groupWithFeedList.addFeed(
Feed(
id = UUID.randomUUID().toString(),
name = outline.attributes["title"] ?: outline.text!!,
url = outline.attributes["xmlUrl"]!!,
groupId = groupId,
accountId = accountId,
isNotification = outline.attributes["isNotification"].toBoolean(),
isFullContent = outline.attributes["isFullContent"].toBoolean(),
)
)
)
}
} else {
var groupId = defaultGroup.id
if (!it.attributes["isDefault"].toBoolean()) {
groupId = UUID.randomUUID().toString()
groupWithFeedList.addGroup(
Group(
id = groupId,
name = it.attributes["title"] ?: it.text!!,
accountId = accountId,
)
)
}
it.subElements.forEach { outline ->
groupWithFeedList.addFeed(
Feed(
id = UUID.randomUUID().toString(),
name = outline.attributes["title"] ?: outline.text!!,
url = outline.attributes["xmlUrl"]!!,
groupId = groupId,
accountId = accountId,
isNotification = outline.attributes["isNotification"].toBoolean(),
isFullContent = outline.attributes["isFullContent"].toBoolean(),
)
)
}
}
}
groupWithFeedList
}
return groupWithFeedList
}
private fun MutableList<GroupWithFeed>.addGroup(group: Group) {

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.drawer.feed
import android.util.Log
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi
@ -8,10 +9,10 @@ import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material.icons.rounded.RssFeed
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -31,7 +32,6 @@ fun FeedOptionDrawer(
viewModel: FeedOptionViewModel = hiltViewModel(),
content: @Composable () -> Unit = {},
) {
val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue()
val feed = viewState.feed

View File

@ -49,14 +49,22 @@ class FeedsViewModel @Inject constructor(
private fun importFromInputStream(inputStream: InputStream) {
viewModelScope.launch(Dispatchers.IO) {
opmlRepository.saveToDatabase(inputStream)
rssRepository.get().doSync()
try {
opmlRepository.saveToDatabase(inputStream)
rssRepository.get().doSync()
} catch (e: Exception) {
Log.e("FeedsViewModel", "importFromInputStream: ", e)
}
}
}
private fun exportAsOpml(callback: (String) -> Unit = {}) {
viewModelScope.launch(Dispatchers.Default) {
opmlRepository.saveToString()?.let { callback(it) }
try {
callback(opmlRepository.saveToString())
} catch (e: Exception) {
Log.e("FeedsViewModel", "exportAsOpml: ", e)
}
}
}
@ -74,7 +82,6 @@ class FeedsViewModel @Inject constructor(
rssRepository.get().pullFeeds(),
rssRepository.get().pullImportant(isStarred, isUnread),
) { groupWithFeedList, importantList ->
Log.i("RLog", "thread:combine ${Thread.currentThread().name}")
val groupImportantMap = mutableMapOf<String, Int>()
val feedImportantMap = mutableMapOf<String, Int>()
importantList.groupBy { it.groupId }.forEach { (i, list) ->
@ -109,8 +116,6 @@ class FeedsViewModel @Inject constructor(
}.onStart {
}.onEach { groupWithFeedList ->
Log.i("RLog", "thread:onEach ${Thread.currentThread().name}")
_viewState.update {
it.copy(
filter = when {

View File

@ -148,7 +148,7 @@ class SubscribeViewModel @Inject constructor(
lockLinkInput = true,
)
}
if (rssRepository.get().isExist(_viewState.value.linkContent)) {
if (rssRepository.get().isFeedExist(_viewState.value.linkContent)) {
_viewState.update {
it.copy(
title = stringsRepository.getString(R.string.subscribe),

View File

@ -1,6 +1,5 @@
package me.ash.reader.ui.page.home.flow
import android.util.Log
import androidx.compose.foundation.lazy.LazyListState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -55,7 +54,6 @@ class FlowViewModel @Inject constructor(
_viewState.update {
it.copy(
pagingData = Pager(PagingConfig(pageSize = 10)) {
Log.i("RLog", "thread:Pager ${Thread.currentThread().name}")
rssRepository.get().pullArticles(
groupId = filterState.group?.id,
feedId = filterState.feed?.id,

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.read
import android.util.Log
import androidx.compose.foundation.lazy.LazyListState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -55,12 +56,23 @@ class ReadViewModel @Inject constructor(
private fun renderFullContent() {
changeLoading(true)
rssHelper.parseFullContent(
_viewState.value.articleWithFeed?.article?.link ?: "",
_viewState.value.articleWithFeed?.article?.title ?: ""
) { content ->
_viewState.update {
it.copy(content = content)
viewModelScope.launch {
try {
_viewState.update {
it.copy(
content = rssHelper.parseFullContent(
_viewState.value.articleWithFeed?.article?.link ?: "",
_viewState.value.articleWithFeed?.article?.title ?: ""
)
)
}
} catch (e: Exception) {
Log.i("RLog", "renderFullContent: ${e.message}")
_viewState.update {
it.copy(
content = e.message
)
}
}
}
}