Profile detailed device info + verify manually

This commit is contained in:
Valere 2020-01-27 17:55:00 +01:00
parent 08ae0b485a
commit 6cece03998
13 changed files with 545 additions and 54 deletions

View File

@ -57,6 +57,8 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment
import im.vector.riotx.features.roommemberprofile.devices.DeviceListFragment
import im.vector.riotx.features.roommemberprofile.devices.DeviceTrustInfoActionFragment
import im.vector.riotx.features.roomprofile.RoomProfileFragment import im.vector.riotx.features.roomprofile.RoomProfileFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
@ -319,4 +321,14 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(QrCodeScannerFragment::class) @FragmentKey(QrCodeScannerFragment::class)
fun bindQrCodeScannerFragment(fragment: QrCodeScannerFragment): Fragment fun bindQrCodeScannerFragment(fragment: QrCodeScannerFragment): Fragment
@Binds
@IntoMap
@FragmentKey(DeviceListFragment::class)
fun bindDeviceListFragment(fragment: DeviceListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(DeviceTrustInfoActionFragment::class)
fun bindDeviceTrustInfoActionFragment(fragment: DeviceTrustInfoActionFragment): Fragment
} }

View File

@ -226,11 +226,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
mainActivityStarted = true mainActivityStarted = true
MainActivity.restartApp(this, MainActivity.restartApp(this,
MainActivityArgs( MainActivityArgs(
clearCredentials = !globalError.softLogout, clearCredentials = !globalError.softLogout,
isUserLoggedOut = true, isUserLoggedOut = true,
isSoftLogout = globalError.softLogout isSoftLogout = globalError.softLogout
) )
) )
} }

View File

