From 71f04678cfc252ab0c3083a1a3f38adea7e24853 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Wed, 10 Jul 2024 23:38:07 +0930 Subject: [PATCH 1/8] Error-out when you scan an account id when trying to recover an account --- .../org/session/libsignal/crypto/MnemonicCodec.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 8999638edb..cb81af0fda 100644 --- a/libsignal/src/main/java/org/session/libsignal/crypto/MnemonicCodec.kt +++ b/libsignal/src/main/java/org/session/libsignal/crypto/MnemonicCodec.kt @@ -128,13 +128,12 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) { decode(mnemonic = mnemonicOrHex).let(Hex::fromStringCondensed) } catch (decodeException: Exception) { // 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 - } + // hexadecimal-byte encoded mnemonic... unless it's 66 chars or longer, then it could be + // an account id. + mnemonicOrHex.takeIf { it.length < 66 && it.isHex() } + .runCatching { Hex.fromStringCondensed(this) } + .getOrNull() + ?: throw decodeException } } From 4a2f6e7b4e8029134ce3838002023f877158c0f7 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Wed, 10 Jul 2024 23:48:05 +0930 Subject: [PATCH 2/8] Add test to ensure we error on scan account id when trying to recover an account --- .../org/session/libsignal/crypto/MnemonicCodecTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) 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 1a5ed95503..48fd12cbf4 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 @@ -96,6 +96,14 @@ class MnemonicCodecTest { assertEquals("0f2ccde528622876b8f16e14db97dafc", result) } + + @Test + fun `decodeMnemonicOrHexAsByteArray with account id throws`() { + assertThrows(InvalidWord::class.java) { + codec.decodeMnemonicOrHexAsByteArray("0582e1421da6f584a4795d30b654b4f25fed860afdf081075cb26a2b997e492f14").let(Hex::toStringCondensed) + } + } + @Test fun `decodeMnemonicOrHexAsByteArray with bad hex`() { // throws InvalidWord as 0f2ccde528622876b8f16e14db97dafcg is not a valid word on the english wordlist. From 95ecbe664d70cdafc6831970e78ce05cf5a3aa46 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 11 Jul 2024 11:26:41 +0930 Subject: [PATCH 3/8] SES-2386 Account Creation Modal --- .../securesms/onboarding/OnboardingBackPressAlertDialog.kt | 4 +++- .../onboarding/messagenotifications/MessageNotifications.kt | 2 +- .../securesms/onboarding/pickname/PickDisplayName.kt | 6 +++++- app/src/main/res/values/strings.xml | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/OnboardingBackPressAlertDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/OnboardingBackPressAlertDialog.kt index 3e6ac4a0c9..c620f32b6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/OnboardingBackPressAlertDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/OnboardingBackPressAlertDialog.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.onboarding +import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import network.loki.messenger.R @@ -11,12 +12,13 @@ import org.thoughtcrime.securesms.ui.color.LocalColors @Composable fun OnboardingBackPressAlertDialog( dismissDialog: () -> Unit, + @StringRes textId: Int = R.string.you_cannot_go_back_further_in_order_to_stop_loading_your_account_session_needs_to_quit, quit: () -> Unit ) { AlertDialog( onDismissRequest = dismissDialog, title = stringResource(R.string.warning), - text = stringResource(R.string.you_cannot_go_back_further_in_order_to_stop_loading_your_account_session_needs_to_quit), + text = stringResource(textId), buttons = listOf( DialogButtonModel( GetString(stringResource(R.string.quit)), diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt index dcd3975c45..c8cc30ce79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt @@ -58,7 +58,7 @@ internal fun MessageNotificationsScreen( return } - if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit) + if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit = quit) Column { Spacer(Modifier.weight(1f)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayName.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayName.kt index 0a3a50370a..f5e6e1a9a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayName.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayName.kt @@ -40,7 +40,11 @@ internal fun PickDisplayName( quit: () -> Unit = {} ) { - if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit) + if (state.showDialog) OnboardingBackPressAlertDialog( + dismissDialog, + R.string.you_cannot_go_back_further_cancel_account_creation, + quit + ) Column( modifier = Modifier diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cb27192dee..a0d6975547 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1140,5 +1140,6 @@ QR code Warning You cannot go back further. In order to stop loading your account, Session needs to quit. + You cannot go back further. In order to cancel your account creation, Session needs to quit. Quit From eeabd32da4f35eb29fad3cfa0105a9a99c179fa8 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 11 Jul 2024 11:46:34 +0930 Subject: [PATCH 4/8] Remove easing on progress animation --- .../thoughtcrime/securesms/onboarding/loading/Loading.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/Loading.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/Loading.kt index 6c7c639a9a..29f7572922 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/Loading.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/Loading.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.onboarding.loading import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.TweenSpec import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -27,7 +28,10 @@ internal fun LoadingScreen(state: State) { animatable.stop() animatable.animateTo( targetValue = 1f, - animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt()) + animationSpec = TweenSpec( + durationMillis = state.duration.inWholeMilliseconds.toInt(), + easing = LinearEasing + ) ) } From e139afed6ac0a841b923566dd4aac10960bcc12a Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 11 Jul 2024 14:58:18 +0930 Subject: [PATCH 5/8] Make loading animation work when animations are off --- .../securesms/onboarding/loading/Loading.kt | 22 +------- .../onboarding/loading/LoadingActivity.kt | 4 +- .../onboarding/loading/LoadingViewModel.kt | 56 +++++++++++++++++-- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/Loading.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/Loading.kt index 29f7572922..30d9a5acdb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/Loading.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/Loading.kt @@ -1,15 +1,10 @@ package org.thoughtcrime.securesms.onboarding.loading -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.TweenSpec import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -21,24 +16,11 @@ import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.h7 @Composable -internal fun LoadingScreen(state: State) { - val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) } - - LaunchedEffect(state) { - animatable.stop() - animatable.animateTo( - targetValue = 1f, - animationSpec = TweenSpec( - durationMillis = state.duration.inWholeMilliseconds.toInt(), - easing = LinearEasing - ) - ) - } - +internal fun LoadingScreen(progress: Float) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Spacer(modifier = Modifier.weight(1f)) ProgressArc( - animatable.value, + progress, modifier = Modifier.contentDescription(R.string.AccessibilityId_loading_animation) ) Text( diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt index 0b76dd829d..db61cefd05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt @@ -47,8 +47,8 @@ class LoadingActivity: BaseActionBarActivity() { ApplicationContext.getInstance(this).newAccount = false setComposeContent { - val state by viewModel.states.collectAsState() - LoadingScreen(state) + val progress by viewModel.progress.collectAsState() + LoadingScreen(progress) } lifecycleScope.launch { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingViewModel.kt index 9cfa86c693..a7871d5620 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingViewModel.kt @@ -4,15 +4,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.launch @@ -23,25 +28,43 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds -data class State(val duration: Duration) +enum class State { + LOADING, + SUCCESS, + FAIL +} private val ANIMATE_TO_DONE_TIME = 500.milliseconds private val IDLE_DONE_TIME = 1.seconds private val TIMEOUT_TIME = 15.seconds -@OptIn(FlowPreview::class) +private val REFRESH_TIME = 50.milliseconds + +@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @HiltViewModel internal class LoadingViewModel @Inject constructor( val prefs: TextSecurePreferences ): ViewModel() { - private val _states = MutableStateFlow(State(TIMEOUT_TIME)) - val states = _states.asStateFlow() + private val state = MutableStateFlow(State.LOADING) + + private val _progress = MutableStateFlow(0f) + val progress = _progress.asStateFlow() private val _events = MutableSharedFlow() val events = _events.asSharedFlow() init { + viewModelScope.launch(Dispatchers.IO) { + state.flatMapLatest { + when (it) { + State.LOADING -> progress(0f, 1f, TIMEOUT_TIME) + else -> progress(progress.value, 1f, ANIMATE_TO_DONE_TIME) + } + }.buffer(0, BufferOverflow.DROP_OLDEST) + .collectLatest { _progress.value = it } + } + viewModelScope.launch(Dispatchers.IO) { try { TextSecurePreferences.events @@ -58,7 +81,7 @@ internal class LoadingViewModel @Inject constructor( private suspend fun onSuccess() { withContext(Dispatchers.Main) { - _states.value = State(ANIMATE_TO_DONE_TIME) + state.value = State.SUCCESS delay(IDLE_DONE_TIME) _events.emit(Event.SUCCESS) } @@ -66,6 +89,8 @@ internal class LoadingViewModel @Inject constructor( private suspend fun onFail() { withContext(Dispatchers.Main) { + state.value = State.FAIL + delay(IDLE_DONE_TIME) _events.emit(Event.TIMEOUT) } } @@ -75,3 +100,22 @@ sealed interface Event { object SUCCESS: Event object TIMEOUT: Event } + +private fun progress( + init: Float, + target: Float, + time: Duration, + refreshRate: Duration = REFRESH_TIME +): Flow = flow { + val startMs = System.currentTimeMillis() + val timeMs = time.inWholeMilliseconds + val finishMs = startMs + timeMs + val range = target - init + + generateSequence { System.currentTimeMillis() }.takeWhile { it < finishMs }.forEach { + emit((it - startMs) * range / timeMs + init) + delay(refreshRate) + } + + emit(target) +} From b453f69bfd7324f4884b97c75f66893ea78a07e2 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 11 Jul 2024 21:48:53 +0930 Subject: [PATCH 6/8] Prefer to fail with InputTooShort over InvalidWord --- .../org/session/libsignal/crypto/MnemonicCodecTest.kt | 8 ++++---- .../java/org/session/libsignal/crypto/MnemonicCodec.kt | 7 ++----- 2 files changed, 6 insertions(+), 9 deletions(-) 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 48fd12cbf4..cbf3458f0d 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 @@ -35,14 +35,14 @@ class MnemonicCodecTest { @Test fun `decode one invalid word that is too short`() { - assertThrows(InvalidWord::class.java) { + assertThrows(InputTooShort::class.java) { codec.decode("a") } } @Test fun `decode one invalid word`() { - assertThrows(InvalidWord::class.java) { + assertThrows(InputTooShort::class.java) { codec.decode("abcd") } } @@ -99,7 +99,7 @@ class MnemonicCodecTest { @Test fun `decodeMnemonicOrHexAsByteArray with account id throws`() { - assertThrows(InvalidWord::class.java) { + assertThrows(InputTooShort::class.java) { codec.decodeMnemonicOrHexAsByteArray("0582e1421da6f584a4795d30b654b4f25fed860afdf081075cb26a2b997e492f14").let(Hex::toStringCondensed) } } @@ -109,7 +109,7 @@ class MnemonicCodecTest { // throws InvalidWord as 0f2ccde528622876b8f16e14db97dafcg is not a valid word on the english wordlist. // It is also not a valid hex string, but we assume that a non-hex string is a recovery password. - assertThrows(InvalidWord::class.java) { + assertThrows(InputTooShort::class.java) { codec.decodeMnemonicOrHexAsByteArray("0f2ccde528622876b8f16e14db97dafcg").let(Hex::toStringCondensed) } } 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 cb81af0fda..19a3207b3c 100644 --- a/libsignal/src/main/java/org/session/libsignal/crypto/MnemonicCodec.kt +++ b/libsignal/src/main/java/org/session/libsignal/crypto/MnemonicCodec.kt @@ -83,8 +83,8 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) { val prefixLength = languageConfiguration.prefixLength val n = truncatedWordSet.size.toLong() - if (mnemonic.isEmpty()) throw DecodingError.InputTooShort - if (words.isEmpty()) throw DecodingError.InputTooShort + // Check preconditions + if (words.size < 13) throw DecodingError.InputTooShort fun String.prefix() = substring(0 until prefixLength) @@ -96,9 +96,6 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) { val wordIndexes = wordPrefixes.map { truncatedWordSet.indexOf(it) } .onEach { if (it < 0) throw DecodingError.InvalidWord } - // Check preconditions - if (words.size < 13) throw DecodingError.InputTooShort - // Verify checksum val checksumIndex = determineChecksumIndex(words.dropLast(1), prefixLength) val expectedChecksumWord = words[checksumIndex] From e3b4636b42f13cdc9d647ef1dd08705da2202983 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 11 Jul 2024 23:52:29 +0930 Subject: [PATCH 7/8] Reopen app to pcurrently open activity --- app/src/main/AndroidManifest.xml | 2 +- .../main/java/org/thoughtcrime/securesms/home/HomeActivity.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d70c9e080e..7e04ad44ca 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -125,7 +125,7 @@ Date: Fri, 12 Jul 2024 00:08:42 +0930 Subject: [PATCH 8/8] SES-2249 fix contentDescriptions --- .../securesms/conversation/start/home/StartConversation.kt | 3 +++ app/src/main/res/values/strings.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt index d12e44f66f..452968429e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt @@ -49,17 +49,20 @@ internal fun StartConversationScreen( ItemButton( textId = R.string.messageNew, icon = R.drawable.ic_message, + modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message), onClick = delegate::onNewMessageSelected) Divider(startIndent = LocalDimensions.current.dividerIndent) ItemButton( textId = R.string.activity_create_group_title, icon = R.drawable.ic_group, + modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group), onClick = delegate::onCreateGroupSelected ) Divider(startIndent = LocalDimensions.current.dividerIndent) ItemButton( textId = R.string.dialog_join_community_title, icon = R.drawable.ic_globe, + modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community), onClick = delegate::onJoinCommunitySelected ) Divider(startIndent = LocalDimensions.current.dividerIndent) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0d6975547..123137ee03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,7 +49,7 @@ New conversation button New direct message Create group - Join community button + Join community Community input Join community button