Keys backup: migrate settings to Epoxy and MvRx

This commit is contained in:
Benoit Marty 2019-06-12 13:09:32 +02:00
parent 53dd9c3427
commit b47ef9220e
25 changed files with 672 additions and 539 deletions

View File

@ -18,11 +18,11 @@ package im.vector.riotredesign

import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.os.Handler
import android.os.HandlerThread
import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat
import android.content.res.Configuration
import androidx.multidex.MultiDex
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
@ -33,6 +33,7 @@ import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.di.AppModule
import im.vector.riotredesign.features.configuration.VectorConfiguration
import im.vector.riotredesign.features.crypto.keysbackup.KeysBackupModule
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotredesign.features.rageshake.VectorFileLogger
@ -73,7 +74,8 @@ class VectorApplication : Application() {
val appModule = AppModule(applicationContext).definition
val homeModule = HomeModule().definition
val roomDirectoryModule = RoomDirectoryModule().definition
val koin = startKoin(listOf(appModule, homeModule, roomDirectoryModule), logger = EmptyLogger())
val keysBackupModule = KeysBackupModule().definition
val koin = startKoin(listOf(appModule, homeModule, roomDirectoryModule, keysBackupModule), logger = EmptyLogger())
Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION)
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())


View File

@ -20,7 +20,7 @@ import android.widget.TextView
import androidx.core.view.isVisible

