This commit is contained in:
2026-05-31 06:48:21 +03:00
parent 96ee5ea45e
commit 66deae6fc3
20 changed files with 210 additions and 150 deletions
@@ -29,61 +29,30 @@ import dev.meloda.fast.navigation.Main
import dev.meloda.fast.settings.navigation.Settings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
interface MainViewModel {
val startDestination: StateFlow<Any?>
val isNeedToReplaceWithAuth: StateFlow<Boolean>
val currentUser: StateFlow<VkUser?>
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
val isNeedToRequestNotifications: StateFlow<Boolean>
fun onError(error: BaseError)
fun onNavigatedToAuth()
fun onAppResumed(intent: Intent)
@OptIn(ExperimentalPermissionsApi::class)
fun onPermissionCheckStatus(status: PermissionStatus)
fun onPermissionsRequested()
fun onNotificationsDeniedDialogConfirmClicked()
fun onNotificationsDeniedDialogCancelClicked()
fun onNotificationsDeniedDialogDismissed()
fun onNotificationsRationaleDialogDismissed()
fun onNotificationsRationaleDialogCancelClicked()
fun onUserAuthenticated()
}
class MainViewModelImpl(
class MainViewModel(
private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val userSettings: UserSettings,
private val longPollController: LongPollController,
private val logger: FastLogger
) : MainViewModel, ViewModel() {
) : ViewModel() {
override val startDestination = MutableStateFlow<Any?>(null)
override val isNeedToReplaceWithAuth = MutableStateFlow(false)
override val currentUser = MutableStateFlow<VkUser?>(null)
val startDestination = MutableStateFlow<Any?>(null)
val isNeedToReplaceWithAuth = MutableStateFlow(false)
val currentUser = MutableStateFlow<VkUser?>(null)
override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
override val isNeedToRequestNotifications = MutableStateFlow(false)
val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
val isNeedToRequestNotifications = MutableStateFlow(false)
private var openNotificationsSettings = false
private var openAppSettings = false
override fun onError(error: BaseError) {
fun onError(error: BaseError) {
when (error) {
BaseError.SessionExpired,
BaseError.AccountBlocked -> {
@@ -94,11 +63,11 @@ class MainViewModelImpl(
}
}
override fun onNavigatedToAuth() {
fun onNavigatedToAuth() {
isNeedToReplaceWithAuth.update { false }
}
override fun onAppResumed(intent: Intent) {
fun onAppResumed(intent: Intent) {
openNotificationsSettings =
intent.hasCategory(NotificationCompat.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
openAppSettings =
@@ -125,7 +94,7 @@ class MainViewModelImpl(
}
@ExperimentalPermissionsApi
override fun onPermissionCheckStatus(status: PermissionStatus) {
fun onPermissionCheckStatus(status: PermissionStatus) {
isNeedToCheckNotificationsPermission.update { false }
when (status) {
@@ -147,33 +116,33 @@ class MainViewModelImpl(
}
}
override fun onPermissionsRequested() {
fun onPermissionsRequested() {
isNeedToRequestNotifications.update { false }
}
override fun onNotificationsDeniedDialogConfirmClicked() {
fun onNotificationsDeniedDialogConfirmClicked() {
isNeedToRequestNotifications.update { true }
}
override fun onNotificationsDeniedDialogCancelClicked() {
fun onNotificationsDeniedDialogCancelClicked() {
isNeedToShowNotificationsDeniedDialog.update { false }
disableBackgroundLongPoll()
}
override fun onNotificationsDeniedDialogDismissed() {
fun onNotificationsDeniedDialogDismissed() {
isNeedToShowNotificationsDeniedDialog.update { false }
}
override fun onNotificationsRationaleDialogDismissed() {
fun onNotificationsRationaleDialogDismissed() {
isNeedToShowNotificationsRationaleDialog.update { false }
}
override fun onNotificationsRationaleDialogCancelClicked() {
fun onNotificationsRationaleDialogCancelClicked() {
isNeedToShowNotificationsRationaleDialog.update { false }
disableBackgroundLongPoll()
}
override fun onUserAuthenticated() {
fun onUserAuthenticated() {
loadProfile()
}
@@ -202,11 +171,17 @@ class MainViewModelImpl(
private fun loadAccounts() {
viewModelScope.launch(Dispatchers.IO) {
val currentAccount = getCurrentAccountUseCase()
val currentAccount = getCurrentAccountUseCase()?.mapToDto()
logger.debug(
this@MainViewModelImpl::class,
"loadAccounts(): currentAccount: $currentAccount"
this@MainViewModel::class,
"loadAccounts(): currentAccount: %s"
.format(
currentAccount?.copy(
accessToken = if (currentAccount.accessToken.isNotEmpty()) "<redacted>"
else "null"
)
)
)
listenLongPollState()
@@ -0,0 +1,18 @@
package dev.meloda.fast.common.di
import android.content.Context
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.PowerManager
import androidx.preference.PreferenceManager
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
val androidModule = module {
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
factoryOf(PreferenceManager::getDefaultSharedPreferences)
factory<Resources> { androidContext().resources }
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
factory<ConnectivityManager> { androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
}
@@ -1,16 +1,13 @@
package dev.meloda.fast.common.di
import android.content.Context
import android.content.res.Resources
import android.os.PowerManager
import androidx.preference.PreferenceManager
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.MainViewModel
import dev.meloda.fast.auth.authModule
import dev.meloda.fast.chatmaterials.di.chatMaterialsModule
import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.LongPollControllerImpl
import dev.meloda.fast.common.NetworkStateListener
import dev.meloda.fast.common.provider.Provider
import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.common.provider.ResourceProviderImpl
@@ -27,7 +24,7 @@ import dev.meloda.fast.profile.di.profileModule
import dev.meloda.fast.provider.ApiLanguageProvider
import dev.meloda.fast.service.longpolling.di.longPollModule
import dev.meloda.fast.settings.di.settingsModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.core.qualifier.qualifier
@@ -52,27 +49,24 @@ val applicationModule = module {
)
includes(loggerModule)
includes(androidModule)
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
singleOf(PreferenceManager::getDefaultSharedPreferences)
single<Resources> { androidContext().resources }
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
factoryOf(::ApiLanguageProvider) bind Provider::class
singleOf(::ApiLanguageProvider) bind Provider::class
viewModelOf(::MainViewModelImpl) {
qualifier = qualifier("main")
}
viewModelOf(::MainViewModel) { qualifier = qualifier("main") }
single<ImageLoader> {
ImageLoader.Builder(get())
.crossfade(true)
.build()
.also { it.diskCache?.directory?.toFile()?.listFiles() }
.also {
it.diskCache?.directory?.toFile()?.listFiles()
}
}
singleOf(::LongPollControllerImpl) bind LongPollController::class
singleOf(::ResourceProviderImpl) bind ResourceProvider::class
singleOf(::NetworkObserver)
singleOf(::NetworkStateListener)
singleOf(::NetworkObserver) { qualifier = qualifier("main") }
}
@@ -20,7 +20,6 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LifecycleResumeEffect
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.domain.LongPollEventsHandler
@@ -70,7 +69,7 @@ class MainActivity : AppCompatActivity() {
setContent {
val logger: FastLogger = koinInject()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val viewModel: MainViewModel = koinViewModel()
LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent)
onPauseOrDispose {}
@@ -184,7 +183,6 @@ class MainActivity : AppCompatActivity() {
super.onDestroy()
stopServices()
get<LongPollEventsHandler>().onDestroy()
get<NetworkObserver>().onDestroy()
}
companion object {
@@ -1,30 +1,24 @@
package dev.meloda.fast.presentation
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import dev.meloda.fast.common.NetworkStateListener
import dev.meloda.fast.common.model.NetworkState
import dev.meloda.fast.common.model.NetworkStatus
import dev.meloda.fast.common.model.NetworkType
import dev.meloda.fast.logger.FastLogger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.concurrent.ConcurrentHashMap
class NetworkObserver(
context: Context,
private val logger: FastLogger
internal class NetworkObserver(
private val connectivityManager: ConnectivityManager,
private val logger: FastLogger,
private val networkStateListener: NetworkStateListener
) {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val networkStatus = MutableStateFlow(NetworkStatus.UNAVAILABLE)
val networkStatusFlow = networkStatus.asStateFlow()
private val networkState = MutableStateFlow(NetworkState.DISCONNECTED)
val networkStateFlow = networkState.asStateFlow()
private val networks = ConcurrentHashMap<Network, NetworkModel>()
private var clearCallbacks: (() -> Unit)? = null
@@ -40,12 +34,7 @@ class NetworkObserver(
NetworkState.DISCONNECTED
}
networkState.value = state
networkStatus.value = when (state) {
NetworkState.CONNECTED -> NetworkStatus.AVAILABLE
NetworkState.DISCONNECTED -> NetworkStatus.UNAVAILABLE
}
networkStateListener.updateNetworkState(state)
log("STATE: $state")
}
@@ -58,7 +47,8 @@ class NetworkObserver(
network = network,
capabilities = connectivityManager.getNetworkCapabilities(network),
properties = connectivityManager.getLinkProperties(network),
status = NetworkStatus.AVAILABLE
status = NetworkStatus.AVAILABLE,
assumeInternet = true
)
syncNetworkState()
@@ -156,6 +146,10 @@ class NetworkObserver(
refreshActiveNetwork()
}
private fun log(text: String) {
logger.debug(this::class, text)
}
private fun refreshActiveNetwork() {
val network = connectivityManager.activeNetwork
if (network == null) {
@@ -177,17 +171,14 @@ class NetworkObserver(
syncNetworkState()
}
private fun log(text: String) {
logger.debug(this::class, text)
}
private fun mapNetworkModel(
network: Network,
capabilities: NetworkCapabilities? = null,
properties: LinkProperties? = null,
status: NetworkStatus? = null,
maxMsToLive: Long? = null,
from: NetworkModel? = null
from: NetworkModel? = null,
assumeInternet: Boolean = false
): NetworkModel {
val caps = capabilities
?: from?.networkCapabilities
@@ -199,12 +190,15 @@ class NetworkObserver(
else -> from?.type ?: NetworkType.UNKNOWN
}
val hasInternet = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
?: from?.hasInternet
?: false
val hasInternet = if (caps != null) {
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else {
from?.hasInternet ?: assumeInternet
}
val signalStrength =
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
caps?.signalStrength
} else {
null
@@ -224,27 +218,8 @@ class NetworkObserver(
?: connectivityManager.getLinkProperties(network)
)
}
fun onDestroy() {
clearCallbacks?.let { unregisterCallback ->
runCatching { unregisterCallback() }
.onFailure { throwable ->
logger.error(
this::class.java,
"Failed to unregister network callback",
throwable
)
}
}
clearCallbacks = null
networks.clear()
syncNetworkState()
}
}
enum class NetworkType {
CELLULAR, WIFI, UNKNOWN
}
data class NetworkModel(
val id: Int,
@@ -261,14 +236,3 @@ data class NetworkModel(
fun isInternetAvailable(): Boolean = hasInternet && isStatusOk()
}
enum class NetworkStatus {
AVAILABLE, UNAVAILABLE, LOST, BLOCKED, UNBLOCKED;
fun isOk(): Boolean = when (this) {
AVAILABLE, UNBLOCKED -> true
UNAVAILABLE, LOST, BLOCKED -> false
}
}
enum class NetworkState { CONNECTED, DISCONNECTED }
@@ -38,13 +38,13 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.auth.authNavGraph
import dev.meloda.fast.auth.captcha.presentation.CaptchaScreen
import dev.meloda.fast.auth.navigateToAuth
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.NetworkStateListener
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.convos.navigation.createChatScreen
import dev.meloda.fast.convos.navigation.navigateToCreateChat
@@ -64,6 +64,7 @@ import dev.meloda.fast.settings.navigation.navigateToSettings
import dev.meloda.fast.settings.navigation.settingsScreen
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.LocalLogger
import dev.meloda.fast.ui.common.LocalNetworkState
import dev.meloda.fast.ui.common.LocalSizeConfig
import dev.meloda.fast.ui.model.DeviceSize
import dev.meloda.fast.ui.model.SizeConfig
@@ -93,7 +94,7 @@ fun RootScreen(
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val viewModel: MainViewModel = koinViewModel()
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
val permissionState =
@@ -221,10 +222,17 @@ fun RootScreen(
}
}
val networkStateListener: NetworkStateListener = koinInject()
val networkState by networkStateListener.networkStateFlow.collectAsStateWithLifecycle()
LaunchedEffect(networkState) {
logger.debug("RootScreen", "NetworkState: $networkState")
}
CompositionLocalProvider(
LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig,
LocalUser provides currentUser
LocalUser provides currentUser,
LocalNetworkState provides networkState
) {
AppTheme(
useDarkTheme = themeConfig.darkMode,