mirror of https://github.com/readrops/Readrops.git
Display notifications after background synchronization
This commit is contained in:
parent
76d7f98227
commit
cafb46c727
|
@ -24,9 +24,12 @@
|
|||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<receiver android:name=".sync.SyncBroadcastReceiver" android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.readrops.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
|
@ -10,19 +11,26 @@ import androidx.activity.enableEdgeToEdge
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cafe.adriel.voyager.navigator.CurrentScreen
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
|
||||
import com.readrops.app.account.selection.AccountSelectionScreen
|
||||
import com.readrops.app.account.selection.AccountSelectionScreenModel
|
||||
import com.readrops.app.home.HomeScreen
|
||||
import com.readrops.app.sync.SyncWorker
|
||||
import com.readrops.app.timelime.TimelineTab
|
||||
import com.readrops.app.util.Preferences
|
||||
import com.readrops.app.util.theme.ReadropsTheme
|
||||
import com.readrops.db.Database
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.androidx.compose.KoinAndroidContext
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
|
@ -67,12 +75,16 @@ class MainActivity : ComponentActivity(), KoinComponent {
|
|||
)
|
||||
|
||||
Navigator(
|
||||
screen = if (accountExists) HomeScreen() else AccountSelectionScreen(),
|
||||
screen = if (accountExists) HomeScreen else AccountSelectionScreen(),
|
||||
disposeBehavior = NavigatorDisposeBehavior(
|
||||
// prevent screenModels being recreated when opening a screen from a tab
|
||||
disposeNestedNavigators = false
|
||||
)
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
CurrentScreen()
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +92,29 @@ class MainActivity : ComponentActivity(), KoinComponent {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
handleIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleIntent(intent: Intent) {
|
||||
if (intent.hasExtra(SyncWorker.ACCOUNT_ID_KEY)) {
|
||||
val accountId = intent.getIntExtra(SyncWorker.ACCOUNT_ID_KEY, -1)
|
||||
get<Database>().accountDao()
|
||||
.updateCurrentAccount(accountId)
|
||||
|
||||
HomeScreen.openTab(TimelineTab)
|
||||
|
||||
if (intent.hasExtra(SyncWorker.ITEM_ID_KEY)) {
|
||||
val itemId = intent.getIntExtra(SyncWorker.ITEM_ID_KEY, -1)
|
||||
HomeScreen.openItemScreen(itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun useDarkTheme(mode: String, darkFlag: Int): Boolean {
|
||||
return when (mode) {
|
||||
"light" -> false
|
||||
|
|
|
@ -71,7 +71,7 @@ class AccountCredentialsScreen(
|
|||
|
||||
if (state.exitScreen) {
|
||||
if (mode == AccountCredentialsScreenMode.NEW_CREDENTIALS) {
|
||||
navigator.replaceAll(HomeScreen())
|
||||
navigator.replaceAll(HomeScreen)
|
||||
} else {
|
||||
navigator.pop()
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ class AccountSelectionScreen : AndroidScreen() {
|
|||
when (state.navState) {
|
||||
is NavState.GoToHomeScreen -> {
|
||||
// using replace makes the app crash due to a screen key conflict
|
||||
navigator.replaceAll(HomeScreen())
|
||||
navigator.replaceAll(HomeScreen)
|
||||
}
|
||||
|
||||
is NavState.GoToAccountCredentialsScreen -> {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.readrops.app.home
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
|
@ -20,29 +19,42 @@ import androidx.compose.material3.Scaffold
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.CurrentTab
|
||||
import cafe.adriel.voyager.navigator.tab.Tab
|
||||
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.account.AccountTab
|
||||
import com.readrops.app.feeds.FeedTab
|
||||
import com.readrops.app.item.ItemScreen
|
||||
import com.readrops.app.more.MoreTab
|
||||
import com.readrops.app.timelime.TimelineTab
|
||||
import com.readrops.app.util.components.AndroidScreen
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
|
||||
class HomeScreen : AndroidScreen() {
|
||||
object HomeScreen : AndroidScreen() {
|
||||
|
||||
private val itemChannel = Channel<Int>()
|
||||
private val tabChannel = Channel<Tab>()
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val scaffoldInsets = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
itemChannel.receiveAsFlow()
|
||||
.collect {
|
||||
navigator.push(ItemScreen(it))
|
||||
}
|
||||
}
|
||||
|
||||
TabNavigator(
|
||||
tab = TimelineTab
|
||||
) { tabNavigator ->
|
||||
|
@ -101,6 +113,18 @@ class HomeScreen : AndroidScreen() {
|
|||
},
|
||||
contentWindowInsets = scaffoldInsets
|
||||
) { paddingValues ->
|
||||
LaunchedEffect(Unit) {
|
||||
tabChannel.receiveAsFlow()
|
||||
.collect {
|
||||
tabNavigator.current = TimelineTab
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = tabNavigator.current != TimelineTab,
|
||||
onBack = { tabNavigator.current = TimelineTab }
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
@ -109,13 +133,16 @@ class HomeScreen : AndroidScreen() {
|
|||
) {
|
||||
CurrentTab()
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = tabNavigator.current != TimelineTab,
|
||||
onBack = { tabNavigator.current = TimelineTab }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openItemScreen(itemId: Int) {
|
||||
itemChannel.send(itemId)
|
||||
}
|
||||
|
||||
suspend fun openTab(tab: Tab) {
|
||||
tabChannel.send(tab)
|
||||
}
|
||||
}
|
|
@ -24,8 +24,7 @@ class LocalRSSRepository(
|
|||
account: Account
|
||||
) : BaseRepository(database, account), KoinComponent {
|
||||
|
||||
override suspend fun login(account: Account) { /* useless here */
|
||||
}
|
||||
override suspend fun login(account: Account) { /* useless here */ }
|
||||
|
||||
override suspend fun synchronize(
|
||||
selectedFeeds: List<Feed>,
|
||||
|
@ -52,14 +51,12 @@ class LocalRSSRepository(
|
|||
try {
|
||||
val pair = dataSource.queryRSSResource(feed.url!!, headers.build())
|
||||
|
||||
pair?.let {
|
||||
insertNewItems(it.second, feed)
|
||||
syncResult.items = it.second
|
||||
pair?.let { // temporary
|
||||
syncResult.items += insertNewItems(it.second, feed)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errors[feed] = e
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Pair(syncResult, errors)
|
||||
|
@ -89,9 +86,8 @@ class LocalRSSRepository(
|
|||
return@withContext errors
|
||||
}
|
||||
|
||||
private suspend fun insertNewItems(items: List<Item>, feed: Feed) {
|
||||
items.sortedWith(Item::compareTo) // TODO Check if ordering is useful in this situation
|
||||
val itemsToInsert = mutableListOf<Item>()
|
||||
private suspend fun insertNewItems(items: List<Item>, feed: Feed): List<Item> {
|
||||
val newItems = mutableListOf<Item>()
|
||||
|
||||
for (item in items) {
|
||||
if (!database.itemDao().itemExists(item.guid!!, feed.accountId)) {
|
||||
|
@ -106,14 +102,19 @@ class LocalRSSRepository(
|
|||
}
|
||||
|
||||
item.feedId = feed.id
|
||||
itemsToInsert += item
|
||||
newItems += item
|
||||
}
|
||||
}
|
||||
|
||||
database.itemDao().insert(itemsToInsert)
|
||||
database.itemDao().insert(newItems)
|
||||
.zip(newItems)
|
||||
.forEach { (id, item) -> item.id = id.toInt() }
|
||||
|
||||
return newItems
|
||||
}
|
||||
|
||||
private suspend fun insertFeed(feed: Feed): Feed {
|
||||
// TODO better handle this case
|
||||
require(!database.feedDao().feedExists(feed.url!!, account.id)) {
|
||||
"Feed already exists for account ${account.accountName}"
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ data class NotificationContent(
|
|||
val content: String? = null,
|
||||
val largeIcon: Bitmap? = null,
|
||||
val item: Item? = null,
|
||||
val color: Int = 0,
|
||||
val accountId: Int = 0
|
||||
)
|
||||
|
||||
|
@ -27,24 +28,24 @@ class SyncAnalyzer(
|
|||
val database: Database
|
||||
) : KoinComponent {
|
||||
|
||||
suspend fun getNotificationContent(syncResults: Map<Account, SyncResult>): NotificationContent {
|
||||
suspend fun getNotificationContent(syncResults: Map<Account, SyncResult>): NotificationContent? {
|
||||
return if (newItemsInMultipleAccounts(syncResults)) { // new items from several accounts
|
||||
val feeds = database.feedDao().selectFromIds(getFeedsIdsForNewItems(syncResults))
|
||||
|
||||
var itemCount = 0
|
||||
syncResults.values.forEach { syncResult ->
|
||||
for (syncResult in syncResults.values) {
|
||||
itemCount += syncResult.items.filter {
|
||||
isFeedNotificationEnabledForItem(feeds, it)
|
||||
}.size
|
||||
}
|
||||
|
||||
NotificationContent(title = context.getString(R.string.new_items, itemCount.toString()))
|
||||
} else { // new items from only one account
|
||||
getContentFromOneAccount(syncResults)
|
||||
NotificationContent(title = context.getString(R.string.new_items, "$itemCount"))
|
||||
} else { // new items from a single account
|
||||
getSingleAccountContent(syncResults)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getContentFromOneAccount(syncResults: Map<Account, SyncResult>): NotificationContent {
|
||||
private suspend fun getSingleAccountContent(syncResults: Map<Account, SyncResult>): NotificationContent? {
|
||||
val syncResultMap = syncResults.filterValues { it.items.isNotEmpty() }
|
||||
|
||||
if (syncResultMap.values.isNotEmpty()) {
|
||||
|
@ -59,8 +60,8 @@ class SyncAnalyzer(
|
|||
syncResult.items.filter { isFeedNotificationEnabledForItem(feeds, it) }
|
||||
val itemCount = items.size
|
||||
|
||||
// new items from several feeds from one account
|
||||
return when {
|
||||
// multiple new items from several feeds
|
||||
feedsIdsForNewItems.size > 1 && itemCount > 1 -> {
|
||||
NotificationContent(
|
||||
title = account.accountName!!,
|
||||
|
@ -72,24 +73,24 @@ class SyncAnalyzer(
|
|||
accountId = account.id
|
||||
)
|
||||
}
|
||||
// new items from only one feed from one account
|
||||
// multiple new items from a single feed
|
||||
feedsIdsForNewItems.size == 1 ->
|
||||
oneFeedCase(feedsIdsForNewItems.first(), syncResult.items, account)
|
||||
|
||||
itemCount == 1 -> oneFeedCase(items.first().feedId, items, account)
|
||||
else -> NotificationContent()
|
||||
singleFeedCase(feedsIdsForNewItems.first(), syncResult.items, account)
|
||||
// only one new item from a single feed
|
||||
itemCount == 1 -> singleFeedCase(items.first().feedId, items, account)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NotificationContent()
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun oneFeedCase(
|
||||
private suspend fun singleFeedCase(
|
||||
feedId: Int,
|
||||
items: List<Item>,
|
||||
account: Account
|
||||
): NotificationContent {
|
||||
): NotificationContent? {
|
||||
val feed = database.feedDao().selectFeed(feedId)
|
||||
|
||||
if (feed.isNotificationEnabled) {
|
||||
|
@ -101,15 +102,11 @@ class SyncAnalyzer(
|
|||
.build()
|
||||
)
|
||||
|
||||
target.drawable!!.toBitmap()
|
||||
target.drawable?.toBitmap()
|
||||
}
|
||||
|
||||
val (item, content) = if (items.size == 1) {
|
||||
val item = database.itemDao().selectByRemoteId(
|
||||
items.first().remoteId!!,
|
||||
items.first().feedId
|
||||
)
|
||||
|
||||
val item = items.first()
|
||||
item to item.title
|
||||
} else {
|
||||
null to context.getString(R.string.new_items, items.size.toString())
|
||||
|
@ -120,11 +117,12 @@ class SyncAnalyzer(
|
|||
largeIcon = icon,
|
||||
content = content,
|
||||
item = item,
|
||||
color = feed.backgroundColor,
|
||||
accountId = account.id
|
||||
)
|
||||
}
|
||||
|
||||
return NotificationContent()
|
||||
return null
|
||||
}
|
||||
|
||||
private fun newItemsInMultipleAccounts(syncResults: Map<Account, SyncResult>): Boolean {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package com.readrops.app.sync
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.readrops.db.Database
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
|
||||
class SyncBroadcastReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
private val database by inject<Database>()
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
NotificationManagerCompat.from(context)
|
||||
.cancel(SyncWorker.SYNC_RESULT_NOTIFICATION_ID)
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_MARK_READ -> {
|
||||
val id = intent.getIntExtra(SyncWorker.ITEM_ID_KEY, -1)
|
||||
GlobalScope.launch {
|
||||
database.itemDao().updateReadState(id, true)
|
||||
}
|
||||
}
|
||||
ACTION_SET_FAVORITE -> {
|
||||
val id = intent.getIntExtra(SyncWorker.ITEM_ID_KEY, -1)
|
||||
GlobalScope.launch {
|
||||
database.itemDao().updateStarState(id, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_MARK_READ = "ACTION_MARK_READ"
|
||||
const val ACTION_SET_FAVORITE = "ACTION_SET_FAVORITE"
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
package com.readrops.app.sync
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.Action
|
||||
import androidx.core.app.NotificationCompat.Builder
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.BackoffPolicy
|
||||
|
@ -24,6 +26,7 @@ import androidx.work.workDataOf
|
|||
import com.readrops.api.services.Credentials
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.api.utils.AuthInterceptor
|
||||
import com.readrops.app.MainActivity
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.ReadropsApp
|
||||
import com.readrops.app.repositories.BaseRepository
|
||||
|
@ -51,11 +54,13 @@ class SyncWorker(
|
|||
// TODO handle notification permission for Android 14+ (or 15?)
|
||||
@SuppressLint("MissingPermission")
|
||||
override suspend fun doWork(): Result {
|
||||
val workManager = WorkManager.getInstance(applicationContext)
|
||||
val isManual = tags.contains(WORK_MANUAL)
|
||||
|
||||
val infos = WorkManager.getInstance(applicationContext)
|
||||
.getWorkInfosByTagFlow(TAG).first()
|
||||
|
||||
val infos = workManager.getWorkInfosByTagFlow(TAG).first()
|
||||
if (infos.any { it.state == WorkInfo.State.RUNNING && it.id != id }) {
|
||||
return if (tags.contains(WORK_MANUAL)) {
|
||||
return if (isManual) {
|
||||
Result.failure(
|
||||
workDataOf(
|
||||
SYNC_FAILURE_KEY to true,
|
||||
|
@ -70,41 +75,45 @@ class SyncWorker(
|
|||
}
|
||||
}
|
||||
|
||||
var workResult: Result
|
||||
try {
|
||||
require(notificationManager.areNotificationsEnabled())
|
||||
val notificationBuilder = Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
|
||||
.setProgress(0, 0, true)
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // for Android 7.1 and earlier
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
|
||||
val notificationBuilder =
|
||||
Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
|
||||
.setProgress(0, 0, true)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(applicationContext.resources, R.mipmap.ic_launcher_round))
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // for Android 7.1 and earlier
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
return try {
|
||||
val (workResult, syncResults) = refreshAccounts(notificationBuilder)
|
||||
notificationManager.cancel(SYNC_NOTIFICATION_ID)
|
||||
|
||||
workResult = refreshAccounts(notificationBuilder)
|
||||
if (!isManual) {
|
||||
displaySyncResults(syncResults)
|
||||
}
|
||||
|
||||
workResult
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "${e::class.simpleName}: ${e.message} ${e.printStackTrace()}")
|
||||
workResult = Result.failure(
|
||||
workDataOf(SYNC_FAILURE_KEY to true)
|
||||
.putSerializable(SYNC_FAILURE_EXCEPTION_KEY, e)
|
||||
)
|
||||
} finally {
|
||||
notificationManager.cancel(SYNC_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
return workResult
|
||||
notificationManager.cancel(SYNC_NOTIFICATION_ID)
|
||||
if (isManual) {
|
||||
Result.failure(
|
||||
workDataOf(SYNC_FAILURE_KEY to true)
|
||||
.putSerializable(SYNC_FAILURE_EXCEPTION_KEY, e)
|
||||
)
|
||||
} else {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun refreshAccounts(notificationBuilder: Builder): Result {
|
||||
private suspend fun refreshAccounts(notificationBuilder: Builder): Pair<Result, Map<Account, SyncResult>> {
|
||||
val sharedPreferences = get<SharedPreferences>()
|
||||
var workResult = Result.success(workDataOf(END_SYNC_KEY to true))
|
||||
val syncResults = mutableMapOf<Account, SyncResult>()
|
||||
|
||||
val accountId = inputData.getInt(ACCOUNT_ID_KEY, 0)
|
||||
val accounts = if (accountId == 0) {
|
||||
val accountId = inputData.getInt(ACCOUNT_ID_KEY, -1)
|
||||
val accounts = if (accountId == -1) {
|
||||
database.accountDao().selectAllAccounts().first()
|
||||
} else {
|
||||
listOf(database.accountDao().select(accountId))
|
||||
|
@ -129,21 +138,25 @@ class SyncWorker(
|
|||
if (account.isLocal) {
|
||||
val result = refreshLocalAccount(repository, account, notificationBuilder)
|
||||
|
||||
if (result.second.isNotEmpty()) {
|
||||
if (result.second.isNotEmpty() && tags.contains(WORK_MANUAL)) {
|
||||
workResult = Result.success(
|
||||
workDataOf(END_SYNC_KEY to true)
|
||||
.putSerializable(LOCAL_SYNC_ERRORS_KEY, result.second)
|
||||
)
|
||||
}
|
||||
|
||||
syncResults[account] = result.first
|
||||
} else {
|
||||
get<AuthInterceptor>().credentials = Credentials.toCredentials(account)
|
||||
|
||||
val syncResult = repository.synchronize()
|
||||
fetchFeedColors(syncResult, notificationBuilder)
|
||||
|
||||
syncResults[account] = syncResult
|
||||
}
|
||||
}
|
||||
|
||||
return workResult
|
||||
return workResult to syncResults
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
|
@ -172,6 +185,7 @@ class SyncWorker(
|
|||
selectedFeeds = feeds,
|
||||
onUpdate = { feed ->
|
||||
notificationBuilder.setContentText(feed.name)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(feed.name))
|
||||
.setProgress(feedMax, ++feedCount, false)
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
|
@ -206,6 +220,7 @@ class SyncWorker(
|
|||
val feedName = syncResult.feeds.first { it.id == feedId.toInt() }.name
|
||||
|
||||
notificationBuilder.setContentText(feedName)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(feedName))
|
||||
.setProgress(syncResult.newFeedIds.size, index + 1, false)
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
|
@ -219,6 +234,98 @@ class SyncWorker(
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun displaySyncResults(syncResults: Map<Account, SyncResult>) {
|
||||
val notificationContent = SyncAnalyzer(applicationContext, database)
|
||||
.getNotificationContent(syncResults)
|
||||
|
||||
if (notificationContent != null) {
|
||||
val intent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
if (notificationContent.accountId > 0) {
|
||||
putExtra(ACCOUNT_ID_KEY, notificationContent.accountId)
|
||||
}
|
||||
|
||||
if (notificationContent.item != null) {
|
||||
putExtra(ITEM_ID_KEY, notificationContent.item.id)
|
||||
}
|
||||
}
|
||||
|
||||
val notificationBuilder = Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
|
||||
.setContentTitle(notificationContent.title)
|
||||
.setContentText(notificationContent.content)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(notificationContent.content))
|
||||
.setSmallIcon(R.drawable.ic_notifications)
|
||||
.setColor(notificationContent.color)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
.setAutoCancel(true)
|
||||
|
||||
notificationContent.item?.let { item ->
|
||||
val itemId = item.id
|
||||
|
||||
notificationBuilder
|
||||
.addAction(getMarkReadAction(itemId))
|
||||
.addAction(getMarkFavoriteAction(itemId))
|
||||
}
|
||||
|
||||
notificationContent.largeIcon?.let { notificationBuilder.setLargeIcon(it) }
|
||||
notificationManager.notify(SYNC_RESULT_NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMarkReadAction(itemId: Int): Action {
|
||||
val intent = Intent(applicationContext, SyncBroadcastReceiver::class.java).apply {
|
||||
action = SyncBroadcastReceiver.ACTION_MARK_READ
|
||||
putExtra(ITEM_ID_KEY, itemId)
|
||||
}
|
||||
|
||||
val pendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
applicationContext,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
|
||||
return Action.Builder(
|
||||
R.drawable.ic_done_all,
|
||||
applicationContext.getString(R.string.mark_read),
|
||||
pendingIntent
|
||||
)
|
||||
.setAllowGeneratedReplies(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getMarkFavoriteAction(itemId: Int): Action {
|
||||
val intent = Intent(applicationContext, SyncBroadcastReceiver::class.java).apply {
|
||||
action = SyncBroadcastReceiver.ACTION_SET_FAVORITE
|
||||
putExtra(ITEM_ID_KEY, itemId)
|
||||
}
|
||||
|
||||
val pendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
applicationContext,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return Action.Builder(
|
||||
R.drawable.ic_favorite_border,
|
||||
applicationContext.getString(R.string.add_to_favorite),
|
||||
pendingIntent
|
||||
)
|
||||
.setAllowGeneratedReplies(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = SyncWorker::class.java.simpleName
|
||||
|
||||
|
@ -226,13 +333,14 @@ class SyncWorker(
|
|||
private val WORK_MANUAL = "$TAG-manual"
|
||||
|
||||
private const val SYNC_NOTIFICATION_ID = 2
|
||||
private const val SYNC_RESULT_NOTIFICATION_ID = 3
|
||||
const val SYNC_RESULT_NOTIFICATION_ID = 3
|
||||
|
||||
const val END_SYNC_KEY = "END_SYNC"
|
||||
const val SYNC_FAILURE_KEY = "SYNC_FAILURE"
|
||||
const val SYNC_FAILURE_EXCEPTION_KEY = "SYNC_FAILURE_EXCEPTION"
|
||||
const val ACCOUNT_ID_KEY = "ACCOUNT_ID"
|
||||
const val FEED_ID_KEY = "FEED_ID"
|
||||
const val ITEM_ID_KEY = "ITEM_ID"
|
||||
const val FOLDER_ID_KEY = "FOLDER_ID"
|
||||
const val FEED_NAME_KEY = "FEED_NAME"
|
||||
const val FEED_MAX_KEY = "FEED_MAX"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/>
|
||||
|
||||
</vector>
|
|
@ -189,4 +189,5 @@
|
|||
<string name="disable_battery_optimization_subtitle">Peut aider à éviter que le système n\'empêche la synchronisation d\'arrière-plan</string>
|
||||
<string name="battery_optimization_already_disabled">L\'optimisation de la batterie est déjà optimisée pour cette appli</string>
|
||||
<string name="opml_export_success">Export OPML réussi</string>
|
||||
<string name="add_to_favorite">Ajouter aux favoris</string>
|
||||
</resources>
|
|
@ -198,4 +198,5 @@
|
|||
<string name="disable_battery_optimization_subtitle">Can help with background synchronization not being killed by the system</string>
|
||||
<string name="battery_optimization_already_disabled">Battery optimization already disabled for this app</string>
|
||||
<string name="opml_export_success">OPML export success</string>
|
||||
<string name="add_to_favorite">Add to favorites</string>
|
||||
</resources>
|
|
@ -22,9 +22,6 @@ abstract class ItemDao : BaseDao<Item> {
|
|||
@RawQuery(observedEntities = [Item::class, ItemState::class])
|
||||
abstract fun selectItemById(query: SupportSQLiteQuery): Flow<ItemWithFeed>
|
||||
|
||||
@Query("Select * From Item Where remoteId = :remoteId And feed_id = :feedId")
|
||||
abstract suspend fun selectByRemoteId(remoteId: String, feedId: Int): Item
|
||||
|
||||
@Query("Update Item Set read = :read Where id = :itemId")
|
||||
abstract suspend fun updateReadState(itemId: Int, read: Boolean)
|
||||
|
||||
|
|
Loading…
Reference in New Issue