Merge branch 'develop' into feature/fix_sending_too_long

This commit is contained in:
ganfra 2020-09-21 10:17:52 +02:00
commit 4d7b0e3e68
14 changed files with 176 additions and 39 deletions

10
.github/ISSUE_TEMPLATE/matrix-sdk.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Matrix SDK
about: Report issue or ask for a feature regarding the Android Matrix SDK
title: "[SDK] "
labels: matrix-sdk
assignees: ''
---
<!-- This issue template should be used by third party application maintainers, to report a bug or to request a feature on the SDK module of the application Element Android-->

View File

@ -5,10 +5,12 @@ Features ✨:
- -
Improvements 🙌: Improvements 🙌:
- - Add "show password" in import Megolm keys dialog
Bugfix 🐛: Bugfix 🐛:
- Long message cannot be sent/takes infinite time & blocks other messages #1397 - Long message cannot be sent/takes infinite time & blocks other messages #1397
- User Verification in DM not working
- Manual import of Megolm keys does back up the imported keys
Translations 🗣: Translations 🗣:
- -
@ -20,7 +22,7 @@ Build 🧱:
- -
Other changes: Other changes:
- - Add an advanced action to reset an account data entry
Changes in Element 1.0.7 (2020-09-17) Changes in Element 1.0.7 (2020-09-17)
=================================================== ===================================================

View File

@ -27,6 +27,12 @@ import androidx.lifecycle.LiveData
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.Lazy import dagger.Lazy
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.crypto.MXCryptoConfig
@ -102,12 +108,6 @@ import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.fetchCopied import org.matrix.android.sdk.internal.util.fetchCopied
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -1072,7 +1072,11 @@ internal class DefaultCryptoService @Inject constructor(
throw Exception("Error") throw Exception("Error")
} }
megolmSessionDataImporter.handle(importedSessions, true, progressListener) megolmSessionDataImporter.handle(
megolmSessionsData = importedSessions,
fromBackup = false,
progressListener = progressListener
)
} }
}.foldToCallback(callback) }.foldToCallback(callback)
} }

View File

@ -39,7 +39,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
* Must be call on the crypto coroutine thread * Must be call on the crypto coroutine thread
* *
* @param megolmSessionsData megolm sessions. * @param megolmSessionsData megolm sessions.
* @param backUpKeys true to back up them to the homeserver. * @param fromBackup true if the imported keys are already backed up on the server.
* @param progressListener the progress listener * @param progressListener the progress listener
* @return import room keys result * @return import room keys result
*/ */

View File