/**
* Set a text in the TextView, or set visibility to GONE it if the text is null
* Set a text in the TextView, or set visibility to GONE if the text is null
*/
fun TextView.setTextOrHide(newText: String?, hideWhenBlank: Boolean = true) {
if (newText == null

View File

@ -18,6 +18,7 @@ package im.vector.riotredesign.core.resources

import android.content.res.Resources
import androidx.annotation.NonNull
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes

class StringProvider(private val resources: Resources) {
@ -51,5 +52,9 @@ class StringProvider(private val resources: Resources) {
return resources.getString(resId, *formatArgs)
}

@NonNull
fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String {
return resources.getQuantityString(resId, quantity, *formatArgs)
}

}

View File

@ -0,0 +1,114 @@
/*
* 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.core.ui.list

import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide

/**
* A generic list item.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
@EpoxyModelClass(layout = R.layout.item_generic_list)
abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {

enum class STYLE {
BIG_TEXT,
NORMAL_TEXT
}

class Action(var title: String) {
var perform: Runnable? = null
}

@EpoxyAttribute
var title: String? = null

@EpoxyAttribute
var description: String? = null

@EpoxyAttribute
var style: STYLE = STYLE.NORMAL_TEXT

@EpoxyAttribute
@DrawableRes
var endIconResourceId: Int = -1

@EpoxyAttribute
var hasIndeterminateProcess = false

@EpoxyAttribute
var buttonAction: Action? = null

@EpoxyAttribute
var itemClickAction: Action? = null

override fun bind(holder: Holder) {
holder.titleText.setTextOrHide(title)

when (style) {
STYLE.BIG_TEXT -> holder.titleText.textSize = 18f
STYLE.NORMAL_TEXT -> holder.titleText.textSize = 14f
}

holder.descriptionText.setTextOrHide(description)

if (hasIndeterminateProcess) {
holder.progressBar.isVisible = true
holder.accessoryImage.isVisible = false
} else {
holder.progressBar.isVisible = false
if (endIconResourceId != -1) {
holder.accessoryImage.setImageResource(endIconResourceId)
holder.accessoryImage.isVisible = true
} else {
holder.accessoryImage.isVisible = false
}
}

holder.actionButton.setTextOrHide(buttonAction?.title)
holder.actionButton.setOnClickListener {
buttonAction?.perform?.run()
}

holder.root.setOnClickListener {
itemClickAction?.perform?.run()
}
}

class Holder : VectorEpoxyHolder() {
val root by bind<View>(R.id.item_generic_root)

val titleText by bind<TextView>(R.id.item_generic_title_text)
val descriptionText by bind<TextView>(R.id.item_generic_description_text)
val accessoryImage by bind<ImageView>(R.id.item_generic_accessory_image)
val progressBar by bind<ProgressBar>(R.id.item_generic_progress_bar)
val actionButton by bind<Button>(R.id.item_generic_action_button)
}
}

View File

@ -1,104 +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.riotredesign.core.ui.list

import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.LayoutRes
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotredesign.R

/**
* View Holder for generic list items.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
class GenericItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

companion object {
@LayoutRes
const val resId = R.layout.item_generic_list
}

@BindView(R.id.item_generic_title_text)
lateinit var titleText: TextView

@BindView(R.id.item_generic_description_text)
lateinit var descriptionText: TextView

@BindView(R.id.item_generic_accessory_image)
lateinit var accessoryImage: ImageView

@BindView(R.id.item_generic_progress_bar)
lateinit var progressBar: ProgressBar

@BindView(R.id.item_generic_action_button)
lateinit var actionButton: Button

init {
ButterKnife.bind(this, itemView)
}

fun bind(item: GenericRecyclerViewItem) {
titleText.text = item.title

when (item.style) {
GenericRecyclerViewItem.STYLE.BIG_TEXT -> titleText.textSize = 18f
GenericRecyclerViewItem.STYLE.NORMAL_TEXT -> titleText.textSize = 14f
}

item.description?.let {
descriptionText.isVisible = true
descriptionText.text = it
} ?: run { descriptionText.isVisible = false }

if (item.hasIndeterminateProcess) {
progressBar.isVisible = true
accessoryImage.isVisible = false
} else {
progressBar.isVisible = false
if (item.endIconResourceId != -1) {
accessoryImage.setImageResource(item.endIconResourceId)
accessoryImage.isVisible = true
} else {
accessoryImage.isVisible = false
}
}

val buttonAction = item.buttonAction

if (buttonAction == null) {
actionButton.isVisible = false
} else {
actionButton.text = buttonAction.title
actionButton.setOnClickListener {
buttonAction.perform?.run()
}
actionButton.isVisible = true
}

itemView?.setOnClickListener {
item.itemClickAction?.perform?.run()
}
}
}

View File

@ -1,48 +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.riotredesign.core.ui.list

import androidx.annotation.DrawableRes


/**
* A generic list item.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
class GenericRecyclerViewItem(val title: String,
var description: String? = null,
val style: STYLE = STYLE.NORMAL_TEXT) {

enum class STYLE {
BIG_TEXT,
NORMAL_TEXT
}

@DrawableRes
var endIconResourceId: Int = -1

var hasIndeterminateProcess = false

var buttonAction: Action? = null

var itemClickAction: Action? = null

class Action(var title: String) {
var perform: Runnable? = null
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.keysbackup

import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupSettingsRecyclerViewController
import org.koin.dsl.module.module

class KeysBackupModule {

companion object {
const val KEYS_BACKUP_SCOPE = "KEYS_BACKUP_SCOPE"
}

val definition = module(override = true) {

scope(KEYS_BACKUP_SCOPE) {
KeysBackupSettingsRecyclerViewController(get(), get())
}

}
}

View File

@ -18,12 +18,15 @@ package im.vector.riotredesign.features.crypto.keysbackup.settings
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import im.vector.fragments.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.matrix.android.api.MatrixCallback
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.viewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.SimpleFragmentActivity
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.features.crypto.keysbackup.KeysBackupModule
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope


class KeysBackupManageActivity : SimpleFragmentActivity() {
@ -31,46 +34,47 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
companion object {

fun intent(context: Context): Intent {
val intent = Intent(context, KeysBackupManageActivity::class.java)
return intent
return Intent(context, KeysBackupManageActivity::class.java)
}
}

override fun getTitleRes() = R.string.encryption_message_recovery


private lateinit var viewModel: KeysBackupSettingsViewModel
private val viewModel: KeysBackupSettingsViewModel by viewModel()

override fun initUiAndData() {
super.initUiAndData()
viewModel = ViewModelProviders.of(this).get(KeysBackupSettingsViewModel::class.java)
viewModel.initSession(session)

bindScope(getOrCreateScope(KeysBackupModule.KEYS_BACKUP_SCOPE))

if (supportFragmentManager.fragments.isEmpty()) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupSettingsFragment.newInstance())
.commitNow()

session.getKeysBackupService()
.forceUsingLastVersion(object : MatrixCallback<Boolean> {})
viewModel.init()
}

viewModel.loadingEvent.observe(this, Observer {
updateWaitingView(it)
})
// Observe the deletion of keys backup
viewModel.selectSubscribe(this, KeysBackupSettingViewState::deleteBackupRequest) { asyncDelete ->
when (asyncDelete) {
is Fail -> {
updateWaitingView(null)


viewModel.apiResultError.observe(this, Observer { uxStateEvent ->
uxStateEvent?.getContentIfNotHandled()?.let {
AlertDialog.Builder(this)
.setTitle(R.string.unknown_error)
.setMessage(it)
.setMessage(getString(R.string.keys_backup_get_version_error, asyncDelete.error.localizedMessage))
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
}
})

is Loading -> {
updateWaitingView(WaitingViewData(getString(R.string.keys_backup_settings_deleting_backup)))
}
else -> {
updateWaitingView(null)
}
}
}
}
}

View File

@ -0,0 +1,58 @@
/*
* 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.keysbackup.settings

import android.view.View
import android.widget.Button
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide

@EpoxyModelClass(layout = R.layout.item_keys_backup_settings_button_footer)
abstract class KeysBackupSettingFooterItem : VectorEpoxyModel<KeysBackupSettingFooterItem.Holder>() {

@EpoxyAttribute
var textButton1: String? = null

@EpoxyAttribute
var clickOnButton1: View.OnClickListener? = null

@EpoxyAttribute
var textButton2: String? = null

@EpoxyAttribute
var clickOnButton2: View.OnClickListener? = null

override fun bind(holder: Holder) {
holder.button1.setTextOrHide(textButton1)
holder.button1.setOnClickListener(clickOnButton1)

holder.button2.setTextOrHide(textButton2)
holder.button2.setOnClickListener(clickOnButton2)
}

class Holder : VectorEpoxyHolder() {
val button1 by bind<Button>(R.id.keys_backup_settings_footer_button1)
val button2 by bind<TextView>(R.id.keys_backup_settings_footer_button2)
}

}

View File

@ -0,0 +1,30 @@
/*
* 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.keysbackup.settings

import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult

data class KeysBackupSettingViewState(val keysBackupVersionTrust: Async<KeysBackupVersionTrust> = Uninitialized,
val keysBackupState: KeysBackupState? = null,
val keysBackupVersion: KeysVersionResult? = null,
val deleteBackupRequest: Async<Unit> = Uninitialized)
: MvRxState

View File

@ -13,27 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.fragments.keysbackup.settings
package im.vector.riotredesign.features.crypto.keysbackup.settings

import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.features.crypto.keysbackup.KeysBackupModule
import im.vector.riotredesign.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import kotlinx.android.synthetic.main.fragment_keys_backup_settings.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

class KeysBackupSettingsFragment : VectorBaseFragment(),
KeysBackupSettingsRecyclerViewAdapter.AdapterListener {

KeysBackupSettingsRecyclerViewController.Listener {

companion object {
fun newInstance() = KeysBackupSettingsFragment()
@ -41,63 +39,26 @@ class KeysBackupSettingsFragment : VectorBaseFragment(),

override fun getLayoutResId() = R.layout.fragment_keys_backup_settings

private lateinit var viewModel: KeysBackupSettingsViewModel
private val keysBackupSettingsRecyclerViewController: KeysBackupSettingsRecyclerViewController by inject()

@BindView(R.id.keys_backup_settings_recycler_view)
lateinit var recyclerView: RecyclerView
private val viewModel: KeysBackupSettingsViewModel by activityViewModel()

private var recyclerViewAdapter: KeysBackupSettingsRecyclerViewAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

bindScope(getOrCreateScope(KeysBackupModule.KEYS_BACKUP_SCOPE))
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = layoutManager
keysBackupSettingsRecyclerView.setController(keysBackupSettingsRecyclerViewController)

recyclerViewAdapter = KeysBackupSettingsRecyclerViewAdapter(activity!!)
recyclerView.adapter = recyclerViewAdapter
recyclerViewAdapter?.adapterListener = this


viewModel = activity?.run {
ViewModelProviders.of(this).get(KeysBackupSettingsViewModel::class.java)
} ?: throw Exception("Invalid Activity")


viewModel.keyBackupState.observe(this, Observer { keysBackupState ->
if (keysBackupState == null) {
//Cannot happen?
viewModel.keyVersionTrust.value = null
} else {
when (keysBackupState) {
KeysBackupState.Unknown,
KeysBackupState.CheckingBackUpOnHomeserver -> {
viewModel.loadingEvent.value = WaitingViewData("")
}
else -> {
viewModel.loadingEvent.value = null
//All this cases will be manage by looking at the backup trust object
viewModel.session?.getKeysBackupService()?.keysBackupVersion?.let {
viewModel.getKeysBackupTrust(it)
} ?: run {
viewModel.keyVersionTrust.value = null
}
}
}
keysBackupSettingsRecyclerViewController.listener = this
}

// Update the adapter for each state change
viewModel.session?.let { session ->
recyclerViewAdapter?.updateWithTrust(session, viewModel.keyVersionTrust.value)
}
})

viewModel.keyVersionTrust.observe(this, Observer {
viewModel.session?.let { session ->
recyclerViewAdapter?.updateWithTrust(session, it)
}
})

override fun invalidate() = withState(viewModel) { state ->
keysBackupSettingsRecyclerViewController.setData(state)
}

override fun didSelectSetupMessageRecovery() {
@ -127,4 +88,11 @@ class KeysBackupSettingsFragment : VectorBaseFragment(),
}
}

override fun loadTrustData() {
viewModel.getKeysBackupTrust()
}

override fun loadKeysBackupState() {
viewModel.init()
}
}

View File

@ -1,233 +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.fragments.keysbackup.settings

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.riotredesign.R
import im.vector.riotredesign.core.ui.list.GenericItemViewHolder
import im.vector.riotredesign.core.ui.list.GenericRecyclerViewItem

class KeysBackupSettingsRecyclerViewAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

val inflater: LayoutInflater = LayoutInflater.from(context)

private var infoList: List<GenericRecyclerViewItem> = ArrayList()

private var isBackupAlreadySetup = false

var adapterListener: AdapterListener? = null

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
GenericItemViewHolder.resId -> GenericItemViewHolder(inflater.inflate(viewType, parent, false))
else -> FooterViewHolder(inflater.inflate(viewType, parent, false))
}
}

override fun getItemViewType(position: Int): Int {
return if (position < infoList.size) {
GenericItemViewHolder.resId
} else {
R.layout.item_keys_backup_settings_button_footer
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GenericItemViewHolder) {
holder.bind(infoList[position])
} else if (holder is FooterViewHolder) {
if (isBackupAlreadySetup) {
holder.button1.setText(R.string.keys_backup_settings_restore_backup_button)
holder.button1.isVisible = true
holder.button1.setOnClickListener {
adapterListener?.didSelectRestoreMessageRecovery()
}

holder.button2.setText(R.string.keys_backup_settings_delete_backup_button)
holder.button2.isVisible = true
holder.button2.setOnClickListener {
adapterListener?.didSelectDeleteSetupMessageRecovery()
}
} else {
holder.button1.setText(R.string.keys_backup_setup)
holder.button1.isVisible = true
holder.button1.setOnClickListener {
adapterListener?.didSelectSetupMessageRecovery()
}

holder.button2.isVisible = false
}
}
}

override fun getItemCount(): Int {
return infoList.size + 1 /*footer*/
}


fun updateWithTrust(session: Session, keyBackupVersionTrust: KeysBackupVersionTrust?) {
val keyBackupState = session.getKeysBackupService().state
val keyVersionResult = session.getKeysBackupService().keysBackupVersion

val infos = ArrayList<GenericRecyclerViewItem>()
var itemSummary: GenericRecyclerViewItem? = null

when (keyBackupState) {
KeysBackupState.Unknown,
KeysBackupState.CheckingBackUpOnHomeserver -> {
//In this cases recycler view is hidden any way
//so do nothing
}
KeysBackupState.Disabled -> {
itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_not_setup),
style = GenericRecyclerViewItem.STYLE.BIG_TEXT)

isBackupAlreadySetup = false
}
KeysBackupState.WrongBackUpVersion,
KeysBackupState.NotTrusted,
KeysBackupState.Enabling -> {
itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ko),
style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply {
description = keyBackupState.toString()
endIconResourceId = R.drawable.unit_test_ko
}

isBackupAlreadySetup = true
}
KeysBackupState.ReadyToBackUp -> {
itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ok),
style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply {
endIconResourceId = R.drawable.unit_test_ok
description = context.getString(R.string.keys_backup_info_keys_all_backup_up)
}

