diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt index e45c782b56..3c7a6f6a56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt @@ -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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt new file mode 100644 index 0000000000..1e0a21d571 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadingManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadingManager.kt rename to app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt index c805839740..eeea0d31fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadingManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt index 887345c44b..f29f3badab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt @@ -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() + 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() - else -> startHomeActivity() - } - } } fun Activity.startMessageNotificationsActivity(profileName: String) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt index 003eef5255..a39f270bf2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt @@ -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() + 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 create(modelClass: Class): 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 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt index a0ac2ac08d..11a16978e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt @@ -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() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt index 51f82a677e..947a923202 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt @@ -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() 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) - - 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) - } + when { + loadFailed -> { + prefs.setProfileName(displayName) + configFactory.user?.setName(displayName) - viewModelScope.launch { - if (loadFailed) { - _events.emit(Event.LoadAccountComplete) - } else { - _events.emit(Event.CreateAccount(displayName)) + _events.tryEmit(Event.LoadAccountComplete) } + else -> _events.tryEmit(Event.CreateAccount(displayName)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt index ae58af27e5..d285ad7a39 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt @@ -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 Activity.show() = Intent(this, T::class.java).also(::startActivity).let { overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) } inline fun Activity.push() = Intent(this, T::class.java).also(::startActivity).let { overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out) } -inline fun Context.start() = Intent(this, T::class.java).let(::startActivity) -inline fun Context.start(modify: Intent.() -> Unit) = Intent(this, T::class.java).also(modify).let(::startActivity) +inline fun Context.start(modify: Intent.() -> Unit = {}) = Intent(this, T::class.java).also(modify).apply { addFlags(FLAG_ACTIVITY_SINGLE_TOP) }.let(::startActivity) diff --git a/libsession-util/src/test/java/org/session/libsignal/crypto/MnemonicCodecTest.kt b/libsession-util/src/test/java/org/session/libsignal/crypto/MnemonicCodecTest.kt index f7b7a5382b..1a5ed95503 100644 --- a/libsession-util/src/test/java/org/session/libsignal/crypto/MnemonicCodecTest.kt +++ b/libsession-util/src/test/java/org/session/libsignal/crypto/MnemonicCodecTest.kt @@ -28,7 +28,7 @@ class MnemonicCodecTest { @Test fun `decode empty`() { - assertThrows(IllegalArgumentException::class.java) { + assertThrows(InputTooShort::class.java) { codec.decode("") } } diff --git a/libsignal/src/main/java/org/session/libsignal/crypto/MnemonicCodec.kt b/libsignal/src/main/java/org/session/libsignal/crypto/MnemonicCodec.kt index b0d3966905..8999638edb 100644 --- a/libsignal/src/main/java/org/session/libsignal/crypto/MnemonicCodec.kt +++ b/libsignal/src/main/java/org/session/libsignal/crypto/MnemonicCodec.kt @@ -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 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, 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 determineChecksumIndex(x: List, 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() }