From 654f47bb94df2a3d4a4ca1fa009d35aed7253745 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 15 Jul 2024 05:01:54 +0300 Subject: [PATCH] a lot of improvements for long polling service and notifications --- app/build.gradle.kts | 1 - .../com/meloda/app/fast/MainViewModel.kt | 235 +++++++++++------- .../meloda/app/fast/model/LongPollState.kt | 7 - .../meloda/app/fast/model/MainScreenState.kt | 26 -- .../meloda/app/fast/model/ServicesState.kt | 7 - .../app/fast/presentation/MainActivity.kt | 215 ++++++---------- .../app/fast/presentation/RootScreen.kt | 90 +++++-- .../service/longpolling/LongPollingService.kt | 27 +- .../app/fast/util/NotificationsUtils.kt | 3 +- .../meloda/app/fast/common/AppConstants.kt | 3 + .../app/fast/common/extensions/Extensions.kt | 2 + .../app/fast/data/db/AccountsRepository.kt | 2 + .../fast/data/db/AccountsRepositoryImpl.kt | 3 + .../fast/data/db/GetCurrentAccountUseCase.kt | 13 + .../com/meloda/app/fast/data/di/DataModule.kt | 2 + .../app/fast/database/dao/AccountDao.kt | 3 + .../app/fast/datastore/SettingsController.kt | 11 + .../meloda/app/fast/datastore/UserSettings.kt | 27 +- .../app/fast/datastore/model/LongPollState.kt | 14 ++ .../app/fast/designsystem/Extensions.kt | 33 --- .../app/fast/designsystem/ImmutableList.kt | 4 + .../app/fast/designsystem/MaterialDialog.kt | 53 ++-- .../src/main/res/values-ru/strings.xml | 10 + .../src/main/res/values/strings.xml | 14 +- .../meloda/fast/auth/login/LoginViewModel.kt | 22 +- .../presentation/ConversationsScreen.kt | 18 +- .../app/fast/settings/SettingsViewModel.kt | 10 +- gradle/libs.versions.toml | 6 +- 28 files changed, 473 insertions(+), 388 deletions(-) delete mode 100644 app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt delete mode 100644 app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt delete mode 100644 app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/db/GetCurrentAccountUseCase.kt create mode 100644 core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/LongPollState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 95861bfd..fe005df2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -182,5 +182,4 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.kotlin.serialization) - implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04") } diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt b/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt index 64b43407..f52132e9 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt @@ -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 - val isNeedToOpenAuth: StateFlow + val startDestination: StateFlow + val isNeedToReplaceWithAuth: StateFlow - val longPollState: StateFlow - val startOnlineService: StateFlow - - fun useDynamicColorsChanged(use: Boolean) - - fun useDarkThemeChanged(use: Boolean) - - fun onRequestNotificationsPermissionClicked(fromRationale: Boolean) - fun onNotificationsAlertNegativeClicked() - - fun onNotificationsRequested() - - fun onAppPermissionsOpened() + val isNeedToShowNotificationsDeniedDialog: StateFlow + val isNeedToShowNotificationsRationaleDialog: StateFlow + val isNeedToCheckNotificationsPermission: StateFlow + val isNeedToRequestNotifications: StateFlow 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(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) + } } diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt deleted file mode 100644 index 6ef44939..00000000 --- a/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt deleted file mode 100644 index 93bb33e6..00000000 --- a/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt +++ /dev/null @@ -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, - 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 - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt deleted file mode 100644 index 664bf563..00000000 --- a/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt b/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt index c4158474..7aee864d 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt @@ -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() + + 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 - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/app/fast/presentation/RootScreen.kt b/app/src/main/kotlin/com/meloda/app/fast/presentation/RootScreen.kt index fac53d8a..5b854090 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/presentation/RootScreen.kt @@ -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() - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val isNeedToOpenAuth by viewModel.isNeedToOpenAuth.collectAsStateWithLifecycle() +fun RootScreen( + navController: NavHostController = rememberNavController(), + viewModel: MainViewModel = koinViewModel() +) { + 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() { diff --git a/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt index 3cb12fb2..4b1a4db7 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt @@ -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) diff --git a/app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt b/app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt index 523bc569..fefd6214 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt @@ -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, diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt index 225c576e..d8f700b9 100644 --- a/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt @@ -7,4 +7,7 @@ object AppConstants { const val API_VERSION = "5.173" const val URL_OAUTH = "https://oauth.vk.com" const val URL_API = "https://api.vk.com/method" + + const val NOTIFICATION_CHANNEL_UNCATEGORIZED = "uncategorized" + const val NOTIFICATION_CHANNEL_LONG_POLLING = "long_polling" } diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt index 0b8b2a8a..640b0c44 100644 --- a/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope @@ -141,6 +142,7 @@ fun Any.toList(mapper: (old: Any) -> T): List { } } +@ChecksSdkIntAtLeast(parameter = 0, lambda = 1) fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean { return if (Build.VERSION.SDK_INT >= sdkInt) { action?.invoke() diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt index 18074eef..42055a52 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt @@ -6,5 +6,7 @@ interface AccountsRepository { suspend fun getAccounts(): List + suspend fun getAccountById(userId: Int): AccountEntity? + suspend fun storeAccounts(accounts: List) } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt index 81579668..4d0ad47d 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt @@ -9,6 +9,9 @@ class AccountsRepositoryImpl( override suspend fun getAccounts(): List = accountDao.getAll() + override suspend fun getAccountById(userId: Int): AccountEntity? = + accountDao.getById(userId) + override suspend fun storeAccounts( accounts: List ) = accountDao.insertAll(accounts) diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/GetCurrentAccountUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/GetCurrentAccountUseCase.kt new file mode 100644 index 00000000..3dc10d1d --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/GetCurrentAccountUseCase.kt @@ -0,0 +1,13 @@ +package com.meloda.app.fast.data.db + +import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.model.database.AccountEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class GetCurrentAccountUseCase(private val accountsRepository: AccountsRepository) { + + suspend operator fun invoke(): AccountEntity? = withContext(Dispatchers.IO) { + accountsRepository.getAccountById(UserConfig.currentUserId) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt index 5db78378..e836d6b7 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt @@ -27,6 +27,7 @@ import com.meloda.app.fast.data.api.users.UsersUseCaseImpl import com.meloda.app.fast.data.api.videos.VideosRepository import com.meloda.app.fast.data.db.AccountsRepository import com.meloda.app.fast.data.db.AccountsRepositoryImpl +import com.meloda.app.fast.data.db.GetCurrentAccountUseCase import com.meloda.app.fast.database.di.databaseModule import com.meloda.app.fast.datastore.di.dataStoreModule import com.meloda.app.fast.network.di.networkModule @@ -67,6 +68,7 @@ val dataModule = module { singleOf(::VideosRepository) singleOf(::AccountsRepositoryImpl) bind AccountsRepository::class + singleOf(::GetCurrentAccountUseCase) singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class } diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt index c960accc..ba396cdf 100644 --- a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt @@ -10,6 +10,9 @@ abstract class AccountDao : EntityDao { @Query("SELECT * FROM accounts") abstract suspend fun getAll(): List + @Query("SELECT * FROM accounts WHERE userId = :userId") + abstract suspend fun getById(userId: Int): AccountEntity? + @Query("DELETE FROM accounts WHERE userId = :userId") abstract suspend fun deleteById(userId: Int) } diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt index 8fc99016..5ba9744a 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt @@ -50,4 +50,15 @@ object SettingsController { } } } + + var isLongPollInBackgroundEnabled: Boolean = + SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND + get() = getBoolean( + SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND + ) + set(value) { + field = value + put(SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, value) + } } diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt index ef6e341a..7257aa07 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt @@ -2,6 +2,8 @@ package com.meloda.app.fast.datastore import android.content.res.Resources import android.os.PowerManager +import android.util.Log +import com.meloda.app.fast.datastore.model.LongPollState import com.meloda.app.fast.datastore.model.ThemeConfig import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -9,7 +11,8 @@ import kotlinx.coroutines.flow.update interface UserSettings { val theme: StateFlow - val longPollBackground: StateFlow + val longPollStateToApply: StateFlow + val longPollCurrentState: StateFlow val online: StateFlow val debugSettingsEnabled: StateFlow val useContactNames: StateFlow @@ -21,7 +24,8 @@ interface UserSettings { fun useDynamicColorsChanged(use: Boolean) fun useBlurChanged(use: Boolean) fun useMultiline(use: Boolean) - fun setLongPollBackground(background: Boolean) + fun setLongPollStateToApply(newState: LongPollState) + fun updateLongPollCurrentState(currentState: LongPollState) fun setOnline(use: Boolean) fun enableDebugSettings(enable: Boolean) fun onUseContactNamesChanged(use: Boolean) @@ -45,12 +49,9 @@ class UserSettingsImpl( ) ) - override val longPollBackground = MutableStateFlow( - SettingsController.getBoolean( - SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND - ) - ) + override val longPollStateToApply = MutableStateFlow(LongPollState.Stopped) + override val longPollCurrentState = MutableStateFlow(LongPollState.Stopped) + override val online = MutableStateFlow( SettingsController.getBoolean( SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS, @@ -107,8 +108,14 @@ class UserSettingsImpl( theme.value = theme.value.copy(multiline = use) } - override fun setLongPollBackground(background: Boolean) { - longPollBackground.value = background + override fun setLongPollStateToApply(newState: LongPollState) { + longPollStateToApply.update { newState } + Log.d("UserSettings", "setLongPollState: $newState") + } + + override fun updateLongPollCurrentState(currentState: LongPollState) { + longPollCurrentState.update { currentState } + Log.d("UserSettings", "updateLongPollCurrentState: $currentState") } override fun setOnline(use: Boolean) { diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/LongPollState.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/LongPollState.kt new file mode 100644 index 00000000..e4f77e6d --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/LongPollState.kt @@ -0,0 +1,14 @@ +package com.meloda.app.fast.datastore.model + +sealed class LongPollState { + data object Stopped : LongPollState() + + // TODO: 15/07/2024, Danil Nikolaev: support for android 15 +// data object Terminated : LongPollState() + data object InApp : LongPollState() + data object Background : LongPollState() + data object Exception : LongPollState() + + + fun isLaunched(): Boolean = this in listOf(InApp, Background) +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt index 4e8fd568..194a731a 100644 --- a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt @@ -4,15 +4,11 @@ import android.content.res.Configuration import android.view.KeyEvent import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import com.google.accompanist.permissions.PermissionStatus import com.meloda.app.fast.common.UiText import com.meloda.app.fast.common.util.AndroidUtils import com.meloda.app.fast.datastore.SettingsController @@ -81,32 +77,3 @@ fun Modifier.handleEnterKey( action.invoke() } else false } - -@OptIn(ExperimentalPermissionsApi::class) -@Composable -fun CheckPermission( - showRationale: @Composable () -> Unit, - onDenied: @Composable () -> Unit, - permission: PermissionState, -) { - when (val status = permission.status) { - is PermissionStatus.Denied -> { - if (status.shouldShowRationale) { - showRationale() - } else { - onDenied() - } - } - - is PermissionStatus.Granted -> Unit - } -} - -@OptIn(ExperimentalPermissionsApi::class) -@Composable -fun RequestPermission( - permission: PermissionState -) { - LaunchedEffect(Unit) { permission.launchPermissionRequest() } -} - diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt index 7ea0e6fb..765a88fc 100644 --- a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt @@ -20,6 +20,10 @@ class ImmutableList(val values: List) : Iterable { return values.map(transform).toImmutableList() } + inline fun mapNotNull(transform: (T) -> R?): ImmutableList { + return values.mapNotNull(transform).toImmutableList() + } + inline fun mapIndexed(transform: (index: Int, T) -> R): ImmutableList { return values.mapIndexed(transform).toImmutableList() } diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt index ab92eacc..62fa1341 100644 --- a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt @@ -1,11 +1,5 @@ package com.meloda.app.fast.designsystem -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -32,15 +26,19 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties import com.meloda.app.fast.common.UiText +import com.meloda.app.fast.common.parseString import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList -// TODO: 08.04.2023, Danil Nikolaev: review +// TODO: 08.04.2023, Danil Nikolaev: refactor this +@Deprecated("need refactoring") @OptIn(ExperimentalMaterial3Api::class) @Composable fun MaterialDialog( @@ -58,17 +56,22 @@ fun MaterialDialog( items: ImmutableList = ImmutableList.empty(), onItemClick: ((index: Int) -> Unit)? = null, buttonsInvokeDismiss: Boolean = true, + properties: DialogProperties = DialogProperties(), customContent: (@Composable ColumnScope.() -> Unit)? = null, ) { - var isVisible by remember { + var isVisible by rememberSaveable { mutableStateOf(true) } - val onDismissRequest = { - onDismissAction.invoke() - isVisible = false + val onDismissRequest = remember { + { + onDismissAction.invoke() + isVisible = false + } } - val stringTitles = items.map { it.getString().orEmpty() } + val context = LocalContext.current + + val stringTitles = items.mapNotNull { it.parseString(context.resources) } var alertItems by remember { mutableStateOf( @@ -81,11 +84,10 @@ fun MaterialDialog( ) } - - if (isVisible) { -// AlertAnimation(visible = isVisible) { + AlertAnimation(visible = isVisible) { BasicAlertDialog( - onDismissRequest = onDismissRequest + onDismissRequest = onDismissRequest, + properties = properties ) { val scrollState = rememberScrollState() val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } } @@ -251,15 +253,16 @@ fun MaterialDialog( @Composable fun AlertAnimation( visible: Boolean, - content: @Composable AnimatedVisibilityScope.() -> Unit + content: @Composable () -> Unit ) { - AnimatedVisibility( - visible = visible, - enter = fadeIn(animationSpec = tween(400)) + - scaleIn(animationSpec = tween(400)), - exit = fadeOut(animationSpec = tween(150)), - content = content - ) + if (visible) content() +// AnimatedVisibility( +// visible = visible, +// enter = fadeIn(animationSpec = tween(400)) + +// scaleIn(animationSpec = tween(400)), +// exit = fadeOut(animationSpec = tween(150)), +// content = content +// ) } @Composable diff --git a/core/designsystem/src/main/res/values-ru/strings.xml b/core/designsystem/src/main/res/values-ru/strings.xml index 422e4f33..48df5e84 100644 --- a/core/designsystem/src/main/res/values-ru/strings.xml +++ b/core/designsystem/src/main/res/values-ru/strings.xml @@ -188,4 +188,14 @@ Быть «в сети» Статус «в сети» будет отправляться каждые 5 минут Отладка + Отключить + Приложение не сможет обновлять сообщения в фоне без доступа к уведомлениям + Приложению нужен доступ к уведомлениям для обновления сообщений в фоне + Запросить + Обновление сообщений в фоне + Нажмите, чтобы скрыть + Без категории + Уведомления без категории + Сервис обновления сообщений + Уведомления сервиса обновлений сообщений diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 757330ef..8c9e7ec1 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -246,5 +246,17 @@ Send online status Online status will be sent every five minutes Debug + The app won\'t be able to update messages in the background without access to notifications + Disable + The app needs access to notifications to update messages in the background + Request - + Updating messages in background + Tap to hide + + Uncategorized + Uncategorized notifications + + Message update service + Message update service notifications + diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt index 32cda2e3..9594022e 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt @@ -13,13 +13,16 @@ import com.meloda.app.fast.data.State import com.meloda.app.fast.data.api.users.UsersUseCase import com.meloda.app.fast.data.db.AccountsRepository import com.meloda.app.fast.data.processState +import com.meloda.app.fast.datastore.SettingsController +import com.meloda.app.fast.datastore.UserSettings +import com.meloda.app.fast.datastore.model.LongPollState import com.meloda.app.fast.model.database.AccountEntity import com.meloda.app.fast.network.OAuthErrorDomain import com.meloda.fast.auth.login.model.CaptchaArguments import com.meloda.fast.auth.login.model.LoginError import com.meloda.fast.auth.login.model.LoginScreenState -import com.meloda.fast.auth.login.model.LoginValidationArguments import com.meloda.fast.auth.login.model.LoginUserBannedArguments +import com.meloda.fast.auth.login.model.LoginValidationArguments import com.meloda.fast.auth.login.model.LoginValidationResult import com.meloda.fast.auth.login.validation.LoginValidator import kotlinx.coroutines.Dispatchers @@ -67,7 +70,8 @@ class LoginViewModelImpl( private val oAuthUseCase: OAuthUseCase, private val usersUseCase: UsersUseCase, private val accountsRepository: AccountsRepository, - private val loginValidator: LoginValidator + private val loginValidator: LoginValidator, + private val userSettings: UserSettings ) : ViewModel(), LoginViewModel { override val screenState = MutableStateFlow(LoginScreenState.EMPTY) @@ -155,6 +159,8 @@ class LoginViewModelImpl( UserConfig.trustedHash = account.trustedHash } + startLongPoll() + usersUseCase.get( userIds = null, fields = VkConstants.USER_FIELDS, @@ -238,6 +244,8 @@ class LoginViewModelImpl( UserConfig.trustedHash = account.trustedHash } + startLongPoll() + accountsRepository.storeAccounts(listOf(currentAccount)) captchaArguments.update { null } @@ -338,4 +346,14 @@ class LoginViewModelImpl( } } } + + private fun startLongPoll() { + userSettings.setLongPollStateToApply( + if (SettingsController.isLongPollInBackgroundEnabled) { + LongPollState.Background + } else { + LongPollState.InApp + } + ) + } } diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt index 5fd9830f..2807b0e9 100644 --- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyListState @@ -156,17 +155,13 @@ fun ConversationsScreen( val view = LocalView.current val currentTheme = LocalTheme.current - val maxLines by remember { - derivedStateOf { - if (currentTheme.multiline) 2 else 1 - } + val maxLines by remember(currentTheme) { + mutableIntStateOf(if (currentTheme.multiline) 2 else 1) } val listState = rememberLazyListState() - val isListScrollingUp = listState.isScrollingUp() - - val paginationConditionMet by remember { + val paginationConditionMet by remember(canPaginate, listState) { derivedStateOf { canPaginate && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index @@ -292,7 +287,7 @@ fun ConversationsScreen( Column { AnimatedVisibility( - visible = isListScrollingUp, + visible = listState.isScrollingUp(), enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)), exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200)) ) { @@ -351,10 +346,7 @@ fun ConversationsScreen( .nestedScroll(pullToRefreshState.nestedScrollConnection) ) { ConversationsListComposable( - onConversationsClick = { id -> - onConversationItemClicked(id) - - }, + onConversationsClick = onConversationItemClicked, onConversationsLongClick = onConversationItemLongClicked, screenState = screenState, state = listState, diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt index d3f128c9..7644a657 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt @@ -14,6 +14,7 @@ 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.isDebugSettingsShown +import com.meloda.app.fast.datastore.model.LongPollState import com.meloda.app.fast.model.database.AccountEntity import com.meloda.app.fast.settings.model.SettingsItem import com.meloda.app.fast.settings.model.SettingsScreenState @@ -161,7 +162,14 @@ class SettingsViewModelImpl( when (key) { SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> { val isEnabled = (newValue as? Boolean) == true - userSettings.setLongPollBackground(isEnabled) + userSettings.setLongPollStateToApply( + userSettings.longPollStateToApply.value.let { state -> + if (state.isLaunched()) { + if (isEnabled) LongPollState.Background + else LongPollState.InApp + } else state + } + ) if (isEnabled) { // TODO: 26/11/2023, Danil Nikolaev: implement diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d417e1a4..28ddb18c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,15 @@ [versions] -agp = "8.5.0" +agp = "8.5.1" converterMoshi = "2.11.0" eithernet = "1.9.0" haze = "0.7.3" kotlin = "2.0.0" ksp = "2.0.0-1.0.22" -vkompose = "0.5.4-k2" - compose-bom = "2024.06.00" koin = "3.5.6" -accompanist = "0.34.0" +accompanist = "0.35.1-alpha" coil = "2.6.0" coroutines = "1.9.0-RC" junit = "4.13.2"