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 network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.BaseActionBarActivity 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.onboarding.messagenotifications.MessageNotificationsActivity
import org.thoughtcrime.securesms.ui.setComposeContent import org.thoughtcrime.securesms.ui.setComposeContent
import org.thoughtcrime.securesms.util.start import org.thoughtcrime.securesms.util.start
@ -22,7 +22,7 @@ class LoadAccountActivity : BaseActionBarActivity() {
@Inject @Inject
internal lateinit var prefs: TextSecurePreferences internal lateinit var prefs: TextSecurePreferences
@Inject @Inject
internal lateinit var loadingManager: LoadingManager internal lateinit var loadAccountManager: LoadAccountManager
private val viewModel: LoadAccountViewModel by viewModels() private val viewModel: LoadAccountViewModel by viewModels()
@ -35,7 +35,7 @@ class LoadAccountActivity : BaseActionBarActivity() {
lifecycleScope.launch { lifecycleScope.launch {
viewModel.events.collect { viewModel.events.collect {
loadingManager.load(it.mnemonic) loadAccountManager.load(it.mnemonic)
start<MessageNotificationsActivity>() 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 import javax.inject.Singleton
@Singleton @Singleton
class LoadingManager @Inject constructor( class LoadAccountManager @Inject constructor(
@dagger.hilt.android.qualifiers.ApplicationContext private val context: Context, @dagger.hilt.android.qualifiers.ApplicationContext private val context: Context,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
private val prefs: TextSecurePreferences private val prefs: TextSecurePreferences

View File

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

View File

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

View File

@ -53,7 +53,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
@Composable @Composable
private fun DisplayNameScreen(viewModel: PickDisplayNameViewModel) { private fun DisplayNameScreen(viewModel: PickDisplayNameViewModel) {
val state = viewModel.states.collectAsState() 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 package org.thoughtcrime.securesms.onboarding.pickname
import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -12,15 +10,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import network.loki.messenger.R 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.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH
import org.session.libsession.utilities.TextSecurePreferences 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 org.thoughtcrime.securesms.dependencies.ConfigFactory
internal class PickDisplayNameViewModel( internal class PickDisplayNameViewModel(
@ -34,10 +26,7 @@ internal class PickDisplayNameViewModel(
private val _events = MutableSharedFlow<Event>() private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow() val events = _events.asSharedFlow()
private val database: LokiAPIDatabaseProtocol fun onContinue() {
get() = SnodeModule.shared.storage
fun onContinue(context: Context) {
_states.update { it.copy(displayName = it.displayName.trim()) } _states.update { it.copy(displayName = it.displayName.trim()) }
val displayName = _states.value.displayName 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.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) } } displayName.toByteArray().size > NAME_PADDED_LENGTH -> { _states.update { it.copy(isTextErrorColor = true, error = R.string.displayNameErrorDescriptionShorter) } }
else -> { 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) } _states.update { it.copy(isTextErrorColor = false, error = null) }
prefs.setProfileName(displayName) when {
configFactory.user?.setName(displayName) loadFailed -> {
prefs.setProfileName(displayName)
configFactory.user?.setName(displayName)
if (!loadFailed) { _events.tryEmit(Event.LoadAccountComplete)
// 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))
} }
else -> _events.tryEmit(Event.CreateAccount(displayName))
} }
} }
} }

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import android.view.View import android.view.View
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity 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.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> 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).apply { addFlags(FLAG_ACTIVITY_SINGLE_TOP) }.let(::startActivity)
inline fun <reified T: Activity> Context.start(modify: Intent.() -> Unit) = Intent(this, T::class.java).also(modify).let(::startActivity)

View File

@ -28,7 +28,7 @@ class MnemonicCodecTest {
@Test @Test
fun `decode empty`() { fun `decode empty`() {
assertThrows(IllegalArgumentException::class.java) { assertThrows(InputTooShort::class.java) {
codec.decode("") 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 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 private fun sanitizeRecoveryPhrase(rawMnemonic: String): String = rawMnemonic
.replace("[^\\w]+".toRegex(), " ") // replace any sequence of non-word characters with a space .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 .joinToString(" ") // reassemble
fun decodeMnemonicOrHexAsByteArray(mnemonicOrHex: String): ByteArray = try { fun decodeMnemonicOrHexAsByteArray(mnemonicOrHex: String): ByteArray = try {
// Try to use decode mnemonicOrHex as a mnemonic
decode(mnemonic = mnemonicOrHex).let(Hex::fromStringCondensed) decode(mnemonic = mnemonicOrHex).let(Hex::fromStringCondensed)
} catch (decodeException: Exception) { } 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 { try {
Hex.fromStringCondensed(mnemonicOrHex) Hex.fromStringCondensed(mnemonicOrHex)
} catch (_: Exception) { } catch (_: Exception) {
throw decodeException throw decodeException
} }
} }
}
private fun swap(x: String): String {
val p1 = x.substring(6 until 8) private fun swap(x: String): String {
val p2 = x.substring(4 until 6) val p1 = x.substring(6 until 8)
val p3 = x.substring(2 until 4) val p2 = x.substring(4 until 6)
val p4 = x.substring(0 until 2) val p3 = x.substring(2 until 4)
return p1 + p2 + p3 + p4 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() private fun determineChecksumIndex(x: List<String>, prefixLength: Int): Int {
val checksum = CRC32().apply { update(bytes) }.value val bytes = x.joinToString("") { it.substring(0 until prefixLength) }.toByteArray()
return (checksum % x.size.toLong()).toInt() val checksum = CRC32().apply { update(bytes) }.value
} return (checksum % x.size.toLong()).toInt()
} }