Merge pull request #7189 from vector-im/feature/mna/device-manager-rename-session

[Device management] Rename a session (PSG-747)
This commit is contained in:
Maxime NATUREL 2022-09-26 10:35:15 +02:00 committed by GitHub
commit 223149805b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1026 additions and 10 deletions

1
changelog.d/7158.wip Normal file
View file

@ -0,0 +1 @@
[Device management] Rename a session

View file

@ -3295,6 +3295,10 @@
<string name="device_manager_session_details_session_id">Session ID</string>
<string name="device_manager_session_details_session_last_activity">Last activity</string>
<string name="device_manager_session_details_device_ip_address">IP address</string>
<string name="device_manager_session_rename">Rename session</string>
<string name="device_manager_session_rename_edit_hint">Session name</string>
<string name="device_manager_session_rename_description">Custom session names can help you recognize your devices more easily.</string>
<string name="device_manager_session_rename_warning">Please be aware that session names are also visible to people you communicate with.</string>
<!-- Note to translators: %s will be replaces with selected space name -->
<string name="home_empty_space_no_rooms_title">%s\nis looking a little empty.</string>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SessionWarningInfoView">
<attr name="sessionsWarningInfoDescription" format="string" />
<attr name="sessionsWarningInfoHasLearnMore" format="boolean" />
</declare-styleable>
</resources>

View file

@ -325,6 +325,7 @@
<activity android:name=".features.settings.devices.v2.overview.SessionOverviewActivity" />
<activity android:name=".features.settings.devices.v2.othersessions.OtherSessionsActivity" />
<activity android:name=".features.settings.devices.v2.details.SessionDetailsActivity" />
<activity android:name=".features.settings.devices.v2.rename.RenameSessionActivity" />
<!-- Services -->

View file

@ -91,6 +91,7 @@ import im.vector.app.features.settings.devices.DevicesViewModel
import im.vector.app.features.settings.devices.v2.details.SessionDetailsViewModel
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewModel
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel
import im.vector.app.features.settings.devices.v2.rename.RenameSessionViewModel
import im.vector.app.features.settings.devtools.AccountDataViewModel
import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel
import im.vector.app.features.settings.devtools.KeyRequestListViewModel
@ -653,4 +654,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(SessionDetailsViewModel::class)
fun sessionDetailsViewModelFactory(factory: SessionDetailsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(RenameSessionViewModel::class)
fun renameSessionViewModelFactory(factory: RenameSessionViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.use
import im.vector.app.R
import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.databinding.ViewSessionWarningInfoBinding
class SessionWarningInfoView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding = ViewSessionWarningInfoBinding.inflate(
LayoutInflater.from(context),
this
)
var onLearnMoreClickListener: (() -> Unit)? = null
init {
context.obtainStyledAttributes(
attrs,
R.styleable.SessionWarningInfoView,
0,
0
).use {
setDescription(it)
}
}
private fun setDescription(typedArray: TypedArray) {
val description = typedArray.getString(R.styleable.SessionWarningInfoView_sessionsWarningInfoDescription)
val hasLearnMore = typedArray.getBoolean(R.styleable.SessionWarningInfoView_sessionsWarningInfoHasLearnMore, false)
if (hasLearnMore) {
val learnMore = context.getString(R.string.action_learn_more)
val fullDescription = buildString {
append(description)
append(" ")
append(learnMore)
}
binding.sessionWarningInfoDescription.setTextWithColoredPart(
fullText = fullDescription,
coloredPart = learnMore,
underline = false
) {
onLearnMoreClickListener?.invoke()
}
} else {
binding.sessionWarningInfoDescription.text = description
}
}
}

View file

@ -18,11 +18,11 @@ package im.vector.app.features.settings.devices.v2.overview
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
@ -31,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.databinding.FragmentSessionOverviewBinding
@ -43,7 +44,8 @@ import javax.inject.Inject
*/
@AndroidEntryPoint
class SessionOverviewFragment :
VectorBaseFragment<FragmentSessionOverviewBinding>() {
VectorBaseFragment<FragmentSessionOverviewBinding>(),
VectorMenuProvider {
@Inject lateinit var viewNavigator: SessionOverviewViewNavigator
@ -103,6 +105,22 @@ class SessionOverviewFragment :
views.sessionOverviewInfo.onLearnMoreClickListener = null
}
override fun getMenuRes() = R.menu.menu_session_overview
override fun handleMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.sessionOverviewRename -> {
goToRenameSession()
true
}
else -> false
}
}
private fun goToRenameSession() = withState(viewModel) { state ->
viewNavigator.goToRenameSession(requireContext(), state.deviceId)
}
override fun invalidate() = withState(viewModel) { state ->
updateToolbar(state.isCurrentSession)
updateEntryDetails(state.deviceId)
@ -118,7 +136,7 @@ class SessionOverviewFragment :
private fun updateEntryDetails(deviceId: String) {
views.sessionOverviewEntryDetails.setOnClickListener {
viewNavigator.navigateToSessionDetails(requireContext(), deviceId)
viewNavigator.goToSessionDetails(requireContext(), deviceId)
}
}
@ -136,11 +154,7 @@ class SessionOverviewFragment :
)
views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider)
} else {
hideSessionInfo()
views.sessionOverviewInfo.isVisible = false
}
}
private fun hideSessionInfo() {
views.sessionOverviewInfo.isGone = true
}
}

View file

@ -18,11 +18,16 @@ package im.vector.app.features.settings.devices.v2.overview
import android.content.Context
import im.vector.app.features.settings.devices.v2.details.SessionDetailsActivity
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
import javax.inject.Inject
class SessionOverviewViewNavigator @Inject constructor() {
fun navigateToSessionDetails(context: Context, deviceId: String) {
fun goToSessionDetails(context: Context, deviceId: String) {
context.startActivity(SessionDetailsActivity.newIntent(context, deviceId))
}
fun goToRenameSession(context: Context, deviceId: String) {
context.startActivity(RenameSessionActivity.newIntent(context, deviceId))
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import im.vector.app.core.platform.VectorViewModelAction
sealed class RenameSessionAction : VectorViewModelAction {
object InitWithLastEditedName : RenameSessionAction()
object SaveModifications : RenameSessionAction()
data class EditLocally(val editedName: String) : RenameSessionAction()
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
/**
* Display the screen to rename a Session.
*/
@AndroidEntryPoint
class RenameSessionActivity : VectorBaseActivity<ActivitySimpleBinding>() {
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFirstCreation()) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
addFragment(
container = views.simpleFragmentContainer,
fragmentClass = RenameSessionFragment::class.java,
params = intent.getParcelableExtra(Mavericks.KEY_ARG)
)
}
}
companion object {
fun newIntent(context: Context, deviceId: String): Intent {
return Intent(context, RenameSessionActivity::class.java).apply {
putExtra(Mavericks.KEY_ARG, RenameSessionArgs(deviceId))
}
}
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class RenameSessionArgs(
val deviceId: String
) : Parcelable

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doOnTextChanged
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentSessionRenameBinding
import javax.inject.Inject
/**
* Display the screen to rename a Session.
*/
@AndroidEntryPoint
class RenameSessionFragment :
VectorBaseFragment<FragmentSessionRenameBinding>() {
private val viewModel: RenameSessionViewModel by fragmentViewModel()
@Inject lateinit var viewNavigator: RenameSessionViewNavigator
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionRenameBinding {
return FragmentSessionRenameBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeViewEvents()
initToolbar()
initEditText()
initSaveButton()
initWithLastEditedName()
}
private fun initToolbar() {
setupToolbar(views.renameSessionToolbar)
.allowBack(useCross = true)
}
private fun initEditText() {
views.renameSessionEditText.showKeyboard(andRequestFocus = true)
views.renameSessionEditText.doOnTextChanged { text, _, _, _ ->
viewModel.handle(RenameSessionAction.EditLocally(text.toString()))
}
}
private fun initSaveButton() {
views.renameSessionSave.debouncedClicks {
viewModel.handle(RenameSessionAction.SaveModifications)
}
}
private fun initWithLastEditedName() {
viewModel.handle(RenameSessionAction.InitWithLastEditedName)
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is RenameSessionViewEvent.Initialized -> {
views.renameSessionEditText.setText(it.deviceName)
views.renameSessionEditText.setSelection(views.renameSessionEditText.length())
}
is RenameSessionViewEvent.SessionRenamed -> {
viewNavigator.goBack(requireActivity())
}
is RenameSessionViewEvent.Failure -> {
showFailure(it.throwable)
}
}
}
}
override fun invalidate() = withState(viewModel) { state ->
views.renameSessionSave.isEnabled = state.editedDeviceName.isNotEmpty()
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.andThen
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import org.matrix.android.sdk.api.util.awaitCallback
import javax.inject.Inject
class RenameSessionUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val refreshDevicesUseCase: RefreshDevicesUseCase,
) {
suspend fun execute(deviceId: String, newName: String): Result<Unit> {
return renameDevice(deviceId, newName)
.andThen { refreshDevices() }
}
private suspend fun renameDevice(deviceId: String, newName: String) = runCatching {
awaitCallback<Unit> { matrixCallback ->
activeSessionHolder.getActiveSession()
.cryptoService()
.setDeviceName(deviceId, newName, matrixCallback)
}
}
private fun refreshDevices() = runCatching { refreshDevicesUseCase.execute() }
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import im.vector.app.core.platform.VectorViewEvents
sealed class RenameSessionViewEvent : VectorViewEvents {
data class Initialized(val deviceName: String) : RenameSessionViewEvent()
object SessionRenamed : RenameSessionViewEvent()
data class Failure(val throwable: Throwable) : RenameSessionViewEvent()
}

View file

@ -0,0 +1,97 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import androidx.annotation.VisibleForTesting
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.settings.devices.v2.overview.GetDeviceFullInfoUseCase
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
class RenameSessionViewModel @AssistedInject constructor(
@Assisted val initialState: RenameSessionViewState,
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
private val renameSessionUseCase: RenameSessionUseCase,
) : VectorViewModel<RenameSessionViewState, RenameSessionAction, RenameSessionViewEvent>(initialState) {
companion object : MavericksViewModelFactory<RenameSessionViewModel, RenameSessionViewState> by hiltMavericksViewModelFactory()
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<RenameSessionViewModel, RenameSessionViewState> {
override fun create(initialState: RenameSessionViewState): RenameSessionViewModel
}
@VisibleForTesting
var hasRetrievedOriginalDeviceName = false
override fun handle(action: RenameSessionAction) {
when (action) {
is RenameSessionAction.InitWithLastEditedName -> handleInitWithLastEditedName()
is RenameSessionAction.EditLocally -> handleEditLocally(action.editedName)
is RenameSessionAction.SaveModifications -> handleSaveModifications()
}
}
private fun handleInitWithLastEditedName() = withState { state ->
if (hasRetrievedOriginalDeviceName) {
postInitEvent()
} else {
hasRetrievedOriginalDeviceName = true
viewModelScope.launch {
setStateWithOriginalDeviceName(state.deviceId)
postInitEvent()
}
}
}
private suspend fun setStateWithOriginalDeviceName(deviceId: String) {
getDeviceFullInfoUseCase.execute(deviceId)
.firstOrNull()
?.let { deviceFullInfo ->
setState { copy(editedDeviceName = deviceFullInfo.deviceInfo.displayName.orEmpty()) }
}
}
private fun postInitEvent() = withState { state ->
_viewEvents.post(RenameSessionViewEvent.Initialized(state.editedDeviceName))
}
private fun handleEditLocally(editedName: String) {
setState { copy(editedDeviceName = editedName) }
}
private fun handleSaveModifications() = withState { viewState ->
viewModelScope.launch {
val result = renameSessionUseCase.execute(
deviceId = viewState.deviceId,
newName = viewState.editedDeviceName,
)
val viewEvent = if (result.isSuccess) {
RenameSessionViewEvent.SessionRenamed
} else {
RenameSessionViewEvent.Failure(result.exceptionOrNull() ?: Exception())
}
_viewEvents.post(viewEvent)
}
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import androidx.fragment.app.FragmentActivity
import javax.inject.Inject
class RenameSessionViewNavigator @Inject constructor() {
fun goBack(activity: FragmentActivity) {
activity.finish()
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import com.airbnb.mvrx.MavericksState
data class RenameSessionViewState(
val deviceId: String,
val editedDeviceName: String = "",
) : MavericksState {
constructor(args: RenameSessionArgs) : this(
deviceId = args.deviceId
)
}

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/renameSessionToolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:title="@string/device_manager_session_rename">
<Button
android:id="@+id/renameSessionSave"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/action_save" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/renameSessionInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginTop="28dp"
android:hint="@string/device_manager_session_rename_edit_hint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/renameSessionEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/renameSessionDescription"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginTop="8dp"
android:text="@string/device_manager_session_rename_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/renameSessionInputLayout" />
<im.vector.app.features.settings.devices.v2.SessionWarningInfoView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginVertical="@dimen/layout_vertical_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/renameSessionDescription"
app:sessionsWarningInfoDescription="@string/device_manager_session_rename_warning"
app:sessionsWarningInfoHasLearnMore="false" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<View
android:id="@+id/sessionWarningInfoBackground"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/rounded_rect_shape_8"
android:backgroundTint="?colorSurface"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/sessionWarningInfoIcon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="14dp"
android:layout_marginVertical="14dp"
android:layout_marginEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/sessionWarningInfoDescription"
android:importantForAccessibility="no"
android:src="@drawable/ic_info"
app:tint="?vctr_content_secondary" />
<TextView
android:id="@+id/sessionWarningInfoDescription"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="12dp"
android:layout_marginEnd="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/sessionWarningInfoIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/sessionWarningInfoBackground"
tools:text="Please be aware that session names are also visible to people you communicate with. Learn more" />
</merge>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu 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"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/sessionOverviewRename"
android:title="@string/device_manager_session_rename"
app:showAsAction="withText|never" />
</menu>

View file

@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2.overview
import android.content.Intent
import im.vector.app.features.settings.devices.v2.details.SessionDetailsActivity
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
import im.vector.app.test.fakes.FakeContext
import io.mockk.every
import io.mockk.mockk
@ -38,6 +39,7 @@ class SessionOverviewViewNavigatorTest {
@Before
fun setUp() {
mockkObject(SessionDetailsActivity)
mockkObject(RenameSessionActivity)
}
@After
@ -52,7 +54,22 @@ class SessionOverviewViewNavigatorTest {
context.givenStartActivity(intent)
// When
sessionOverviewViewNavigator.navigateToSessionDetails(context.instance, A_SESSION_ID)
sessionOverviewViewNavigator.goToSessionDetails(context.instance, A_SESSION_ID)
// Then
verify {
context.instance.startActivity(intent)
}
}
@Test
fun `given a session id when navigating to rename screen then it starts the correct activity`() {
// Given
val intent = givenIntentForRenameSession(A_SESSION_ID)
context.givenStartActivity(intent)
// When
sessionOverviewViewNavigator.goToRenameSession(context.instance, A_SESSION_ID)
// Then
verify {
@ -65,4 +82,10 @@ class SessionOverviewViewNavigatorTest {
every { SessionDetailsActivity.newIntent(context.instance, sessionId) } returns intent
return intent
}
private fun givenIntentForRenameSession(sessionId: String): Intent {
val intent = mockk<Intent>()
every { RenameSessionActivity.newIntent(context.instance, sessionId) } returns intent
return intent
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private const val A_DEVICE_ID = "device-id"
private const val A_DEVICE_NAME = "device-name"
class RenameSessionUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>()
private val renameSessionUseCase = RenameSessionUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
refreshDevicesUseCase = refreshDevicesUseCase
)
@Test
fun `given a device id and a new name when no error during rename then the device is renamed with success`() = runTest {
// Given
fakeActiveSessionHolder.fakeSession.fakeCryptoService.givenSetDeviceNameSucceeds()
every { refreshDevicesUseCase.execute() } just runs
// When
val result = renameSessionUseCase.execute(A_DEVICE_ID, A_DEVICE_NAME)
// Then
result.isSuccess shouldBe true
verify {
fakeActiveSessionHolder.fakeSession
.cryptoService()
.setDeviceName(A_DEVICE_ID, A_DEVICE_NAME, any())
refreshDevicesUseCase.execute()
}
}
@Test
fun `given a device id and a new name when an error occurs during rename then result is failure`() = runTest {
// Given
val error = Exception()
fakeActiveSessionHolder.fakeSession.fakeCryptoService.givenSetDeviceNameFailsWithError(error)
// When
val result = renameSessionUseCase.execute(A_DEVICE_ID, A_DEVICE_NAME)
// Then
result.isFailure shouldBe true
result.exceptionOrNull() shouldBeEqualTo error
}
@Test
fun `given a device id and a new name when an error occurs during devices refresh then result is failure`() = runTest {
// Given
val error = Exception()
fakeActiveSessionHolder.fakeSession.fakeCryptoService.givenSetDeviceNameSucceeds()
every { refreshDevicesUseCase.execute() } throws error
// When
val result = renameSessionUseCase.execute(A_DEVICE_ID, A_DEVICE_NAME)
// Then
result.isFailure shouldBe true
result.exceptionOrNull() shouldBeEqualTo error
}
}

View file

@ -0,0 +1,169 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.overview.GetDeviceFullInfoUseCase
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
private const val A_SESSION_ID = "session-id"
private const val A_SESSION_NAME = "session-name"
private const val AN_EDITED_SESSION_NAME = "edited-session-name"
class RenameSessionViewModelTest {
@get:Rule
val mvRxTestRule = MvRxTestRule(testDispatcher = testDispatcher)
private val args = RenameSessionArgs(
deviceId = A_SESSION_ID
)
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
private val renameSessionUseCase = mockk<RenameSessionUseCase>()
private fun createViewModel() = RenameSessionViewModel(
initialState = RenameSessionViewState(args),
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
renameSessionUseCase = renameSessionUseCase,
)
@Test
fun `given the original device name has not been retrieved when handling init with last edited name action then view state and view events are updated`() {
// Given
givenSessionWithName(A_SESSION_NAME)
val action = RenameSessionAction.InitWithLastEditedName
val expectedState = RenameSessionViewState(
deviceId = A_SESSION_ID,
editedDeviceName = A_SESSION_NAME,
)
val expectedEvent = RenameSessionViewEvent.Initialized(
deviceName = A_SESSION_NAME,
)
val viewModel = createViewModel()
viewModel.hasRetrievedOriginalDeviceName = false
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest.assertLatestState { state -> state == expectedState }
.assertEvent { event -> event == expectedEvent }
.finish()
verify {
getDeviceFullInfoUseCase.execute(A_SESSION_ID)
}
}
@Test
fun `given the original device name has been retrieved when handling init with last edited name action then view state and view events are updated`() {
// Given
val action = RenameSessionAction.InitWithLastEditedName
val expectedState = RenameSessionViewState(
deviceId = A_SESSION_ID,
editedDeviceName = AN_EDITED_SESSION_NAME,
)
val expectedEvent = RenameSessionViewEvent.Initialized(
deviceName = AN_EDITED_SESSION_NAME,
)
val viewModel = createViewModel()
viewModel.handle(RenameSessionAction.EditLocally(AN_EDITED_SESSION_NAME))
viewModel.hasRetrievedOriginalDeviceName = true
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest.assertLatestState { state -> state == expectedState }
.assertEvent { event -> event == expectedEvent }
.finish()
verify(inverse = true) {
getDeviceFullInfoUseCase.execute(A_SESSION_ID)
}
}
@Test
fun `given a new edited name when handling edit name locally action then view state is updated accordingly`() {
// Given
val action = RenameSessionAction.EditLocally(AN_EDITED_SESSION_NAME)
val expectedState = RenameSessionViewState(
deviceId = A_SESSION_ID,
editedDeviceName = AN_EDITED_SESSION_NAME,
)
val viewModel = createViewModel()
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertLatestState { state -> state == expectedState }
.finish()
}
@Test
fun `given current edited name when handling save modifications action with success then correct view event is posted`() {
// Given
coEvery { renameSessionUseCase.execute(A_SESSION_ID, any()) } returns Result.success(Unit)
val action = RenameSessionAction.SaveModifications
val viewModel = createViewModel()
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { event -> event is RenameSessionViewEvent.SessionRenamed }
.finish()
}
@Test
fun `given current edited name when handling save modifications action with error then correct view event is posted`() {
// Given
val error = Exception()
coEvery { renameSessionUseCase.execute(A_SESSION_ID, any()) } returns Result.failure(error)
val action = RenameSessionAction.SaveModifications
val viewModel = createViewModel()
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { event -> event is RenameSessionViewEvent.Failure && event.throwable == error }
.finish()
}
private fun givenSessionWithName(sessionName: String) {
val deviceFullInfo = mockk<DeviceFullInfo>()
every { deviceFullInfo.deviceInfo.displayName } returns sessionName
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo)
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.rename
import androidx.fragment.app.FragmentActivity
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import org.junit.Test
class RenameSessionViewNavigatorTest {
private val renameSessionViewNavigator = RenameSessionViewNavigator()
@Test
fun `given an activity when going back then the activity is finished`() {
// Given
val fragmentActivity = mockk<FragmentActivity>()
every { fragmentActivity.finish() } just runs
// When
renameSessionViewNavigator.goBack(fragmentActivity)
// Then
verify {
fragmentActivity.finish()
}
}
}

View file

@ -17,7 +17,10 @@
package im.vector.app.test.fakes
import androidx.lifecycle.MutableLiveData
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
@ -50,4 +53,18 @@ class FakeCryptoService(
override fun getLiveCryptoDeviceInfoWithId(deviceId: String) = cryptoDeviceInfoWithIdLiveData
override fun getMyDevicesInfoLive(deviceId: String) = myDevicesInfoWithIdLiveData
fun givenSetDeviceNameSucceeds() {
val matrixCallback = slot<MatrixCallback<Unit>>()
every { setDeviceName(any(), any(), capture(matrixCallback)) } answers {
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
}
}
fun givenSetDeviceNameFailsWithError(error: Exception) {
val matrixCallback = slot<MatrixCallback<Unit>>()
every { setDeviceName(any(), any(), capture(matrixCallback)) } answers {
thirdArg<MatrixCallback<Unit>>().onFailure(error)
}
}
}