Defer setting display name until necessary in create flow in onboarding

This commit is contained in:
bemusementpark 2024-07-02 13:41:01 +09:30
parent 9cf3a37a2b
commit 508547a013
10 changed files with 134 additions and 83 deletions

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.onboarding.manager.LoadingManager
import org.thoughtcrime.securesms.onboarding.manager.LoadAccountManager
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity
import org.thoughtcrime.securesms.ui.setComposeContent
import org.thoughtcrime.securesms.util.start
@ -22,7 +22,7 @@ class LoadAccountActivity : BaseActionBarActivity() {
@Inject
internal lateinit var prefs: TextSecurePreferences
@Inject
internal lateinit var loadingManager: LoadingManager
internal lateinit var loadAccountManager: LoadAccountManager
private val viewModel: LoadAccountViewModel by viewModels()
@ -35,7 +35,7 @@ class LoadAccountActivity : BaseActionBarActivity() {
lifecycleScope.launch {
viewModel.events.collect {
loadingManager.load(it.mnemonic)
loadAccountManager.load(it.mnemonic)
start<MessageNotificationsActivity>()
}
}

View File

@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.onboarding.manager
import android.app.Application
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.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CreateAccountManager @Inject constructor(
private val application: Application,
private val prefs: TextSecurePreferences,
private val configFactory: ConfigFactory,
) {
private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
fun createAccount(displayName: String) {
prefs.setProfileName(displayName)
configFactory.user?.setName(displayName)
// 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()
val keyPairGenerationResult = KeyPairUtilities.generate()
val seed = keyPairGenerationResult.seed
val ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
KeyPairUtilities.store(application, seed, ed25519KeyPair, x25519KeyPair)
configFactory.keyPairChanged()
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
val registrationID = KeyHelper.generateRegistrationId(false)
prefs.setLocalRegistrationId(registrationID)
prefs.setLocalNumber(userHexEncodedPublicKey)
prefs.setRestorationTime(0)
}
}

View File

@ -16,7 +16,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LoadingManager @Inject constructor(
class LoadAccountManager @Inject constructor(
@dagger.hilt.android.qualifiers.ApplicationContext private val context: Context,
private val configFactory: ConfigFactory,
private val prefs: TextSecurePreferences

View File

@ -6,14 +6,14 @@ import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.home.startHomeActivity
import org.thoughtcrime.securesms.notifications.PushRegistry
import org.thoughtcrime.securesms.onboarding.loading.LoadingActivity
import org.thoughtcrime.securesms.onboarding.manager.LoadingManager
import org.thoughtcrime.securesms.onboarding.manager.LoadAccountManager
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity.Companion.EXTRA_PROFILE_NAME
import org.thoughtcrime.securesms.ui.setComposeContent
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
@ -30,9 +30,8 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
@Inject
internal lateinit var viewModelFactory: MessageNotificationsViewModel.AssistedFactory
@Inject lateinit var pushRegistry: PushRegistry
@Inject lateinit var prefs: TextSecurePreferences
@Inject lateinit var loadingManager: LoadingManager
@Inject lateinit var loadAccountManager: LoadAccountManager
val profileName by lazy { intent.getStringExtra(EXTRA_PROFILE_NAME) }
@ -46,6 +45,15 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
prefs.setHasSeenWelcomeScreen(true)
setComposeContent { MessageNotificationsScreen() }
lifecycleScope.launch {
viewModel.events.collect {
when (it) {
Event.Loading -> start<LoadingActivity>()
Event.OnboardingComplete -> startHomeActivity()
}
}
}
}
@Deprecated("Deprecated in Java")
@ -62,22 +70,11 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
MessageNotificationsScreen(
uiState,
setEnabled = viewModel::setEnabled,
onContinue = ::register,
onContinue = viewModel::onContinue,
quit = viewModel::quit,
dismissDialog = viewModel::dismissDialog
)
}
private fun register() {
prefs.setPushEnabled(viewModel.uiStates.value.pushEnabled)
ApplicationContext.getInstance(this).startPollingIfNeeded()
pushRegistry.refresh(true)
when {
prefs.getHasViewedSeed() && !prefs.getConfigurationMessageSynced() -> start<LoadingActivity>()
else -> startHomeActivity()
}
}
}
fun Activity.startMessageNotificationsActivity(profileName: String) {

View File

@ -8,23 +8,50 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.notifications.PushRegistry
import org.thoughtcrime.securesms.onboarding.manager.CreateAccountManager
internal class MessageNotificationsViewModel(
private val state: State,
private val application: Application
private val application: Application,
private val prefs: TextSecurePreferences,
private val pushRegistry: PushRegistry,
private val createAccountManager: CreateAccountManager
): AndroidViewModel(application) {
private val _uiStates = MutableStateFlow(UiState())
val uiStates = _uiStates.asStateFlow()
private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow()
fun setEnabled(enabled: Boolean) {
_uiStates.update { UiState(pushEnabled = enabled) }
}
fun onContinue() {
viewModelScope.launch(Dispatchers.IO) {
if (state is State.CreateAccount) createAccountManager.createAccount(state.displayName)
prefs.setPushEnabled(uiStates.value.pushEnabled)
pushRegistry.refresh(true)
_events.emit(
when (state) {
is State.CreateAccount -> Event.OnboardingComplete
else -> Event.Loading
}
)
}
}
/**
* @return [true] if the back press was handled.
*/
@ -70,14 +97,24 @@ internal class MessageNotificationsViewModel(
@Suppress("UNCHECKED_CAST")
class Factory @AssistedInject constructor(
@Assisted private val profileName: String?,
private val application: Application
private val application: Application,
private val prefs: TextSecurePreferences,
private val pushRegistry: PushRegistry,
private val createAccountManager: CreateAccountManager,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MessageNotificationsViewModel(
state = profileName?.let(State::CreateAccount) ?: State.LoadAccount,
application = application
application = application,
prefs = prefs,
pushRegistry = pushRegistry,
createAccountManager = createAccountManager
) as T
}
}
}
enum class Event {
OnboardingComplete, Loading
}

View File

@ -53,7 +53,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
@Composable
private fun DisplayNameScreen(viewModel: PickDisplayNameViewModel) {
val state = viewModel.states.collectAsState()
DisplayName(state.value, viewModel::onChange) { viewModel.onContinue(this) }
DisplayName(state.value, viewModel::onChange) { viewModel.onContinue() }
}
}

View File

@ -1,10 +1,8 @@
package org.thoughtcrime.securesms.onboarding.pickname
import android.content.Context
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableSharedFlow
@ -12,15 +10,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.snode.SnodeModule
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH
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.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory
internal class PickDisplayNameViewModel(
@ -34,10 +26,7 @@ internal class PickDisplayNameViewModel(
private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow()
private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
fun onContinue(context: Context) {
fun onContinue() {
_states.update { it.copy(displayName = it.displayName.trim()) }
val displayName = _states.value.displayName
@ -46,37 +35,18 @@ internal class PickDisplayNameViewModel(
displayName.isEmpty() -> { _states.update { it.copy(isTextErrorColor = true, error = R.string.displayNameErrorDescription) } }
displayName.toByteArray().size > NAME_PADDED_LENGTH -> { _states.update { it.copy(isTextErrorColor = true, error = R.string.displayNameErrorDescriptionShorter) } }
else -> {
// success - clear the error as we can still see it during the transition to the
// next screen.
_states.update { it.copy(isTextErrorColor = false, error = null) }
prefs.setProfileName(displayName)
configFactory.user?.setName(displayName)
when {
loadFailed -> {
prefs.setProfileName(displayName)
configFactory.user?.setName(displayName)
if (!loadFailed) {
// 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()
val keyPairGenerationResult = KeyPairUtilities.generate()
val seed = keyPairGenerationResult.seed
val ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
KeyPairUtilities.store(context, seed, ed25519KeyPair, x25519KeyPair)
configFactory.keyPairChanged()
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
val registrationID = KeyHelper.generateRegistrationId(false)
prefs.setLocalRegistrationId(registrationID)
prefs.setLocalNumber(userHexEncodedPublicKey)
prefs.setRestorationTime(0)
}
viewModelScope.launch {
if (loadFailed) {
_events.emit(Event.LoadAccountComplete)
} else {
_events.emit(Event.CreateAccount(displayName))
_events.tryEmit(Event.LoadAccountComplete)
}
else -> _events.tryEmit(Event.CreateAccount(displayName))
}
}
}

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import android.view.View
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
@ -108,5 +109,4 @@ data class ThemeState (
inline fun <reified T: Activity> Activity.show() = Intent(this, T::class.java).also(::startActivity).let { overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) }
inline fun <reified T: Activity> Activity.push() = Intent(this, T::class.java).also(::startActivity).let { overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out) }
inline fun <reified T: Activity> Context.start() = Intent(this, T::class.java).let(::startActivity)
inline fun <reified T: Activity> Context.start(modify: Intent.() -> Unit) = Intent(this, T::class.java).also(modify).let(::startActivity)
inline fun <reified T: Activity> Context.start(modify: Intent.() -> Unit = {}) = Intent(this, T::class.java).also(modify).apply { addFlags(FLAG_ACTIVITY_SINGLE_TOP) }.let(::startActivity)

View File

@ -28,7 +28,7 @@ class MnemonicCodecTest {
@Test
fun `decode empty`() {
assertThrows(IllegalArgumentException::class.java) {
assertThrows(InputTooShort::class.java) {
codec.decode("")
}
}

View File

@ -116,7 +116,6 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
}
fun sanitizeAndDecodeAsByteArray(mnemonic: String): ByteArray = sanitizeRecoveryPhrase(mnemonic).let(::decode).let(Hex::fromStringCondensed)
fun decodeAsByteArray(mnemonic: String): ByteArray = decode(mnemonic = mnemonic).let(Hex::fromStringCondensed)
private fun sanitizeRecoveryPhrase(rawMnemonic: String): String = rawMnemonic
.replace("[^\\w]+".toRegex(), " ") // replace any sequence of non-word characters with a space
@ -125,27 +124,30 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
.joinToString(" ") // reassemble
fun decodeMnemonicOrHexAsByteArray(mnemonicOrHex: String): ByteArray = try {
// Try to use decode mnemonicOrHex as a mnemonic
decode(mnemonic = mnemonicOrHex).let(Hex::fromStringCondensed)
} catch (decodeException: Exception) {
if (mnemonicOrHex.isHex()) throw decodeException
// It's not a valid mnemonic, if it's pure-hexadecimal then we'll interpret it as a
// hexadecimal-byte encoded mnemonic.
if (!mnemonicOrHex.isHex()) throw decodeException
try {
Hex.fromStringCondensed(mnemonicOrHex)
} catch (_: Exception) {
throw decodeException
}
}
private fun swap(x: String): String {
val p1 = x.substring(6 until 8)
val p2 = x.substring(4 until 6)
val p3 = x.substring(2 until 4)
val p4 = x.substring(0 until 2)
return p1 + p2 + p3 + p4
}
private fun determineChecksumIndex(x: List<String>, prefixLength: Int): Int {
val bytes = x.joinToString("") { it.substring(0 until prefixLength) }.toByteArray()
val checksum = CRC32().apply { update(bytes) }.value
return (checksum % x.size.toLong()).toInt()
}
}
private fun swap(x: String): String {
val p1 = x.substring(6 until 8)
val p2 = x.substring(4 until 6)
val p3 = x.substring(2 until 4)
val p4 = x.substring(0 until 2)
return p1 + p2 + p3 + p4
}
private fun determineChecksumIndex(x: List<String>, prefixLength: Int): Int {
val bytes = x.joinToString("") { it.substring(0 until prefixLength) }.toByteArray()
val checksum = CRC32().apply { update(bytes) }.value
return (checksum % x.size.toLong()).toInt()
}