Refactor data layer

This commit is contained in:
Ash 2022-05-17 20:39:07 +08:00
parent ee55f671bc
commit b33e0a7ac5
31 changed files with 335 additions and 433 deletions

View File

@ -5,9 +5,6 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- Disable automatic updates in F-Droid -->
<!-- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />-->
<application <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"

View File

@ -16,12 +16,9 @@ import me.ash.reader.data.source.AppNetworkDataSource
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.ui.ext.* import me.ash.reader.ui.ext.*
import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import java.io.File
import java.security.Security import java.security.Security
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
@ -50,6 +47,9 @@ class App : Application(), Configuration.Provider {
@Inject @Inject
lateinit var rssHelper: RssHelper lateinit var rssHelper: RssHelper
@Inject
lateinit var notificationHelper: NotificationHelper
@Inject @Inject
lateinit var appRepository: AppRepository lateinit var appRepository: AppRepository
@ -62,9 +62,6 @@ class App : Application(), Configuration.Provider {
@Inject @Inject
lateinit var localRssRepository: LocalRssRepository lateinit var localRssRepository: LocalRssRepository
// @Inject
// lateinit var feverRssRepository: FeverRssRepository
@Inject @Inject
lateinit var opmlRepository: OpmlRepository lateinit var opmlRepository: OpmlRepository
@ -92,7 +89,7 @@ class App : Application(), Configuration.Provider {
applicationScope.launch(dispatcherDefault) { applicationScope.launch(dispatcherDefault) {
accountInit() accountInit()
workerInit() workerInit()
if (BuildConfig.FLAVOR != "fdroid") { if (notFdroid) {
checkUpdate() checkUpdate()
} }
} }
@ -128,28 +125,3 @@ class App : Application(), Configuration.Provider {
.setMinimumLoggingLevel(android.util.Log.DEBUG) .setMinimumLoggingLevel(android.util.Log.DEBUG)
.build() .build()
} }
fun cachingHttpClient(
cacheDirectory: File? = null,
cacheSize: Long = 10L * 1024L * 1024L,
trustAllCerts: Boolean = true,
connectTimeoutSecs: Long = 30L,
readTimeoutSecs: Long = 30L
): OkHttpClient {
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
if (cacheDirectory != null) {
builder.cache(Cache(cacheDirectory, cacheSize))
}
builder
.connectTimeout(connectTimeoutSecs, TimeUnit.SECONDS)
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
.followRedirects(true)
// if (trustAllCerts) {
// builder.trustAllCerts()
// }
return builder.build()
}

View File

@ -5,7 +5,7 @@ import androidx.room.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import me.ash.reader.data.entity.Article import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.entity.ImportantCount import me.ash.reader.data.model.ImportantCount
import java.util.* import java.util.*
@Dao @Dao

View File

@ -1,23 +0,0 @@
package me.ash.reader.data.entity
data class LatestRelease(
val html_url: String? = null,
val tag_name: String? = null,
val name: String? = null,
val draft: Boolean? = null,
val prerelease: Boolean? = null,
val created_at: String? = null,
val published_at: String? = null,
val assets: List<AssetsItem>? = null,
val body: String? = null,
)
data class AssetsItem(
val name: String? = null,
val content_type: String? = null,
val size: Int? = null,
val download_count: Int? = null,
val created_at: String? = null,
val updated_at: String? = null,
val browser_download_url: String? = null,
)

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity package me.ash.reader.data.model
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FiberManualRecord import androidx.compose.material.icons.outlined.FiberManualRecord
@ -6,7 +6,10 @@ import androidx.compose.material.icons.rounded.FiberManualRecord
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subject import androidx.compose.material.icons.rounded.Subject
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
class Filter( class Filter(
val index: Int, val index: Int,
@ -33,5 +36,13 @@ class Filter(
iconOutline = Icons.Rounded.Subject, iconOutline = Icons.Rounded.Subject,
iconFilled = Icons.Rounded.Subject, iconFilled = Icons.Rounded.Subject,
) )
val values = listOf(Starred, Unread, All)
} }
}
@Composable
fun Filter.getName(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread)
Filter.Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all)
} }

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity package me.ash.reader.data.model
data class ImportantCount( data class ImportantCount(
val important: Int, val important: Int,

View File

@ -0,0 +1,2 @@
package me.ash.reader.data.model

View File

@ -1,4 +1,4 @@
package me.ash.reader.data.entity package me.ash.reader.data.model
class Version(identifiers: List<String>) { class Version(identifiers: List<String>) {
private var major: Int = 0 private var major: Int = 0

View File

@ -1,3 +1,23 @@
/*
* Feeder: Android RSS reader app
* https://gitlab.com/spacecowboy/Feeder
*
* Copyright (C) 2022 Jonas Kalderstam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.ash.reader.data.module package me.ash.reader.data.module
import android.content.Context import android.content.Context
@ -7,11 +27,20 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import me.ash.reader.BuildConfig import me.ash.reader.BuildConfig
import me.ash.reader.cachingHttpClient import okhttp3.Cache
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import java.io.File
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -28,6 +57,56 @@ object OkHttpClientModule {
.build() .build()
} }
fun cachingHttpClient(
cacheDirectory: File? = null,
cacheSize: Long = 10L * 1024L * 1024L,
trustAllCerts: Boolean = true,
connectTimeoutSecs: Long = 30L,
readTimeoutSecs: Long = 30L
): OkHttpClient {
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
if (cacheDirectory != null) {
builder.cache(Cache(cacheDirectory, cacheSize))
}
builder
.connectTimeout(connectTimeoutSecs, TimeUnit.SECONDS)
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
.followRedirects(true)
if (trustAllCerts) {
builder.trustAllCerts()
}
return builder.build()
}
fun OkHttpClient.Builder.trustAllCerts() {
try {
val trustManager = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
val sslSocketFactory = sslContext.socketFactory
sslSocketFactory(sslSocketFactory, trustManager)
.hostnameVerifier(HostnameVerifier { _, _ -> true })
} catch (e: NoSuchAlgorithmException) {
// ignore
} catch (e: KeyManagementException) {
// ignore
}
}
object UserAgentInterceptor : Interceptor { object UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed( return chain.proceed(

View File

@ -2,11 +2,11 @@ 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 dagger.assisted.Assisted import androidx.work.ExistingPeriodicWorkPolicy
import dagger.assisted.AssistedInject import androidx.work.ListenableWorker
import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ -15,9 +15,9 @@ import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.* import me.ash.reader.data.entity.*
import me.ash.reader.data.model.ImportantCount
import me.ash.reader.ui.ext.currentAccountId import me.ash.reader.ui.ext.currentAccountId
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
abstract class AbstractRssRepository constructor( abstract class AbstractRssRepository constructor(
private val context: Context, private val context: Context,
@ -130,10 +130,6 @@ abstract class AbstractRssRepository constructor(
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty() return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
} }
fun peekWork(): String {
return workManager.getWorkInfosByTag("sync").get().size.toString()
}
suspend fun updateGroup(group: Group) { suspend fun updateGroup(group: Group) {
groupDao.update(group) groupDao.update(group)
} }
@ -207,34 +203,3 @@ abstract class AbstractRssRepository constructor(
} }
} }
} }
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val rssRepository: RssRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ")
return rssRepository.get().sync(this)
}
companion object {
const val WORK_NAME = "article.sync"
val UUID: UUID
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.build()
).addTag(WORK_NAME).build().also {
UUID = it.id
}
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
}
}

View File

@ -4,13 +4,11 @@ import android.content.Context
import android.util.Log import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.model.toVersion
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherIO import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.module.DispatcherMain import me.ash.reader.data.module.DispatcherMain
import me.ash.reader.data.source.AppNetworkDataSource import me.ash.reader.data.source.AppNetworkDataSource
@ -23,8 +21,6 @@ class AppRepository @Inject constructor(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val appNetworkDataSource: AppNetworkDataSource, private val appNetworkDataSource: AppNetworkDataSource,
@ApplicationScope
private val applicationScope: CoroutineScope,
@DispatcherIO @DispatcherIO
private val dispatcherIO: CoroutineDispatcher, private val dispatcherIO: CoroutineDispatcher,
@DispatcherMain @DispatcherMain
@ -55,14 +51,8 @@ class AppRepository @Inject constructor(
val currentVersion = context.getCurrentVersion() val currentVersion = context.getCurrentVersion()
val latestLog = latest.body ?: "" val latestLog = latest.body ?: ""
val latestPublishDate = latest.published_at ?: latest.created_at ?: "" val latestPublishDate = latest.published_at ?: latest.created_at ?: ""
val latestSize = latest.assets val latestSize = latest.assets?.first()?.size ?: 0
?.first() val latestDownloadUrl = latest.assets?.first()?.browser_download_url ?: ""
?.size
?: 0
val latestDownloadUrl = latest.assets
?.first()
?.browser_download_url
?: ""
Log.i("RLog", "current version $currentVersion") Log.i("RLog", "current version $currentVersion")
if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) { if (latestVersion.whetherNeedUpdate(currentVersion, skipVersion)) {

View File

@ -1,163 +0,0 @@
//package me.ash.reader.data.repository
//
//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.data.dao.AccountDao
//import me.ash.reader.data.dao.ArticleDao
//import me.ash.reader.data.dao.FeedDao
//import me.ash.reader.data.dao.GroupDao
//import me.ash.reader.data.entity.Article
//import me.ash.reader.data.entity.Feed
//import me.ash.reader.data.entity.Group
//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.ui.ext.currentAccountId
//import me.ash.reader.ui.ext.spacerDollar
//import net.dankito.readability4j.extended.Readability4JExtended
//import java.util.*
//import javax.inject.Inject
//import kotlin.collections.set
//
//class FeverRssRepository @Inject constructor(
// @ApplicationContext
// private val context: Context,
// private val articleDao: ArticleDao,
// private val feedDao: FeedDao,
// private val groupDao: GroupDao,
// private val rssHelper: RssHelper,
// 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)
// }
//
// override suspend fun subscribe(feed: Feed, articles: List<Article>) {
// feedDao.insert(feed)
// articleDao.insertList(articles.map {
// it.copy(feedId = feed.id)
// })
// }
//
// override suspend fun addGroup(name: String): String {
// return UUID.randomUUID().toString().also {
// groupDao.insert(
// Group(
// id = it,
// name = name,
// accountId = context.currentAccountId
// )
// )
// }
// }
//
// override suspend fun sync() {
// applicationScope.launch(dispatcherDefault) {
// mutex.withLock {
// val accountId = context.currentAccountId
//
// 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(
// 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,
// 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,
// )
// )
// }
// articleDao.insertList(articles)
//
// // Complete sync
// accountDao.update(accountDao.queryById(accountId)!!.apply {
// updateAt = Date()
// })
// updateSyncState {
// it.copy(
// feedCount = 0,
// syncedCount = 0,
// currentFeedName = ""
// )
// }
// }
// }
// }
//}

View File

@ -1,12 +1,7 @@
package me.ash.reader.data.repository package me.ash.reader.data.repository
import android.app.*
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker import androidx.work.ListenableWorker
import androidx.work.WorkManager import androidx.work.WorkManager
@ -15,8 +10,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.ash.reader.MainActivity
import me.ash.reader.R
import me.ash.reader.data.dao.AccountDao import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.FeedDao
@ -30,8 +23,6 @@ import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
import me.ash.reader.ui.ext.currentAccountId import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar import me.ash.reader.ui.ext.spacerDollar
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -41,6 +32,7 @@ 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,
private val notificationHelper: NotificationHelper,
private val accountDao: AccountDao, private val accountDao: AccountDao,
private val groupDao: GroupDao, private val groupDao: GroupDao,
@DispatcherDefault @DispatcherDefault
@ -52,16 +44,6 @@ class LocalRssRepository @Inject constructor(
context, accountDao, articleDao, groupDao, context, accountDao, articleDao, groupDao,
feedDao, workManager, dispatcherIO feedDao, workManager, dispatcherIO
) { ) {
private val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(context).apply {
createNotificationChannel(
NotificationChannel(
NotificationGroupName.ARTICLE_UPDATE,
NotificationGroupName.ARTICLE_UPDATE,
NotificationManager.IMPORTANCE_DEFAULT
)
)
}
override suspend fun updateArticleInfo(article: Article) { override suspend fun updateArticleInfo(article: Article) {
articleDao.update(article) articleDao.update(article)
@ -98,7 +80,7 @@ class LocalRssRepository @Inject constructor(
.awaitAll() .awaitAll()
.forEach { .forEach {
if (it.isNotify) { if (it.isNotify) {
notify( notificationHelper.notify(
FeedWithArticle( FeedWithArticle(
it.feedWithArticle.feed, it.feedWithArticle.feed,
articleDao.insertIfNotExist(it.feedWithArticle.articles) articleDao.insertIfNotExist(it.feedWithArticle.articles)
@ -183,75 +165,4 @@ class LocalRssRepository @Inject constructor(
isNotify = articles.isNotEmpty() && feed.isNotification isNotify = articles.isNotEmpty() && feed.isNotification
) )
} }
private fun notify(
feedWithArticle: FeedWithArticle,
) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(
feedWithArticle.feed.id,
feedWithArticle.feed.name
)
)
feedWithArticle.articles.forEach { article ->
val builder = NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setContentTitle(article.title)
.setContentIntent(
PendingIntent.getActivity(
context,
Random().nextInt() + article.id.hashCode(),
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(
ExtraName.ARTICLE_ID,
article.id
)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setGroup(feedWithArticle.feed.id)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(article.shortDescription)
.setSummaryText(feedWithArticle.feed.name)
)
notificationManager.notify(
Random().nextInt() + article.id.hashCode(),
builder.build().apply {
flags = Notification.FLAG_AUTO_CANCEL
}
)
}
if (feedWithArticle.articles.size > 1) {
notificationManager.notify(
Random().nextInt() + feedWithArticle.feed.id.hashCode(),
NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setStyle(
NotificationCompat.InboxStyle()
.setSummaryText(feedWithArticle.feed.name)
)
.setGroup(feedWithArticle.feed.id)
.setGroupSummary(true)
.build()
)
}
}
} }

View File

@ -0,0 +1,103 @@
package me.ash.reader.data.repository
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.MainActivity
import me.ash.reader.R
import me.ash.reader.data.entity.FeedWithArticle
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.*
import javax.inject.Inject
class NotificationHelper @Inject constructor(
@ApplicationContext
private val context: Context,
) {
private val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(context).apply {
createNotificationChannel(
NotificationChannel(
NotificationGroupName.ARTICLE_UPDATE,
NotificationGroupName.ARTICLE_UPDATE,
NotificationManager.IMPORTANCE_DEFAULT
)
)
}
fun notify(
feedWithArticle: FeedWithArticle,
) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(
feedWithArticle.feed.id,
feedWithArticle.feed.name
)
)
feedWithArticle.articles.forEach { article ->
val builder = NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setContentTitle(article.title)
.setContentIntent(
PendingIntent.getActivity(
context,
Random().nextInt() + article.id.hashCode(),
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(
ExtraName.ARTICLE_ID,
article.id
)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setGroup(feedWithArticle.feed.id)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(article.shortDescription)
.setSummaryText(feedWithArticle.feed.name)
)
notificationManager.notify(
Random().nextInt() + article.id.hashCode(),
builder.build().apply {
flags = Notification.FLAG_AUTO_CANCEL
}
)
}
if (feedWithArticle.articles.size > 1) {
notificationManager.notify(
Random().nextInt() + feedWithArticle.feed.id.hashCode(),
NotificationCompat.Builder(context, NotificationGroupName.ARTICLE_UPDATE)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(
(BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_notification
))
)
.setStyle(
NotificationCompat.InboxStyle()
.setSummaryText(feedWithArticle.feed.name)
)
.setGroup(feedWithArticle.feed.id)
.setGroupSummary(true)
.build()
)
}
}
}

View File

@ -43,7 +43,7 @@ class OpmlRepository @Inject constructor(
repeatList.add(it) repeatList.add(it)
} }
} }
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList()) feedDao.insertList((groupWithFeed.feeds subtract repeatList.toSet()).toList())
} }
} }

View File

@ -183,27 +183,4 @@ class RssHelper @Inject constructor(
} }
) )
} }
private fun parseDate(
inputDate: String, patterns: Array<String> = arrayOf(
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm:ss",
"yyyyMMdd",
"yyyy/MM/dd",
"yyyy年MM月dd日",
"yyyy MM dd",
)
): Date? {
val df = SimpleDateFormat()
for (pattern in patterns) {
df.applyPattern(pattern)
df.isLenient = false
val date = df.parse(inputDate, ParsePosition(0))
if (date != null) {
return date
}
}
return null
}
} }

View File

@ -0,0 +1,41 @@
package me.ash.reader.data.repository
import android.content.Context
import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.*
import java.util.concurrent.TimeUnit
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val rssRepository: RssRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ")
return rssRepository.get().sync(this)
}
companion object {
const val WORK_NAME = "article.sync"
val UUID: UUID
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.build()
).addTag(WORK_NAME).build().also {
UUID = it.id
}
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
}
}

View File

@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import me.ash.reader.data.entity.LatestRelease
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit import retrofit2.Retrofit
@ -15,12 +14,6 @@ import retrofit2.http.Streaming
import retrofit2.http.Url import retrofit2.http.Url
import java.io.File import java.io.File
sealed class Download {
object NotYet : Download()
data class Progress(val percent: Int) : Download()
data class Finished(val file: File) : Download()
}
interface AppNetworkDataSource { interface AppNetworkDataSource {
@GET @GET
suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease> suspend fun getReleaseLatest(@Url url: String): Response<LatestRelease>
@ -92,4 +85,32 @@ fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
saveFile.delete() saveFile.delete()
} }
} }
}.flowOn(Dispatchers.IO).distinctUntilChanged() }.flowOn(Dispatchers.IO).distinctUntilChanged()
data class LatestRelease(
val html_url: String? = null,
val tag_name: String? = null,
val name: String? = null,
val draft: Boolean? = null,
val prerelease: Boolean? = null,
val created_at: String? = null,
val published_at: String? = null,
val assets: List<AssetsItem>? = null,
val body: String? = null,
)
data class AssetsItem(
val name: String? = null,
val content_type: String? = null,
val size: Int? = null,
val download_count: Int? = null,
val created_at: String? = null,
val updated_at: String? = null,
val browser_download_url: String? = null,
)
sealed class Download {
object NotYet : Download()
data class Progress(val percent: Int) : Download()
data class Finished(val file: File) : Download()
}

View File

@ -7,8 +7,8 @@ import android.content.Intent
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import me.ash.reader.data.entity.Version import me.ash.reader.data.model.Version
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.model.toVersion
import java.io.File import java.io.File
fun Context.findActivity(): Activity? = when (this) { fun Context.findActivity(): Activity? = when (this) {

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import me.ash.reader.R import me.ash.reader.R
import java.text.DateFormat import java.text.DateFormat
import java.text.ParsePosition
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -40,4 +41,27 @@ fun Date.formatAsString(
} }
} }
} }
}
private fun String.parseToDate(
patterns: Array<String> = arrayOf(
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm:ss",
"yyyyMMdd",
"yyyy/MM/dd",
"yyyy年MM月dd日",
"yyyy MM dd",
)
): Date? {
val df = SimpleDateFormat()
for (pattern in patterns) {
df.applyPattern(pattern)
df.isLenient = false
val date = df.parse(this, ParsePosition(0))
if (date != null) {
return date
}
}
return null
} }

View File

@ -1,13 +0,0 @@
package me.ash.reader.ui.ext
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
import me.ash.reader.data.entity.Filter
@Composable
fun Filter.getName(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread)
Filter.Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all)
}

View File

@ -0,0 +1,11 @@
@file:Suppress("SpellCheckingInspection")
package me.ash.reader.ui.ext
import me.ash.reader.BuildConfig
const val GITHUB = "github"
const val FDROID = "fdroid"
const val isFdroid = BuildConfig.FLAVOR == FDROID
const val notFdroid = !isFdroid

View File

@ -12,7 +12,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.data.entity.Filter import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.LocalDarkTheme import me.ash.reader.data.preference.LocalDarkTheme
import me.ash.reader.ui.ext.* import me.ash.reader.ui.ext.*
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction

View File

@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import me.ash.reader.data.entity.Filter import me.ash.reader.data.model.Filter
import me.ash.reader.data.model.getName
import me.ash.reader.data.preference.FlowFilterBarStylePreference import me.ash.reader.data.preference.FlowFilterBarStylePreference
import me.ash.reader.data.preference.LocalThemeIndex import me.ash.reader.data.preference.LocalThemeIndex
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.ext.surfaceColorAtElevation import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.theme.palette.onDark import me.ash.reader.ui.theme.palette.onDark
@ -39,11 +39,7 @@ fun FilterBar(
tonalElevation = filterBarTonalElevation, tonalElevation = filterBarTonalElevation,
) { ) {
Spacer(modifier = Modifier.width(filterBarPadding)) Spacer(modifier = Modifier.width(filterBarPadding))
listOf( Filter.values.forEach { item ->
Filter.Starred,
Filter.Unread,
Filter.All,
).forEach { item ->
NavigationBarItem( NavigationBarItem(
// modifier = Modifier.height(60.dp), // modifier = Modifier.height(60.dp),
alwaysShowLabel = when (filterBarStyle) { alwaysShowLabel = when (filterBarStyle) {

View File

@ -7,7 +7,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter import me.ash.reader.data.model.Filter
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.data.module.ApplicationScope import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository

View File

@ -28,7 +28,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.model.getName
import me.ash.reader.data.model.toVersion
import me.ash.reader.data.preference.* import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.Banner

View File

@ -25,13 +25,13 @@ import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.model.getName
import me.ash.reader.data.preference.* import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.SwipeRefresh import me.ash.reader.ui.component.SwipeRefresh
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.ext.surfaceColorAtElevation import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.FilterBar import me.ash.reader.ui.page.home.FilterBar

View File

@ -19,7 +19,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.toVersion import me.ash.reader.data.model.toVersion
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.FeedbackIconButton

View File

@ -22,7 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter import me.ash.reader.data.model.Filter
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.data.preference.* import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.* import me.ash.reader.ui.component.*

View File

@ -25,7 +25,7 @@ import me.ash.reader.R
import me.ash.reader.data.entity.Article import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.* import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.* import me.ash.reader.ui.component.*
import me.ash.reader.ui.ext.surfaceColorAtElevation import me.ash.reader.ui.ext.surfaceColorAtElevation

View File

@ -5,9 +5,9 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.BuildConfig
import me.ash.reader.data.repository.AppRepository import me.ash.reader.data.repository.AppRepository
import me.ash.reader.data.source.Download import me.ash.reader.data.source.Download
import me.ash.reader.ui.ext.notFdroid
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -33,7 +33,7 @@ class UpdateViewModel @Inject constructor(
preProcessor: suspend () -> Unit = {}, preProcessor: suspend () -> Unit = {},
postProcessor: suspend (Boolean) -> Unit = {} postProcessor: suspend (Boolean) -> Unit = {}
) { ) {
if (BuildConfig.FLAVOR != "fdroid") { if (notFdroid) {
viewModelScope.launch { viewModelScope.launch {
preProcessor() preProcessor()
appRepository.checkUpdate().let { appRepository.checkUpdate().let {