mirror of https://github.com/readrops/Readrops.git
Ask notifications permission for api 33+
This commit is contained in:
parent
ca9a93d731
commit
9210b8b7fe
|
@ -1,6 +1,7 @@
|
|||
package com.readrops.app
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.preferences.SharedPreferencesMigration
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
|
@ -50,14 +51,12 @@ val appModule = module {
|
|||
AccountCredentialsScreenModel(accountType, mode, get())
|
||||
}
|
||||
|
||||
factory { (account: Account) -> NotificationsScreenModel(account, get(), get()) }
|
||||
factory { (account: Account) -> NotificationsScreenModel(account, get(), get(), get()) }
|
||||
|
||||
factory { PreferencesScreenModel(get()) }
|
||||
|
||||
single { GetFoldersWithFeeds(get()) }
|
||||
|
||||
// repositories
|
||||
|
||||
factory<BaseRepository> { (account: Account) ->
|
||||
when (account.accountType) {
|
||||
AccountType.LOCAL -> LocalRSSRepository(get(), get(), account)
|
||||
|
@ -101,4 +100,6 @@ val appModule = module {
|
|||
single { DataStorePreferences(get()) }
|
||||
|
||||
single { Preferences(get()) }
|
||||
|
||||
single { NotificationManagerCompat.from(get()) }
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
package com.readrops.app.notifications
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
@ -19,18 +25,25 @@ import androidx.compose.material3.TopAppBar
|
|||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.more.preferences.PreferencesScreen
|
||||
import com.readrops.app.more.preferences.components.BasePreference
|
||||
import com.readrops.app.util.components.AndroidScreen
|
||||
import com.readrops.app.util.components.ThreeDotsMenu
|
||||
import com.readrops.app.util.components.dialog.TwoChoicesDialog
|
||||
|
@ -41,10 +54,12 @@ import org.koin.core.parameter.parametersOf
|
|||
|
||||
class NotificationsScreen(val account: Account) : AndroidScreen() {
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("InlinedApi")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val screenModel = getScreenModel<NotificationsScreenModel> { parametersOf(account) }
|
||||
|
||||
val state by screenModel.state.collectAsStateWithLifecycle()
|
||||
|
@ -52,6 +67,17 @@ class NotificationsScreen(val account: Account) : AndroidScreen() {
|
|||
val topAppBarScrollBehavior =
|
||||
TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
val permissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
|
||||
screenModel.refreshNotificationManager()
|
||||
}
|
||||
|
||||
LaunchedEffect(permissionState.status) {
|
||||
if (permissionState.status.isGranted) {
|
||||
screenModel.refreshNotificationManager()
|
||||
}
|
||||
}
|
||||
|
||||
if (state.showBackgroundSyncDialog) {
|
||||
TwoChoicesDialog(
|
||||
title = stringResource(id = R.string.auto_synchro_disabled),
|
||||
|
@ -105,6 +131,29 @@ class NotificationsScreen(val account: Account) : AndroidScreen() {
|
|||
.padding(paddingValues)
|
||||
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection)
|
||||
) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU
|
||||
&& !state.areNotificationsEnabled
|
||||
) {
|
||||
item {
|
||||
BasePreference(
|
||||
title = stringResource(R.string.grant_access_notifications),
|
||||
subtitle = stringResource(R.string.system_notifications_disabled),
|
||||
onClick = {
|
||||
if (!permissionState.status.shouldShowRationale) {
|
||||
val intent = Intent().apply {
|
||||
action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
}
|
||||
|
||||
launcher.launch(intent)
|
||||
} else {
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
@ -118,7 +167,7 @@ class NotificationsScreen(val account: Account) : AndroidScreen() {
|
|||
)
|
||||
|
||||
Switch(
|
||||
checked = state.isNotificationsEnabled,
|
||||
checked = state.areAccountNotificationsEnabled,
|
||||
onCheckedChange = {
|
||||
screenModel.setAccountNotificationsState(it)
|
||||
|
||||
|
@ -147,9 +196,9 @@ class NotificationsScreen(val account: Account) : AndroidScreen() {
|
|||
iconUrl = feedWithFolder.feed.iconUrl,
|
||||
folderName = feedWithFolder.folderName,
|
||||
checked = feedWithFolder.feed.isNotificationEnabled,
|
||||
enabled = state.isNotificationsEnabled,
|
||||
enabled = state.areAccountNotificationsEnabled,
|
||||
onCheckChange = {
|
||||
if (state.isNotificationsEnabled) {
|
||||
if (state.areAccountNotificationsEnabled) {
|
||||
screenModel.setFeedNotificationsState(feedWithFolder.feed.id, it)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.readrops.app.notifications
|
||||
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.readrops.app.util.Preferences
|
||||
|
@ -15,14 +16,15 @@ class NotificationsScreenModel(
|
|||
private val account: Account,
|
||||
private val database: Database,
|
||||
private val preferences: Preferences,
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : StateScreenModel<NotificationsState>(NotificationsState(isNotificationsEnabled = account.isNotificationsEnabled)) {
|
||||
) : StateScreenModel<NotificationsState>(NotificationsState(areAccountNotificationsEnabled = account.isNotificationsEnabled)) {
|
||||
|
||||
init {
|
||||
screenModelScope.launch(dispatcher) {
|
||||
database.accountDao().selectAccountNotificationsState(account.id)
|
||||
.collect { isNotificationsEnabled ->
|
||||
mutableState.update { it.copy(isNotificationsEnabled = isNotificationsEnabled) }
|
||||
mutableState.update { it.copy(areAccountNotificationsEnabled = isNotificationsEnabled) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,13 +71,18 @@ class NotificationsScreenModel(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshNotificationManager() {
|
||||
mutableState.update { it.copy(areNotificationsEnabled = notificationManager.areNotificationsEnabled()) }
|
||||
}
|
||||
}
|
||||
|
||||
data class NotificationsState(
|
||||
val isNotificationsEnabled: Boolean = false,
|
||||
val areAccountNotificationsEnabled: Boolean = false,
|
||||
val feedsWithFolder: List<FeedWithFolder> = emptyList(),
|
||||
val showBackgroundSyncDialog: Boolean = false,
|
||||
val isBackGroundSyncEnabled: Boolean = false
|
||||
val isBackGroundSyncEnabled: Boolean = false,
|
||||
val areNotificationsEnabled: Boolean = false
|
||||
) {
|
||||
|
||||
val allFeedNotificationsEnabled
|
||||
|
|
|
@ -14,12 +14,12 @@ import org.koin.core.component.inject
|
|||
|
||||
class SyncBroadcastReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
private val notificationManager by inject<NotificationManagerCompat>()
|
||||
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)
|
||||
notificationManager.cancel(SyncWorker.SYNC_RESULT_NOTIFICATION_ID)
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_MARK_READ -> {
|
||||
|
|
|
@ -48,11 +48,9 @@ class SyncWorker(
|
|||
params: WorkerParameters
|
||||
) : CoroutineWorker(appContext, params), KoinComponent {
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(appContext)
|
||||
private val notificationManager by inject<NotificationManagerCompat>()
|
||||
private val database by inject<Database>()
|
||||
|
||||
// TODO handle notification permission for Android 14+ (or 15?)
|
||||
@SuppressLint("MissingPermission")
|
||||
override suspend fun doWork(): Result {
|
||||
val isManual = tags.contains(WORK_MANUAL)
|
||||
|
||||
|
@ -106,7 +104,6 @@ class SyncWorker(
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
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))
|
||||
|
@ -133,7 +130,10 @@ class SyncWorker(
|
|||
account.accountName
|
||||
)
|
||||
)
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
if (notificationManager.areNotificationsEnabled()) {
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
|
||||
if (account.isLocal) {
|
||||
val result = refreshLocalAccount(repository, account, notificationBuilder)
|
||||
|
@ -209,7 +209,6 @@ class SyncWorker(
|
|||
return result
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun fetchFeedColors(
|
||||
syncResult: SyncResult,
|
||||
notificationBuilder: Builder
|
||||
|
@ -221,7 +220,9 @@ class SyncWorker(
|
|||
.setStyle(NotificationCompat.BigTextStyle().bigText(feed.name))
|
||||
.setProgress(feeds.size, index + 1, false)
|
||||
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
if (notificationManager.areNotificationsEnabled()) {
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
|
||||
try {
|
||||
if (feed.iconUrl != null) {
|
||||
|
@ -234,7 +235,6 @@ class SyncWorker(
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun displaySyncResults(syncResults: Map<Account, SyncResult>) {
|
||||
val notificationContent = SyncAnalyzer(applicationContext, database)
|
||||
.getNotificationContent(syncResults)
|
||||
|
@ -275,7 +275,10 @@ class SyncWorker(
|
|||
}
|
||||
|
||||
notificationContent.largeIcon?.let { notificationBuilder.setLargeIcon(it) }
|
||||
notificationManager.notify(SYNC_RESULT_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
if (notificationManager.areNotificationsEnabled()) {
|
||||
notificationManager.notify(SYNC_RESULT_NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -127,9 +127,10 @@ class TimelineScreenModel(
|
|||
screenModelScope.launch(dispatcher) {
|
||||
combine(
|
||||
preferences.timelineItemSize.flow,
|
||||
preferences.scrollRead.flow
|
||||
) { itemSize, scrollRead -> itemSize to scrollRead }
|
||||
.collect { (itemSize, scrollRead) ->
|
||||
preferences.scrollRead.flow,
|
||||
preferences.displayNotificationsPermission.flow
|
||||
) { a, b, c -> Triple(a, b, c) }
|
||||
.collect { (itemSize, scrollRead, notificationPermission) ->
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
itemSize = when (itemSize) {
|
||||
|
@ -137,7 +138,8 @@ class TimelineScreenModel(
|
|||
"regular" -> TimelineItemSize.REGULAR
|
||||
else -> TimelineItemSize.LARGE
|
||||
},
|
||||
markReadOnScroll = scrollRead
|
||||
markReadOnScroll = scrollRead,
|
||||
displayNotificationsPermission = notificationPermission
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -370,6 +372,12 @@ class TimelineScreenModel(
|
|||
fun updateLastFirstVisibleItemIndex(index: Int) {
|
||||
_listIndexState.update { index }
|
||||
}
|
||||
|
||||
fun disableDisplayNotificationsPermission() {
|
||||
screenModelScope.launch {
|
||||
preferences.displayNotificationsPermission.write(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
@ -392,7 +400,8 @@ data class TimelineState(
|
|||
val isAccountLocal: Boolean = false,
|
||||
val hideReadAllFAB: Boolean = false,
|
||||
val itemSize: TimelineItemSize = TimelineItemSize.LARGE,
|
||||
val markReadOnScroll: Boolean = false
|
||||
val markReadOnScroll: Boolean = false,
|
||||
val displayNotificationsPermission: Boolean = false
|
||||
) {
|
||||
|
||||
val showSubtitle = filters.subFilter != SubFilter.ALL
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package com.readrops.app.timelime
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -79,6 +83,7 @@ object TimelineTab : Tab {
|
|||
title = "Timeline",
|
||||
)
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
|
@ -93,8 +98,19 @@ object TimelineTab : Tab {
|
|||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val topAppBarState = rememberTopAppBarState()
|
||||
val topAppBarScrollBehavior =
|
||||
TopAppBarDefaults.pinnedScrollBehavior(topAppBarState)
|
||||
val topAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarState)
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
|
||||
screenModel.disableDisplayNotificationsPermission()
|
||||
}
|
||||
|
||||
LaunchedEffect(state.displayNotificationsPermission) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU
|
||||
&& state.displayNotificationsPermission
|
||||
) {
|
||||
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.isRefreshing) {
|
||||
if (state.isRefreshing) {
|
||||
|
@ -382,7 +398,10 @@ object TimelineTab : Tab {
|
|||
screenModel.updateStarState(itemWithFeed.item)
|
||||
},
|
||||
onShare = {
|
||||
screenModel.shareItem(itemWithFeed.item, context)
|
||||
screenModel.shareItem(
|
||||
itemWithFeed.item,
|
||||
context
|
||||
)
|
||||
},
|
||||
size = state.itemSize
|
||||
)
|
||||
|
|
|
@ -60,6 +60,12 @@ class Preferences(
|
|||
key = stringPreferencesKey("timeline_item_size"),
|
||||
default = "large"
|
||||
)
|
||||
|
||||
val displayNotificationsPermission = Preference(
|
||||
dataStore = dataStore,
|
||||
key = booleanPreferencesKey("display_notification_permission"),
|
||||
default = true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -190,4 +190,6 @@
|
|||
<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>
|
||||
<string name="grant_access_notifications">Autoriser l\'accès aux notifications</string>
|
||||
<string name="system_notifications_disabled">Les notfications système sont désactivées. Cliquez ici pour les activer</string>
|
||||
</resources>
|
|
@ -199,4 +199,6 @@
|
|||
<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>
|
||||
<string name="grant_access_notifications">Grant access to Notifications</string>
|
||||
<string name="system_notifications_disabled">System notifications are currently disabled. Click here to enable them</string>
|
||||
</resources>
|
|
@ -34,6 +34,7 @@ compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview"
|
|||
# specify material3 version is required for gradle to find the dependency
|
||||
compose-material3 = { module = "androidx.compose.material3:material3", version = "1.2.1" }
|
||||
compose-activity = "androidx.activity:activity-compose:1.9.0"
|
||||
compose-permissions = "com.google.accompanist:accompanist-permissions:0.34.0"
|
||||
|
||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
|
||||
|
@ -106,7 +107,7 @@ expressocore = "androidx.test.espresso:espresso-core:3.6.1"
|
|||
|
||||
[bundles]
|
||||
compose = ["compose-foundation", "compose-runtime", "compose-animation",
|
||||
"compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview", "compose-material3", "compose-activity"]
|
||||
"compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview", "compose-material3", "compose-activity", "compose-permissions"]
|
||||
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-koin", "voyager-transitions"]
|
||||
lifecycle = ["lifecycle-viewmodel-ktx", "lifecycle-viewmodel-compose", "lifecycle-viewmodel-savedstate",
|
||||
"lifecyle-runtime-compose"]
|
||||
|
|
Loading…
Reference in New Issue