Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 60 additions & 24 deletions app/src/main/java/me/ash/reader/domain/data/DiffMapHolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import me.ash.reader.domain.model.account.Account
import me.ash.reader.domain.model.account.AccountType
Expand Down Expand Up @@ -64,6 +66,8 @@ class DiffMapHolder @Inject constructor(

private val cacheFile: File get() = userCacheDir.resolve("diff_map.json")

private val writeMutex = Mutex()

var dbJob: Job? = null
var remoteJob: Job? = null

Expand Down Expand Up @@ -199,25 +203,53 @@ class DiffMapHolder @Inject constructor(

fun commitDiffsToDb() {
applicationScope.launch(ioDispatcher) {
val markAsReadArticles = diffMap.filter { !it.value.isUnread }.map { it.key }.toSet()
val markAsUnreadArticles = diffMap.filter { it.value.isUnread }.map { it.key }.toSet()
clearDiffs()
val snapshot = diffMap.toMap()
val markAsReadArticles = snapshot.filter { !it.value.isUnread }.map { it.key }.toSet()
val markAsUnreadArticles = snapshot.filter { it.value.isUnread }.map { it.key }.toSet()

rssService.get().batchMarkAsRead(articleIds = markAsReadArticles, isUnread = false)
rssService.get().batchMarkAsRead(articleIds = markAsUnreadArticles, isUnread = true)

writeMutex.withLock {
val keysToRemove = markAsReadArticles + markAsUnreadArticles
keysToRemove.forEach { key ->
val current = diffMap[key]
if (current != null) {
val committedState = if (markAsReadArticles.contains(key)) false else true
if (current.isUnread == committedState) {
diffMap.remove(key)
}
}
}

if (diffMap.isEmpty()) {
if (cacheFile.exists()) cacheFile.delete()
} else {
try {
val tmpJson = gson.toJson(diffMap)
userCacheDir.mkdirs()
if (!cacheFile.exists()) cacheFile.createNewFile()
cacheFile.writeText(tmpJson)
} catch (_: Exception) {
}
}
}
}
}

private fun writeDiffsToCache() {
fun writeDiffsToCache() {
applicationScope.launch(ioDispatcher) {
try {
val tmpJson = gson.toJson(diffMap)
userCacheDir.mkdirs()
cacheFile.createNewFile()
if (cacheFile.exists() && cacheFile.canWrite()) {
cacheFile.writeText(tmpJson)
}
} catch (_: Exception) {
writeMutex.withLock {
try {
val tmpJson = gson.toJson(diffMap)
userCacheDir.mkdirs()
cacheFile.createNewFile()
if (cacheFile.exists() && cacheFile.canWrite()) {
cacheFile.writeText(tmpJson)
}
} catch (_: Exception) {

}
}
}
}
Expand Down Expand Up @@ -256,15 +288,17 @@ class DiffMapHolder @Inject constructor(

private fun commitDiffsFromCache() {
applicationScope.launch(ioDispatcher) {
if (cacheFile.exists() && cacheFile.canRead()) {
val tmpJson = cacheFile.readText()
val mapType = object : TypeToken<Map<String, Diff>>() {}.type
val diffMapFromCache = gson.fromJson<Map<String, Diff>>(
tmpJson, mapType
)
diffMapFromCache?.let {
diffMap.clear()
diffMap.putAll(it)
writeMutex.withLock {
if (cacheFile.exists() && cacheFile.canRead()) {
val tmpJson = cacheFile.readText()
val mapType = object : TypeToken<Map<String, Diff>>() {}.type
val diffMapFromCache = gson.fromJson<Map<String, Diff>>(
tmpJson, mapType
)
diffMapFromCache?.let {
diffMap.clear()
diffMap.putAll(it)
}
}
}
}.invokeOnCompletion {
Expand All @@ -274,10 +308,12 @@ class DiffMapHolder @Inject constructor(

private fun clearDiffs() {
applicationScope.launch(ioDispatcher) {
if (cacheFile.exists() && cacheFile.canWrite()) {
cacheFile.delete()
writeMutex.withLock {
if (cacheFile.exists() && cacheFile.canWrite()) {
cacheFile.delete()
}
diffMap.clear()
}
diffMap.clear()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ constructor(
.setStyle(NotificationCompat.InboxStyle().setSummaryText(feed.name))
.setGroup(feed.id)
.setGroupSummary(true)
.setContentIntent(
PendingIntent.getActivity(
context,
feed.id.hashCode(),
Intent(context, MainActivity::class.java).apply {
flags =
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra(ExtraName.FEED_ID, feed.id)
putExtra(ExtraName.ACCOUNT_ID, feed.accountId)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
)
.build(),
)

Expand All @@ -89,25 +104,31 @@ constructor(
.setContentIntent(
PendingIntent.getActivity(
context,
Random().nextInt() + article.id.hashCode(),
article.id.hashCode(),
Intent(context, MainActivity::class.java).apply {
flags =
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra(ExtraName.ARTICLE_ID, article.id)
putExtra(ExtraName.ACCOUNT_ID, article.accountId)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
)
.setGroup(feed.id)
notificationManager.notify(
Random().nextInt() + article.id.hashCode(),
article.id.hashCode(),
builder.build(),
)
}
}
}

fun cancel(articleId: String) {
notificationManager.cancel(articleId.hashCode())
}

fun notify(feedWithArticle: FeedWithArticle) {
notify(feedWithArticle.feed, feedWithArticle.articles)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import be.ceau.opml.OpmlParser
import be.ceau.opml.entity.Outline
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.ash.reader.domain.model.feed.Feed
import me.ash.reader.domain.model.group.Group
import me.ash.reader.domain.model.group.GroupWithFeed
Expand All @@ -28,7 +29,11 @@ class OPMLDataSource @Inject constructor(
defaultGroup: Group,
targetAccountId: Int,
): List<GroupWithFeed> {
val opml = OpmlParser().parse(inputStream)
val opml = withContext(ioDispatcher) {
val content = inputStream.bufferedReader().use { it.readText() }
val sanitizedContent = content.replace(Regex("&(?!(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);)"), "&amp;")
OpmlParser().parse(java.io.ByteArrayInputStream(sanitizedContent.toByteArray(Charsets.UTF_8)))
}
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {
it.addGroup(defaultGroup)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import me.ash.reader.domain.service.LocalRssService
import me.ash.reader.domain.service.RssService
import me.ash.reader.domain.service.SyncWorker
import me.ash.reader.infrastructure.android.AndroidImageDownloader
import me.ash.reader.infrastructure.android.NotificationHelper
import me.ash.reader.infrastructure.android.TextToSpeechManager
import me.ash.reader.infrastructure.di.ApplicationScope
import me.ash.reader.infrastructure.di.IODispatcher
Expand All @@ -65,6 +66,7 @@ constructor(
private val readerCacheHelper: ReaderCacheHelper,
val textToSpeechManager: TextToSpeechManager,
private val imageDownloader: AndroidImageDownloader,
private val notificationHelper: NotificationHelper,
private val articleListUseCase: ArticlePagingListUseCase,
workManager: WorkManager,
) : ViewModel() {
Expand Down Expand Up @@ -147,6 +149,24 @@ constructor(
conditions: MarkAsReadConditions,
isUnread: Boolean,
) {
if (!isUnread) {
val idsToCancel =
if (articleId != null) {
listOf(articleId)
} else {
val date = conditions.toDate()
articleListUseCase.itemSnapshotList.items
.filterIsInstance<ArticleFlowItem.Article>()
.map { it.articleWithFeed.article }
.filter {
val matchesDate = if (date == null) true else it.date < date
it.isUnread && matchesDate
}
.map { it.id }
}

applicationScope.launch(ioDispatcher) { idsToCancel.forEach { notificationHelper.cancel(it) } }
}
applicationScope.launch(ioDispatcher) {
rssService
.get()
Expand Down Expand Up @@ -183,7 +203,9 @@ constructor(
}
.distinctBy { it.article.id }

items.forEach { notificationHelper.cancel(it.article.id) }
diffMapHolder.updateDiff(articleWithFeed = items.toTypedArray(), isUnread = false)
diffMapHolder.writeDiffsToCache()
}
}

Expand All @@ -206,7 +228,35 @@ constructor(
.filterIsInstance<ArticleFlowItem.Article>()
.map { it.articleWithFeed }

items.forEach { notificationHelper.cancel(it.article.id) }
diffMapHolder.updateDiff(articleWithFeed = items.toTypedArray(), isUnread = false)
diffMapHolder.writeDiffsToCache()
}
}

fun toggleReadStatus(articleWithFeed: ArticleWithFeed) {
if (diffMapHolder.checkIfUnread(articleWithFeed)) {
notificationHelper.cancel(articleWithFeed.article.id)
}
viewModelScope.launch(ioDispatcher) {
diffMapHolder.updateDiff(articleWithFeed)
diffMapHolder.writeDiffsToCache()
}
}

fun markAsRead(articleWithFeed: ArticleWithFeed) {
notificationHelper.cancel(articleWithFeed.article.id)
viewModelScope.launch(ioDispatcher) {
diffMapHolder.updateDiff(articleWithFeed, isUnread = false)
diffMapHolder.writeDiffsToCache()
}
}

fun markAsReadList(articles: List<ArticleWithFeed>) {
articles.forEach { notificationHelper.cancel(it.article.id) }
viewModelScope.launch(ioDispatcher) {
diffMapHolder.updateDiff(articleWithFeed = articles.toTypedArray(), isUnread = false)
diffMapHolder.writeDiffsToCache()
}
}

Expand Down Expand Up @@ -295,6 +345,8 @@ constructor(

if (diffMapHolder.checkIfUnread(item)) {
diffMapHolder.updateDiff(item, isUnread = false)
diffMapHolder.writeDiffsToCache()
notificationHelper.cancel(item.article.id)
}
item.run {
_readingUiState.update {
Expand Down
12 changes: 3 additions & 9 deletions app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ fun FlowPage(
}

val onToggleRead: (ArticleWithFeed) -> Unit = remember {
{ articleWithFeed -> viewModel.diffMapHolder.updateDiff(articleWithFeed) }
{ articleWithFeed -> viewModel.toggleReadStatus(articleWithFeed) }
}

val sortByEarliest =
Expand Down Expand Up @@ -544,10 +544,7 @@ fun FlowPage(
}
}
if (items.isNotEmpty() && found) {
viewModel.diffMapHolder.updateDiff(
articleWithFeed = items.toTypedArray(),
isUnread = false,
)
viewModel.markAsReadList(items)
}
}
}
Expand Down Expand Up @@ -674,10 +671,7 @@ fun FlowPage(
isSwipeEnabled = { listState.isScrollInProgress },
onClick = { articleWithFeed, index ->
if (articleWithFeed.feed.isBrowser) {
viewModel.diffMapHolder.updateDiff(
articleWithFeed,
isUnread = false,
)
viewModel.markAsRead(articleWithFeed)
context.openURL(
articleWithFeed.article.link,
openLink,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package me.ash.reader.infrastructure.rss

import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import me.ash.reader.domain.model.group.Group
import me.ash.reader.domain.model.group.GroupWithFeed
Expand Down Expand Up @@ -30,8 +31,7 @@ class OPMLDataSourceTest {
@Mock
private lateinit var mockContext: Context

@Mock
private lateinit var mockIODispatcher: CoroutineDispatcher
private lateinit var ioDispatcher: CoroutineDispatcher

private lateinit var opmlDataSource: OPMLDataSource

Expand All @@ -42,8 +42,8 @@ class OPMLDataSourceTest {
@Before
fun setUp() {
mockContext = mock<Context> { }
mockIODispatcher = mock<CoroutineDispatcher> {}
opmlDataSource = OPMLDataSource(mockContext, mockIODispatcher)
ioDispatcher = Dispatchers.Unconfined
opmlDataSource = OPMLDataSource(mockContext, ioDispatcher)
}

private fun fill(value: String): String = OPML_TEMPLATE.replace("{{var}}", value)
Expand Down
Loading