diff --git a/app/src/main/java/com/aurora/store/ComposeActivity.kt b/app/src/main/java/com/aurora/store/ComposeActivity.kt index ca2d4131a..da6576e03 100644 --- a/app/src/main/java/com/aurora/store/ComposeActivity.kt +++ b/app/src/main/java/com/aurora/store/ComposeActivity.kt @@ -12,21 +12,29 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.core.content.IntentCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.aurora.extensions.getPackageName +import com.aurora.store.compose.composition.LocalNetworkStatus import com.aurora.store.compose.composition.LocalUI import com.aurora.store.compose.composition.UI import com.aurora.store.compose.navigation.NavDisplay import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.theme.AuroraTheme +import com.aurora.store.data.model.NetworkStatus +import com.aurora.store.data.providers.NetworkProvider import com.aurora.store.data.receiver.MigrationReceiver import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class ComposeActivity : ComponentActivity() { + @Inject lateinit var networkProvider: NetworkProvider + override fun onCreate(savedInstanceState: Bundle?) { MigrationReceiver.runMigrationsIfRequired(this) enableEdgeToEdge() @@ -42,7 +50,13 @@ class ComposeActivity : ComponentActivity() { } setContent { - CompositionLocalProvider(LocalUI provides localUI) { + val networkStatus by networkProvider.status.collectAsStateWithLifecycle( + initialValue = NetworkStatus.AVAILABLE + ) + CompositionLocalProvider( + LocalUI provides localUI, + LocalNetworkStatus provides networkStatus + ) { AuroraTheme { NavDisplay(startDestination = startDestination) } diff --git a/app/src/main/java/com/aurora/store/MainViewModel.kt b/app/src/main/java/com/aurora/store/MainViewModel.kt index 8d70dbf25..8d2d6513e 100644 --- a/app/src/main/java/com/aurora/store/MainViewModel.kt +++ b/app/src/main/java/com/aurora/store/MainViewModel.kt @@ -7,12 +7,10 @@ package com.aurora.store import androidx.lifecycle.ViewModel import com.aurora.store.data.helper.UpdateHelper -import com.aurora.store.data.providers.NetworkProvider import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - val networkProvider: NetworkProvider, val updateHelper: UpdateHelper ) : ViewModel() diff --git a/app/src/main/java/com/aurora/store/compose/composition/LocalNetworkStatus.kt b/app/src/main/java/com/aurora/store/compose/composition/LocalNetworkStatus.kt new file mode 100644 index 000000000..46d30dc6d --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composition/LocalNetworkStatus.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composition + +import androidx.compose.runtime.compositionLocalOf +import com.aurora.store.data.model.NetworkStatus + +/** + * CompositionLocal carrying the current device network status. Provided once at the + * activity root from a single [com.aurora.store.data.providers.NetworkProvider] subscription, + * so any screen can read `LocalNetworkStatus.current` without injecting the provider + * or duplicating the flow collection. + * + * Uses [compositionLocalOf] (not static) so only readers recompose on change. + */ +val LocalNetworkStatus = compositionLocalOf { NetworkStatus.AVAILABLE } diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkScreen.kt new file mode 100644 index 000000000..f14143b5e --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkScreen.kt @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.commons + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.provider.Settings +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.compose.ui.unit.dp +import com.aurora.extensions.isQAndAbove +import com.aurora.store.R +import com.aurora.store.compose.preview.ThemePreviewProvider + +private const val TAG = "NetworkScreen" + +/** + * Full-screen "no network" placeholder shown while [com.aurora.store.data.model.NetworkStatus] + * is UNAVAILABLE. Replaces the legacy modal sheet. Auto-dismisses by virtue of its caller + * only rendering it while offline. + */ +@Composable +fun NetworkScreen() { + val context = LocalContext.current + + Scaffold { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = dimensionResource(R.dimen.spacing_xlarge)), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.spacing_medium) + ) + ) { + Icon( + painter = painterResource(R.drawable.ic_network), + contentDescription = null, + modifier = Modifier.requiredSize(48.dp) + ) + Text( + text = stringResource(R.string.title_no_network), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(R.string.check_connectivity), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + try { + if (isQAndAbove) { + context.startActivity( + Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY) + ) + } else { + context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) + } + } catch (_: ActivityNotFoundException) { + Log.i(TAG, "Unable to launch network settings") + try { + context.startActivity(Intent(Settings.ACTION_SETTINGS)) + } catch (_: Exception) { } + } + } + ) { + Text(stringResource(R.string.action_check)) + } + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun NetworkScreenPreview() { + NetworkScreen() +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkSheet.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkSheet.kt deleted file mode 100644 index 2631eb993..000000000 --- a/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkSheet.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 Aurora OSS - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.aurora.store.compose.ui.commons - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.provider.Settings -import android.util.Log -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetValue -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.aurora.extensions.isQAndAbove -import com.aurora.store.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun NetworkSheet() { - val context = LocalContext.current - - ModalBottomSheet( - onDismissRequest = {}, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - confirmValueChange = { it != SheetValue.Hidden } - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = dimensionResource(R.dimen.spacing_xlarge)) - .padding(bottom = dimensionResource(R.dimen.spacing_large)), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - painter = painterResource(R.drawable.ic_network), - contentDescription = null, - modifier = Modifier.requiredSize(48.dp) - ) - Spacer(Modifier.padding(top = dimensionResource(R.dimen.spacing_large))) - Text( - text = stringResource(R.string.title_no_network), - style = MaterialTheme.typography.titleLarge - ) - Spacer(Modifier.padding(top = dimensionResource(R.dimen.spacing_small))) - Text( - text = stringResource(R.string.check_connectivity), - style = MaterialTheme.typography.bodyMedium - ) - Spacer(Modifier.padding(top = dimensionResource(R.dimen.spacing_xlarge))) - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - try { - if (isQAndAbove) { - context.startActivity( - Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY) - ) - } else { - context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) - } - } catch (e: ActivityNotFoundException) { - Log.i("NetworkSheet", "Unable to launch network settings") - try { - context.startActivity(Intent(Settings.ACTION_SETTINGS)) - } catch (_: Exception) { } - } - } - ) { - Text(stringResource(R.string.action_check)) - } - Spacer(Modifier.navigationBarsPadding()) - } - } -} diff --git a/app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt index 1976ab6fb..beea1e85a 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,10 +38,11 @@ import com.aurora.extensions.requiresObbDir import com.aurora.store.MainViewModel import com.aurora.store.R import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.composition.LocalNetworkStatus import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.ui.apps.AppsGamesScreen import com.aurora.store.compose.ui.commons.MoreSheet -import com.aurora.store.compose.ui.commons.NetworkSheet +import com.aurora.store.compose.ui.commons.NetworkScreen import com.aurora.store.compose.ui.sheets.AppUpdateSheet import com.aurora.store.compose.ui.updates.UpdatesScreen import com.aurora.store.data.model.NetworkStatus @@ -69,9 +69,7 @@ fun MainScreen( onNavigateTo: (Destination) -> Unit = {} ) { val context = LocalContext.current - val networkStatus by mainViewModel.networkProvider.status.collectAsStateWithLifecycle( - initialValue = NetworkStatus.AVAILABLE - ) + val networkStatus = LocalNetworkStatus.current val updates by mainViewModel.updateHelper.updates.collectAsStateWithLifecycle( initialValue = null ) @@ -89,7 +87,6 @@ fun MainScreen( var showMoreSheet by remember { mutableStateOf(false) } var appUpdateTarget by remember { mutableStateOf(null) } - var showNetworkDialog by remember { mutableStateOf(false) } fun handleNavigation(destination: Destination) { when (destination) { @@ -98,12 +95,9 @@ fun MainScreen( } } - LaunchedEffect(networkStatus) { - showNetworkDialog = networkStatus == NetworkStatus.UNAVAILABLE - } - - if (showNetworkDialog) { - NetworkSheet() + if (networkStatus == NetworkStatus.UNAVAILABLE) { + NetworkScreen() + return } if (showMoreSheet) { diff --git a/app/src/main/java/com/aurora/store/compose/ui/splash/SplashScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/splash/SplashScreen.kt index cab620f00..388cac66e 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/splash/SplashScreen.kt @@ -59,6 +59,7 @@ import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.BuildConfig import com.aurora.store.R +import com.aurora.store.compose.composition.LocalNetworkStatus import com.aurora.store.compose.navigation.Destination import com.aurora.store.data.model.AuthState import com.aurora.store.data.model.NetworkStatus @@ -81,10 +82,7 @@ fun SplashScreen( val activity = LocalActivity.current as? ComponentActivity val authState by viewModel.authState.collectAsStateWithLifecycle() - val networkStatus by viewModel.networkProvider.status.collectAsStateWithLifecycle( - initialValue = NetworkStatus.AVAILABLE - ) - val isOnline = networkStatus == NetworkStatus.AVAILABLE + val isOnline = LocalNetworkStatus.current == NetworkStatus.AVAILABLE val canMicroGLogin = PackageUtil.hasSupportedMicroGVariant(context) && Preferences.getBoolean(context, Preferences.PREFERENCE_MICROG_AUTH, true) diff --git a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt index ed8ec0541..428ca10b9 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt @@ -34,7 +34,6 @@ import com.aurora.store.data.model.AccountType import com.aurora.store.data.model.AuthState import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AuthProvider -import com.aurora.store.data.providers.NetworkProvider import com.aurora.store.util.AC2DMTask import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences @@ -51,7 +50,6 @@ import kotlinx.coroutines.launch @HiltViewModel class AuthViewModel @Inject constructor( val authProvider: AuthProvider, - val networkProvider: NetworkProvider, @ApplicationContext private val context: Context, private val aC2DMTask: AC2DMTask ) : ViewModel() {