mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-23 01:45:52 +03:00
Merge pull request #1266 from vector-im/feature/update_ssss_activity
Feature/update ssss activity
This commit is contained in:
commit
88c70a2c10
20 changed files with 467 additions and 67 deletions
|
@ -26,6 +26,7 @@ Improvements 🙌:
|
|||
- Cross-Signing | Consider not using a spinner on the 'complete security' prompt (#1271)
|
||||
- Restart broken Olm sessions ([MSC1719](https://github.com/matrix-org/matrix-doc/pull/1719))
|
||||
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
|
||||
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix summary notification staying after "mark as read"
|
||||
|
|
|
@ -26,6 +26,8 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
|
|||
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
|
||||
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
|
||||
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
||||
|
@ -456,4 +458,14 @@ interface FragmentModule {
|
|||
@IntoMap
|
||||
@FragmentKey(DeactivateAccountFragment::class)
|
||||
fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SharedSecuredStoragePassphraseFragment::class)
|
||||
fun bindSharedSecuredStoragePassphraseFragment(fragment: SharedSecuredStoragePassphraseFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SharedSecuredStorageKeyFragment::class)
|
||||
fun bindSharedSecuredStorageKeyFragment(fragment: SharedSecuredStorageKeyFragment): Fragment
|
||||
}
|
||||
|
|
|
@ -23,8 +23,11 @@ import im.vector.riotx.core.platform.WaitingViewData
|
|||
sealed class SharedSecureStorageAction : VectorViewModelAction {
|
||||
|
||||
object TogglePasswordVisibility : SharedSecureStorageAction()
|
||||
object UseKey : SharedSecureStorageAction()
|
||||
object Back : SharedSecureStorageAction()
|
||||
object Cancel : SharedSecureStorageAction()
|
||||
data class SubmitPassphrase(val passphrase: String) : SharedSecureStorageAction()
|
||||
data class SubmitKey(val recoveryKey: String) : SharedSecureStorageAction()
|
||||
}
|
||||
|
||||
sealed class SharedSecureStorageViewEvent : VectorViewEvents {
|
||||
|
@ -33,6 +36,7 @@ sealed class SharedSecureStorageViewEvent : VectorViewEvents {
|
|||
data class FinishSuccess(val cypherResult: String) : SharedSecureStorageViewEvent()
|
||||
data class Error(val message: String, val dismiss: Boolean = false) : SharedSecureStorageViewEvent()
|
||||
data class InlineError(val message: String) : SharedSecureStorageViewEvent()
|
||||
data class KeyInlineError(val message: String) : SharedSecureStorageViewEvent()
|
||||
object ShowModalLoading : SharedSecureStorageViewEvent()
|
||||
object HideModalLoading : SharedSecureStorageViewEvent()
|
||||
data class UpdateLoadingState(val waitingData: WaitingViewData) : SharedSecureStorageViewEvent()
|
||||
|
|
|
@ -23,17 +23,19 @@ import android.os.Bundle
|
|||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.extensions.addFragment
|
||||
import im.vector.riotx.core.extensions.commitTransaction
|
||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.activity.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
|
||||
|
@ -56,9 +58,6 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
toolbar.visibility = View.GONE
|
||||
if (isFirstCreation()) {
|
||||
addFragment(R.id.container, SharedSecuredStoragePassphraseFragment::class.java)
|
||||
}
|
||||
|
||||
viewModel.viewEvents
|
||||
.observe()
|
||||
|
@ -69,10 +68,22 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
|||
.disposeOnDestroy()
|
||||
|
||||
viewModel.subscribe(this) {
|
||||
// renderState(it)
|
||||
renderState(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
viewModel.handle(SharedSecureStorageAction.Back)
|
||||
}
|
||||
|
||||
private fun renderState(state: SharedSecureStorageViewState) {
|
||||
if (!state.ready) return
|
||||
val fragment = if (state.hasPassphrase) {
|
||||
if (state.useKey) SharedSecuredStorageKeyFragment::class else SharedSecuredStoragePassphraseFragment::class
|
||||
} else SharedSecuredStorageKeyFragment::class
|
||||
showFragment(fragment, Bundle())
|
||||
}
|
||||
|
||||
private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
|
||||
when (it) {
|
||||
is SharedSecureStorageViewEvent.Dismiss -> {
|
||||
|
@ -108,6 +119,18 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
||||
if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
||||
supportFragmentManager.commitTransaction {
|
||||
replace(R.id.container,
|
||||
fragmentClass.java,
|
||||
bundle,
|
||||
fragmentClass.simpleName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
|
||||
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
|
||||
|
|
|
@ -17,9 +17,14 @@
|
|||
package im.vector.riotx.features.crypto.quads
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
|
@ -31,6 +36,7 @@ import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
|
|||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
|
@ -41,7 +47,11 @@ import timber.log.Timber
|
|||
import java.io.ByteArrayOutputStream
|
||||
|
||||
data class SharedSecureStorageViewState(
|
||||
val passphraseVisible: Boolean = false
|
||||
val ready: Boolean = false,
|
||||
val hasPassphrase: Boolean = true,
|
||||
val useKey: Boolean = false,
|
||||
val passphraseVisible: Boolean = false,
|
||||
val checkingSSSSAction: Async<Unit> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
|
@ -66,6 +76,30 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
val keyResult = args.keyId?.let { session.sharedSecretStorageService.getKey(it) }
|
||||
?: session.sharedSecretStorageService.getDefaultKey()
|
||||
|
||||
if (!keyResult.isSuccess()) {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
|
||||
} else {
|
||||
val info = (keyResult as KeyInfoResult.Success).keyInfo
|
||||
if (info.content.passphrase != null) {
|
||||
setState {
|
||||
copy(
|
||||
ready = true,
|
||||
hasPassphrase = true,
|
||||
useKey = false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
ready = true,
|
||||
hasPassphrase = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: SharedSecureStorageAction) = withState {
|
||||
|
@ -73,14 +107,98 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
|||
is SharedSecureStorageAction.TogglePasswordVisibility -> handleTogglePasswordVisibility()
|
||||
is SharedSecureStorageAction.Cancel -> handleCancel()
|
||||
is SharedSecureStorageAction.SubmitPassphrase -> handleSubmitPassphrase(action)
|
||||
SharedSecureStorageAction.UseKey -> handleUseKey()
|
||||
is SharedSecureStorageAction.SubmitKey -> handleSubmitKey(action)
|
||||
SharedSecureStorageAction.Back -> handleBack()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleUseKey() {
|
||||
setState {
|
||||
copy(
|
||||
useKey = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBack() = withState { state ->
|
||||
if (state.checkingSSSSAction is Loading) return@withState // ignore
|
||||
if (state.hasPassphrase && state.useKey) {
|
||||
setState {
|
||||
copy(
|
||||
useKey = false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSubmitKey(action: SharedSecureStorageAction.SubmitKey) {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
|
||||
val decryptedSecretMap = HashMap<String, String>()
|
||||
setState { copy(checkingSSSSAction = Loading()) }
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val recoveryKey = action.recoveryKey
|
||||
val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
|
||||
if (!keyInfoResult.isSuccess()) {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.Error(stringProvider.getString(R.string.failed_to_access_secure_storage)))
|
||||
return@launch
|
||||
}
|
||||
val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo
|
||||
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
|
||||
WaitingViewData(
|
||||
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
|
||||
isIndeterminate = true
|
||||
)
|
||||
))
|
||||
val keySpec = RawBytesKeySpec.fromRecoveryKey(recoveryKey) ?: return@launch Unit.also {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.KeyInlineError(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
args.requestedSecrets.forEach {
|
||||
if (session.getAccountDataEvent(it) != null) {
|
||||
val res = awaitCallback<String> { callback ->
|
||||
session.sharedSecretStorageService.getSecret(
|
||||
name = it,
|
||||
keyId = keyInfo.id,
|
||||
secretKey = keySpec,
|
||||
callback = callback)
|
||||
}
|
||||
decryptedSecretMap[it] = res
|
||||
} else {
|
||||
Timber.w("## Cannot find secret $it in SSSS, skip")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.fold({
|
||||
setState { copy(checkingSSSSAction = Success(Unit)) }
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||
val safeForIntentCypher = ByteArrayOutputStream().also {
|
||||
it.use {
|
||||
session.securelyStoreObject(decryptedSecretMap as Map<String, String>, args.resultKeyStoreAlias, it)
|
||||
}
|
||||
}.toByteArray().toBase64NoPadding()
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
|
||||
}, {
|
||||
setState { copy(checkingSSSSAction = Fail(it)) }
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.KeyInlineError(stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSubmitPassphrase(action: SharedSecureStorageAction.SubmitPassphrase) {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
|
||||
val decryptedSecretMap = HashMap<String, String>()
|
||||
setState { copy(checkingSSSSAction = Loading()) }
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
|
||||
val passphrase = action.passphrase
|
||||
val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
|
||||
if (!keyInfoResult.isSuccess()) {
|
||||
|
@ -100,7 +218,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
|||
passphrase,
|
||||
keyInfo.content.passphrase?.salt ?: "",
|
||||
keyInfo.content.passphrase?.iterations ?: 0,
|
||||
// TODO
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
|
||||
|
@ -132,6 +249,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}.fold({
|
||||
setState { copy(checkingSSSSAction = Success(Unit)) }
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||
val safeForIntentCypher = ByteArrayOutputStream().also {
|
||||
it.use {
|
||||
|
@ -140,6 +258,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
|||
}.toByteArray().toBase64NoPadding()
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
|
||||
}, {
|
||||
setState { copy(checkingSSSSAction = Fail(it)) }
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.InlineError(stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)))
|
||||
})
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.crypto.quads
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
import com.jakewharton.rxbinding3.widget.editorActionEvents
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.startImportTextFromFileIntent
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.*
|
||||
import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class SharedSecuredStorageKeyFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_ssss_access_from_key
|
||||
|
||||
val sharedViewModel: SharedSecureStorageViewModel by activityViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
ssss_restore_with_key_text.text = getString(R.string.enter_secret_storage_input_key)
|
||||
|
||||
ssss_key_enter_edittext.editorActionEvents()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_key_enter_edittext.textChanges()
|
||||
.skipInitialValue()
|
||||
.subscribe {
|
||||
ssss_key_enter_til.error = null
|
||||
ssss_key_submit.isEnabled = it.isNotBlank()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_key_use_file.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
startImportTextFromFileIntent(this, IMPORT_FILE_REQ)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
sharedViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is SharedSecureStorageViewEvent.KeyInlineError -> {
|
||||
ssss_key_enter_til.error = it.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ssss_key_submit.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
submit()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
fun submit() {
|
||||
val text = ssss_key_enter_edittext.text.toString()
|
||||
if (text.isBlank()) return // Should not reach this point as button disabled
|
||||
ssss_key_submit.isEnabled = false
|
||||
sharedViewModel.handle(SharedSecureStorageAction.SubmitKey(text))
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) {
|
||||
data?.data?.let { dataURI ->
|
||||
tryThis {
|
||||
activity?.contentResolver?.openInputStream(dataURI)
|
||||
?.bufferedReader()
|
||||
?.use { it.readText() }
|
||||
?.let {
|
||||
ssss_key_enter_edittext.setText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { _ ->
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val IMPORT_FILE_REQ = 0
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ package im.vector.riotx.features.crypto.quads
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.text.toSpannable
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
|
@ -27,12 +28,16 @@ import com.jakewharton.rxbinding3.widget.textChanges
|
|||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.*
|
||||
import me.gujun.android.span.span
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
|
||||
class SharedSecuredStoragePassphraseFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
): VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase
|
||||
|
||||
|
@ -41,15 +46,17 @@ class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
ssss_restore_with_passphrase_warning_text.text = span {
|
||||
span(getString(R.string.enter_secret_storage_passphrase_warning)) {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" "
|
||||
+getString(R.string.enter_secret_storage_passphrase_warning_text)
|
||||
}
|
||||
|
||||
ssss_restore_with_passphrase_warning_reason.text = getString(R.string.enter_secret_storage_passphrase_reason_verify)
|
||||
// If has passphrase
|
||||
val pass = getString(R.string.recovery_passphrase)
|
||||
val key = getString(R.string.recovery_key)
|
||||
ssss_restore_with_passphrase_warning_text.text = getString(
|
||||
R.string.enter_secret_storage_passphrase_or_key,
|
||||
pass,
|
||||
key
|
||||
)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(pass, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
.colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
ssss_passphrase_enter_edittext.editorActionEvents()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
|
@ -84,11 +91,11 @@ class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
|
|||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_passphrase_cancel.clicks()
|
||||
ssss_passphrase_use_key.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.Cancel)
|
||||
sharedViewModel.handle(SharedSecureStorageAction.UseKey)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
|
|
|
@ -211,6 +211,12 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
passphrase = null,
|
||||
passphraseRepeat = null
|
||||
)
|
||||
}
|
||||
startInitializeFlow(action.auth)
|
||||
}
|
||||
}
|
||||
|
@ -438,7 +444,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||
}
|
||||
}
|
||||
is BootstrapStep.GetBackupSecretKeyForMigration -> {
|
||||
is BootstrapStep.GetBackupSecretKeyForMigration -> {
|
||||
// do we let you cancel from here?
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||
}
|
||||
|
|
112
vector/src/main/res/layout/fragment_ssss_access_from_key.xml
Normal file
112
vector/src/main/res/layout/fragment_ssss_access_from_key.xml
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/ssss__root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ssss_shield"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:tint="?riotx_text_primary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_key"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key"
|
||||
android:src="@drawable/ic_message_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_restore_with_key"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="36dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/recovery_key"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/ssss_shield"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_restore_with_key_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_key"
|
||||
tools:text="@string/enter_secret_storage_input_key" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ssss_key_enter_til"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_key_text">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/ssss_key_enter_edittext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/recovery_key"
|
||||
android:imeOptions="actionDone"
|
||||
android:maxLines="3"
|
||||
android:singleLine="false"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:inputType="textMultiLine" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- -->
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_key_use_file"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/use_file"
|
||||
app:icon="@drawable/ic_file"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_key_submit"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/_continue"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
|
||||
app:constraint_referenced_ids="ssss_key_use_file,ssss_key_submit"
|
||||
app:flow_horizontalStyle="spread_inside"
|
||||
app:flow_wrapMode="chain"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
|
@ -12,14 +12,14 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/ssss_shield"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:src="@drawable/key_big"
|
||||
android:tint="?riotx_text_primary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_passphrase"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_passphrase" />
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_passphrase"
|
||||
android:src="@drawable/ic_message_password" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_restore_with_passphrase"
|
||||
|
@ -28,7 +28,7 @@
|
|||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="36dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/enter_secret_storage_passphrase"
|
||||
android:text="@string/recovery_passphrase"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
|
@ -47,20 +47,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase"
|
||||
tools:text="@string/enter_secret_storage_passphrase_warning_text" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_restore_with_passphrase_warning_reason"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase_warning_text"
|
||||
tools:text="@string/enter_secret_storage_passphrase_reason_verify" />
|
||||
tools:text="@string/enter_secret_storage_passphrase_or_key" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
|
@ -74,7 +61,7 @@
|
|||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase_warning_reason">
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase_warning_text">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/ssss_passphrase_enter_edittext"
|
||||
|
@ -102,30 +89,37 @@
|
|||
app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til"
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til" />
|
||||
|
||||
<!-- -->
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_passphrase_use_key"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/use_recovery_key"
|
||||
app:icon="@drawable/ic_message_key"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_passphrase_submit"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin_big"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
|
||||
android:minWidth="200dp"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/_continue"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_passphrase_cancel"
|
||||
style="@style/VectorButtonStyleDestructive"
|
||||
android:layout_marginTop="8dp"
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
|
||||
android:minWidth="200dp"
|
||||
android:text="@string/cancel"
|
||||
app:constraint_referenced_ids="ssss_passphrase_use_key,ssss_passphrase_submit"
|
||||
app:flow_horizontalStyle="spread_inside"
|
||||
app:flow_wrapMode="chain"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_submit" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
|
@ -2251,7 +2251,6 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine
|
|||
<string name="enter_secret_storage_passphrase">Gib die geheime Speicherpassphrase ein</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">Warnung:</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">Du solltest nur von einem vertrauenswürdigen Gerät auf den geheimen Speicher zugreifen</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">Greife auf deinen sicheren Nachrichtenverlauf und deine Cross-Signing-Identität zu, um andere Sitzungen zu überprüfen, indem du deine Passphrase eingibst</string>
|
||||
|
||||
<string name="message_action_item_redact">Entfernen…</string>
|
||||
<string name="share_confirm_room">Möchtest du diesen Anhang an %1$s senden\?</string>
|
||||
|
|
|
@ -2192,7 +2192,6 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.</string>
|
|||
<string name="enter_secret_storage_passphrase">Sartu biltegi sekretuko pasa-esaldia</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">Abisua:</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">Biltegi sekretura gailu fidagarri batetik konektatu beharko zinateke beti</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">Atzitu zure mezu seguruen historiala eta zeharkako sinatzerako identitatea beste saioak egiaztatzeko zure pasa-esaldia sartuz</string>
|
||||
|
||||
<string name="message_action_item_redact">Kendu…</string>
|
||||
<string name="share_confirm_room">Eranskin hau %1$s gelara bidali nahi duzu\?</string>
|
||||
|
|
|
@ -2201,7 +2201,6 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq
|
|||
<string name="enter_secret_storage_passphrase">Saisir la phrase secrète du coffre secret</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">Attention :</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">Vous devriez accéder à votre coffre secret uniquement depuis un appareil de confiance</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">Accédez à l’historique de vos messages sécurisés et à votre identité de signature croisée pour vérifier d’autres sessions en saisissant votre phrase secrète</string>
|
||||
|
||||
<string name="message_action_item_redact">Supprimer…</string>
|
||||
<string name="share_confirm_room">Voulez-vous envoyer cette pièce jointe à %1$s \?</string>
|
||||
|
|
|
@ -2196,7 +2196,6 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
|
|||
<string name="enter_secret_storage_passphrase">Add meg a jelmondatot a biztonsági tárolóhoz</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">Figyelem:</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">Csak biztonságos eszközről férj hozzá a biztonsági tárolóhoz</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">A jelmondat megadásával hozzáférhetsz a biztonságos üzeneteidhez és az eszközök közötti hitelesítéshez használt személyazonosságodhoz, hogy más munkameneteket hitelesíthess</string>
|
||||
|
||||
<string name="message_action_item_redact">Töröl…</string>
|
||||
<string name="share_confirm_room">Ezt a csatolmányt el szeretnéd küldeni ide: %1$s\?</string>
|
||||
|
|
|
@ -2246,7 +2246,6 @@
|
|||
<string name="enter_secret_storage_passphrase">Inserisci la password dell\'archivio segreto</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">Attenzione:</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">Dovresti accedere all\'archivio segreto solo da un dispositivo fidato</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">Accedi alla cronologia dei messaggi sicuri e all\'identità di firma incrociata per verificare altre sessioni inserendo la tua password</string>
|
||||
|
||||
<string name="message_action_item_redact">Rimuovi…</string>
|
||||
<string name="share_confirm_room">Vuoi inviare questo allegato a %1$s\?</string>
|
||||
|
|
|
@ -2172,7 +2172,6 @@ Spróbuj uruchomić ponownie aplikację.</string>
|
|||
<string name="enter_secret_storage_passphrase">Wprowadź hasło tajemnej przestrzeni</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">Ostrzeżenie:</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">Powinieneś(-nnaś) uzyskać dostęp do tajnej przestrzeni jedynie z zaufanego urządzenia</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">Uzyskaj dostęp do historii bezpiecznych wiadomości i Twojego(-jej) tożsamości podpisu krzyżowego poprzez weryfikację innej sesji za pomocą hasła</string>
|
||||
|
||||
<string name="message_action_item_redact">Usuń…</string>
|
||||
<string name="share_confirm_room">Czy chcesz wysłać załącznik do %1$s\?</string>
|
||||
|
|
|
@ -2156,7 +2156,6 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
|
|||
<string name="enter_secret_storage_passphrase">Jepni frazëkalimin e fshehtë për në depozitim</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">Kujdes:</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">Duhet të hyni në depozitim të fshehtë vetëm nga një pajisje e besuar</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">Për verifikim sesionesh të tjerë përmes dhënies së frazëkalimit tuaj, hyni te historiku i mesazheve tuaj të sigurt dhe identiteti juaj për <em>cross-signing</em></string>
|
||||
|
||||
<string name="message_action_item_redact">Hiqni…</string>
|
||||
<string name="share_confirm_room">Doni të dërgohet kjo bashkëngjitje te %1$s\?</string>
|
||||
|
|
|
@ -2144,7 +2144,6 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
|
|||
<string name="enter_secret_storage_passphrase">輸入秘密儲存空間通關密語</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">警告:</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">您僅能從受信任的裝置存取秘密儲存空間</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">透過輸入通關密語來存取您的安全訊息歷史與您的交叉簽章身份以驗證其他工作階段</string>
|
||||
|
||||
<string name="message_action_item_redact">移除……</string>
|
||||
<string name="share_confirm_room">您想要傳送此附件到 %1$s 嗎?</string>
|
||||
|
|
|
@ -2175,7 +2175,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
<string name="enter_secret_storage_passphrase">Enter secret storage passphrase</string>
|
||||
<string name="enter_secret_storage_passphrase_warning">Warning:</string>
|
||||
<string name="enter_secret_storage_passphrase_warning_text">You should only access secret storage from a trusted device</string>
|
||||
<string name="enter_secret_storage_passphrase_reason_verify">Access your secure message history and your cross-signing identity for verifying other sessions by entering your passphrase</string>
|
||||
|
||||
<string name="message_action_item_redact">Remove…</string>
|
||||
<string name="share_confirm_room">Do you want to send this attachment to %1$s?</string>
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
<string name="use_latest_riot">Use the latest Riot on your other devices:</string>
|
||||
<string name="command_description_discard_session">Forces the current outbound group session in an encrypted room to be discarded</string>
|
||||
<string name="command_description_discard_session_not_handled">Only supported in encrypted rooms</string>
|
||||
<!-- first will be replaced by recovery_passphrase, second will be replaced by recovery_key-->
|
||||
<string name="enter_secret_storage_passphrase_or_key">Use your %1$s or use your %2$s to continue.</string>
|
||||
<string name="use_recovery_key">Use Recovery Key</string>
|
||||
<string name="enter_secret_storage_input_key">Select your Recovery Key, or input it manually by typing it or pasting from your clipboard</string>
|
||||
<string name="keys_backup_recovery_key_error_decrypt">Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.</string>
|
||||
<string name="failed_to_access_secure_storage">Failed to access secure storage</string>
|
||||
<!-- END Strings added by Valere -->
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue