feat: leverage android 12 splash screen API
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
3e7768146d
commit
8be81649b2
|
|
@ -236,6 +236,7 @@ dependencies {
|
|||
implementation "androidx.webkit:webkit:1.9.0"
|
||||
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha07"
|
||||
implementation "androidx.datastore:datastore:1.1.0-alpha07"
|
||||
implementation "androidx.core:core-splashscreen:1.0.1"
|
||||
|
||||
// Libraries used for legacy View-based UI
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha13"
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Revolt">
|
||||
android:theme="@style/Theme.Revolt.Starting">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
package chat.revolt.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.core.EaseInOutExpo
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
|
|
@ -14,20 +21,31 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz
|
|||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.dialog
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import chat.revolt.BuildConfig
|
||||
import chat.revolt.R
|
||||
import chat.revolt.RevoltApplication
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.routes.onboard.needsOnboarding
|
||||
import chat.revolt.api.settings.GlobalState
|
||||
import chat.revolt.api.settings.SyncedSettings
|
||||
import chat.revolt.ndk.NativeLibraries
|
||||
import chat.revolt.persistence.KVStorage
|
||||
import chat.revolt.screens.DefaultDestinationScreen
|
||||
import chat.revolt.screens.SplashScreen
|
||||
import chat.revolt.screens.about.AboutScreen
|
||||
import chat.revolt.screens.about.AttributionScreen
|
||||
|
|
@ -50,11 +68,151 @@ import chat.revolt.screens.settings.ProfileSettingsScreen
|
|||
import chat.revolt.screens.settings.SessionSettingsScreen
|
||||
import chat.revolt.screens.settings.SettingsScreen
|
||||
import chat.revolt.ui.theme.RevoltTheme
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.ktor.client.request.get
|
||||
import io.sentry.android.core.SentryAndroid
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class MainActivityViewModel @Inject constructor(
|
||||
private val kvStorage: KVStorage,
|
||||
@ApplicationContext private val context: Context
|
||||
) : ViewModel() {
|
||||
val nextDestination = MutableStateFlow<String?>(null)
|
||||
var isConnected = MutableStateFlow(false)
|
||||
val isReady = MutableStateFlow(false)
|
||||
|
||||
private fun hasInternetConnection(): Boolean {
|
||||
val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
val network = connectivityManager.activeNetwork ?: return false
|
||||
val capabilities =
|
||||
connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||
|
||||
return when {
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun canReachRevolt(): Boolean {
|
||||
val res = RevoltHttp.get("/")
|
||||
return res.status.value == 200
|
||||
}
|
||||
|
||||
private suspend fun startWithDestination(destination: String) {
|
||||
nextDestination.emit(destination)
|
||||
isReady.emit(true)
|
||||
}
|
||||
|
||||
private suspend fun startWithoutDestination() {
|
||||
isReady.emit(true)
|
||||
}
|
||||
|
||||
fun checkLoggedInState() {
|
||||
viewModelScope.launch {
|
||||
Log.d("MainActivity", "Checking logged in state")
|
||||
|
||||
isConnected.emit(hasInternetConnection())
|
||||
|
||||
Log.d("MainActivity", "Checking if we can reach Revolt")
|
||||
|
||||
if (!isConnected.value) return@launch startWithoutDestination()
|
||||
|
||||
Log.d("MainActivity", "We can reach Revolt, checking if we're logged in")
|
||||
|
||||
val token = kvStorage.get("sessionToken")
|
||||
?: return@launch startWithDestination("login/greeting")
|
||||
val id = kvStorage.get("sessionId") ?: ""
|
||||
|
||||
Log.d(
|
||||
"MainActivity",
|
||||
"We have a session token, checking if it's valid and if we can still reach Revolt"
|
||||
)
|
||||
|
||||
val canReachRevolt = canReachRevolt()
|
||||
val valid = try {
|
||||
RevoltAPI.checkSessionToken(token)
|
||||
} catch (e: Throwable) {
|
||||
false
|
||||
}
|
||||
|
||||
if (canReachRevolt && !valid) {
|
||||
Log.d("MainActivity", "Session token is invalid, clearing session")
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.token_invalid_toast),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
kvStorage.remove("sessionToken")
|
||||
kvStorage.remove("sessionId")
|
||||
startWithDestination("login/greeting")
|
||||
} else {
|
||||
try {
|
||||
Log.d("MainActivity", "Session token is valid, checking onboarding state")
|
||||
val onboard = needsOnboarding(token)
|
||||
if (onboard) {
|
||||
Log.d("MainActivity", "Onboarding state is incomplete, starting onboarding")
|
||||
startWithDestination("register/onboarding")
|
||||
return@launch
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Failed to check onboarding state, clearing session", e)
|
||||
kvStorage.remove("sessionToken")
|
||||
kvStorage.remove("sessionId")
|
||||
startWithDestination("login/greeting")
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d("MainActivity", "Onboarding state is complete, logging in")
|
||||
RevoltAPI.loginAs(token)
|
||||
RevoltAPI.setSessionId(id)
|
||||
startWithDestination("chat")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Failed to login, clearing session", e)
|
||||
kvStorage.remove("sessionToken")
|
||||
kvStorage.remove("sessionId")
|
||||
startWithDestination("login/greeting")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNextDestination(destination: String) {
|
||||
viewModelScope.launch {
|
||||
nextDestination.emit(null)
|
||||
nextDestination.emit(destination)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d("MainActivity", "Starting up")
|
||||
checkLoggedInState()
|
||||
}
|
||||
}
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : FragmentActivity() {
|
||||
private val viewModel by viewModels<MainActivityViewModel>()
|
||||
|
||||
// Fix for SDK >=31, where core-splashscreen accidentally removes dynamic colours
|
||||
// See the other one in DefaultDestinationScreen.kt
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
DynamicColors.applyToActivitiesIfAvailable(RevoltApplication.instance)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -66,9 +224,21 @@ class MainActivity : FragmentActivity() {
|
|||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition {
|
||||
!viewModel.isReady.value
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
AppEntrypoint(windowSizeClass)
|
||||
AppEntrypoint(
|
||||
windowSizeClass,
|
||||
viewModel.nextDestination.collectAsState().value,
|
||||
viewModel.isConnected.collectAsState().value,
|
||||
viewModel::checkLoggedInState,
|
||||
viewModel::updateNextDestination
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +255,13 @@ val RevoltTweenDp: FiniteAnimationSpec<Dp> = tween(400, easing = EaseInOutExpo)
|
|||
val RevoltTweenColour: FiniteAnimationSpec<Color> = tween(400, easing = EaseInOutExpo)
|
||||
|
||||
@Composable
|
||||
fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
|
||||
fun AppEntrypoint(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
nextDestination: String?,
|
||||
isConnected: Boolean,
|
||||
onRetryConnection: () -> Unit,
|
||||
onUpdateNextDestination: (String) -> Unit = {}
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
RevoltTheme(
|
||||
|
|
@ -98,7 +274,7 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
|
|||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "splash",
|
||||
startDestination = "default",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||
|
|
@ -124,6 +300,14 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
|
|||
)
|
||||
}
|
||||
) {
|
||||
composable("default") {
|
||||
DefaultDestinationScreen(
|
||||
navController,
|
||||
nextDestination,
|
||||
isConnected,
|
||||
onRetryConnection
|
||||
)
|
||||
}
|
||||
composable("splash") { SplashScreen(navController) }
|
||||
|
||||
composable("login/greeting") { LoginGreetingScreen(navController) }
|
||||
|
|
@ -143,9 +327,34 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
|
|||
|
||||
RegisterVerifyScreen(navController, email)
|
||||
}
|
||||
composable("register/onboarding") { OnboardingScreen(navController) }
|
||||
composable("register/onboarding") {
|
||||
OnboardingScreen(
|
||||
navController,
|
||||
onOnboardingComplete = {
|
||||
onUpdateNextDestination("chat")
|
||||
navController.popBackStack(
|
||||
navController.graph.startDestinationRoute!!,
|
||||
inclusive = true
|
||||
)
|
||||
navController.navigate("default")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("chat") { ChatRouterScreen(navController, windowSizeClass) }
|
||||
composable("chat") {
|
||||
ChatRouterScreen(
|
||||
navController,
|
||||
windowSizeClass,
|
||||
onNullifiedUser = {
|
||||
onRetryConnection()
|
||||
navController.popBackStack(
|
||||
navController.graph.startDestinationRoute!!,
|
||||
inclusive = true
|
||||
)
|
||||
navController.navigate("default")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("discover") { DiscoverScreen(navController) }
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,11 @@ val RevoltHttp = HttpClient(OkHttp) {
|
|||
engine {
|
||||
addInterceptor { chain ->
|
||||
val request = chain.request().newBuilder()
|
||||
.header(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||
.apply {
|
||||
if (chain.request().headers[RevoltAPI.TOKEN_HEADER_NAME] == null) {
|
||||
header(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package chat.revolt.api.internals
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.activity.ComponentActivity
|
||||
|
||||
fun Context.getComponentActivity(): ComponentActivity? = when (this) {
|
||||
is ComponentActivity -> this
|
||||
is ContextWrapper -> baseContext.getComponentActivity()
|
||||
else -> null
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ suspend fun fetchSelf(): User {
|
|||
|
||||
try {
|
||||
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
|
||||
throw Error(error.type)
|
||||
throw Exception(error.type)
|
||||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ suspend fun fetchSelf(): User {
|
|||
val user = RevoltJson.decodeFromString(User.serializer(), response)
|
||||
|
||||
if (user.id == null) {
|
||||
throw Error("Self user ID is null")
|
||||
throw Exception("Self user ID is null")
|
||||
}
|
||||
|
||||
RevoltAPI.userCache[user.id] = user
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package chat.revolt.screens
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavController
|
||||
import chat.revolt.RevoltApplication
|
||||
import chat.revolt.api.internals.getComponentActivity
|
||||
import chat.revolt.components.screens.splash.DisconnectedScreen
|
||||
import com.google.android.material.color.DynamicColors
|
||||
|
||||
@Composable
|
||||
fun DefaultDestinationScreen(
|
||||
navController: NavController,
|
||||
nextDestination: String? = null,
|
||||
isConnected: Boolean = false,
|
||||
onRetryConnection: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
if (!isConnected) {
|
||||
DisconnectedScreen(
|
||||
onRetry = {
|
||||
onRetryConnection()
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
LaunchedEffect(nextDestination) {
|
||||
nextDestination?.let {
|
||||
// Fix for SDK >=31, where core-splashscreen accidentally removes dynamic colours
|
||||
// See the other one in MainActivity.kt
|
||||
DynamicColors.applyToActivityIfAvailable(context.getComponentActivity() as Activity)
|
||||
DynamicColors.applyToActivitiesIfAvailable(RevoltApplication.instance)
|
||||
|
||||
navController.popBackStack(navController.graph.startDestinationRoute!!, true)
|
||||
navController.navigate(it)
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize())
|
||||
}
|
||||
|
|
@ -293,6 +293,7 @@ class ChatRouterViewModel @Inject constructor(
|
|||
fun ChatRouterScreen(
|
||||
topNav: NavController,
|
||||
windowSizeClass: WindowSizeClass,
|
||||
onNullifiedUser: () -> Unit,
|
||||
viewModel: ChatRouterViewModel = hiltViewModel()
|
||||
) {
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
|
|
@ -385,11 +386,7 @@ fun ChatRouterScreen(
|
|||
.distinctUntilChanged()
|
||||
.collect { selfId ->
|
||||
if (selfId == null) {
|
||||
topNav.popBackStack(
|
||||
topNav.graph.startDestinationRoute!!,
|
||||
inclusive = true
|
||||
)
|
||||
topNav.navigate("splash")
|
||||
onNullifiedUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import chat.revolt.persistence.KVStorage
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun OnboardingScreen(navController: NavController) {
|
||||
fun OnboardingScreen(navController: NavController, onOnboardingComplete: () -> Unit) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
|
|
@ -40,9 +40,12 @@ fun OnboardingScreen(navController: NavController) {
|
|||
val error = remember { mutableStateOf("") }
|
||||
|
||||
fun onboardingComplete() {
|
||||
navController.navigate("splash") {
|
||||
popUpTo("register/onboarding") { inclusive = true }
|
||||
}
|
||||
onOnboardingComplete()
|
||||
navController.popBackStack(
|
||||
navController.graph.startDestinationRoute!!,
|
||||
inclusive = true
|
||||
)
|
||||
navController.navigate("default")
|
||||
}
|
||||
|
||||
suspend fun onboard() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="foreground">#FFFFFFFF</color>
|
||||
<color name="splashscreen_background">#FF131722</color>
|
||||
</resources>
|
||||
|
|
@ -2,5 +2,6 @@
|
|||
<resources>
|
||||
<color name="primary">#FFDA4E5B</color>
|
||||
<color name="background">#FF131722</color>
|
||||
<color name="splashscreen_background">#FFFFFFFF</color>
|
||||
<color name="foreground">#FF000000</color>
|
||||
</resources>
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.Revolt" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Revolt.Starting" parent="Theme.SplashScreen" tools:targetApi="tiramisu">
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="android:windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="android:windowSplashScreenBehavior">icon_preferred</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Revolt</item>
|
||||
</style>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue