mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 20:06:51 +03:00
Crypto: export room keys
This commit is contained in:
parent
8c8a4dcbd1
commit
5f0d1d9536
9 changed files with 240 additions and 150 deletions
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2019 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.matrix.android.api.util
|
||||
|
||||
import arrow.core.*
|
||||
|
||||
inline fun <A> TryOf<A>.onError(f: (Throwable) -> Unit): Try<A> = fix()
|
||||
.fold(
|
||||
{
|
||||
f(it)
|
||||
Failure(it)
|
||||
},
|
||||
{ Success(it) }
|
||||
)
|
|
@ -64,6 +64,7 @@ import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificat
|
|||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
|
@ -787,7 +788,10 @@ internal class CryptoManager(
|
|||
* @param anIterationCount the encryption iteration count (0 means no encryption)
|
||||
* @param callback the exported keys
|
||||
*/
|
||||
fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
|
||||
private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
Try {
|
||||
val iterationCount = Math.max(0, anIterationCount)
|
||||
|
||||
val exportedSessions = ArrayList<MegolmSessionData>()
|
||||
|
@ -802,20 +806,14 @@ internal class CryptoManager(
|
|||
}
|
||||
}
|
||||
|
||||
val encryptedRoomKeys: ByteArray
|
||||
|
||||
try {
|
||||
val adapter = MoshiProvider.providesMoshi()
|
||||
.adapter(List::class.java)
|
||||
|
||||
encryptedRoomKeys = MXMegolmExportEncryption
|
||||
MXMegolmExportEncryption
|
||||
.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
|
||||
} catch (e: Exception) {
|
||||
callback.onFailure(e)
|
||||
return
|
||||
}
|
||||
|
||||
callback.onSuccess(encryptedRoomKeys)
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -830,6 +828,7 @@ internal class CryptoManager(
|
|||
password: String,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
// TODO Use coroutines
|
||||
Timber.v("## importRoomKeys starts")
|
||||
|
||||
val t0 = System.currentTimeMillis()
|
||||
|
|
|
@ -21,7 +21,7 @@ import android.text.TextUtils
|
|||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.util.onError
|
||||
import im.vector.matrix.android.internal.extensions.onError
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2019 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.matrix.android.internal.extensions
|
||||
|
||||
import arrow.core.*
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
|
||||
inline fun <A> TryOf<A>.onError(f: (Throwable) -> Unit): Try<A> = fix()
|
||||
.fold(
|
||||
{
|
||||
f(it)
|
||||
Failure(it)
|
||||
},
|
||||
{ Success(it) }
|
||||
)
|
||||
|
||||
fun <A> Try<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold(
|
||||
{ callback.onFailure(it) },
|
||||
{ callback.onSuccess(it) })
|
|
@ -19,34 +19,30 @@ package im.vector.riotredesign.core.dialogs
|
|||
import android.app.Activity
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.showPassword
|
||||
import im.vector.riotredesign.core.platform.SimpleTextWatcher
|
||||
|
||||
class ExportKeysDialog {
|
||||
|
||||
var passwordVisible = false
|
||||
|
||||
fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) {
|
||||
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null)
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.encryption_export_room_keys)
|
||||
.setView(dialogLayout)
|
||||
|
||||
val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
|
||||
val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_confirm_passphrase_edit_text)
|
||||
val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.dialog_e2e_keys_confirm_passphrase_til)
|
||||
val exportButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_export_button)
|
||||
val textWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
|
||||
}
|
||||
|
||||
val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEt)
|
||||
val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEtConfirm)
|
||||
val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.exportDialogTilConfirm)
|
||||
val exportButton = dialogLayout.findViewById<Button>(R.id.exportDialogSubmit)
|
||||
val textWatcher = object : SimpleTextWatcher() {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
when {
|
||||
TextUtils.isEmpty(passPhrase1EditText.text) -> {
|
||||
|
@ -68,6 +64,14 @@ class ExportKeysDialog {
|
|||
passPhrase1EditText.addTextChangedListener(textWatcher)
|
||||
passPhrase2EditText.addTextChangedListener(textWatcher)
|
||||
|
||||
val showPassword = dialogLayout.findViewById<ImageView>(R.id.exportDialogShowPassword)
|
||||
showPassword.setOnClickListener {
|
||||
passwordVisible = !passwordVisible
|
||||
passPhrase1EditText.showPassword(passwordVisible)
|
||||
passPhrase2EditText.showPassword(passwordVisible)
|
||||
showPassword.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
}
|
||||
|
||||
val exportDialog = builder.show()
|
||||
|
||||
exportButton.setOnClickListener {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2019 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.riotredesign.features.crypto.keys
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import androidx.annotation.WorkerThread
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.riotredesign.core.files.addEntryToDownloadManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.Okio
|
||||
import java.io.File
|
||||
|
||||
class KeysExporter(private val session: Session) {
|
||||
|
||||
/**
|
||||
* Export keys and return the file path with the callback
|
||||
*/
|
||||
fun export(context: Context, password: String, callback: MatrixCallback<String>) {
|
||||
session.exportRoomKeys(password, object : MatrixCallback<ByteArray> {
|
||||
|
||||
override fun onSuccess(data: ByteArray) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
copyToFile(context, data)
|
||||
}
|
||||
.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun copyToFile(context: Context, data: ByteArray): Try<String> {
|
||||
return Try {
|
||||
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
|
||||
|
||||
val sink = Okio.sink(file)
|
||||
|
||||
val bufferedSink = Okio.buffer(sink)
|
||||
|
||||
bufferedSink.write(data)
|
||||
|
||||
bufferedSink.close()
|
||||
sink.close()
|
||||
|
||||
addEntryToDownloadManager(context, file, "text/plain")
|
||||
|
||||
file.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,10 +23,13 @@ import androidx.fragment.app.FragmentManager
|
|||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.dialogs.ExportKeysDialog
|
||||
import im.vector.riotredesign.core.extensions.observeEvent
|
||||
import im.vector.riotredesign.core.platform.SimpleFragmentActivity
|
||||
import im.vector.riotredesign.core.utils.toast
|
||||
import im.vector.riotredesign.features.crypto.keys.KeysExporter
|
||||
|
||||
class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
|
||||
|
@ -118,19 +121,21 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||
})
|
||||
}
|
||||
|
||||
fun exportKeysManually() {
|
||||
private fun exportKeysManually() {
|
||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
notImplemented()
|
||||
/*
|
||||
showWaitingView()
|
||||
|
||||
CommonActivityUtils.exportKeys(session, passphrase, object : SimpleApiCallback<String>(this@KeysBackupSetupActivity) {
|
||||
override fun onSuccess(filename: String) {
|
||||
KeysExporter(session)
|
||||
.export(this@KeysBackupSetupActivity,
|
||||
passphrase,
|
||||
object : MatrixCallback<String> {
|
||||
|
||||
override fun onSuccess(data: String) {
|
||||
hideWaitingView()
|
||||
|
||||
AlertDialog.Builder(this@KeysBackupSetupActivity)
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, filename))
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { dialog, which ->
|
||||
val resultIntent = Intent()
|
||||
|
@ -141,26 +146,13 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||
.show()
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
super.onNetworkError(e)
|
||||
hideWaitingView()
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
super.onMatrixError(e)
|
||||
hideWaitingView()
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
super.onUnexpectedError(e)
|
||||
override fun onFailure(failure: Throwable) {
|
||||
toast(failure.localizedMessage)
|
||||
hideWaitingView()
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ import im.vector.riotredesign.core.dialogs.ExportKeysDialog
|
|||
import im.vector.riotredesign.core.extensions.showPassword
|
||||
import im.vector.riotredesign.core.extensions.withArgs
|
||||
import im.vector.riotredesign.core.platform.SimpleTextWatcher
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import im.vector.riotredesign.core.platform.VectorPreferenceFragment
|
||||
import im.vector.riotredesign.core.preference.BingRule
|
||||
import im.vector.riotredesign.core.preference.ProgressBarPreference
|
||||
|
@ -64,6 +65,7 @@ import im.vector.riotredesign.core.preference.VectorPreference
|
|||
import im.vector.riotredesign.core.utils.*
|
||||
import im.vector.riotredesign.features.MainActivity
|
||||
import im.vector.riotredesign.features.configuration.VectorConfiguration
|
||||
import im.vector.riotredesign.features.crypto.keys.KeysExporter
|
||||
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import org.koin.android.ext.android.inject
|
||||
|
@ -885,9 +887,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
|
|||
PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
// TODO Test
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
/* TODO
|
||||
if (allGranted(grantResults)) {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
|
||||
changeAvatar()
|
||||
|
@ -895,7 +895,6 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
|
|||
exportKeys()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
|
@ -2594,38 +2593,27 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
|
|||
activity?.let { activity ->
|
||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
notImplemented()
|
||||
/*
|
||||
|
||||
displayLoadingView()
|
||||
|
||||
CommonActivityUtils.exportKeys(session, passphrase, object : SimpleApiCallback<String>(activity) {
|
||||
override fun onSuccess(filename: String) {
|
||||
KeysExporter(mSession)
|
||||
.export(requireContext(),
|
||||
passphrase,
|
||||
object : MatrixCallback<String> {
|
||||
override fun onSuccess(data: String) {
|
||||
hideLoadingView()
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, filename))
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
super.onNetworkError(e)
|
||||
hideLoadingView()
|
||||
override fun onFailure(failure: Throwable) {
|
||||
onCommonDone(failure.localizedMessage)
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
super.onMatrixError(e)
|
||||
hideLoadingView()
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
super.onUnexpectedError(e)
|
||||
hideLoadingView()
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?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"
|
||||
android:id="@+id/layout_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingLeft="?dialogPreferredPadding"
|
||||
|
@ -13,21 +13,41 @@
|
|||
android:paddingBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/exportDialogText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/encryption_export_notice"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp" />
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/exportDialogShowPassword"
|
||||
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_black"
|
||||
android:tint="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/exportDialogTil"
|
||||
app:layout_constraintTop_toTopOf="@id/exportDialogTil" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/exportDialogTil"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColorHint="?attr/vctr_default_text_hint_color">
|
||||
android:textColorHint="?attr/vctr_default_text_hint_color"
|
||||
app:layout_constraintEnd_toStartOf="@+id/exportDialogShowPassword"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/exportDialogText">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/dialog_e2e_keys_passphrase_edit_text"
|
||||
android:id="@+id/exportDialogEt"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/passphrase_create_passphrase"
|
||||
|
@ -38,16 +58,19 @@
|
|||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/dialog_e2e_keys_confirm_passphrase_til"
|
||||
android:id="@+id/exportDialogTilConfirm"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textColorHint="?attr/vctr_default_text_hint_color"
|
||||
app:errorEnabled="true">
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="@+id/exportDialogTil"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/exportDialogTil">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/dialog_e2e_keys_confirm_passphrase_edit_text"
|
||||
android:id="@+id/exportDialogEtConfirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/passphrase_confirm_passphrase"
|
||||
|
@ -57,10 +80,14 @@
|
|||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/dialog_e2e_keys_export_button"
|
||||
android:id="@+id/exportDialogSubmit"
|
||||
style="@style/VectorButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:enabled="false"
|
||||
android:text="@string/encryption_export_export" />
|
||||
</LinearLayout>
|
||||
android:text="@string/encryption_export_export"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/exportDialogTilConfirm" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in a new issue