mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 20:06:51 +03:00
FTUE - Choose a display picture (#5323)
* adding tests around the onboarding view model - cases for the personalisation and display name actions * adding base choose name fragment with UI * add click handling for the display name actions * adding tests around the onboarding view model - cases for the personalisation and display name actions * adding barebones profile picture fragment with ability to select a user avatar * extracting uri filename resolving to a class which can be injected - includes tests * updating upstream avatar on profile picture save and continue step - moves the personalisation state to a dedicated model to allow for back and forth state restoration * adding test case for skipping profile picture setting * taking the profile loading into account when rendering the onboarding loading * extracting method for the handling of the profile picture selection * adding dedicated camera icon for choosing profile picture * adding toolbar to back to profile picture page - this toolbar will fade in with the fragment as it sits at the fragment level, probably worth revisiting once more pages have a toolbar * changing edit/add picture icon based on if we're already selected an image * making use of debounced clicks to avoid potential extra clicks * making the avatar height and camera icon relative percentage based - also makes the avatar itself clicking, including a foreground ripple * fixing formatting * making use of fake session id for user id assertion * using a real matrix id syntax for the fake session user id * removing duplicated dimens * using self closing imageview tag
This commit is contained in:
parent
9af2f1cdc6
commit
9a02543afd
26 changed files with 807 additions and 43 deletions
|
@ -17,7 +17,12 @@
|
||||||
package im.vector.lib.multipicker.utils
|
package im.vector.lib.multipicker.utils
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
|
|
||||||
fun Cursor.getColumnIndexOrNull(column: String): Int? {
|
fun Cursor.getColumnIndexOrNull(column: String): Int? {
|
||||||
return getColumnIndex(column).takeIf { it != -1 }
|
return getColumnIndex(column).takeIf { it != -1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Cursor.readStringColumnOrNull(column: String): String? {
|
||||||
|
return getColumnIndexOrNull(column)?.let { getStringOrNull(it) }
|
||||||
|
}
|
||||||
|
|
|
@ -67,4 +67,7 @@
|
||||||
|
|
||||||
<item name="ftue_auth_carousel_item_spacing" format="float" type="dimen">0.01</item>
|
<item name="ftue_auth_carousel_item_spacing" format="float" type="dimen">0.01</item>
|
||||||
<item name="ftue_auth_carousel_item_image_height" format="float" type="dimen">0.35</item>
|
<item name="ftue_auth_carousel_item_image_height" format="float" type="dimen">0.35</item>
|
||||||
|
|
||||||
|
<item name="ftue_auth_profile_picture_height" format="float" type="dimen">0.15</item>
|
||||||
|
<item name="ftue_auth_profile_picture_icon_height" format="float" type="dimen">0.05</item>
|
||||||
</resources>
|
</resources>
|
|
@ -100,6 +100,7 @@ import im.vector.app.features.matrixto.MatrixToUserFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment
|
||||||
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
|
||||||
|
@ -485,6 +486,11 @@ interface FragmentModule {
|
||||||
@FragmentKey(FtueAuthChooseDisplayNameFragment::class)
|
@FragmentKey(FtueAuthChooseDisplayNameFragment::class)
|
||||||
fun bindFtueAuthChooseDisplayNameFragment(fragment: FtueAuthChooseDisplayNameFragment): Fragment
|
fun bindFtueAuthChooseDisplayNameFragment(fragment: FtueAuthChooseDisplayNameFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(FtueAuthChooseProfilePictureFragment::class)
|
||||||
|
fun bindFtueAuthChooseProfilePictureFragment(fragment: FtueAuthChooseProfilePictureFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(UserListFragment::class)
|
@FragmentKey(UserListFragment::class)
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.home
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
@ -48,6 +49,7 @@ import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,6 +102,16 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
|
||||||
DrawableImageViewTarget(imageView))
|
DrawableImageViewTarget(imageView))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
fun render(matrixItem: MatrixItem, localUri: Uri?, imageView: ImageView) {
|
||||||
|
val placeholder = getPlaceholderDrawable(matrixItem)
|
||||||
|
GlideApp.with(imageView)
|
||||||
|
.load(localUri?.let { File(localUri.path!!) })
|
||||||
|
.apply(RequestOptions.circleCropTransform())
|
||||||
|
.placeholder(placeholder)
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
fun render(mappedContact: MappedContact, imageView: ImageView) {
|
fun render(mappedContact: MappedContact, imageView: ImageView) {
|
||||||
// Create a Fake MatrixItem, for the placeholder
|
// Create a Fake MatrixItem, for the placeholder
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.onboarding
|
package im.vector.app.features.onboarding
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.login.LoginConfig
|
import im.vector.app.features.login.LoginConfig
|
||||||
import im.vector.app.features.login.ServerType
|
import im.vector.app.features.login.ServerType
|
||||||
|
@ -76,4 +77,7 @@ sealed class OnboardingAction : VectorViewModelAction {
|
||||||
|
|
||||||
data class UpdateDisplayName(val displayName: String) : OnboardingAction()
|
data class UpdateDisplayName(val displayName: String) : OnboardingAction()
|
||||||
object UpdateDisplayNameSkipped : OnboardingAction()
|
object UpdateDisplayNameSkipped : OnboardingAction()
|
||||||
|
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction()
|
||||||
|
object SaveSelectedProfilePicture : OnboardingAction()
|
||||||
|
object UpdateProfilePictureSkipped : OnboardingAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,4 +54,6 @@ sealed class OnboardingViewEvents : VectorViewEvents {
|
||||||
object OnPersonalizeProfile : OnboardingViewEvents()
|
object OnPersonalizeProfile : OnboardingViewEvents()
|
||||||
object OnDisplayNameUpdated : OnboardingViewEvents()
|
object OnDisplayNameUpdated : OnboardingViewEvents()
|
||||||
object OnDisplayNameSkipped : OnboardingViewEvents()
|
object OnDisplayNameSkipped : OnboardingViewEvents()
|
||||||
|
object OnPersonalizationComplete : OnboardingViewEvents()
|
||||||
|
object OnBack : OnboardingViewEvents()
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixIdFailure
|
import org.matrix.android.sdk.api.failure.MatrixIdFailure
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
import java.util.concurrent.CancellationException
|
import java.util.concurrent.CancellationException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,6 +81,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
private val homeServerHistoryService: HomeServerHistoryService,
|
private val homeServerHistoryService: HomeServerHistoryService,
|
||||||
private val vectorFeatures: VectorFeatures,
|
private val vectorFeatures: VectorFeatures,
|
||||||
private val analyticsTracker: AnalyticsTracker,
|
private val analyticsTracker: AnalyticsTracker,
|
||||||
|
private val uriFilenameResolver: UriFilenameResolver,
|
||||||
private val vectorOverrides: VectorOverrides
|
private val vectorOverrides: VectorOverrides
|
||||||
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
|
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@ -157,6 +159,9 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
|
is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
|
||||||
is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName)
|
is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName)
|
||||||
OnboardingAction.UpdateDisplayNameSkipped -> _viewEvents.post(OnboardingViewEvents.OnDisplayNameSkipped)
|
OnboardingAction.UpdateDisplayNameSkipped -> _viewEvents.post(OnboardingViewEvents.OnDisplayNameSkipped)
|
||||||
|
OnboardingAction.UpdateProfilePictureSkipped -> _viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
|
||||||
|
is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action)
|
||||||
|
OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -899,7 +904,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
val activeSession = activeSessionHolder.getActiveSession()
|
val activeSession = activeSessionHolder.getActiveSession()
|
||||||
try {
|
try {
|
||||||
activeSession.setDisplayName(activeSession.myUserId, displayName)
|
activeSession.setDisplayName(activeSession.myUserId, displayName)
|
||||||
setState { copy(asyncDisplayName = Success(Unit)) }
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncDisplayName = Success(Unit),
|
||||||
|
personalizationState = personalizationState.copy(displayName = displayName)
|
||||||
|
)
|
||||||
|
}
|
||||||
_viewEvents.post(OnboardingViewEvents.OnDisplayNameUpdated)
|
_viewEvents.post(OnboardingViewEvents.OnDisplayNameUpdated)
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
setState { copy(asyncDisplayName = Fail(error)) }
|
setState { copy(asyncDisplayName = Fail(error)) }
|
||||||
|
@ -907,6 +917,46 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleProfilePictureSelected(action: OnboardingAction.ProfilePictureSelected) {
|
||||||
|
setState {
|
||||||
|
copy(personalizationState = personalizationState.copy(selectedPictureUri = action.uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProfilePicture() {
|
||||||
|
withState { state ->
|
||||||
|
when (val pictureUri = state.personalizationState.selectedPictureUri) {
|
||||||
|
null -> _viewEvents.post(OnboardingViewEvents.Failure(NullPointerException("picture uri is missing from state")))
|
||||||
|
else -> {
|
||||||
|
setState { copy(asyncProfilePicture = Loading()) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val activeSession = activeSessionHolder.getActiveSession()
|
||||||
|
try {
|
||||||
|
activeSession.updateAvatar(
|
||||||
|
activeSession.myUserId,
|
||||||
|
pictureUri,
|
||||||
|
uriFilenameResolver.getFilenameFromUri(pictureUri) ?: UUID.randomUUID().toString()
|
||||||
|
)
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncProfilePicture = Success(Unit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onProfilePictureSaved()
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
setState { copy(asyncProfilePicture = Fail(error)) }
|
||||||
|
_viewEvents.post(OnboardingViewEvents.Failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onProfilePictureSaved() {
|
||||||
|
_viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LoginMode.supportsSignModeScreen(): Boolean {
|
private fun LoginMode.supportsSignModeScreen(): Boolean {
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package im.vector.app.features.onboarding
|
package im.vector.app.features.onboarding
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Parcelable
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
@ -25,6 +27,7 @@ import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.app.features.login.LoginMode
|
import im.vector.app.features.login.LoginMode
|
||||||
import im.vector.app.features.login.ServerType
|
import im.vector.app.features.login.ServerType
|
||||||
import im.vector.app.features.login.SignMode
|
import im.vector.app.features.login.SignMode
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
data class OnboardingViewState(
|
data class OnboardingViewState(
|
||||||
val asyncLoginAction: Async<Unit> = Uninitialized,
|
val asyncLoginAction: Async<Unit> = Uninitialized,
|
||||||
|
@ -33,6 +36,7 @@ data class OnboardingViewState(
|
||||||
val asyncResetMailConfirmed: Async<Unit> = Uninitialized,
|
val asyncResetMailConfirmed: Async<Unit> = Uninitialized,
|
||||||
val asyncRegistration: Async<Unit> = Uninitialized,
|
val asyncRegistration: Async<Unit> = Uninitialized,
|
||||||
val asyncDisplayName: Async<Unit> = Uninitialized,
|
val asyncDisplayName: Async<Unit> = Uninitialized,
|
||||||
|
val asyncProfilePicture: Async<Unit> = Uninitialized,
|
||||||
|
|
||||||
@PersistState
|
@PersistState
|
||||||
val onboardingFlow: OnboardingFlow? = null,
|
val onboardingFlow: OnboardingFlow? = null,
|
||||||
|
@ -65,6 +69,9 @@ data class OnboardingViewState(
|
||||||
val loginModeSupportedTypes: List<String> = emptyList(),
|
val loginModeSupportedTypes: List<String> = emptyList(),
|
||||||
val knownCustomHomeServersUrls: List<String> = emptyList(),
|
val knownCustomHomeServersUrls: List<String> = emptyList(),
|
||||||
val isForceLoginFallbackEnabled: Boolean = false,
|
val isForceLoginFallbackEnabled: Boolean = false,
|
||||||
|
|
||||||
|
@PersistState
|
||||||
|
val personalizationState: PersonalizationState = PersonalizationState()
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
fun isLoading(): Boolean {
|
fun isLoading(): Boolean {
|
||||||
|
@ -73,7 +80,8 @@ data class OnboardingViewState(
|
||||||
asyncResetPassword is Loading ||
|
asyncResetPassword is Loading ||
|
||||||
asyncResetMailConfirmed is Loading ||
|
asyncResetMailConfirmed is Loading ||
|
||||||
asyncRegistration is Loading ||
|
asyncRegistration is Loading ||
|
||||||
asyncDisplayName is Loading
|
asyncDisplayName is Loading ||
|
||||||
|
asyncProfilePicture is Loading
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isAuthTaskCompleted(): Boolean {
|
fun isAuthTaskCompleted(): Boolean {
|
||||||
|
@ -86,3 +94,9 @@ enum class OnboardingFlow {
|
||||||
SignUp,
|
SignUp,
|
||||||
SignInSignUp
|
SignInSignUp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class PersonalizationState(
|
||||||
|
val displayName: String? = null,
|
||||||
|
val selectedPictureUri: Uri? = null
|
||||||
|
) : Parcelable
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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.onboarding
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import im.vector.lib.multipicker.utils.readStringColumnOrNull
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class UriFilenameResolver @Inject constructor(private val context: Context) {
|
||||||
|
|
||||||
|
fun getFilenameFromUri(uri: Uri): String? {
|
||||||
|
val fallback = uri.path?.substringAfterLast('/')
|
||||||
|
return when (uri.scheme) {
|
||||||
|
"content" -> readResolvedDisplayName(uri) ?: fallback
|
||||||
|
else -> fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readResolvedDisplayName(uri: Uri): String? {
|
||||||
|
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
cursor.takeIf { cursor.moveToFirst() }
|
||||||
|
?.readStringColumnOrNull(OpenableColumns.DISPLAY_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import im.vector.app.core.platform.SimpleTextWatcher
|
||||||
import im.vector.app.databinding.FragmentFtueDisplayNameBinding
|
import im.vector.app.databinding.FragmentFtueDisplayNameBinding
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueDisplayNameBinding>() {
|
class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueDisplayNameBinding>() {
|
||||||
|
@ -41,7 +42,6 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViews() {
|
private fun setupViews() {
|
||||||
views.displayNameSubmit.isEnabled = views.displayNameInput.hasContentEmpty()
|
|
||||||
views.displayNameInput.editText?.addTextChangedListener(object : SimpleTextWatcher() {
|
views.displayNameInput.editText?.addTextChangedListener(object : SimpleTextWatcher() {
|
||||||
override fun afterTextChanged(s: Editable) {
|
override fun afterTextChanged(s: Editable) {
|
||||||
val newContent = s.toString()
|
val newContent = s.toString()
|
||||||
|
@ -58,10 +58,7 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
views.displayNameSubmit.debouncedClicks {
|
views.displayNameSubmit.debouncedClicks { updateDisplayName() }
|
||||||
updateDisplayName()
|
|
||||||
}
|
|
||||||
|
|
||||||
views.displayNameSkip.debouncedClicks { viewModel.handle(OnboardingAction.UpdateDisplayNameSkipped) }
|
views.displayNameSkip.debouncedClicks { viewModel.handle(OnboardingAction.UpdateDisplayNameSkipped) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +67,11 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth
|
||||||
viewModel.handle(OnboardingAction.UpdateDisplayName(newDisplayName))
|
viewModel.handle(OnboardingAction.UpdateDisplayName(newDisplayName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateWithState(state: OnboardingViewState) {
|
||||||
|
views.displayNameInput.editText?.setText(state.personalizationState.displayName)
|
||||||
|
views.displayNameSubmit.isEnabled = views.displayNameInput.hasContentEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
override fun resetViewModel() {
|
override fun resetViewModel() {
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* 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.onboarding.ftueauth
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||||
|
import im.vector.app.core.extensions.singletonEntryPoint
|
||||||
|
import im.vector.app.core.resources.ColorProvider
|
||||||
|
import im.vector.app.databinding.FragmentFtueProfilePictureBinding
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FtueAuthChooseProfilePictureFragment @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
colorProvider: ColorProvider
|
||||||
|
) : AbstractFtueAuthFragment<FragmentFtueProfilePictureBinding>(), GalleryOrCameraDialogHelper.Listener {
|
||||||
|
|
||||||
|
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
|
||||||
|
private val avatarRenderer: AvatarRenderer by lazy { requireContext().singletonEntryPoint().avatarRenderer() }
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueProfilePictureBinding {
|
||||||
|
return FragmentFtueProfilePictureBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setupViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupViews() {
|
||||||
|
views.profilePictureToolbar.setNavigationOnClickListener {
|
||||||
|
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnBack))
|
||||||
|
}
|
||||||
|
views.changeProfilePictureButton.debouncedClicks { galleryOrCameraDialogHelper.show() }
|
||||||
|
views.profilePictureView.debouncedClicks { galleryOrCameraDialogHelper.show() }
|
||||||
|
|
||||||
|
views.profilePictureSubmit.debouncedClicks {
|
||||||
|
withState(viewModel) {
|
||||||
|
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
views.profilePictureSkip.debouncedClicks { viewModel.handle(OnboardingAction.UpdateProfilePictureSkipped) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateWithState(state: OnboardingViewState) {
|
||||||
|
val hasSetPicture = state.personalizationState.selectedPictureUri != null
|
||||||
|
views.profilePictureSubmit.isEnabled = hasSetPicture
|
||||||
|
views.changeProfilePictureIcon.setImageResource(if (hasSetPicture) R.drawable.ic_edit else R.drawable.ic_camera_plain)
|
||||||
|
|
||||||
|
val session = activeSessionHolder.getActiveSession()
|
||||||
|
val matrixItem = MatrixItem.UserItem(
|
||||||
|
id = session.myUserId,
|
||||||
|
displayName = state.personalizationState.displayName ?: ""
|
||||||
|
)
|
||||||
|
avatarRenderer.render(matrixItem, localUri = state.personalizationState.selectedPictureUri, imageView = views.profilePictureView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImageReady(uri: Uri?) {
|
||||||
|
if (uri == null) {
|
||||||
|
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
viewModel.handle(OnboardingAction.ProfilePictureSelected(uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetViewModel() {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
|
||||||
import im.vector.app.core.extensions.addFragment
|
import im.vector.app.core.extensions.addFragment
|
||||||
import im.vector.app.core.extensions.addFragmentToBackstack
|
import im.vector.app.core.extensions.addFragmentToBackstack
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.extensions.popBackstack
|
||||||
import im.vector.app.core.extensions.replaceFragment
|
import im.vector.app.core.extensions.replaceFragment
|
||||||
import im.vector.app.core.platform.ScreenOrientationLocker
|
import im.vector.app.core.platform.ScreenOrientationLocker
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
|
@ -235,6 +236,8 @@ class FtueAuthVariant(
|
||||||
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
|
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
|
||||||
OnboardingViewEvents.OnDisplayNameUpdated -> onDisplayNameUpdated()
|
OnboardingViewEvents.OnDisplayNameUpdated -> onDisplayNameUpdated()
|
||||||
OnboardingViewEvents.OnDisplayNameSkipped -> onDisplayNameUpdated()
|
OnboardingViewEvents.OnDisplayNameSkipped -> onDisplayNameUpdated()
|
||||||
|
OnboardingViewEvents.OnPersonalizationComplete -> navigateToHome(createdAccount = true)
|
||||||
|
OnboardingViewEvents.OnBack -> activity.popBackstack()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,7 +424,9 @@ class FtueAuthVariant(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisplayNameUpdated() {
|
private fun onDisplayNameUpdated() {
|
||||||
// TODO go to the real profile picture fragment
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
navigateToHome(createdAccount = true)
|
FtueAuthChooseProfilePictureFragment::class.java,
|
||||||
|
option = commonOption
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
164
vector/src/main/res/layout/fragment_ftue_profile_picture.xml
Normal file
164
vector/src/main/res/layout/fragment_ftue_profile_picture.xml
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
style="@style/LoginFormScrollView"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:paddingTop="0dp"
|
||||||
|
android:paddingBottom="0dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/profilePictureGutterStart"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/profilePictureGutterEnd"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/profilePictureToolbar"
|
||||||
|
style="@style/Widget.Vector.Toolbar.Settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/profilePictureView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/profilePictureToolbar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:navigationIcon="@drawable/ic_back_24dp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/profilePictureView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:foreground="@drawable/bg_rounded_button"
|
||||||
|
android:src="@drawable/ic_user_round"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/avatarTitleSpacing"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
|
||||||
|
app:layout_constraintHeight_percent="@dimen/ftue_auth_profile_picture_height"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/profilePictureToolbar" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/changeProfilePictureButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@drawable/bg_rounded_button"
|
||||||
|
android:backgroundTint="?vctr_system"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/profilePictureView"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/profilePictureView"
|
||||||
|
app:layout_constraintHeight_percent="@dimen/ftue_auth_profile_picture_icon_height"
|
||||||
|
app:layout_constraintHorizontal_bias="1"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/profilePictureView"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/profilePictureView"
|
||||||
|
app:layout_constraintVertical_bias="1">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/pos"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintDimensionRatio="1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/changeProfilePictureIcon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:contentDescription="@string/ftue_profile_picture_title"
|
||||||
|
android:src="@drawable/ic_camera_plain"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pos"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHeight_percent="0.55"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pos"
|
||||||
|
app:tint="?vctr_content_secondary" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/avatarTitleSpacing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/profilePictureHeaderTitle"
|
||||||
|
app:layout_constraintHeight_percent="0.05"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/profilePictureView" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/profilePictureHeaderTitle"
|
||||||
|
style="@style/Widget.Vector.TextView.Title.Medium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/ftue_profile_picture_title"
|
||||||
|
android:textColor="?vctr_content_primary"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/profilePictureHeaderSubtitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/avatarTitleSpacing" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/profilePictureHeaderSubtitle"
|
||||||
|
style="@style/Widget.Vector.TextView.Subtitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/ftue_profile_picture_subtitle"
|
||||||
|
android:textColor="?vctr_content_secondary"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/actionsSpacing"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/profilePictureHeaderTitle" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/actionsSpacing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/profilePictureSubmit"
|
||||||
|
app:layout_constraintHeight_percent="0.05"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/profilePictureHeaderSubtitle" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/profilePictureSubmit"
|
||||||
|
style="@style/Widget.Vector.Button.Login"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ftue_personalize_submit"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/profilePictureSkip"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/actionsSpacing" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/profilePictureSkip"
|
||||||
|
style="@style/Widget.Vector.Button.Text.Login"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ftue_personalize_skip_this_step"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/profilePictureSubmit" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,10 @@
|
||||||
<string name="ftue_display_name_entry_title" translatable="false">Display Name</string>
|
<string name="ftue_display_name_entry_title" translatable="false">Display Name</string>
|
||||||
<string name="ftue_display_name_entry_footer" translatable="false">You can change this later</string>
|
<string name="ftue_display_name_entry_footer" translatable="false">You can change this later</string>
|
||||||
|
|
||||||
|
<string name="ftue_profile_picture_title" translatable="false">Add a profile picture</string>
|
||||||
|
<string name="ftue_profile_picture_subtitle" translatable="false">You can change this anytime.</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="ftue_personalize_submit" translatable="false">Save and continue</string>
|
<string name="ftue_personalize_submit" translatable="false">Save and continue</string>
|
||||||
<string name="ftue_personalize_skip_this_step" translatable="false">Skip this step</string>
|
<string name="ftue_personalize_skip_this_step" translatable="false">Skip this step</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -43,7 +43,7 @@ class SharedSecureStorageViewModelTest {
|
||||||
val mvrxTestRule = MvRxTestRule()
|
val mvrxTestRule = MvRxTestRule()
|
||||||
|
|
||||||
private val stringProvider = FakeStringProvider()
|
private val stringProvider = FakeStringProvider()
|
||||||
private val session = FakeSession()
|
private val fakeSession = FakeSession()
|
||||||
val args = SharedSecureStorageActivity.Args(keyId = null, emptyList(), "alias")
|
val args = SharedSecureStorageActivity.Args(keyId = null, emptyList(), "alias")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -164,7 +164,7 @@ class SharedSecureStorageViewModelTest {
|
||||||
return SharedSecureStorageViewModel(
|
return SharedSecureStorageViewModel(
|
||||||
SharedSecureStorageViewState(args),
|
SharedSecureStorageViewState(args),
|
||||||
stringProvider.instance,
|
stringProvider.instance,
|
||||||
session
|
fakeSession
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,15 +175,15 @@ class SharedSecureStorageViewModelTest {
|
||||||
step = step,
|
step = step,
|
||||||
activeDeviceCount = 0,
|
activeDeviceCount = 0,
|
||||||
showResetAllAction = false,
|
showResetAllAction = false,
|
||||||
userId = ""
|
userId = fakeSession.myUserId
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun givenKey(keyInfo: KeyInfo) {
|
private fun givenKey(keyInfo: KeyInfo) {
|
||||||
givenHasAccessToSecrets()
|
givenHasAccessToSecrets()
|
||||||
session.fakeSharedSecretStorageService._defaultKey = KeyInfoResult.Success(keyInfo)
|
fakeSession.fakeSharedSecretStorageService._defaultKey = KeyInfoResult.Success(keyInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenHasAccessToSecrets() {
|
private fun givenHasAccessToSecrets() {
|
||||||
session.fakeSharedSecretStorageService.integrityResult = IntegrityResult.Success(passphraseBased = IGNORED_PASSPHRASE_INTEGRITY)
|
fakeSession.fakeSharedSecretStorageService.integrityResult = IntegrityResult.Success(passphraseBased = IGNORED_PASSPHRASE_INTEGRITY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.onboarding
|
package im.vector.app.features.onboarding
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
|
@ -30,6 +31,8 @@ import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory
|
||||||
import im.vector.app.test.fakes.FakeHomeServerHistoryService
|
import im.vector.app.test.fakes.FakeHomeServerHistoryService
|
||||||
import im.vector.app.test.fakes.FakeSession
|
import im.vector.app.test.fakes.FakeSession
|
||||||
import im.vector.app.test.fakes.FakeStringProvider
|
import im.vector.app.test.fakes.FakeStringProvider
|
||||||
|
import im.vector.app.test.fakes.FakeUri
|
||||||
|
import im.vector.app.test.fakes.FakeUriFilenameResolver
|
||||||
import im.vector.app.test.fakes.FakeVectorFeatures
|
import im.vector.app.test.fakes.FakeVectorFeatures
|
||||||
import im.vector.app.test.test
|
import im.vector.app.test.test
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
@ -38,18 +41,23 @@ import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
private const val A_DISPLAY_NAME = "a display name"
|
private const val A_DISPLAY_NAME = "a display name"
|
||||||
|
private const val A_PICTURE_FILENAME = "a-picture.png"
|
||||||
|
private val AN_ERROR = RuntimeException("an error!")
|
||||||
|
|
||||||
class OnboardingViewModelTest {
|
class OnboardingViewModelTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val mvrxTestRule = MvRxTestRule()
|
val mvrxTestRule = MvRxTestRule()
|
||||||
|
|
||||||
|
private val fakeUri = FakeUri()
|
||||||
private val fakeContext = FakeContext()
|
private val fakeContext = FakeContext()
|
||||||
lateinit var viewModel: OnboardingViewModel
|
|
||||||
private val initialState = OnboardingViewState()
|
private val initialState = OnboardingViewState()
|
||||||
private val fakeSession = FakeSession()
|
private val fakeSession = FakeSession()
|
||||||
|
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
|
||||||
|
|
||||||
|
lateinit var viewModel: OnboardingViewModel
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
viewModel = createViewModel()
|
viewModel = createViewModel()
|
||||||
|
@ -67,7 +75,7 @@ class OnboardingViewModelTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when handling display name updates action then updates user display name and emits name updated event`() = runBlockingTest {
|
fun `when handling display name update then updates upstream user display name`() = runBlockingTest {
|
||||||
val test = viewModel.test(this)
|
val test = viewModel.test(this)
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||||
|
@ -76,33 +84,105 @@ class OnboardingViewModelTest {
|
||||||
.assertStates(
|
.assertStates(
|
||||||
initialState,
|
initialState,
|
||||||
initialState.copy(asyncDisplayName = Loading()),
|
initialState.copy(asyncDisplayName = Loading()),
|
||||||
initialState.copy(asyncDisplayName = Success(Unit)),
|
initialState.copy(
|
||||||
|
asyncDisplayName = Success(Unit),
|
||||||
|
personalizationState = initialState.personalizationState.copy(displayName = A_DISPLAY_NAME)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.assertEvents(OnboardingViewEvents.OnDisplayNameUpdated)
|
.assertEvents(OnboardingViewEvents.OnDisplayNameUpdated)
|
||||||
.finish()
|
.finish()
|
||||||
|
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given upstream failure when handling display name update then emits failure event`() = runBlockingTest {
|
||||||
|
val test = viewModel.test(this)
|
||||||
|
fakeSession.fakeProfileService.givenSetDisplayNameErrors(AN_ERROR)
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStates(
|
||||||
|
initialState,
|
||||||
|
initialState.copy(asyncDisplayName = Loading()),
|
||||||
|
initialState.copy(asyncDisplayName = Fail(AN_ERROR)),
|
||||||
|
)
|
||||||
|
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given failure when handling display name updates action then emits failure event`() = runBlockingTest {
|
fun `when handling profile picture selected then updates selected picture state`() = runBlockingTest {
|
||||||
val test = viewModel.test(this)
|
val test = viewModel.test(this)
|
||||||
val errorCause = RuntimeException("an error!")
|
|
||||||
fakeSession.fakeProfileService.givenSetDisplayNameErrors(errorCause)
|
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
viewModel.handle(OnboardingAction.ProfilePictureSelected(fakeUri.instance))
|
||||||
|
|
||||||
test
|
test
|
||||||
.assertStates(
|
.assertStates(
|
||||||
initialState,
|
initialState,
|
||||||
initialState.copy(asyncDisplayName = Loading()),
|
initialState.copy(personalizationState = initialState.personalizationState.copy(selectedPictureUri = fakeUri.instance))
|
||||||
initialState.copy(asyncDisplayName = Fail(errorCause)),
|
|
||||||
)
|
)
|
||||||
.assertEvents(OnboardingViewEvents.Failure(errorCause))
|
.assertNoEvents()
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createViewModel(): OnboardingViewModel {
|
@Test
|
||||||
|
fun `given a selected picture when handling save selected profile picture then updates upstream avatar and completes personalization`() = runBlockingTest {
|
||||||
|
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
|
||||||
|
viewModel = createViewModel(initialStateWithPicture)
|
||||||
|
val test = viewModel.test(this)
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStates(expectedProfilePictureSuccessStates(initialStateWithPicture))
|
||||||
|
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
||||||
|
.finish()
|
||||||
|
fakeSession.fakeProfileService.verifyAvatarUpdated(fakeSession.myUserId, fakeUri.instance, A_PICTURE_FILENAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given upstream update avatar fails when saving selected profile picture then emits failure event`() = runBlockingTest {
|
||||||
|
fakeSession.fakeProfileService.givenUpdateAvatarErrors(AN_ERROR)
|
||||||
|
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
|
||||||
|
viewModel = createViewModel(initialStateWithPicture)
|
||||||
|
val test = viewModel.test(this)
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStates(expectedProfilePictureFailureStates(initialStateWithPicture, AN_ERROR))
|
||||||
|
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no selected picture when saving selected profile picture then emits failure event`() = runBlockingTest {
|
||||||
|
val test = viewModel.test(this)
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStates(initialState)
|
||||||
|
.assertEvent { it is OnboardingViewEvents.Failure && it.throwable is NullPointerException }
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when handling profile picture skipped then completes personalization`() = runBlockingTest {
|
||||||
|
val test = viewModel.test(this)
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.UpdateProfilePictureSkipped)
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStates(initialState)
|
||||||
|
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(state: OnboardingViewState = initialState): OnboardingViewModel {
|
||||||
return OnboardingViewModel(
|
return OnboardingViewModel(
|
||||||
initialState,
|
state,
|
||||||
fakeContext.instance,
|
fakeContext.instance,
|
||||||
FakeAuthenticationService(),
|
FakeAuthenticationService(),
|
||||||
fakeActiveSessionHolder.instance,
|
fakeActiveSessionHolder.instance,
|
||||||
|
@ -112,7 +192,26 @@ class OnboardingViewModelTest {
|
||||||
FakeHomeServerHistoryService(),
|
FakeHomeServerHistoryService(),
|
||||||
FakeVectorFeatures(),
|
FakeVectorFeatures(),
|
||||||
FakeAnalyticsTracker(),
|
FakeAnalyticsTracker(),
|
||||||
DefaultVectorOverrides(),
|
fakeUriFilenameResolver.instance,
|
||||||
|
DefaultVectorOverrides()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun givenPictureSelected(fileUri: Uri, filename: String): OnboardingViewState {
|
||||||
|
val initialStateWithPicture = OnboardingViewState(personalizationState = PersonalizationState(selectedPictureUri = fileUri))
|
||||||
|
fakeUriFilenameResolver.givenFilename(fileUri, name = filename)
|
||||||
|
return initialStateWithPicture
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectedProfilePictureSuccessStates(state: OnboardingViewState) = listOf(
|
||||||
|
state,
|
||||||
|
state.copy(asyncProfilePicture = Loading()),
|
||||||
|
state.copy(asyncProfilePicture = Success(Unit))
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun expectedProfilePictureFailureStates(state: OnboardingViewState, cause: Exception) = listOf(
|
||||||
|
state,
|
||||||
|
state.copy(asyncProfilePicture = Loading()),
|
||||||
|
state.copy(asyncProfilePicture = Fail(cause))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* 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.onboarding
|
||||||
|
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import im.vector.app.test.fakes.FakeContentResolver
|
||||||
|
import im.vector.app.test.fakes.FakeContext
|
||||||
|
import im.vector.app.test.fakes.FakeCursor
|
||||||
|
import im.vector.app.test.fakes.FakeUri
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_LAST_SEGMENT = "a-file-name.foo"
|
||||||
|
private const val A_DISPLAY_NAME = "file-display-name.foo"
|
||||||
|
|
||||||
|
class UriFilenameResolverTest {
|
||||||
|
|
||||||
|
private val fakeUri = FakeUri()
|
||||||
|
private val fakeContentResolver = FakeContentResolver()
|
||||||
|
private val uriFilenameResolver = UriFilenameResolver(FakeContext(fakeContentResolver.instance).instance)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a non hierarchical Uri when querying file name then is null`() {
|
||||||
|
fakeUri.givenNonHierarchical()
|
||||||
|
|
||||||
|
val result = uriFilenameResolver.getFilenameFromUri(fakeUri.instance)
|
||||||
|
|
||||||
|
result shouldBeEqualTo null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a non content schema Uri when querying file name then returns last segment`() {
|
||||||
|
fakeUri.givenContent(schema = "file", path = "path/to/$A_LAST_SEGMENT")
|
||||||
|
|
||||||
|
val result = uriFilenameResolver.getFilenameFromUri(fakeUri.instance)
|
||||||
|
|
||||||
|
result shouldBeEqualTo A_LAST_SEGMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given content schema Uri with no backing content when querying file name then returns last segment`() {
|
||||||
|
fakeUri.givenContent(schema = "content", path = "path/to/$A_LAST_SEGMENT")
|
||||||
|
fakeContentResolver.givenUriResult(fakeUri.instance, null)
|
||||||
|
|
||||||
|
val result = uriFilenameResolver.getFilenameFromUri(fakeUri.instance)
|
||||||
|
|
||||||
|
result shouldBeEqualTo A_LAST_SEGMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given content schema Uri with empty backing content when querying file name then returns last segment`() {
|
||||||
|
fakeUri.givenContent(schema = "content", path = "path/to/$A_LAST_SEGMENT")
|
||||||
|
val emptyCursor = FakeCursor().also { it.givenEmpty() }
|
||||||
|
fakeContentResolver.givenUriResult(fakeUri.instance, emptyCursor.instance)
|
||||||
|
|
||||||
|
val result = uriFilenameResolver.getFilenameFromUri(fakeUri.instance)
|
||||||
|
|
||||||
|
result shouldBeEqualTo A_LAST_SEGMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given content schema Uri with backing content when querying file name then returns display name column`() {
|
||||||
|
fakeUri.givenContent(schema = "content", path = "path/to/$A_DISPLAY_NAME")
|
||||||
|
val aCursor = FakeCursor().also { it.givenString(OpenableColumns.DISPLAY_NAME, A_DISPLAY_NAME) }
|
||||||
|
fakeContentResolver.givenUriResult(fakeUri.instance, aCursor.instance)
|
||||||
|
|
||||||
|
val result = uriFilenameResolver.getFilenameFromUri(fakeUri.instance)
|
||||||
|
|
||||||
|
result shouldBeEqualTo A_DISPLAY_NAME
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,8 +25,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
fun String.trimIndentOneLine() = trimIndent().replace("\n", "")
|
fun String.trimIndentOneLine() = trimIndent().replace("\n", "")
|
||||||
|
|
||||||
fun <S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents> VectorViewModel<S, VA, VE>.test(coroutineScope: CoroutineScope): ViewModelTest<S, VE> {
|
fun <S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents> VectorViewModel<S, VA, VE>.test(coroutineScope: CoroutineScope): ViewModelTest<S, VE> {
|
||||||
// val state = { com.airbnb.mvrx.withState(this) { it } }
|
|
||||||
|
|
||||||
val state = stateFlow.test(coroutineScope)
|
val state = stateFlow.test(coroutineScope)
|
||||||
val viewEvents = viewEvents.stream().test(coroutineScope)
|
val viewEvents = viewEvents.stream().test(coroutineScope)
|
||||||
return ViewModelTest(state, viewEvents)
|
return ViewModelTest(state, viewEvents)
|
||||||
|
@ -37,16 +35,31 @@ class ViewModelTest<S, VE>(
|
||||||
val viewEvents: FlowTestObserver<VE>
|
val viewEvents: FlowTestObserver<VE>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun assertNoEvents(): ViewModelTest<S, VE> {
|
||||||
|
viewEvents.assertNoValues()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun assertEvents(vararg expected: VE): ViewModelTest<S, VE> {
|
fun assertEvents(vararg expected: VE): ViewModelTest<S, VE> {
|
||||||
viewEvents.assertValues(*expected)
|
viewEvents.assertValues(*expected)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun assertEvent(position: Int = 0, predicate: (VE) -> Boolean): ViewModelTest<S, VE> {
|
||||||
|
viewEvents.assertValue(position, predicate)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun assertStates(vararg expected: S): ViewModelTest<S, VE> {
|
fun assertStates(vararg expected: S): ViewModelTest<S, VE> {
|
||||||
states.assertValues(*expected)
|
states.assertValues(*expected)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun assertStates(expected: List<S>): ViewModelTest<S, VE> {
|
||||||
|
states.assertValues(expected)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun assertState(expected: S): ViewModelTest<S, VE> {
|
fun assertState(expected: S): ViewModelTest<S, VE> {
|
||||||
states.assertValues(expected)
|
states.assertValues(expected)
|
||||||
return this
|
return this
|
||||||
|
|
|
@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
|
||||||
fun <T> Flow<T>.test(scope: CoroutineScope): FlowTestObserver<T> {
|
fun <T> Flow<T>.test(scope: CoroutineScope): FlowTestObserver<T> {
|
||||||
return FlowTestObserver(scope, this)
|
return FlowTestObserver(scope, this)
|
||||||
|
@ -37,13 +38,17 @@ class FlowTestObserver<T>(
|
||||||
values.add(it)
|
values.add(it)
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
|
|
||||||
fun assertNoValues(): FlowTestObserver<T> {
|
fun assertNoValues() = assertValues(emptyList())
|
||||||
assertEquals(emptyList<T>(), this.values)
|
|
||||||
|
fun assertValues(vararg values: T) = assertValues(values.toList())
|
||||||
|
|
||||||
|
fun assertValue(position: Int, predicate: (T) -> Boolean): FlowTestObserver<T> {
|
||||||
|
assertTrue(predicate(values[position]))
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertValues(vararg values: T): FlowTestObserver<T> {
|
fun assertValues(values: List<T>): FlowTestObserver<T> {
|
||||||
assertEquals(values.toList(), this.values)
|
assertEquals(values, this.values)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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.ContentResolver
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeContentResolver {
|
||||||
|
|
||||||
|
val instance = mockk<ContentResolver>()
|
||||||
|
|
||||||
|
fun givenUriResult(uri: Uri, cursor: Cursor?) {
|
||||||
|
every { instance.query(uri, null, null, null, null) } returns cursor
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,9 +24,10 @@ import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
class FakeContext {
|
class FakeContext(
|
||||||
|
private val contentResolver: ContentResolver = mockk()
|
||||||
|
) {
|
||||||
|
|
||||||
private val contentResolver = mockk<ContentResolver>()
|
|
||||||
val instance = mockk<Context>()
|
val instance = mockk<Context>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
43
vector/src/test/java/im/vector/app/test/fakes/FakeCursor.kt
Normal file
43
vector/src/test/java/im/vector/app/test/fakes/FakeCursor.kt
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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.database.Cursor
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeCursor {
|
||||||
|
|
||||||
|
val instance = mockk<Cursor>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { instance.close() } answers {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenEmpty() {
|
||||||
|
every { instance.count } returns 0
|
||||||
|
every { instance.moveToFirst() } returns false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenString(columnName: String, content: String?) {
|
||||||
|
val columnId = columnName.hashCode()
|
||||||
|
every { instance.moveToFirst() } returns true
|
||||||
|
every { instance.isNull(columnId) } returns (content == null)
|
||||||
|
every { instance.getColumnIndex(columnName) } returns columnId
|
||||||
|
every { instance.getString(columnId) } returns content
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,18 +16,27 @@
|
||||||
|
|
||||||
package im.vector.app.test.fakes
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||||
|
|
||||||
class FakeProfileService : ProfileService by mockk() {
|
class FakeProfileService : ProfileService by mockk(relaxed = true) {
|
||||||
|
|
||||||
private var setDisplayNameError: Throwable? = null
|
|
||||||
|
|
||||||
override suspend fun setDisplayName(userId: String, newDisplayName: String) {
|
|
||||||
setDisplayNameError?.let { throw it }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun givenSetDisplayNameErrors(errorCause: RuntimeException) {
|
fun givenSetDisplayNameErrors(errorCause: RuntimeException) {
|
||||||
setDisplayNameError = errorCause
|
coEvery { setDisplayName(any(), any()) } throws errorCause
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenUpdateAvatarErrors(errorCause: RuntimeException) {
|
||||||
|
coEvery { updateAvatar(any(), any(), any()) } throws errorCause
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyUpdatedName(userId: String, newName: String) {
|
||||||
|
coVerify { setDisplayName(userId, newName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyAvatarUpdated(userId: String, newAvatarUri: Uri, fileName: String) {
|
||||||
|
coVerify { updateAvatar(userId, newAvatarUri, fileName) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.test.fakes
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import im.vector.app.core.extensions.vectorStore
|
import im.vector.app.core.extensions.vectorStore
|
||||||
import im.vector.app.features.session.VectorSessionStore
|
import im.vector.app.features.session.VectorSessionStore
|
||||||
import im.vector.app.test.testCoroutineDispatchers
|
import im.vector.app.test.testCoroutineDispatchers
|
||||||
|
@ -34,10 +35,13 @@ class FakeSession(
|
||||||
mockkStatic("im.vector.app.core.extensions.SessionKt")
|
mockkStatic("im.vector.app.core.extensions.SessionKt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val myUserId: String = "@fake:server.fake"
|
||||||
|
|
||||||
override fun cryptoService() = fakeCryptoService
|
override fun cryptoService() = fakeCryptoService
|
||||||
override val sharedSecretStorageService = fakeSharedSecretStorageService
|
override val sharedSecretStorageService = fakeSharedSecretStorageService
|
||||||
override val coroutineDispatchers = testCoroutineDispatchers
|
override val coroutineDispatchers = testCoroutineDispatchers
|
||||||
override suspend fun setDisplayName(userId: String, newDisplayName: String) = fakeProfileService.setDisplayName(userId, newDisplayName)
|
override suspend fun setDisplayName(userId: String, newDisplayName: String) = fakeProfileService.setDisplayName(userId, newDisplayName)
|
||||||
|
override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) = fakeProfileService.updateAvatar(userId, newAvatarUri, fileName)
|
||||||
|
|
||||||
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
|
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
|
||||||
coEvery {
|
coEvery {
|
||||||
|
|
34
vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt
Normal file
34
vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt
Normal 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.test.fakes
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeUri {
|
||||||
|
val instance = mockk<Uri>()
|
||||||
|
|
||||||
|
fun givenNonHierarchical() {
|
||||||
|
givenContent(schema = "mail", path = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenContent(schema: String, path: String?) {
|
||||||
|
every { instance.scheme } returns schema
|
||||||
|
every { instance.path } returns path
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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.net.Uri
|
||||||
|
import im.vector.app.features.onboarding.UriFilenameResolver
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeUriFilenameResolver {
|
||||||
|
|
||||||
|
val instance = mockk<UriFilenameResolver>()
|
||||||
|
|
||||||
|
fun givenFilename(uri: Uri, name: String?) {
|
||||||
|
every { instance.getFilenameFromUri(uri) } returns name
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue