Add LoadingActivity

This commit is contained in:
andrew 2023-09-22 18:26:18 +09:30
parent 0bc7b4dc4c
commit d159a0bcab
7 changed files with 269 additions and 105 deletions

View File

@ -109,6 +109,11 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.onboarding.LoadingActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.onboarding.DisplayNameActivity"
android:screenOrientation="portrait"

View File

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.onboarding
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
@ -10,54 +9,28 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
import org.session.libsession.snode.SnodeModule
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.crypto.MnemonicCodec
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import javax.inject.Inject
@AndroidEntryPoint
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
@Inject
lateinit var configFactory: ConfigFactory
private lateinit var binding: ActivityLinkDeviceBinding
internal val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
private val adapter = LinkDeviceActivityAdapter(this)
private var restoreJob: Job? = null
override fun onBackPressed() {
if (restoreJob?.isActive == true) return // Don't allow going back with a pending job
super.onBackPressed()
}
private val adapter = LinkDeviceActivityAdapter(this)
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
@ -106,58 +79,7 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
}
private fun continueWithSeed(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 = lifecycleScope.launch {
// 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(this@LinkDeviceActivity, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
configFactory.keyPairChanged()
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
val registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(this@LinkDeviceActivity, registrationID)
TextSecurePreferences.setLocalNumber(this@LinkDeviceActivity, userHexEncodedPublicKey)
TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true)
binding.loader.isVisible = true
val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.registration_activity__skip) { register(true) }
val skipJob = launch {
delay(15_000L)
snackBar.show()
}
// start polling and wait for updated message
ApplicationContext.getInstance(this@LinkDeviceActivity).apply {
startPollingIfNeeded()
}
TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect {
// handle we've synced
snackBar.dismiss()
skipJob.cancel()
register(false)
}
binding.loader.isVisible = false
}
}
private fun register(skipped: Boolean) {
restoreJob?.cancel()
binding.loader.isVisible = false
TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis())
val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
push(intent)
startLoadingActivity(seed)
}
// endregion
}

View File

@ -0,0 +1,106 @@
package org.thoughtcrime.securesms.onboarding
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.ui.AppTheme
import org.thoughtcrime.securesms.ui.ProgressArc
import org.thoughtcrime.securesms.util.push
import javax.inject.Inject
private const val EXTRA_MNEMONIC = "mnemonic"
@AndroidEntryPoint
class LoadingActivity: BaseActionBarActivity() {
@Inject
lateinit var configFactory: ConfigFactory
@Inject
lateinit var prefs: TextSecurePreferences
private val viewModel: LoadingViewModel by viewModels()
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
return
}
private fun register(skipped: Boolean) {
prefs.setLastConfigurationSyncTime(System.currentTimeMillis())
Intent(this, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java)
.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK }
.also(::push)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ComposeView(this)
.apply { setContent { LoadingScreen() } }
.let(::setContentView)
viewModel.restore(application, intent.getByteArrayExtra(EXTRA_MNEMONIC)!!)
lifecycleScope.launch {
viewModel.eventFlow.collect {
when (it) {
Event.TIMEOUT -> 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)
}

View File

@ -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<Event>()
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
}

View File

@ -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)

View File

@ -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)
)
}
}

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/containerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -18,28 +17,4 @@
</androidx.viewpager.widget.ViewPager>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<RelativeLayout
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A4000000"
android:visibility="gone">
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Large.ThreeBounce"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="8dp"
app:SpinKit_Color="@android:color/white" />
</RelativeLayout>
</FrameLayout>
</RelativeLayout>