diff --git a/app/src/main/java/me/ash/reader/App.kt b/app/src/main/java/me/ash/reader/App.kt index a6879d4..e0a8903 100644 --- a/app/src/main/java/me/ash/reader/App.kt +++ b/app/src/main/java/me/ash/reader/App.kt @@ -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() } diff --git a/app/src/main/java/me/ash/reader/DataStoreExt.kt b/app/src/main/java/me/ash/reader/DataStoreExt.kt index 8d67d13..142f246 100644 --- a/app/src/main/java/me/ash/reader/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/DataStoreExt.kt @@ -17,6 +17,8 @@ import java.io.IOException val Context.dataStore: DataStore 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 DataStore.put(dataStoreKeys: DataStoreKeys, value: T) { this.edit { diff --git a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt index 7d4d027..a0253e1 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt @@ -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> { - return groupDao.queryAllGroup(context.currentAccountId).flowOn(Dispatchers.IO) + return groupDao.queryAllGroup(context.currentAccountId).flowOn(dispatcherIO) } fun pullFeeds(): Flow> { - 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 { - 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> { - 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() } diff --git a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt index b26cc60..6a64c2a 100644 --- a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt @@ -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() + 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() - 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
() - 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
() + 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 = "" - ) } } } diff --git a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt index 503502d..ad7daf1 100644 --- a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt @@ -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
() - 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
() + 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
? = 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( diff --git a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt index a115539..cd52a3e 100644 --- a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt @@ -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() - 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() + 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) + } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt index f19fe8d..faad558 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt @@ -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
{ - val a = mutableListOf
() - try { + return withContext(dispatcherIO) { + val a = mutableListOf
() 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(""" 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)!! } diff --git a/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt b/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt index 01be475..54a7e2d 100644 --- a/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt +++ b/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt @@ -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 { + return withContext(dispatcherIO) { + val accountId = context.currentAccountId + val opml = OpmlParser().parse(inputStream) + val groupWithFeedList = mutableListOf().also { + it.addGroup(defaultGroup) + } - // @Throws(XmlPullParserException::class, IOException::class) - fun parseFileInputStream(inputStream: InputStream, defaultGroup: Group): List { - val accountId = context.currentAccountId - val opml = OpmlParser().parse(inputStream) - val groupWithFeedList = mutableListOf().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.addGroup(group: Group) { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionDrawer.kt index f5c738c..e922894 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionDrawer.kt @@ -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 diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt index 984c726..04183e3 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt @@ -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() val feedImportantMap = mutableMapOf() 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 { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt index c53154c..e8c79aa 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt @@ -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), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt index 70f9906..bf32807 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt @@ -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, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt index 6d9d27d..13a5b28 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt @@ -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 + ) + } } } }