@ -17,12 +17,14 @@ package im.vector.riotx.core.ui.list
import android.view.Gravity import android.view.Gravity
import android.widget.TextView import android.widget.TextView
import androidx.annotation.ColorInt
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.themes.ThemeUtils
/** /**
* A generic list item. * A generic list item.
@ -45,13 +47,23 @@ abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>()
@EpoxyAttribute @EpoxyAttribute
var centered: Boolean = true var centered: Boolean = true
@EpoxyAttribute
@ColorInt
var textColor: Int? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.text.setTextOrHide(text) holder.text.setTextOrHide(text)
when (style) { when (style) {
GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f
GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f
} }
holder.text.gravity = if(centered) Gravity.CENTER_HORIZONTAL else Gravity.START holder.text.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START
if (textColor != null) {
holder.text.setTextColor(textColor!!)
} else {
holder.text.setTextColor(ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary))
}
holder.view.setOnClickListener { holder.view.setOnClickListener {
itemClickAction?.perform?.run() itemClickAction?.perform?.run()

View File

@ -40,7 +40,7 @@ import im.vector.riotx.features.themes.ThemeUtils
abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Holder>() { abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var title: String? = null var title: CharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var value: CharSequence? = null var value: CharSequence? = null

View File

@ -180,8 +180,8 @@ class RoomMemberProfileFragment @Inject constructor(
DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST") DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST")
} }
override fun onShowDeviceListNoCrossSigning() { override fun onShowDeviceListNoCrossSigning() = withState(viewModel) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST")
} }
override fun onJumpToReadReceiptClicked() { override fun onJumpToReadReceiptClicked() {

View File

@ -16,65 +16,98 @@
*/ */
package im.vector.riotx.features.roommemberprofile.devices package im.vector.riotx.features.roommemberprofile.devices
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import androidx.core.view.isVisible import android.view.KeyEvent
import androidx.recyclerview.widget.RecyclerView import androidx.fragment.app.Fragment
import butterknife.BindView
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.crypto.verification.VerificationAction
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.* import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KClass
class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment(), DeviceListEpoxyController.InteractionListener { class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun getLayoutResId() = R.layout.bottom_sheet_generic_list_with_title override fun getLayoutResId() = R.layout.bottom_sheet_with_fragments
private val viewModel: DeviceListBottomSheetViewModel by fragmentViewModel(DeviceListBottomSheetViewModel::class) private val viewModel: DeviceListBottomSheetViewModel by fragmentViewModel(DeviceListBottomSheetViewModel::class)
@Inject lateinit var viewModelFactory: DeviceListBottomSheetViewModel.Factory @Inject lateinit var viewModelFactory: DeviceListBottomSheetViewModel.Factory
@Inject lateinit var dimensionConverter: DimensionConverter
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
} }
@Inject lateinit var epoxyController: DeviceListEpoxyController
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
recyclerView.setPadding(0, dimensionConverter.dpToPx(16 ),0, dimensionConverter.dpToPx(16 )) viewModel.requestLiveData.observeEvent(this) { async ->
recyclerView.configureWith( when (async) {
epoxyController, is Success -> {
showDivider = false, when (val action = async.invoke()) {
hasFixedSize = false) is VerificationAction.StartSASVerification -> {
epoxyController.interactionListener = this VerificationBottomSheet.withArgs(
bottomSheetTitle.isVisible = false roomId = null,
otherUserId = action.userID,
transactionId = action.pendingRequestTransactionId
).show(requireActivity().supportFragmentManager, "REQPOP")
}
}
}
}
}
} }
override fun onDestroyView() { private val onKeyListener = DialogInterface.OnKeyListener { _, keyCode, _ ->
recyclerView.cleanup() withState(viewModel) {
super.onDestroyView() if (keyCode == KeyEvent.KEYCODE_BACK) {
if (it.selectedDevice != null) {
viewModel.selectDevice(null)
return@withState true
} else {
return@withState false
}
}
return@withState false
}
}
override fun onResume() {
super.onResume()
dialog?.setOnKeyListener(onKeyListener)
}
override fun onPause() {
super.onPause()
dialog?.setOnKeyListener(null)
} }
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
super.invalidate() super.invalidate()
if (it.selectedDevice == null) {
showFragment(DeviceListFragment::class, arguments ?: Bundle())
} else {
showFragment(DeviceTrustInfoActionFragment::class, arguments ?: Bundle())
}
} }
override fun onDeviceSelected(device: CryptoDeviceInfo) { private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
// TODO if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
childFragmentManager.commitTransaction {
replace(R.id.bottomSheetFragmentContainer,
fragmentClass.java,
bundle,
fragmentClass.simpleName
)
}
}
} }
companion object { companion object {

View File

@ -1,22 +1,52 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.riotx.features.roommemberprofile.devices package im.vector.riotx.features.roommemberprofile.devices
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
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 im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.crypto.verification.VerificationAction
data class DeviceListViewState( data class DeviceListViewState(
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Loading() val userItem: MatrixItem? = null,
val isMine: Boolean = false,
val memberCrossSigningKey: MXCrossSigningInfo? = null,
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Loading(),
val selectedDevice: CryptoDeviceInfo? = null
) : MvRxState ) : MvRxState
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState, class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
@ -24,16 +54,56 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val session: Session) : VectorViewModel<DeviceListViewState, EmptyAction>(initialState) { private val session: Session) : VectorViewModel<DeviceListViewState, EmptyAction>(initialState) {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
get() = _requestLiveData
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel
} }
init { init {
session.rx().liveUserCryptoDevices(userId) session.rx().liveUserCryptoDevices(userId)
.execute { .execute {
copy(cryptoDevices = it) copy(cryptoDevices = it).also {
refreshSelectedId()
}
} }
session.rx().liveCrossSigningInfo(userId)
.map {
it.getOrNull()
}
.execute {
copy(memberCrossSigningKey = it.invoke())
}
}
private fun refreshSelectedId() = withState { state ->
if (state.selectedDevice != null) {
state.cryptoDevices.invoke()?.firstOrNull { state.selectedDevice.deviceId == it.deviceId }?.let {
setState {
copy(
selectedDevice = it
)
}
}
}
}
fun selectDevice(device: CryptoDeviceInfo?) {
setState {
copy(selectedDevice = device)
}
}
fun manuallyVerify(device: CryptoDeviceInfo) {
session.getSasVerificationService().beginKeyVerification(VerificationMethod.SAS, deviceID = device.deviceId, userId = userId)?.let { txID ->
_requestLiveData.postValue(LiveEvent(Success(VerificationAction.StartSASVerification(userId, txID))))
}
} }
override fun handle(action: EmptyAction) {} override fun handle(action: EmptyAction) {}
@ -45,5 +115,16 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
val userId = viewModelContext.args<String>() val userId = viewModelContext.args<String>()
return fragment.viewModelFactory.create(state, userId) return fragment.viewModelFactory.create(state, userId)
} }
override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? {
val userId = viewModelContext.args<String>()
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return session.getUser(userId)?.toMatrixItem()?.let {
DeviceListViewState(
userItem = it,
isMine = userId == session.myUserId
)
} ?: return super.initialState(viewModelContext)
}
} }
} }

