diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 52c8b840e3..b84c6219bc 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -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.roompreview.RoomPreviewNoPreviewFragment 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.members.RoomMemberListFragment import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment @@ -319,4 +321,14 @@ interface FragmentModule { @IntoMap @FragmentKey(QrCodeScannerFragment::class) 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 } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 15f541f72d..2f2ce130a8 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -226,11 +226,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { mainActivityStarted = true MainActivity.restartApp(this, - MainActivityArgs( - clearCredentials = !globalError.softLogout, - isUserLoggedOut = true, - isSoftLogout = globalError.softLogout - ) + MainActivityArgs( + clearCredentials = !globalError.softLogout, + isUserLoggedOut = true, + isSoftLogout = globalError.softLogout + ) ) } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericFooterItem.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericFooterItem.kt index a2ed4ba851..3f1c134d5d 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericFooterItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericFooterItem.kt @@ -17,12 +17,14 @@ package im.vector.riotx.core.ui.list import android.view.Gravity import android.widget.TextView +import androidx.annotation.ColorInt import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.themes.ThemeUtils /** * A generic list item. @@ -45,13 +47,23 @@ abstract class GenericFooterItem : VectorEpoxyModel() @EpoxyAttribute var centered: Boolean = true + @EpoxyAttribute + @ColorInt + var textColor: Int? = null + override fun bind(holder: Holder) { holder.text.setTextOrHide(text) when (style) { GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f 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 { itemClickAction?.perform?.run() diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemWithValue.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemWithValue.kt index 2f1e77e965..8e338c4bc4 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemWithValue.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemWithValue.kt @@ -40,7 +40,7 @@ import im.vector.riotx.features.themes.ThemeUtils abstract class GenericItemWithValue : VectorEpoxyModel() { @EpoxyAttribute - var title: String? = null + var title: CharSequence? = null @EpoxyAttribute var value: CharSequence? = null diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt index b07fa5a596..86f13cfb73 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -180,8 +180,8 @@ class RoomMemberProfileFragment @Inject constructor( DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST") } - override fun onShowDeviceListNoCrossSigning() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun onShowDeviceListNoCrossSigning() = withState(viewModel) { + DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST") } override fun onJumpToReadReceiptClicked() { diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheet.kt index c4a1e102fc..f3d26911f4 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheet.kt @@ -16,65 +16,98 @@ */ package im.vector.riotx.features.roommemberprofile.devices +import android.content.DialogInterface import android.os.Bundle -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import butterknife.BindView +import android.view.KeyEvent +import androidx.fragment.app.Fragment import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.extensions.cleanup -import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.riotx.core.utils.DimensionConverter -import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.* +import im.vector.riotx.features.crypto.verification.VerificationAction +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet 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) @Inject lateinit var viewModelFactory: DeviceListBottomSheetViewModel.Factory - @Inject lateinit var dimensionConverter: DimensionConverter - - @BindView(R.id.bottomSheetRecyclerView) - lateinit var recyclerView: RecyclerView - override fun injectWith(injector: ScreenComponent) { injector.inject(this) } - @Inject lateinit var epoxyController: DeviceListEpoxyController - 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 - bottomSheetTitle.isVisible = false + viewModel.requestLiveData.observeEvent(this) { async -> + when (async) { + is Success -> { + when (val action = async.invoke()) { + is VerificationAction.StartSASVerification -> { + VerificationBottomSheet.withArgs( + roomId = null, + otherUserId = action.userID, + transactionId = action.pendingRequestTransactionId + ).show(requireActivity().supportFragmentManager, "REQPOP") + } + } + } + } + } } - override fun onDestroyView() { - recyclerView.cleanup() - super.onDestroyView() + private val onKeyListener = DialogInterface.OnKeyListener { _, keyCode, _ -> + withState(viewModel) { + 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) { - epoxyController.setData(it) super.invalidate() + if (it.selectedDevice == null) { + showFragment(DeviceListFragment::class, arguments ?: Bundle()) + } else { + showFragment(DeviceTrustInfoActionFragment::class, arguments ?: Bundle()) + } } - override fun onDeviceSelected(device: CryptoDeviceInfo) { - // TODO + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + childFragmentManager.commitTransaction { + replace(R.id.bottomSheetFragmentContainer, + fragmentClass.java, + bundle, + fragmentClass.simpleName + ) + } + } } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt index 59abbebeab..6970298513 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt @@ -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 +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject 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.rx.rx +import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.VectorViewModel 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( - val cryptoDevices: Async> = Loading() + val userItem: MatrixItem? = null, + val isMine: Boolean = false, + val memberCrossSigningKey: MXCrossSigningInfo? = null, + val cryptoDevices: Async> = Loading(), + val selectedDevice: CryptoDeviceInfo? = null ) : MvRxState 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 session: Session) : VectorViewModel(initialState) { + // Can be used for several actions, for a one shot result + private val _requestLiveData = MutableLiveData>>() + val requestLiveData: LiveData>> + get() = _requestLiveData + @AssistedInject.Factory interface Factory { fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel } init { + session.rx().liveUserCryptoDevices(userId) .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) {} @@ -45,5 +115,16 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva val userId = viewModelContext.args() return fragment.viewModelFactory.create(state, userId) } + + override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? { + val userId = viewModelContext.args() + 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) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListEpoxyController.kt index e00d5afda9..525927c8f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListEpoxyController.kt @@ -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 +import android.view.View import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail 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.resources.ColorProvider 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.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.settings.VectorPreferences +import me.gujun.android.span.span import javax.inject.Inject class DeviceListEpoxyController @Inject constructor(private val stringProvider: StringProvider, private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, private val vectorPreferences: VectorPreferences) : TypedEpoxyController() { @@ -46,7 +65,10 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: } is Success -> { - val deviceList = data.cryptoDevices.invoke() + val deviceList = data.cryptoDevices.invoke().sortedByDescending { + it.isVerified + } + // Build top header val allGreen = deviceList.fold(true, { prev, device -> @@ -57,10 +79,19 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: id("title") style(GenericItem.STYLE.BIG_TEXT) 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)) } + if (vectorPreferences.developerMode()) { + // Display the cross signing keys + addDebugInfo(data) + } + genericItem { id("sessions") style(GenericItem.STYLE.BIG_TEXT) @@ -79,18 +110,22 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: genericItemWithValue { id(device.deviceId) titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning) - title( - buildString { - append(device.displayName() ?: device.deviceId) - apply { - if (vectorPreferences.developerMode()) { - append("\n") - append(device.deviceId) - } + apply { + if (vectorPreferences.developerMode()) { + val seq = span { + +(device.displayName() ?: device.deviceId) + +"\n" + span { + text = "(${device.deviceId})" + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = dimensionConverter.spToPx(14) } } - - ) + title(seq) + } else { + title(device.displayName() ?: device.deviceId) + } + } value( stringProvider.getString( 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 ) ) + 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) + } + } + ) + } + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt new file mode 100644 index 0000000000..c598d051f7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt new file mode 100644 index 0000000000..7ecff5409b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt @@ -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) + } +} + + diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt new file mode 100644 index 0000000000..907c019f39 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -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() { + + 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) + } + } + } + } + } +} diff --git a/vector/src/main/res/layout/bottom_sheet_with_fragments.xml b/vector/src/main/res/layout/bottom_sheet_with_fragments.xml new file mode 100644 index 0000000000..e013b2a7b1 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_with_fragments.xml @@ -0,0 +1,7 @@ + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 91a57e7a5a..900ac3ea99 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -34,6 +34,7 @@ Verify this device + Manually verify You @@ -129,4 +130,8 @@ Trusted Not Trusted + This device is trusted for secure messaging because %1$s (%2$s) verified it: + %1$s (%2$s) signed in using a new device: + Until this user trusts this device, messages sent to and from it are labelled with warnings. Alternatively, you can manually verify it. +