a lot of improvements for long polling service and notifications
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user