diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c0e06f0a89..1ab19508d5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -109,6 +109,11 @@ android:screenOrientation="portrait" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> + register(skipped = true) + Event.SUCCESS -> register(skipped = false) + } + } + } + } + + @Composable + fun LoadingScreen() { + val state by viewModel.stateFlow.collectAsState() + + val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) } + + LaunchedEffect(state) { + animatable.stop() + animatable.animateTo( + targetValue = 1f, + animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt()) + ) + } + + AppTheme { + Column { + Spacer(modifier = Modifier.weight(1f)) + ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally)) + Text("One moment please..", modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6) + Text("Loading your account", modifier = Modifier.align(Alignment.CenterHorizontally)) + Spacer(modifier = Modifier.weight(2f)) + } + } + } +} + +fun Context.startLoadingActivity(mnemonic: ByteArray) { + Intent(this, LoadingActivity::class.java) + .apply { putExtra(EXTRA_MNEMONIC, mnemonic) } + .also(::startActivity) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt new file mode 100644 index 0000000000..68bc5cc127 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt @@ -0,0 +1,102 @@ +package org.thoughtcrime.securesms.onboarding + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import org.session.libsession.snode.SnodeModule +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.database.LokiAPIDatabaseProtocol +import org.session.libsignal.utilities.KeyHelper +import org.session.libsignal.utilities.hexEncodedPublicKey +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +data class State(val duration: Duration) + +private val DONE_TIME = 1.seconds +private val DONE_ANIMATE_TIME = 500.milliseconds + +private val TOTAL_ANIMATE_TIME = 14.seconds +private val TOTAL_TIME = 15.seconds + +@HiltViewModel +class LoadingViewModel @Inject constructor( + private val configFactory: ConfigFactory, + private val prefs: TextSecurePreferences, +) : ViewModel() { + + private val state = MutableStateFlow(State(TOTAL_ANIMATE_TIME)) + val stateFlow = state.asStateFlow() + + private val event = Channel() + val eventFlow = event.receiveAsFlow() + + private var restoreJob: Job? = null + + internal val database: LokiAPIDatabaseProtocol + get() = SnodeModule.shared.storage + + fun restore(context: Context, seed: ByteArray) { + + // only have one sync job running at a time (prevent QR from trying to spawn a new job) + if (restoreJob?.isActive == true) return + + restoreJob = viewModelScope.launch(Dispatchers.IO) { + // This is here to resolve a case where the app restarts before a user completes onboarding + // which can result in an invalid database state + database.clearAllLastMessageHashes() + database.clearReceivedMessageHashValues() + + // RestoreActivity handles seed this way + val keyPairGenerationResult = KeyPairUtilities.generate(seed) + val x25519KeyPair = keyPairGenerationResult.x25519KeyPair + KeyPairUtilities.store(context, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) + configFactory.keyPairChanged() + val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey + val registrationID = KeyHelper.generateRegistrationId(false) + prefs.apply { + setLocalRegistrationId(registrationID) + setLocalNumber(userHexEncodedPublicKey) + setRestorationTime(System.currentTimeMillis()) + setHasViewedSeed(true) + } + + val skipJob = launch(Dispatchers.IO) { + delay(TOTAL_TIME) + event.send(Event.TIMEOUT) + } + + // start polling and wait for updated message + ApplicationContext.getInstance(context).apply { startPollingIfNeeded() } + TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect { + // handle we've synced + skipJob.cancel() + + state.value = State(DONE_ANIMATE_TIME) + delay(DONE_TIME) + event.send(Event.SUCCESS) + } + } + } +} + +sealed interface Event { + object SUCCESS: Event + object TIMEOUT: Event +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index 55bc1be62e..32c375e54d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -42,6 +42,7 @@ const val oceanLight5 = 0xffE7F3F4 const val oceanLight6 = 0xffECFAFB const val oceanLight7 = 0xffFCFFFF +val session_accent = Color(0xFF31F196) val ocean_accent = Color(0xff57C9FA) val oceanLights = arrayOf(oceanLight0, oceanLight1, oceanLight2, oceanLight3, oceanLight4, oceanLight5, oceanLight6, oceanLight7) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 1724bde8a6..26949241dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes +import androidx.compose.foundation.Canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.pager.PagerState @@ -26,9 +28,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -37,6 +43,7 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.ProfilePictureView +import kotlin.math.roundToInt @Composable fun ItemButton( @@ -180,3 +187,49 @@ fun RowScope.Avatar(recipient: Recipient) { ) } } + +@Composable +fun ProgressArc(progress: Float, modifier: Modifier = Modifier) { + val text = (progress * 100).roundToInt() + + Box(modifier = modifier) { + Arc(percentage = progress, modifier = Modifier.align(Alignment.Center)) + Text("${text}%", color = Color.White, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.h2) + } +} + +@Composable +fun Arc( + modifier: Modifier = Modifier, + percentage: Float = 0.25f, + fillColor: Color = session_accent, + backgroundColor: Color = classicDarkColors[3], + strokeWidth: Dp = 18.dp, + sweepAngle: Float = 310f, + startAngle: Float = (360f - sweepAngle) / 2 + 90f +) { + Canvas( + modifier = modifier + .padding(strokeWidth) + .size(186.dp) + ) { + // Background Line + drawArc( + color = backgroundColor, + startAngle, + sweepAngle, + false, + style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round), + size = Size(size.width, size.height) + ) + + drawArc( + color = fillColor, + startAngle, + percentage * sweepAngle, + false, + style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round), + size = Size(size.width, size.height) + ) + } +} diff --git a/app/src/main/res/layout/activity_link_device.xml b/app/src/main/res/layout/activity_link_device.xml index b267c08ac8..5c93e251c9 100644 --- a/app/src/main/res/layout/activity_link_device.xml +++ b/app/src/main/res/layout/activity_link_device.xml @@ -1,7 +1,6 @@ @@ -18,28 +17,4 @@ - - - - - - - - - - \ No newline at end of file