isBackupAlreadySetup = true
}
KeysBackupState.WillBackUp,
KeysBackupState.BackingUp -> {
itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ok),
style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply {
hasIndeterminateProcess = true

val totalKeys = session.inboundGroupSessionsCount(false)
?: 0
val backedUpKeys = session.inboundGroupSessionsCount(true)
?: 0

val remainingKeysToBackup = totalKeys - backedUpKeys

description = context.resources.getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup)
}

isBackupAlreadySetup = true
}
}

itemSummary?.let {
infos.add(it)
}

if (keyBackupVersionTrust != null) {

if (!keyBackupVersionTrust.usable) {
itemSummary?.description = context.getString(R.string.keys_backup_settings_untrusted_backup)
}

//Add infos
infos.add(GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_version), keyVersionResult?.version
?: ""))
infos.add(GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_algorithm), keyVersionResult?.algorithm
?: ""))

keyBackupVersionTrust.signatures.forEach {
val signatureInfo = GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_signature))
val isDeviceKnown = it.device != null
val isDeviceVerified = it.device?.isVerified ?: false
val isSignatureValid = it.valid
val deviceId: String = it.deviceId ?: ""

if (!isDeviceKnown) {
signatureInfo.description = context.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)
signatureInfo.endIconResourceId = R.drawable.e2e_warning
} else {
if (isSignatureValid) {
if (session.sessionParams.credentials.deviceId == it.deviceId) {
signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_this_device)
signatureInfo.endIconResourceId = R.drawable.e2e_verified
} else {
if (isDeviceVerified) {
signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)
signatureInfo.endIconResourceId = R.drawable.e2e_verified
} else {
signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)
signatureInfo.endIconResourceId = R.drawable.e2e_warning
}
}
} else {
//Invalid signature
signatureInfo.endIconResourceId = R.drawable.e2e_warning
if (isDeviceVerified) {
signatureInfo.description = context.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)
} else {
signatureInfo.description = context.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)
}
}
}