View File

@ -1,5 +1,22 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.riotx.features.roommemberprofile.devices package im.vector.riotx.features.roommemberprofile.devices
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController 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
@ -11,16 +28,18 @@ import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.ui.list.GenericItem import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemWithValue import im.vector.riotx.core.ui.list.genericItemWithValue
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.span
import javax.inject.Inject import javax.inject.Inject
class DeviceListEpoxyController @Inject constructor(private val stringProvider: StringProvider, class DeviceListEpoxyController @Inject constructor(private val stringProvider: StringProvider,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val vectorPreferences: VectorPreferences) private val vectorPreferences: VectorPreferences)
: TypedEpoxyController<DeviceListViewState>() { : TypedEpoxyController<DeviceListViewState>() {
@ -46,7 +65,10 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
} }
is Success -> { is Success -> {
val deviceList = data.cryptoDevices.invoke() val deviceList = data.cryptoDevices.invoke().sortedByDescending {
it.isVerified
}
// Build top header // Build top header
val allGreen = deviceList.fold(true, { prev, device -> val allGreen = deviceList.fold(true, { prev, device ->
@ -57,10 +79,19 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
id("title") id("title")
style(GenericItem.STYLE.BIG_TEXT) style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(if (allGreen) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) titleIconResourceId(if (allGreen) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
title(stringProvider.getString(R.string.verification_profile_verified)) title(
stringProvider.getString(
if (allGreen) R.string.verification_profile_verified else R.string.verification_profile_warning
)
)
description(stringProvider.getString(R.string.verification_conclusion_ok_notice)) description(stringProvider.getString(R.string.verification_conclusion_ok_notice))
} }
if (vectorPreferences.developerMode()) {
// Display the cross signing keys
addDebugInfo(data)
}
genericItem { genericItem {
id("sessions") id("sessions")
style(GenericItem.STYLE.BIG_TEXT) style(GenericItem.STYLE.BIG_TEXT)
@ -79,18 +110,22 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
genericItemWithValue { genericItemWithValue {
id(device.deviceId) id(device.deviceId)
titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
title( apply {
buildString { if (vectorPreferences.developerMode()) {
append(device.displayName() ?: device.deviceId) val seq = span {
apply { +(device.displayName() ?: device.deviceId)
if (vectorPreferences.developerMode()) { +"\n"
append("\n") span {
append(device.deviceId) text = "(${device.deviceId})"
} textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(14)
} }
} }
title(seq)
) } else {
title(device.displayName() ?: device.deviceId)
}
}
value( value(
stringProvider.getString( stringProvider.getString(
if (device.isVerified) R.string.trusted else R.string.not_trusted if (device.isVerified) R.string.trusted else R.string.not_trusted
@ -101,6 +136,9 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
if (device.isVerified) R.color.riotx_positive_accent else R.color.riotx_destructive_accent if (device.isVerified) R.color.riotx_positive_accent else R.color.riotx_destructive_accent
) )
) )
itemClickAction(View.OnClickListener {
interactionListener?.onDeviceSelected(device)
})
} }
} }
} }
@ -116,4 +154,55 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
} }
} }
} }
private fun addDebugInfo(data: DeviceListViewState) {
data.memberCrossSigningKey?.masterKey()?.let {
genericItemWithValue {
id("msk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"Master Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
data.memberCrossSigningKey?.userKey()?.let {
genericItemWithValue {
id("usk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"User Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
data.memberCrossSigningKey?.selfSigningKey()?.let {
genericItemWithValue {
id("ssk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"Self Signed Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
}
} }

View File

@ -0,0 +1,67 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.riotx.features.roommemberprofile.devices
import android.os.Bundle
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter
import javax.inject.Inject
class DeviceListFragment @Inject constructor(
val dimensionConverter: DimensionConverter,
val epoxyController: DeviceListEpoxyController
) : VectorBaseFragment(), DeviceListEpoxyController.InteractionListener {
override fun getLayoutResId() = R.layout.bottom_sheet_generic_list
private val viewModel: DeviceListBottomSheetViewModel by parentFragmentViewModel(DeviceListBottomSheetViewModel::class)
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.setPadding(0, dimensionConverter.dpToPx(16), 0, dimensionConverter.dpToPx(16))
recyclerView.configureWith(
epoxyController,
showDivider = false,
hasFixedSize = false)
epoxyController.interactionListener = this
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
super.invalidate()
}
override fun onDeviceSelected(device: CryptoDeviceInfo) {
viewModel.selectDevice(device)
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.riotx.features.roommemberprofile.devices
import android.os.Bundle
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter
import javax.inject.Inject
class DeviceTrustInfoActionFragment @Inject constructor(
val dimensionConverter: DimensionConverter,
val epoxyController: DeviceTrustInfoEpoxyController
) : VectorBaseFragment(), DeviceTrustInfoEpoxyController.InteractionListener {
override fun getLayoutResId() = R.layout.bottom_sheet_generic_list
private val viewModel: DeviceListBottomSheetViewModel by parentFragmentViewModel(DeviceListBottomSheetViewModel::class)
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.setPadding(0, dimensionConverter.dpToPx(16), 0, dimensionConverter.dpToPx(16))
recyclerView.configureWith(
epoxyController,
showDivider = false,
hasFixedSize = false)
epoxyController.interactionListener = this
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
super.invalidate()
}
override fun onVerifyManually(device: CryptoDeviceInfo) {
viewModel.manuallyVerify(device)
}
}

View File

@ -0,0 +1,115 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.riotx.features.roommemberprofile.devices
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemWithValue
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.span
import javax.inject.Inject
class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val vectorPreferences: VectorPreferences)
: TypedEpoxyController<DeviceListViewState>() {
interface InteractionListener {
fun onVerifyManually(device: CryptoDeviceInfo)
}
var interactionListener: InteractionListener? = null
override fun buildModels(data: DeviceListViewState?) {
data?.selectedDevice?.let {
val isVerified = it.trustLevel?.isVerified() == true
genericItem {
id("title")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
title(
stringProvider.getString(
if (isVerified) R.string.verification_profile_verified else R.string.verification_profile_warning
)
)
}
genericFooterItem {
id("desc")
centered(false)
textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
apply {
if (isVerified) {
// TODO FORMAT
text(stringProvider.getString(R.string.verification_profile_device_verified_because,
data.userItem?.displayName ?: "",
data.userItem?.id ?: ""))
} else {
// TODO what if mine
text(stringProvider.getString(R.string.verification_profile_device_new_signing,
data.userItem?.displayName ?: "",
data.userItem?.id ?: ""))
}
}
// text(stringProvider.getString(R.string.verification_profile_device_untrust_info))
}
genericItemWithValue {
id(it.deviceId)
titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
title(
span {
+(it.displayName() ?: "")
span {
text = " (${it.deviceId})"
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(14)
}
}
)
}
if (!isVerified) {
genericFooterItem {
id("warn")
centered(false)
textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
text(stringProvider.getString(R.string.verification_profile_device_untrust_info))
}
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.verification_verify_device_manually))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
interactionListener?.onVerifyManually(it)
}
}
}
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bottomSheetFragmentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View File

@ -34,6 +34,7 @@
<string name="verification_verify_device">Verify this device</string> <string name="verification_verify_device">Verify this device</string>
<string name="verification_verify_device_manually">Manually verify</string>
<!-- Sender name of a message when it is send by you, e.g. You: Hello!--> <!-- Sender name of a message when it is send by you, e.g. You: Hello!-->
<string name="you">You</string> <string name="you">You</string>
@ -129,4 +130,8 @@
<string name="trusted">Trusted</string> <string name="trusted">Trusted</string>
<string name="not_trusted">Not Trusted</string> <string name="not_trusted">Not Trusted</string>
<string name="verification_profile_device_verified_because">This device is trusted for secure messaging because %1$s (%2$s) verified it:</string>
<string name="verification_profile_device_new_signing">%1$s (%2$s) signed in using a new device:</string>
<string name="verification_profile_device_untrust_info">Until this user trusts this device, messages sent to and from it are labelled with warnings. Alternatively, you can manually verify it.</string>
</resources> </resources>