a lot of improvements for long polling service and notifications

This commit is contained in:
2024-07-15 05:01:54 +03:00
parent 9481541806
commit 654f47bb94
28 changed files with 473 additions and 388 deletions
-1
View File
@@ -182,5 +182,4 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlin.serialization)
implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04")
}
@@ -1,18 +1,24 @@
package com.meloda.app.fast
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.meloda.app.fast.auth.AuthGraph
import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.common.extensions.ifEmpty
import com.meloda.app.fast.common.extensions.listenValue
import com.meloda.app.fast.common.extensions.setValue
import com.meloda.app.fast.common.extensions.updateValue
import com.meloda.app.fast.data.db.AccountsRepository
import com.meloda.app.fast.data.db.GetCurrentAccountUseCase
import com.meloda.app.fast.datastore.SettingsController
import com.meloda.app.fast.datastore.SettingsKeys
import com.meloda.app.fast.datastore.UserSettings
import com.meloda.app.fast.datastore.model.LongPollState
import com.meloda.app.fast.model.BaseError
import com.meloda.app.fast.model.LongPollState
import com.meloda.app.fast.model.MainScreenState
import com.meloda.app.fast.navigation.Main
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -21,30 +27,33 @@ import kotlinx.coroutines.launch
interface MainViewModel {
val screenState: StateFlow<MainScreenState>
val isNeedToOpenAuth: StateFlow<Boolean>
val startDestination: StateFlow<Any?>
val isNeedToReplaceWithAuth: StateFlow<Boolean>
val longPollState: StateFlow<LongPollState>
val startOnlineService: StateFlow<Boolean>
fun useDynamicColorsChanged(use: Boolean)
fun useDarkThemeChanged(use: Boolean)
fun onRequestNotificationsPermissionClicked(fromRationale: Boolean)
fun onNotificationsAlertNegativeClicked()
fun onNotificationsRequested()
fun onAppPermissionsOpened()
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
val isNeedToRequestNotifications: StateFlow<Boolean>
fun onError(error: BaseError)
fun onNavigatedToAuth()
fun onAppResumed()
@OptIn(ExperimentalPermissionsApi::class)
fun onPermissionCheckStatus(status: PermissionStatus)
fun onPermissionsRequested()
fun onNotificationsDeniedDialogConfirmClicked()
fun onNotificationsDeniedDialogCancelClicked()
fun onNotificationsDeniedDialogDismissed()
fun onNotificationsRationaleDialogDismissed()
fun onNotificationsRationaleDialogCancelClicked()
}
class MainViewModelImpl(
private val accountsRepository: AccountsRepository,
private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
private val userSettings: UserSettings
) : MainViewModel, ViewModel() {
@@ -52,99 +61,137 @@ class MainViewModelImpl(
loadAccounts()
}
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
override val isNeedToOpenAuth = MutableStateFlow(false)
override val startDestination = MutableStateFlow<Any?>(null)
override val isNeedToReplaceWithAuth = MutableStateFlow(false)
override val longPollState = MutableStateFlow(
if (SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
) {
LongPollState.ForegroundService
} else {
LongPollState.DefaultService
}
)
override val startOnlineService = MutableStateFlow(
SettingsController.getBoolean(
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS,
SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS
)
)
override fun useDynamicColorsChanged(use: Boolean) {
screenState.updateValue(screenState.value.copy(useDynamicColors = use))
}
override fun useDarkThemeChanged(use: Boolean) {
screenState.updateValue(screenState.value.copy(useDarkTheme = use))
}
override fun onRequestNotificationsPermissionClicked(fromRationale: Boolean) {
screenState.setValue { old ->
if (fromRationale) {
old.copy(isNeedToOpenAppPermissions = true)
} else {
old.copy(isNeedToRequestNotifications = true)
}
}
}
override fun onNotificationsAlertNegativeClicked() {
SettingsController.edit {
putBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
false
)
}
userSettings.setLongPollBackground(false)
}
override fun onNotificationsRequested() {
screenState.setValue { old -> old.copy(isNeedToRequestNotifications = false) }
}
override fun onAppPermissionsOpened() {
screenState.setValue { old -> old.copy(isNeedToOpenAppPermissions = false) }
}
override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
override val isNeedToRequestNotifications = MutableStateFlow(false)
override fun onError(error: BaseError) {
when (error) {
BaseError.SessionExpired -> {
isNeedToOpenAuth.update { true }
isNeedToReplaceWithAuth.update { true }
}
}
}
override fun onNavigatedToAuth() {
isNeedToOpenAuth.update { false }
isNeedToReplaceWithAuth.update { false }
}
override fun onAppResumed() {
if (isNeedToShowNotificationsRationaleDialog.value) {
isNeedToCheckNotificationsPermission.update { true }
}
userSettings.onLanguageChanged(
AppCompatDelegate.getApplicationLocales()
.toLanguageTags()
.ifEmpty { null }
?: LocaleListCompat.getDefault()
.toLanguageTags()
.split(",")
.firstOrNull()
.orEmpty()
.take(5)
)
userSettings.updateUsingDarkTheme()
}
@ExperimentalPermissionsApi
override fun onPermissionCheckStatus(status: PermissionStatus) {
isNeedToCheckNotificationsPermission.update { false }
when (status) {
is PermissionStatus.Denied -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
if (status.shouldShowRationale) {
isNeedToShowNotificationsRationaleDialog.update { true }
} else {
isNeedToShowNotificationsDeniedDialog.update { true }
}
}
PermissionStatus.Granted -> {
if (isNeedToShowNotificationsRationaleDialog.value) {
isNeedToShowNotificationsRationaleDialog.update { false }
}
}
}
}
override fun onPermissionsRequested() {
isNeedToRequestNotifications.update { false }
}
override fun onNotificationsDeniedDialogConfirmClicked() {
isNeedToRequestNotifications.update { true }
}
override fun onNotificationsDeniedDialogCancelClicked() {
isNeedToShowNotificationsDeniedDialog.update { false }
disableBackgroundLongPoll()
}
override fun onNotificationsDeniedDialogDismissed() {
isNeedToShowNotificationsDeniedDialog.update { false }
}
override fun onNotificationsRationaleDialogDismissed() {
isNeedToShowNotificationsRationaleDialog.update { false }
}
override fun onNotificationsRationaleDialogCancelClicked() {
isNeedToShowNotificationsRationaleDialog.update { false }
disableBackgroundLongPoll()
}
private fun listenLongPollState() {
userSettings.longPollStateToApply.listenValue { newState ->
if (newState == LongPollState.Background) {
isNeedToCheckNotificationsPermission.update { true }
}
}
}
private fun loadAccounts() {
viewModelScope.launch(Dispatchers.IO) {
val accounts = accountsRepository.getAccounts()
val currentAccount = getCurrentAccountUseCase()
Log.d("MainViewModel", "initUserConfig: accounts: $accounts")
Log.d("MainViewModel", "currentAccount: $currentAccount")
if (accounts.isNotEmpty()) {
val currentAccount = accounts.find { it.userId == UserConfig.currentUserId }
if (currentAccount != null) {
UserConfig.apply {
this.userId = currentAccount.userId
this.accessToken = currentAccount.accessToken
this.fastToken = currentAccount.fastToken
this.trustedHash = currentAccount.trustedHash
}
listenLongPollState()
if (currentAccount != null) {
UserConfig.apply {
this.userId = currentAccount.userId
this.accessToken = currentAccount.accessToken
this.fastToken = currentAccount.fastToken
this.trustedHash = currentAccount.trustedHash
}
userSettings.setLongPollStateToApply(
if (SettingsController.isLongPollInBackgroundEnabled) {
LongPollState.Background
} else {
LongPollState.InApp
}
)
}
screenState.setValue { old ->
old.copy(
accounts = accounts,
accountsLoaded = true
)
startDestination.setValue {
if (currentAccount == null) AuthGraph
else Main
}
}
}
private fun disableBackgroundLongPoll() {
SettingsController.isLongPollInBackgroundEnabled = false
userSettings.setLongPollStateToApply(LongPollState.InApp)
}
}
@@ -1,7 +0,0 @@
package com.meloda.app.fast.model
sealed class LongPollState {
data object ForegroundService : LongPollState()
data object DefaultService : LongPollState()
data object Stop : LongPollState()
}
@@ -1,26 +0,0 @@
package com.meloda.app.fast.model
import androidx.compose.runtime.Immutable
import com.meloda.app.fast.model.database.AccountEntity
@Immutable
data class MainScreenState(
val accounts: List<AccountEntity>,
val accountsLoaded: Boolean,
val useDarkTheme: Boolean,
val useDynamicColors: Boolean,
val isNeedToRequestNotifications: Boolean,
val isNeedToOpenAppPermissions: Boolean
) {
companion object {
val EMPTY: MainScreenState = MainScreenState(
accounts = emptyList(),
accountsLoaded = false,
useDarkTheme = false,
useDynamicColors = false,
isNeedToRequestNotifications = false,
isNeedToOpenAppPermissions = false
)
}
}
@@ -1,7 +0,0 @@
package com.meloda.app.fast.model
sealed class ServicesState {
data object Started : ServicesState()
data object Stopped : ServicesState()
data object Unknown : ServicesState()
}
@@ -6,17 +6,13 @@ import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -26,34 +22,33 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import com.conena.nanokt.android.content.pxToDp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.meloda.app.fast.MainViewModel
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.extensions.ifEmpty
import com.meloda.app.fast.MainViewModelImpl
import com.meloda.app.fast.common.AppConstants
import com.meloda.app.fast.common.extensions.isSdkAtLeast
import com.meloda.app.fast.datastore.SettingsController
import com.meloda.app.fast.datastore.SettingsKeys
import com.meloda.app.fast.datastore.UserSettings
import com.meloda.app.fast.datastore.model.LongPollState
import com.meloda.app.fast.datastore.model.ThemeConfig
import com.meloda.app.fast.designsystem.AppTheme
import com.meloda.app.fast.designsystem.CheckPermission
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.RequestPermission
import com.meloda.app.fast.model.MainScreenState
import com.meloda.app.fast.service.OnlineService
import com.meloda.app.fast.service.longpolling.LongPollingService
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.KoinContext
import org.koin.compose.koinInject
import com.meloda.app.fast.designsystem.R as UiR
class MainActivity : AppCompatActivity() {
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -76,30 +71,64 @@ class MainActivity : AppCompatActivity() {
setContent {
KoinContext {
val context = LocalContext.current
val userSettings: UserSettings = koinInject()
LifecycleResumeEffect(true) {
userSettings.onLanguageChanged(
AppCompatDelegate.getApplicationLocales()
.toLanguageTags()
.ifEmpty { null }
?: LocaleListCompat.getDefault()
.toLanguageTags()
.split(",")
.firstOrNull()
.orEmpty()
.take(5)
)
val longPollCurrentState by userSettings.longPollCurrentState.collectAsStateWithLifecycle()
val longPollStateToApply by userSettings.longPollStateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
LifecycleResumeEffect(true) {
viewModel.onAppResumed()
onPauseOrDispose {}
}
LaunchedEffect(true) {
userSettings.updateUsingDarkTheme()
val permissionState =
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToCheckPermission) {
if (isNeedToCheckPermission) {
viewModel.onPermissionCheckStatus(permissionState.status)
if (permissionState.status.isGranted) {
if (longPollCurrentState == LongPollState.InApp) {
toggleLongPollService(false)
}
toggleLongPollService(
enable = true,
inBackground = true
)
}
}
}
val isLongPollInBackground by userSettings.longPollBackground.collectAsStateWithLifecycle()
toggleLongPollService(true, isLongPollInBackground)
LaunchedEffect(isNeedToRequestPermission) {
if (isNeedToRequestPermission) {
viewModel.onPermissionsRequested()
permissionState.launchPermissionRequest()
}
}
LaunchedEffect(longPollStateToApply) {
if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply
) {
toggleLongPollService(false)
Log.d("LongPoll", "recreate()")
}
toggleLongPollService(
enable = longPollStateToApply.isLaunched(),
inBackground = longPollStateToApply == LongPollState.Background
)
}
}
val isOnline by userSettings.online.collectAsStateWithLifecycle()
LifecycleResumeEffect(isOnline) {
@@ -110,11 +139,9 @@ class MainActivity : AppCompatActivity() {
}
}
val windowAdaptiveInfo = currentWindowAdaptiveInfo()
val isDeviceCompact by remember(windowAdaptiveInfo) {
val isDeviceCompact by remember(true) {
derivedStateOf {
windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
context.resources.displayMetrics.widthPixels.pxToDp() <= 360
}
}
@@ -138,7 +165,7 @@ class MainActivity : AppCompatActivity() {
selectedColorScheme = currentTheme.selectedColorScheme,
useAmoledBackground = currentTheme.usingAmoledBackground,
) {
RootScreen()
RootScreen(viewModel = viewModel)
}
}
}
@@ -147,40 +174,37 @@ class MainActivity : AppCompatActivity() {
private fun createNotificationChannels() {
isSdkAtLeast(Build.VERSION_CODES.O) {
val dialogsName = "Dialogs"
val dialogsDescriptionText = "Channel for dialogs notifications"
val dialogsImportance = NotificationManager.IMPORTANCE_HIGH
val dialogsChannel =
NotificationChannel("simple_notifications", dialogsName, dialogsImportance).apply {
description = dialogsDescriptionText
val noCategoryName = getString(UiR.string.notification_channel_no_category_name)
val noCategoryDescriptionText = getString(UiR.string.notification_channel_no_category_description)
val noCategoryImportance = NotificationManager.IMPORTANCE_HIGH
val noCategoryChannel =
NotificationChannel(AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED, noCategoryName, noCategoryImportance).apply {
description = noCategoryDescriptionText
}
val longPollName = "Long Polling"
val longPollDescriptionText = "Channel for long polling service"
val longPollName = getString(UiR.string.notification_channel_long_polling_service_name)
val longPollDescriptionText = getString(UiR.string.notification_channel_long_polling_service_description)
val longPollImportance = NotificationManager.IMPORTANCE_NONE
val longPollChannel =
NotificationChannel("long_polling", longPollName, longPollImportance).apply {
NotificationChannel(AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING, longPollName, longPollImportance).apply {
description = longPollDescriptionText
}
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannels(listOf(dialogsChannel, longPollChannel))
notificationManager.createNotificationChannels(listOf(noCategoryChannel, longPollChannel))
}
}
private fun toggleLongPollService(
enable: Boolean,
asForeground: Boolean = SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
inBackground: Boolean = SettingsController.isLongPollInBackgroundEnabled
) {
if (enable) {
val longPollIntent = Intent(this, LongPollingService::class.java)
if (asForeground) {
if (inBackground) {
ContextCompat.startForegroundService(this, longPollIntent)
} else {
startService(longPollIntent)
@@ -201,104 +225,15 @@ class MainActivity : AppCompatActivity() {
private fun stopServices() {
toggleOnlineService(enable = false)
val asForeground = SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
val asForeground = SettingsController.isLongPollInBackgroundEnabled
if (!asForeground) {
toggleLongPollService(enable = false)
}
}
private fun setNewLanguage(newLanguage: String) {
val appLocales = AppCompatDelegate.getApplicationLocales()
if (newLanguage.isEmpty()) {
if (!appLocales.isEmpty) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
}
} else {
if (!appLocales.toLanguageTags().startsWith(newLanguage)) {
val newLocale = LocaleListCompat.forLanguageTags(newLanguage)
AppCompatDelegate.setApplicationLocales(newLocale)
}
}
}
override fun onDestroy() {
super.onDestroy()
stopServices()
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NotificationsPermissionChecker(
screenState: MainScreenState,
viewModel: MainViewModel
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val permission =
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
if (screenState.isNeedToOpenAppPermissions) {
viewModel.onAppPermissionsOpened()
LocalContext.current.apply {
startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", this.packageName, null)
)
)
}
}
if (screenState.isNeedToRequestNotifications) {
RequestPermission(permission = permission)
}
val isNeedToCheckNotificationsPermission by remember {
derivedStateOf {
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
))
}
}
if (isNeedToCheckNotificationsPermission) {
CheckPermission(
showRationale = {
MaterialDialog(
title = UiText.Resource(UiR.string.warning),
text = UiText.Simple("The application will not be able to work properly without permission to send notifications."),
confirmText = UiText.Simple("Grant"),
confirmAction = {
viewModel.onRequestNotificationsPermissionClicked(true)
},
cancelText = UiText.Resource(UiR.string.cancel),
cancelAction = viewModel::onNotificationsAlertNegativeClicked,
onDismissAction = viewModel::onNotificationsAlertNegativeClicked,
buttonsInvokeDismiss = false
)
},
onDenied = {
MaterialDialog(
title = UiText.Resource(UiR.string.warning),
text = UiText.Simple("The application needs permission to send notifications to update messages and other information."),
confirmText = UiText.Simple("Grant"),
confirmAction = {
viewModel.onRequestNotificationsPermissionClicked(false)
},
cancelText = UiText.Resource(UiR.string.cancel),
onDismissAction = {},
buttonsInvokeDismiss = false
)
},
permission = permission
)
}
}
@@ -1,12 +1,20 @@
package com.meloda.app.fast.presentation
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavHostController
@@ -14,12 +22,11 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.meloda.app.fast.MainViewModel
import com.meloda.app.fast.MainViewModelImpl
import com.meloda.app.fast.auth.AuthGraph
import com.meloda.app.fast.auth.authNavGraph
import com.meloda.app.fast.auth.navigateToAuth
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsScreen
import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials
import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.designsystem.R
import com.meloda.app.fast.languagepicker.navigation.languagePickerScreen
import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker
import com.meloda.app.fast.messageshistory.navigation.messagesHistoryScreen
@@ -31,10 +38,15 @@ import com.meloda.app.fast.settings.navigation.settingsScreen
import org.koin.androidx.compose.koinViewModel
@Composable
fun RootScreen(navController: NavHostController = rememberNavController()) {
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val isNeedToOpenAuth by viewModel.isNeedToOpenAuth.collectAsStateWithLifecycle()
fun RootScreen(
navController: NavHostController = rememberNavController(),
viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
) {
val context = LocalContext.current
val startDestination by viewModel.startDestination.collectAsStateWithLifecycle()
val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle()
val isNeedToShowDeniedDialog by viewModel.isNeedToShowNotificationsDeniedDialog.collectAsStateWithLifecycle()
val isNeedToShowRationaleDialog by viewModel.isNeedToShowNotificationsRationaleDialog.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToOpenAuth) {
if (isNeedToOpenAuth) {
@@ -43,14 +55,63 @@ fun RootScreen(navController: NavHostController = rememberNavController()) {
}
}
if (screenState.accountsLoaded) {
val isNeedToShowConversations = remember {
screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn()
}
if (isNeedToShowDeniedDialog) {
AlertDialog(
onDismissRequest = viewModel::onNotificationsDeniedDialogDismissed,
title = { Text(text = stringResource(id = R.string.warning)) },
text = { Text(text = stringResource(id = R.string.background_long_poll_denied_text)) },
confirmButton = {
TextButton(onClick = viewModel::onNotificationsDeniedDialogConfirmClicked) {
Text(text = stringResource(id = R.string.action_request))
}
},
dismissButton = {
TextButton(onClick = viewModel::onNotificationsDeniedDialogCancelClicked) {
Text(text = stringResource(id = R.string.action_disable))
}
},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
)
}
if (isNeedToShowRationaleDialog) {
AlertDialog(
onDismissRequest = viewModel::onNotificationsRationaleDialogDismissed,
title = { Text(text = stringResource(id = R.string.warning)) },
text = { Text(text = stringResource(id = R.string.background_long_poll_rationale_text)) },
confirmButton = {
TextButton(
onClick = {
context.startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", context.packageName, null)
)
)
}
) {
Text(text = stringResource(id = R.string.title_settings))
}
},
dismissButton = {
TextButton(onClick = viewModel::onNotificationsRationaleDialogCancelClicked) {
Text(text = stringResource(id = R.string.action_disable))
}
},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
)
}
if (startDestination != null) {
NavHost(
navController = navController,
startDestination = if (isNeedToShowConversations) Main else AuthGraph,
startDestination = requireNotNull(startDestination),
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
@@ -81,11 +142,6 @@ fun RootScreen(navController: NavHostController = rememberNavController()) {
languagePickerScreen(onBack = navController::navigateUp)
}
}
NotificationsPermissionChecker(
screenState = screenState,
viewModel = viewModel
)
}
fun NavController.navigateToMain() {
@@ -12,6 +12,7 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.conena.nanokt.android.app.stopForegroundCompat
import com.meloda.app.fast.common.AppConstants
import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.common.VkConstants
import com.meloda.app.fast.common.extensions.listenValue
@@ -20,6 +21,9 @@ import com.meloda.app.fast.data.LongPollUseCase
import com.meloda.app.fast.data.processState
import com.meloda.app.fast.datastore.SettingsController
import com.meloda.app.fast.datastore.SettingsKeys
import com.meloda.app.fast.datastore.UserSettings
import com.meloda.app.fast.datastore.model.LongPollState
import com.meloda.app.fast.designsystem.R
import com.meloda.app.fast.model.api.data.LongPollUpdates
import com.meloda.app.fast.model.api.data.VkLongPollData
import com.meloda.app.fast.util.NotificationsUtils
@@ -36,6 +40,8 @@ import kotlin.coroutines.suspendCoroutine
class LongPollingService : Service() {
private val userSettings: UserSettings by inject()
private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
@@ -44,6 +50,9 @@ class LongPollingService : Service() {
if (throwable !is NoAccessTokenException) {
throwable.printStackTrace()
}
userSettings.updateLongPollCurrentState(LongPollState.Exception)
userSettings.setLongPollStateToApply(LongPollState.Exception)
}
private val coroutineContext: CoroutineContext
@@ -70,14 +79,14 @@ class LongPollingService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (startId > 1) return START_STICKY
val asForeground = preferences.getBoolean(
val inBackground = preferences.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
Log.d(
STATE_TAG,
"onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId;\ninstance: $this"
"onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this"
)
if (currentJob != null) {
@@ -105,14 +114,19 @@ class LongPollingService : Service() {
PendingIntent.FLAG_IMMUTABLE
)
if (asForeground) {
userSettings.updateLongPollCurrentState(
if (inBackground) LongPollState.Background
else LongPollState.InApp
)
if (inBackground) {
val notification =
NotificationsUtils.createNotification(
context = this,
title = "LongPoll",
contentText = "нажмите, чтобы убрать уведомление",
title = getString(R.string.long_polling_service_notification_title),
contentText = getString(R.string.long_polling_service_notification_content),
notRemovable = false,
channelId = "long_polling",
channelId = AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING,
priority = NotificationsUtils.NotificationPriority.Low,
category = NotificationCompat.CATEGORY_SERVICE,
customNotificationId = NOTIFICATION_ID,
@@ -240,6 +254,7 @@ class LongPollingService : Service() {
override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy")
userSettings.updateLongPollCurrentState(LongPollState.Stopped)
try {
SettingsController.edit {
putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true)
@@ -5,6 +5,7 @@ import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.meloda.app.fast.common.AppConstants
import com.meloda.app.fast.designsystem.R as UiR
object NotificationsUtils {
@@ -20,7 +21,7 @@ object NotificationsUtils {
timeStampWhen: Long? = null,
notify: Boolean = false,
notRemovable: Boolean = false,
channelId: String = "simple_notifications",
channelId: String = AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED,
priority: NotificationPriority = NotificationPriority.Default,
contentIntent: PendingIntent? = null,
category: String? = null,