infos.add(signatureInfo)
} //end for each
}

infoList = infos

notifyDataSetChanged()
}

class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
ButterKnife.bind(this, itemView)
}

@BindView(R.id.keys_backup_settings_footer_button1)
lateinit var button1: Button

@BindView(R.id.keys_backup_settings_footer_button2)
lateinit var button2: Button

fun bind() {

}
}

interface AdapterListener {
fun didSelectSetupMessageRecovery()
fun didSelectRestoreMessageRecovery()
fun didSelectDeleteSetupMessageRecovery()
}

}

View File

@ -0,0 +1,233 @@
/*
* 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.keysbackup.settings

import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.errorWithRetryItem
import im.vector.riotredesign.core.epoxy.loadingItem
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.core.ui.list.GenericItem
import im.vector.riotredesign.core.ui.list.genericItem
import java.util.*

class KeysBackupSettingsRecyclerViewController(val stringProvider: StringProvider,
val session: Session) : TypedEpoxyController<KeysBackupSettingViewState>() {

var listener: Listener? = null

override fun buildModels(data: KeysBackupSettingViewState?) {
if (data == null) {
return
}

var isBackupAlreadySetup = false

val keyBackupState = data.keysBackupState
val keyVersionResult = data.keysBackupVersion

when (keyBackupState) {
KeysBackupState.Unknown -> {
errorWithRetryItem {
id("summary")
text(stringProvider.getString(R.string.keys_backup_unable_to_get_keys_backup_data))
listener { listener?.loadKeysBackupState() }
}

// Nothing else to display
return
}
KeysBackupState.CheckingBackUpOnHomeserver -> {
loadingItem {
id("summary")
}
}
KeysBackupState.Disabled -> {
genericItem {
id("summary")
title(stringProvider.getString(R.string.keys_backup_settings_status_not_setup))
style(GenericItem.STYLE.BIG_TEXT)

if (data.keysBackupVersionTrust()?.usable == false) {
description(stringProvider.getString(R.string.keys_backup_settings_untrusted_backup))
}
}

isBackupAlreadySetup = false
}
KeysBackupState.WrongBackUpVersion,
KeysBackupState.NotTrusted,
KeysBackupState.Enabling -> {
genericItem {
id("summary")
title(stringProvider.getString(R.string.keys_backup_settings_status_ko))
style(GenericItem.STYLE.BIG_TEXT)
if (data.keysBackupVersionTrust()?.usable == false) {
description(stringProvider.getString(R.string.keys_backup_settings_untrusted_backup))
} else {
description(keyBackupState.toString())
}
endIconResourceId(R.drawable.unit_test_ko)
}

isBackupAlreadySetup = true
}
KeysBackupState.ReadyToBackUp -> {
genericItem {
id("summary")
title(stringProvider.getString(R.string.keys_backup_settings_status_ok))
style(GenericItem.STYLE.BIG_TEXT)
if (data.keysBackupVersionTrust()?.usable == false) {
description(stringProvider.getString(R.string.keys_backup_settings_untrusted_backup))
} else {
description(stringProvider.getString(R.string.keys_backup_info_keys_all_backup_up))
}
endIconResourceId(R.drawable.unit_test_ok)
}

isBackupAlreadySetup = true
}
KeysBackupState.WillBackUp,
KeysBackupState.BackingUp -> {
genericItem {
id("summary")
title(stringProvider.getString(R.string.keys_backup_settings_status_ok))
style(GenericItem.STYLE.BIG_TEXT)
hasIndeterminateProcess(true)

val totalKeys = session.inboundGroupSessionsCount(false)
val backedUpKeys = session.inboundGroupSessionsCount(true)

val remainingKeysToBackup = totalKeys - backedUpKeys

if (data.keysBackupVersionTrust()?.usable == false) {
description(stringProvider.getString(R.string.keys_backup_settings_untrusted_backup))
} else {
description(stringProvider.getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup))
}
}

isBackupAlreadySetup = true
}
}

if (isBackupAlreadySetup) {
//Add infos
genericItem {
id("version")
title(stringProvider.getString(R.string.keys_backup_info_title_version))
description(keyVersionResult?.version ?: "")
}

genericItem {
id("algorithm")
title(stringProvider.getString(R.string.keys_backup_info_title_algorithm))
description(keyVersionResult?.algorithm ?: "")
}

buildKeysBackupTrust(data.keysBackupVersionTrust)
}

// Footer
keysBackupSettingFooterItem {
id("footer")

if (isBackupAlreadySetup) {
textButton1(stringProvider.getString(R.string.keys_backup_settings_restore_backup_button))
clickOnButton1(View.OnClickListener { listener?.didSelectRestoreMessageRecovery() })

textButton2(stringProvider.getString(R.string.keys_backup_settings_delete_backup_button))
clickOnButton2(View.OnClickListener { listener?.didSelectDeleteSetupMessageRecovery() })
} else {
textButton1(stringProvider.getString(R.string.keys_backup_setup))
clickOnButton1(View.OnClickListener { listener?.didSelectSetupMessageRecovery() })
}
}
}

private fun buildKeysBackupTrust(keysVersionTrust: Async<KeysBackupVersionTrust>) {
when (keysVersionTrust) {
is Uninitialized -> Unit
is Loading -> {
loadingItem {
id("trust")
}
}
is Success -> {
keysVersionTrust().signatures.forEach {
genericItem {
id(UUID.randomUUID().toString())
title(stringProvider.getString(R.string.keys_backup_info_title_signature))

val isDeviceKnown = it.device != null
val isDeviceVerified = it.device?.isVerified ?: false
val isSignatureValid = it.valid
val deviceId: String = it.deviceId ?: ""

if (!isDeviceKnown) {
description(stringProvider.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId))
endIconResourceId(R.drawable.e2e_warning)
} else {
if (isSignatureValid) {
if (session.sessionParams.credentials.deviceId == it.deviceId) {
description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_this_device))
endIconResourceId(R.drawable.e2e_verified)
} else {
if (isDeviceVerified) {
description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId))
endIconResourceId(R.drawable.e2e_verified)
} else {
description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId))
endIconResourceId(R.drawable.e2e_warning)
}
}
} else {
//Invalid signature
endIconResourceId(R.drawable.e2e_warning)
if (isDeviceVerified) {
description(stringProvider.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId))
} else {
description(stringProvider.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId))
}
}
}
}
} //end for each
}
is Fail -> {
errorWithRetryItem {
id("trust")
text(stringProvider.getString(R.string.keys_backup_unable_to_get_trust_info))
listener { listener?.loadTrustData() }
}
}
}
}

interface Listener {
fun didSelectSetupMessageRecovery()
fun didSelectRestoreMessageRecovery()
fun didSelectDeleteSetupMessageRecovery()
fun loadTrustData()
fun loadKeysBackupState()
}
}

View File

@ -16,76 +16,125 @@
package im.vector.riotredesign.features.crypto.keysbackup.settings

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.core.platform.VectorViewModel
import org.koin.android.ext.android.get


class KeysBackupSettingsViewModel : ViewModel(),
class KeysBackupSettingsViewModel(initialState: KeysBackupSettingViewState,
session: Session) : VectorViewModel<KeysBackupSettingViewState>(initialState),
KeysBackupService.KeysBackupStateListener {

var session: Session? = null
companion object : MvRxViewModelFactory<KeysBackupSettingsViewModel, KeysBackupSettingViewState> {

var keyVersionTrust: MutableLiveData<KeysBackupVersionTrust> = MutableLiveData()
var keyBackupState: MutableLiveData<KeysBackupState> = MutableLiveData()
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: KeysBackupSettingViewState): KeysBackupSettingsViewModel? {
val session = viewModelContext.activity.get<Session>()

private var _apiResultError: MutableLiveData<LiveEvent<String>> = MutableLiveData()
val apiResultError: LiveData<LiveEvent<String>>
get() = _apiResultError
val firstState = state.copy(
keysBackupState = session.getKeysBackupService().state,
keysBackupVersion = session.getKeysBackupService().keysBackupVersion
)

var loadingEvent: MutableLiveData<WaitingViewData> = MutableLiveData()

fun initSession(session: Session) {
keyBackupState.value = session.getKeysBackupService().state
if (this.session == null) {
this.session = session
session.getKeysBackupService().addListener(this)
return KeysBackupSettingsViewModel(firstState, session)
}
}

fun getKeysBackupTrust(versionResult: KeysVersionResult) {
val keysBackup = session?.getKeysBackupService()
keysBackup?.getKeysBackupTrust(versionResult, object : MatrixCallback<KeysBackupVersionTrust> {
private var keysBackupService: KeysBackupService = session.getKeysBackupService()

init {
keysBackupService.addListener(this)

getKeysBackupTrust()
}

fun init() {
keysBackupService.forceUsingLastVersion(object : MatrixCallback<Boolean> {})
}

fun getKeysBackupTrust() = withState { state ->
val versionResult = keysBackupService.keysBackupVersion

if (state.keysBackupVersionTrust is Uninitialized && versionResult != null) {
setState {
copy(
keysBackupVersionTrust = Loading(),
deleteBackupRequest = Uninitialized
)
}

keysBackupService
.getKeysBackupTrust(versionResult, object : MatrixCallback<KeysBackupVersionTrust> {
override fun onSuccess(data: KeysBackupVersionTrust) {
keyVersionTrust.value = data
setState {
copy(
keysBackupVersionTrust = Success(data)
)
}
}

override fun onFailure(failure: Throwable) {
setState {
copy(
keysBackupVersionTrust = Fail(failure)
)
}
}
})
}
}

override fun onCleared() {
super.onCleared()
session?.getKeysBackupService()?.removeListener(this)
keysBackupService.removeListener(this)
}

override fun onStateChange(newState: KeysBackupState) {
keyBackupState.value = newState
setState {
copy(
keysBackupState = newState,
keysBackupVersion = keysBackupService.keysBackupVersion
)
}

getKeysBackupTrust()
}

fun deleteCurrentBackup(context: Context) {
session?.getKeysBackupService()?.run {
loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_settings_deleting_backup))
if (currentBackupVersion != null) {
deleteBackup(currentBackupVersion!!, object : MatrixCallback<Unit> {
override fun onSuccess(info: Unit) {
//mmmm if state is stil unknown/checking..
loadingEvent.value = null
val keysBackupService = keysBackupService

if (keysBackupService.currentBackupVersion != null) {
setState {
copy(
deleteBackupRequest = Loading()
)
}

keysBackupService.deleteBackup(keysBackupService.currentBackupVersion!!, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
setState {
copy(
keysBackupVersion = null,
keysBackupVersionTrust = Uninitialized,
// We do not care about the success data
deleteBackupRequest = Uninitialized
)
}
}

override fun onFailure(failure: Throwable) {
loadingEvent.value = null
_apiResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, failure.localizedMessage))
setState {
copy(
deleteBackupRequest = Fail(failure)
)
}
}
})
}
}
}
}

View File

@ -117,6 +117,10 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
.chunked(4)
.joinToString(" ")
}

it.setOnClickListener {
copyToClipboard(activity!!, recoveryKey)
}
}
}

@ -151,7 +155,7 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
fun exportRecoveryKeyToFile(it: String) {
val stream = ByteArrayInputStream(it.toByteArray())

TODO()
vectorBaseActivity.notImplemented("export recovery key to file")
/*
val url = viewModel.session.mediaCache.saveMedia(stream, "recovery-key" + System.currentTimeMillis() + ".txt", "text/plain")
stream.close()

View File

@ -46,8 +46,7 @@

<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_height="wrap_content">

<com.google.android.material.textfield.TextInputLayout
style="@style/VectorTextInputLayout"

View File

@ -66,6 +66,7 @@
android:layout_height="@dimen/layout_touch_size"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:layout_marginTop="8dp"
android:src="@drawable/ic_eye_black"
android:tint="?colorAccent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<com.airbnb.epoxy.EpoxyRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/keys_backup_settings_recycler_view"
android:id="@+id/keysBackupSettingsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"

View File

@ -51,6 +51,7 @@
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_margin="16dp"
android:maxWidth="@dimen/button_max_width"
android:minWidth="200dp"
android:padding="8dp"
android:text="@string/keys_backup_setup"

View File

@ -72,6 +72,7 @@
android:id="@+id/keys_backup_view_show_password"
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"
@ -145,6 +146,8 @@
style="@style/VectorButtonStyleFlat"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="@dimen/layout_vertical_margin"
android:maxWidth="@dimen/button_max_width"
android:text="@string/keys_backup_setup_step2_skip_button_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -46,7 +46,7 @@
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:text="@string/keys_backup_setup_step3_text_line1"
android:textAlignment="center"
android:textColor="?riotx_text_secondary"
android:textColor="?riotx_text_primary"
android:textSize="15sp" />

<TextView
@ -80,6 +80,7 @@
android:id="@+id/keys_backup_setup_step3_copy_button"
style="@style/VectorButtonStyleFlat"
android:layout_gravity="center"
android:maxWidth="@dimen/button_max_width"
android:text="@string/keys_backup_setup_step3_copy_button_title" />

<com.google.android.material.button.MaterialButton

View File

@ -2,6 +2,7 @@
<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"
android:id="@+id/item_generic_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"
@ -9,22 +10,26 @@

<TextView
android:id="@+id/item_generic_title_text"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_marginBottom="4dp"
android:textColor="?riotx_text_primary"
android:textStyle="bold"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/item_generic_description_text"
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Item Title"
tools:textSize="14sp" />
tools:textSize="14sp"
tools:visibility="visible" />

<TextView
android:id="@+id/item_generic_description_text"
@ -32,10 +37,10 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_marginBottom="4dp"
android:textColor="?riotx_text_secondary"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/item_generic_action_button"
@ -49,6 +54,7 @@
android:id="@+id/item_generic_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="item_generic_accessory_image,item_generic_progress_bar"
tools:ignore="MissingConstraints" />

@ -76,17 +82,20 @@
app:layout_constraintTop_toTopOf="@+id/item_generic_title_text"
tools:visibility="visible" />

<!-- Set a maw width because the text can be long -->
<com.google.android.material.button.MaterialButton
android:id="@+id/item_generic_action_button"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:padding="8dp"
android:maxWidth="@dimen/button_max_width"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/item_generic_description_text"
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
app:layout_constraintTop_toBottomOf="@+id/item_generic_description_text"
tools:text="@string/settings_troubleshoot_test_device_settings_quickfix"
tools:visibility="visible" />

View File

@ -5,15 +5,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="vertical">
android:orientation="vertical"
android:padding="16dp">

<com.google.android.material.button.MaterialButton
android:id="@+id/keys_backup_settings_footer_button1"
style="@style/VectorButtonStyle"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:minWidth="200dp"
android:layout_width="@dimen/button_max_width"
android:visibility="gone"
tools:text="@string/keys_backup_settings_restore_backup_button"
tools:visibility="visible" />
@ -21,10 +19,8 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/keys_backup_settings_footer_button2"
style="@style/VectorButtonStyle"
android:layout_width="@dimen/button_max_width"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:minWidth="200dp"
android:visibility="gone"
app:backgroundTint="@color/vector_warning_color"
tools:text="@string/keys_backup_settings_delete_backup_button"

View File

@ -30,4 +30,7 @@

<dimen name="item_form_min_height">76dp</dimen>

<!-- Max width for some buttons -->
<dimen name="button_max_width">280dp</dimen>

</resources>

View File

@ -51,4 +51,7 @@
<string name="create_room_directory_title">"Room Directory"</string>
<string name="create_room_directory_description">"Publish this room in the room directory"</string>

<string name="keys_backup_unable_to_get_trust_info">"An error occurred getting trust info"</string>
<string name="keys_backup_unable_to_get_keys_backup_data">"An error occurred getting keys backup data"</string>

</resources>