Long press on the whole content item

This commit is contained in:
Maxime NATUREL 2022-09-09 17:06:32 +02:00
parent 6cd0fbb614
commit 279820224c
12 changed files with 242 additions and 11 deletions

View file

@ -0,0 +1,34 @@
/*
* 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.core.utils
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.core.content.getSystemService
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class CopyToClipboardUseCase @Inject constructor(
@ApplicationContext private val context: Context,
) {
fun execute(text: CharSequence) {
context.getSystemService<ClipboardManager>()
?.setPrimaryClip(ClipData.newPlainText("", text))
}
}

View file

@ -19,8 +19,6 @@ package im.vector.app.core.utils
import android.annotation.TargetApi
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@ -100,8 +98,7 @@ fun requestDisablingBatteryOptimization(activity: Activity, activityResultLaunch
* @param toastMessage content of the toast message as a String resource
*/
fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true, @StringRes toastMessage: Int = R.string.copied_to_clipboard) {
val clipboard = context.getSystemService<ClipboardManager>()!!
clipboard.setPrimaryClip(ClipData.newPlainText("", text))
CopyToClipboardUseCase(context).execute(text)
if (showToast) {
context.toast(toastMessage)
}

View file

@ -18,4 +18,6 @@ package im.vector.app.features.settings.devices.v2.details
import im.vector.app.core.platform.VectorViewModelAction
sealed class SessionDetailsAction : VectorViewModelAction
sealed class SessionDetailsAction : VectorViewModelAction {
data class CopyToClipboard(val content: String) : SessionDetailsAction()
}

View file

@ -24,7 +24,6 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.copyOnLongClick
@EpoxyModelClass
abstract class SessionDetailsContentItem : VectorEpoxyModel<SessionDetailsContentItem.Holder>(R.layout.item_session_details_content) {
@ -38,11 +37,15 @@ abstract class SessionDetailsContentItem : VectorEpoxyModel<SessionDetailsConten
@EpoxyAttribute
var hasDivider: Boolean = true
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var onLongClickListener: View.OnLongClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.sessionDetailsContentTitle.text = title
holder.sessionDetailsContentDescription.text = description
holder.sessionDetailsContentDescription.copyOnLongClick()
holder.view.isClickable = onLongClickListener != null
holder.view.setOnLongClickListener(onLongClickListener)
holder.sessionDetailsContentDivider.isVisible = hasDivider
}

View file

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2.details
import android.view.View
import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
@ -34,6 +35,12 @@ class SessionDetailsController @Inject constructor(
private val dimensionConverter: DimensionConverter,
) : TypedEpoxyController<DeviceInfo>() {
var callback: Callback? = null
interface Callback {
fun onItemLongClicked(content: String)
}
override fun buildModels(data: DeviceInfo?) {
data?.let { info ->
val hasSectionSession = hasSectionSession(data)
@ -64,6 +71,10 @@ class SessionDetailsController @Inject constructor(
title(host.stringProvider.getString(titleResId))
description(value)
hasDivider(hasDivider)
onLongClickListener(View.OnLongClickListener {
host.callback?.onItemLongClicked(value)
true
})
}
}

View file

@ -31,6 +31,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.showOptimizedSnackbar
import im.vector.app.databinding.FragmentSessionDetailsBinding
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import javax.inject.Inject
@ -54,6 +55,7 @@ class SessionDetailsFragment :
super.onViewCreated(view, savedInstanceState)
initToolbar()
initSessionDetails()
observeViewEvents()
}
private fun initToolbar() {
@ -63,15 +65,29 @@ class SessionDetailsFragment :
}
private fun initSessionDetails() {
sessionDetailsController.callback = object : SessionDetailsController.Callback {
override fun onItemLongClicked(content: String) {
viewModel.handle(SessionDetailsAction.CopyToClipboard(content))
}
}
views.sessionDetails.configureWith(sessionDetailsController)
}
private fun observeViewEvents() {
viewModel.observeViewEvents { viewEvent ->
when (viewEvent) {
SessionDetailsViewEvent.ContentCopiedToClipboard -> view?.showOptimizedSnackbar(getString(R.string.copied_to_clipboard))
}
}
}
override fun onDestroyView() {
cleanUpSessionDetails()
super.onDestroyView()
}
private fun cleanUpSessionDetails() {
sessionDetailsController.callback = null
views.sessionDetails.cleanup()
}

View file

@ -0,0 +1,23 @@
/*
* 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.details
import im.vector.app.core.platform.VectorViewEvents
sealed class SessionDetailsViewEvent : VectorViewEvents {
object ContentCopiedToClipboard : SessionDetailsViewEvent()
}

View file

@ -23,8 +23,8 @@ 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.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.CopyToClipboardUseCase
import im.vector.app.features.settings.devices.v2.overview.GetDeviceFullInfoUseCase
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -32,7 +32,8 @@ import kotlinx.coroutines.flow.onEach
class SessionDetailsViewModel @AssistedInject constructor(
@Assisted val initialState: SessionDetailsViewState,
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
) : VectorViewModel<SessionDetailsViewState, SessionDetailsAction, EmptyViewEvents>(initialState) {
private val copyToClipboardUseCase: CopyToClipboardUseCase,
) : VectorViewModel<SessionDetailsViewState, SessionDetailsAction, SessionDetailsViewEvent>(initialState) {
companion object : MavericksViewModelFactory<SessionDetailsViewModel, SessionDetailsViewState> by hiltMavericksViewModelFactory()
@ -52,6 +53,13 @@ class SessionDetailsViewModel @AssistedInject constructor(
}
override fun handle(action: SessionDetailsAction) {
TODO("Implement when adding the first action")
return when (action) {
is SessionDetailsAction.CopyToClipboard -> handleCopyToClipboard(action)
}
}
private fun handleCopyToClipboard(copyToClipboard: SessionDetailsAction.CopyToClipboard) {
copyToClipboardUseCase.execute(copyToClipboard.content)
_viewEvents.post(SessionDetailsViewEvent.ContentCopiedToClipboard)
}
}

View file

@ -0,0 +1,65 @@
/*
* 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.core.utils
import android.content.ClipData
import im.vector.app.test.fakes.FakeContext
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Before
import org.junit.Test
private const val A_TEXT = "text"
class CopyToClipboardUseCaseTest {
private val fakeContext = FakeContext()
private val copyToClipboardUseCase = CopyToClipboardUseCase(
context = fakeContext.instance
)
@Before
fun setup() {
mockkStatic(ClipData::class)
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a text when executing the use case then the text is copied into the clipboard`() {
// Given
val clipboardManager = fakeContext.givenClipboardManager()
clipboardManager.givenSetPrimaryClip()
val clipData = mockk<ClipData>()
every { ClipData.newPlainText(any(), any()) } returns clipData
// When
copyToClipboardUseCase.execute(A_TEXT)
// Then
clipboardManager.verifySetPrimaryClip(clipData)
verify { ClipData.newPlainText("", A_TEXT) }
}
}

View file

@ -18,12 +18,15 @@ package im.vector.app.features.settings.devices.v2.details
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.core.utils.CopyToClipboardUseCase
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.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
@ -31,6 +34,7 @@ import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
private const val A_SESSION_ID = "session-id"
private const val A_TEXT = "text"
class SessionDetailsViewModelTest {
@ -41,10 +45,12 @@ class SessionDetailsViewModelTest {
deviceId = A_SESSION_ID
)
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
private val copyToClipboardUseCase = mockk<CopyToClipboardUseCase>()
private fun createViewModel() = SessionDetailsViewModel(
initialState = SessionDetailsViewState(args),
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
copyToClipboardUseCase = copyToClipboardUseCase,
)
@Test
@ -68,4 +74,26 @@ class SessionDetailsViewModelTest {
.finish()
verify { getDeviceFullInfoUseCase.execute(A_SESSION_ID) }
}
@Test
fun `given copyToClipboard action when viewModel handle it then related use case is executed and viewEvent is updated`() {
// Given
val deviceFullInfo = mockk<DeviceFullInfo>()
val deviceInfo = mockk<DeviceInfo>()
every { deviceFullInfo.deviceInfo } returns deviceInfo
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo)
val action = SessionDetailsAction.CopyToClipboard(A_TEXT)
every { copyToClipboardUseCase.execute(any()) } just runs
// When
val viewModel = createViewModel()
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { it is SessionDetailsViewEvent.ContentCopiedToClipboard }
.finish()
verify { copyToClipboardUseCase.execute(A_TEXT) }
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.test.fakes
import android.content.ClipData
import android.content.ClipboardManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
class FakeClipboardManager {
val instance = mockk<ClipboardManager>()
fun givenSetPrimaryClip() {
every { instance.setPrimaryClip(any()) } just runs
}
fun verifySetPrimaryClip(clipData: ClipData) {
verify { instance.setPrimaryClip(clipData) }
}
}

View file

@ -16,6 +16,7 @@
package im.vector.app.test.fakes
import android.content.ClipboardManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
@ -74,4 +75,10 @@ class FakeContext(
fun givenStartActivity(intent: Intent) {
every { instance.startActivity(intent) } just runs
}
fun givenClipboardManager(): FakeClipboardManager {
val fakeClipboardManager = FakeClipboardManager()
givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance)
return fakeClipboardManager
}
}