diff --git a/changelog.d/7158.wip b/changelog.d/7158.wip
new file mode 100644
index 0000000000..6c303281d8
--- /dev/null
+++ b/changelog.d/7158.wip
@@ -0,0 +1 @@
+[Device management] Rename a session
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index d8f6222acf..fdb04a6e76 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3295,6 +3295,10 @@
Session IDLast activityIP address
+ Rename session
+ Session name
+ Custom session names can help you recognize your devices more easily.
+ Please be aware that session names are also visible to people you communicate with.%s\nis looking a little empty.
diff --git a/library/ui-styles/src/main/res/values/stylable_session_warning_info_view.xml b/library/ui-styles/src/main/res/values/stylable_session_warning_info_view.xml
new file mode 100644
index 0000000000..6236b31f46
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/stylable_session_warning_info_view.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index bb8ca8cf5f..b16e4505a6 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -325,6 +325,7 @@
+
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 6fb2505386..62e7140742 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -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<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/SessionWarningInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/SessionWarningInfoView.kt
new file mode 100644
index 0000000000..938c6c99ab
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/SessionWarningInfoView.kt
@@ -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
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
index 73991c5f20..ccf68a18bc 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
@@ -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() {
+ VectorBaseFragment(),
+ 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
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigator.kt
index ef61856255..8c4d0345dc 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigator.kt
@@ -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))
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionAction.kt
new file mode 100644
index 0000000000..c60bc7df14
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionAction.kt
@@ -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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt
new file mode 100644
index 0000000000..eb0d994ce3
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt
@@ -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() {
+
+ 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))
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionArgs.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionArgs.kt
new file mode 100644
index 0000000000..d43d472946
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionArgs.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt
new file mode 100644
index 0000000000..df92bee100
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt
@@ -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() {
+
+ 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()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCase.kt
new file mode 100644
index 0000000000..2e44bb33d6
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCase.kt
@@ -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 {
+ return renameDevice(deviceId, newName)
+ .andThen { refreshDevices() }
+ }
+
+ private suspend fun renameDevice(deviceId: String, newName: String) = runCatching {
+ awaitCallback { matrixCallback ->
+ activeSessionHolder.getActiveSession()
+ .cryptoService()
+ .setDeviceName(deviceId, newName, matrixCallback)
+ }
+ }
+
+ private fun refreshDevices() = runCatching { refreshDevicesUseCase.execute() }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewEvent.kt
new file mode 100644
index 0000000000..fd40412547
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewEvent.kt
@@ -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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModel.kt
new file mode 100644
index 0000000000..22170fc702
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModel.kt
@@ -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(initialState) {
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ 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)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigator.kt
new file mode 100644
index 0000000000..e00d7b25ff
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigator.kt
@@ -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()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewState.kt
new file mode 100644
index 0000000000..70e11327ca
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewState.kt
@@ -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
+ )
+}
diff --git a/vector/src/main/res/layout/fragment_session_rename.xml b/vector/src/main/res/layout/fragment_session_rename.xml
new file mode 100644
index 0000000000..12b8af00f0
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_session_rename.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/view_session_warning_info.xml b/vector/src/main/res/layout/view_session_warning_info.xml
new file mode 100644
index 0000000000..818217e71c
--- /dev/null
+++ b/vector/src/main/res/layout/view_session_warning_info.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/menu/menu_session_overview.xml b/vector/src/main/res/menu/menu_session_overview.xml
new file mode 100644
index 0000000000..7de3953dcc
--- /dev/null
+++ b/vector/src/main/res/menu/menu_session_overview.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt
index 56f1e5920d..3d38f3b3bf 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt
@@ -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()
+ every { RenameSessionActivity.newIntent(context.instance, sessionId) } returns intent
+ return intent
+ }
}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCaseTest.kt
new file mode 100644
index 0000000000..9ef4718559
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionUseCaseTest.kt
@@ -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()
+
+ 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
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModelTest.kt
new file mode 100644
index 0000000000..c14f2f3526
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewModelTest.kt
@@ -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()
+ private val renameSessionUseCase = mockk()
+
+ 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()
+ every { deviceFullInfo.deviceInfo.displayName } returns sessionName
+ every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo)
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigatorTest.kt
new file mode 100644
index 0000000000..faf25e5487
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionViewNavigatorTest.kt
@@ -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()
+ every { fragmentActivity.finish() } just runs
+
+ // When
+ renameSessionViewNavigator.goBack(fragmentActivity)
+
+ // Then
+ verify {
+ fragmentActivity.finish()
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
index 538ce671d2..a83da8cb9d 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
@@ -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>()
+ every { setDeviceName(any(), any(), capture(matrixCallback)) } answers {
+ thirdArg>().onSuccess(Unit)
+ }
+ }
+
+ fun givenSetDeviceNameFailsWithError(error: Exception) {
+ val matrixCallback = slot>()
+ every { setDeviceName(any(), any(), capture(matrixCallback)) } answers {
+ thirdArg>().onFailure(error)
+ }
+ }
}