@ -24,6 +24,7 @@ import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
@ -55,6 +56,8 @@ internal class SendVerificationMessageWorker(context: Context,
@Inject @Inject
lateinit var cryptoService: CryptoService lateinit var cryptoService: CryptoService
@Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build() val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build()
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
@ -68,16 +71,26 @@ internal class SendVerificationMessageWorker(context: Context,
sessionComponent.inject(this) sessionComponent.inject(this)
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return Result.success(errorOutputData) val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return Result.success(errorOutputData)
val localEventId = localEvent.eventId ?: ""
val roomId = localEvent.roomId ?: ""
if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) {
return Result.success()
.also {
cancelSendTracker.markCancelled(localEventId, roomId)
Timber.e("## SendEvent: Event sending has been cancelled $localEventId")
}
}
return try { return try {
val eventId = sendVerificationMessageTask.execute( val resultEventId = sendVerificationMessageTask.execute(
SendVerificationMessageTask.Params( SendVerificationMessageTask.Params(
event = localEvent, event = localEvent,
cryptoService = cryptoService cryptoService = cryptoService
) )
) )
Result.success(Data.Builder().putString(params.eventId, eventId).build()) Result.success(Data.Builder().putString(localEventId, resultEventId).build())
} catch (exception: Throwable) { } catch (exception: Throwable) {
if (exception.shouldBeRetried()) { if (exception.shouldBeRetried()) {
Result.retry() Result.retry()

View File

@ -115,20 +115,31 @@ internal class VerificationTransportRoomMessage(
val observer = object : Observer<List<WorkInfo>> { val observer = object : Observer<List<WorkInfo>> {
override fun onChanged(workInfoList: List<WorkInfo>?) { override fun onChanged(workInfoList: List<WorkInfo>?) {
workInfoList workInfoList
?.filter { it.state == WorkInfo.State.SUCCEEDED }
?.firstOrNull { it.id == enqueueInfo.second } ?.firstOrNull { it.id == enqueueInfo.second }
?.let { wInfo -> ?.let { wInfo ->
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) {
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") when (wInfo.state) {
tx?.cancel(onErrorReason) WorkInfo.State.FAILED -> {
} else { tx?.cancel(onErrorReason)
if (onDone != null) { workLiveData.removeObserver(this)
onDone() }
} else { WorkInfo.State.SUCCEEDED -> {
tx?.state = nextState if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) {
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
tx?.cancel(onErrorReason)
} else {
if (onDone != null) {
onDone()
} else {
tx?.state = nextState
}
}
workLiveData.removeObserver(this)
}
else -> {
// nop
} }
} }
workLiveData.removeObserver(this)
} }
} }
} }
@ -184,7 +195,7 @@ internal class VerificationTransportRoomMessage(
.build() .build()
workManagerProvider.workManager workManagerProvider.workManager
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue() .enqueue()
// I cannot just listen to the given work request, because when used in a uniqueWork, // I cannot just listen to the given work request, because when used in a uniqueWork,
@ -280,7 +291,7 @@ internal class VerificationTransportRoomMessage(
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
.build() .build()
return workManagerProvider.workManager return workManagerProvider.workManager
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest) .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue() to workRequest.id .enqueue() to workRequest.id
} }

View File

@ -56,6 +56,9 @@ abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Hold
@EpoxyAttribute @EpoxyAttribute
var itemClickAction: View.OnClickListener? = null var itemClickAction: View.OnClickListener? = null
@EpoxyAttribute
var itemLongClickAction: View.OnLongClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.titleText.setTextOrHide(title) holder.titleText.setTextOrHide(title)
@ -76,6 +79,7 @@ abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Hold
} }
holder.view.setOnClickListener(itemClickAction?.let { DebouncedClickListener(it) }) holder.view.setOnClickListener(itemClickAction?.let { DebouncedClickListener(it) })
holder.view.setOnLongClickListener(itemLongClickAction)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -24,6 +24,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -38,6 +39,7 @@ import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.dialogs.ExportKeysDialog import im.vector.app.core.dialogs.ExportKeysDialog
import im.vector.app.core.extensions.queryExportKeys import im.vector.app.core.extensions.queryExportKeys
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.intent.ExternalIntentData import im.vector.app.core.intent.ExternalIntentData
import im.vector.app.core.intent.analyseIntent import im.vector.app.core.intent.analyseIntent
import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getFilenameFromUri
@ -458,6 +460,15 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text) val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button) val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button)
val showPassword = dialogLayout.findViewById<ImageView>(R.id.importDialogShowPassword)
var passwordVisible = false
showPassword.setOnClickListener {
passwordVisible = !passwordVisible
passPhraseEditText.showPassword(passwordVisible)
showPassword.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye)
}
passPhraseEditText.addTextChangedListener(object : SimpleTextWatcher() { passPhraseEditText.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
importButton.isEnabled = !passPhraseEditText.text.isNullOrEmpty() importButton.isEnabled = !passPhraseEditText.text.isNullOrEmpty()

View File

@ -0,0 +1,23 @@
/*
* 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.app.features.settings.devtools
import im.vector.app.core.platform.VectorViewModelAction
sealed class AccountDataAction : VectorViewModelAction {
data class DeleteAccountData(val type: String) : AccountDataAction()
}

View File

@ -21,13 +21,13 @@ import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericItemWithValue import im.vector.app.core.ui.list.genericItemWithValue
import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DebouncedClickListener
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import javax.inject.Inject import javax.inject.Inject
class AccountDataEpoxyController @Inject constructor( class AccountDataEpoxyController @Inject constructor(
@ -36,6 +36,7 @@ class AccountDataEpoxyController @Inject constructor(
interface InteractionListener { interface InteractionListener {
fun didTap(data: UserAccountDataEvent) fun didTap(data: UserAccountDataEvent)
fun didLongTap(data: UserAccountDataEvent)
} }
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
@ -70,6 +71,10 @@ class AccountDataEpoxyController @Inject constructor(
itemClickAction(DebouncedClickListener(View.OnClickListener { itemClickAction(DebouncedClickListener(View.OnClickListener {
interactionListener?.didTap(accountData) interactionListener?.didTap(accountData)
})) }))
itemLongClickAction(View.OnLongClickListener {
interactionListener?.didLongTap(accountData)
true
})
} }
} }
} }

View File

@ -16,13 +16,14 @@
package im.vector.app.features.settings.devtools package im.vector.app.features.settings.devtools
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
@ -31,6 +32,8 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.core.utils.createJSonViewerStyleProvider
import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.internal.di.MoshiProvider
import javax.inject.Inject import javax.inject.Inject
class AccountDataFragment @Inject constructor( class AccountDataFragment @Inject constructor(
@ -74,4 +77,16 @@ class AccountDataFragment @Inject constructor(
createJSonViewerStyleProvider(colorProvider) createJSonViewerStyleProvider(colorProvider)
).show(childFragmentManager, "JSON_VIEWER") ).show(childFragmentManager, "JSON_VIEWER")
} }
override fun didLongTap(data: UserAccountDataEvent) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.delete)
.setMessage(getString(R.string.delete_account_data_warning, data.type))
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete) { _, _ ->
viewModel.handle(AccountDataAction.DeleteAccountData(data.type))
}
.show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
}
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devtools package im.vector.app.features.settings.devtools
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
@ -24,11 +25,13 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import org.matrix.android.sdk.api.session.Session import im.vector.app.core.extensions.exhaustive
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
data class AccountDataViewState( data class AccountDataViewState(
@ -37,7 +40,7 @@ data class AccountDataViewState(
class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: AccountDataViewState, class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: AccountDataViewState,
private val session: Session) private val session: Session)
: VectorViewModel<AccountDataViewState, EmptyAction, EmptyViewEvents>(initialState) { : VectorViewModel<AccountDataViewState, AccountDataAction, EmptyViewEvents>(initialState) {
init { init {
session.rx().liveAccountData(emptySet()) session.rx().liveAccountData(emptySet())
@ -46,7 +49,19 @@ class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: A
} }
} }
override fun handle(action: EmptyAction) {} override fun handle(action: AccountDataAction) {
when (action) {
is AccountDataAction.DeleteAccountData -> handleDeleteAccountData(action)
}.exhaustive
}
private fun handleDeleteAccountData(action: AccountDataAction.DeleteAccountData) {
viewModelScope.launch {
awaitCallback {
session.updateAccountData(action.type, emptyMap(), it)
}
}
}
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root" android:id="@+id/layout_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding" android:paddingStart="?dialogPreferredPadding"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding" android:paddingEnd="?dialogPreferredPadding"
@ -19,14 +19,20 @@
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="16sp" android:textSize="16sp"
android:visibility="gone" android:visibility="gone"
tools:text="filename.txt" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/import_e2e_keys_from_file"
tools:visibility="visible" /> tools:visibility="visible" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/importDialogTil"
style="@style/VectorTextInputLayout" style="@style/VectorTextInputLayout"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColorHint="?attr/vctr_default_text_hint_color"> android:textColorHint="?attr/vctr_default_text_hint_color"
app:layout_constraintEnd_toStartOf="@+id/importDialogShowPassword"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_e2e_keys_passphrase_filename">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_e2e_keys_passphrase_edit_text" android:id="@+id/dialog_e2e_keys_passphrase_edit_text"
@ -38,6 +44,19 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/importDialogShowPassword"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye"
android:tint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/importDialogTil"
app:layout_constraintTop_toTopOf="@id/importDialogTil" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/dialog_e2e_keys_import_button" android:id="@+id/dialog_e2e_keys_import_button"
style="@style/VectorButtonStyle" style="@style/VectorButtonStyle"
@ -46,5 +65,8 @@
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:enabled="false" android:enabled="false"
android:text="@string/encryption_import_import" /> android:text="@string/encryption_import_import"
</LinearLayout> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/importDialogTil" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2226,6 +2226,8 @@
<string name="settings_dev_tools">Dev Tools</string> <string name="settings_dev_tools">Dev Tools</string>
<string name="settings_account_data">Account Data</string> <string name="settings_account_data">Account Data</string>
<string name="delete_account_data_warning">Delete the account data of type %1$s?\n\nUse with caution, it may lead to unexpected behavior.</string>
<plurals name="poll_info"> <plurals name="poll_info">
<item quantity="zero">%d vote</item> <item quantity="zero">%d vote</item>
<item quantity="other">%d votes</item> <item quantity="other">%d votes</item>