Skip to content

Commit

Permalink
fix: crash when uploading avatar [WPB-5965] (#2674)
Browse files Browse the repository at this point in the history
Co-authored-by: Michał Saleniuk <[email protected]>
Co-authored-by: Michał Saleniuk <[email protected]>
  • Loading branch information
3 people authored Feb 8, 2024
1 parent 9a72c32 commit ecb7ff1
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class AvatarPickerViewModel @Inject constructor(
var pictureState by mutableStateOf<PictureState>(PictureState.Empty)
private set

private var initialPictureLoadingState by mutableStateOf<InitialPictureLoadingState>(InitialPictureLoadingState.None)

private val _infoMessage = MutableSharedFlow<UIText>()
val infoMessage = _infoMessage.asSharedFlow()

Expand All @@ -75,24 +77,27 @@ class AvatarPickerViewModel @Inject constructor(

val temporaryAvatarUri: Uri = avatarImageManager.getShareableTempAvatarUri(defaultAvatarPath)

private lateinit var currentAvatarUri: Uri

init {
loadInitialAvatarState()
}

@Suppress("TooGenericExceptionCaught")
fun loadInitialAvatarState() {
viewModelScope.launch {
initialPictureLoadingState = InitialPictureLoadingState.Loading
try {
dataStore.avatarAssetId.first()?.apply {
val qualifiedAsset = qualifiedIdMapper.fromStringToQualifiedID(this)
val avatarRawPath = (getAvatarAsset(assetKey = qualifiedAsset) as PublicAssetResult.Success).assetPath
currentAvatarUri = avatarImageManager.getWritableAvatarUri(avatarRawPath)

pictureState = PictureState.Initial(currentAvatarUri)
val currentAvatarUri = avatarImageManager.getWritableAvatarUri(avatarRawPath)
initialPictureLoadingState = InitialPictureLoadingState.Loaded(currentAvatarUri)
if (pictureState is PictureState.Empty) {
pictureState = PictureState.Initial(currentAvatarUri)
}
}
} catch (e: ClassCastException) {
} catch (e: Exception) {
appLogger.e("There was an error loading the user avatar", e)
initialPictureLoadingState = InitialPictureLoadingState.None
}
}
}
Expand All @@ -109,7 +114,6 @@ class AvatarPickerViewModel @Inject constructor(

val avatarPath = defaultAvatarPath
val imageDataSize = imgUri.toByteArray(appContext, dispatchers).size.toLong()

when (val result = uploadUserAvatar(avatarPath, imageDataSize)) {
is UploadAvatarResult.Success -> {
dataStore.updateUserAvatarAssetId(result.userAssetId.toString())
Expand All @@ -120,7 +124,12 @@ class AvatarPickerViewModel @Inject constructor(
is NetworkFailure.NoNetworkConnection -> showInfoMessage(InfoMessageType.NoNetworkError)
else -> showInfoMessage(InfoMessageType.UploadAvatarError)
}
pictureState = PictureState.Initial(currentAvatarUri)
with(initialPictureLoadingState) {
pictureState = when (this) {
is InitialPictureLoadingState.Loaded -> PictureState.Initial(avatarUri)
else -> PictureState.Empty
}
}
}
}
}
Expand All @@ -130,16 +139,23 @@ class AvatarPickerViewModel @Inject constructor(
_infoMessage.emit(type.uiText)
}

@Stable
private sealed class InitialPictureLoadingState {
data object None : InitialPictureLoadingState()
data object Loading : InitialPictureLoadingState()
data class Loaded(val avatarUri: Uri) : InitialPictureLoadingState()
}

@Stable
sealed class PictureState(open val avatarUri: Uri) {
data class Uploading(override val avatarUri: Uri) : PictureState(avatarUri)
data class Initial(override val avatarUri: Uri) : PictureState(avatarUri)
data class Picked(override val avatarUri: Uri) : PictureState(avatarUri)
object Empty : PictureState("".toUri())
data object Empty : PictureState("".toUri())
}

sealed class InfoMessageType(override val uiText: UIText) : SnackBarMessage {
object UploadAvatarError : InfoMessageType(UIText.StringResource(R.string.error_uploading_user_avatar))
object NoNetworkError : InfoMessageType(UIText.StringResource(R.string.error_no_network_message))
data object UploadAvatarError : InfoMessageType(UIText.StringResource(R.string.error_uploading_user_avatar))
data object NoNetworkError : InfoMessageType(UIText.StringResource(R.string.error_no_network_message))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import okio.buffer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

Expand Down Expand Up @@ -112,6 +113,32 @@ class AvatarPickerViewModelTest {
}
}

@Test
fun `given current avatar download failed, when uploading the asset fails, then set state as Empty`() = runTest {
// Given
val (arrangement, avatarPickerViewModel) = Arrangement()
.withFailedInitialAvatarLoad()
.withErrorUploadResponse()
.arrange()
// When
avatarPickerViewModel.uploadNewPickedAvatar(arrangement.onSuccess)
// Then
assertInstanceOf(AvatarPickerViewModel.PictureState.Empty::class.java, avatarPickerViewModel.pictureState)
}

@Test
fun `given current avatar download succeeded, when uploading the asset fails, then set state as Initial`() = runTest {
// Given
val (arrangement, avatarPickerViewModel) = Arrangement()
.withSuccessfulInitialAvatarLoad()
.withErrorUploadResponse()
.arrange()
// When
avatarPickerViewModel.uploadNewPickedAvatar(arrangement.onSuccess)
// Then
assertInstanceOf(AvatarPickerViewModel.PictureState.Initial::class.java, avatarPickerViewModel.pictureState)
}

private class Arrangement {

val userDataStore = mockk<UserDataStore>()
Expand Down Expand Up @@ -146,9 +173,12 @@ class AvatarPickerViewModelTest {

private val mockUri = mockk<Uri>()

init {
MockKAnnotations.init(this, relaxUnitFun = true)
}

fun withSuccessfulInitialAvatarLoad(): Arrangement {
val avatarAssetId = "avatar-value@avatar-domain"
MockKAnnotations.init(this, relaxUnitFun = true)
mockkStatic(Uri::class)
mockkStatic(Uri::resampleImageAndCopyToTempPath)
mockkStatic(Uri::toByteArray)
Expand All @@ -169,6 +199,16 @@ class AvatarPickerViewModelTest {
return this
}

fun withFailedInitialAvatarLoad(): Arrangement {
val avatarAssetId = "avatar-value@avatar-domain"
coEvery { getAvatarAsset(any()) } returns PublicAssetResult.Failure(Unknown(RuntimeException("some error")), false)
coEvery { avatarImageManager.getShareableTempAvatarUri(any()) } returns mockUri
every { userDataStore.avatarAssetId } returns flow { emit(avatarAssetId) }
every { qualifiedIdMapper.fromStringToQualifiedID(any()) } returns QualifiedID("avatar-value", "avatar-domain")

return this
}

fun withSuccessfulAvatarUpload(expectedUserAssetId: UserAssetId): Arrangement {
coEvery { userDataStore.updateUserAvatarAssetId(any()) } returns Unit
coEvery { uploadUserAvatarUseCase(any(), any()) } returns UploadAvatarResult.Success(expectedUserAssetId)
Expand Down

0 comments on commit ecb7ff1

Please sign in to comment.