Skip to content

Commit

Permalink
feat: add use case to download assets by its asset key/id (AR-1094) (#…
Browse files Browse the repository at this point in the history
…243)

* feat: create basic structure to update self user, calling api endpoint

* feat: map user data from api and persist into db entity

* feat: uploaded data and linking to user self

* chore: fix tests and configure primiteve adapter for native sq to kotlin types

* chore: fix tests for uploadavatar case, mocks

* refactor: rename of user on persistence module to userentity agreed suffix

* fix: persiste userentity on remote update success

* fix: add comment about not mapped field yet

* fix: wip model asset entity

* feat: add mapping and dao to persist an asset

* feat: map and store the uploaded asset into the table

* test: fix broken test

* feat: add mapping for FKs and define strategy to fetch/download pics

* feat: add logic to persist assets from users response and also after uploading an asset

* feat: change strategy for fk on users, assets

* test: fix tests as vargars was causing issues with mockative

* chore: add todo task to be refined later

* chore: fix reference on cli proj

* chore: fix tests on network module

* feat: add usecase base call to download a public asset

* feature: add map of pictures connections users

* feature: refactor map func

* feature: add map of pictures connections users

* feature: refactor map func

* feature: rename asset usecase for public assets

* test:  fix broken test

* fix: refactor code to use wrapApiRequest

* fix: fix cli module ref

* chore: add test coverage for assetsrepository

* chore: refactor naming of functions and add func to persist asset data

* chore: address pr comments, remove md5 field and calulate it

* chore: address pr comments, remove field from db

* chore: reference on imports fixed

* chore: fix test reference

* chore: fix test reference

* chore: add tests for assetrepository with new structure

* chore: fix returns value from usecase

* chore: refactor asset table to have minimal fields, metadata will be needed for assetmsgs

* chore: rename mappers functions and todo added

* chore: refactor, apply changes to sync contacts pics on one step donwload

* chore: make rawdata notnullable

* chore: change input and return of usecase to not expose either

* chore: fix pr comments, applying better naming

* chore: fix test ref

* chore: fix broken test

* chore: remove hard ref FK on users/assets to allow sync contacts on demand download

* chore: add cause to failure result on usecases for avatar

* fix: returns uploaded assetid in usecase

* enhancement: adds documentation to public interfaces
  • Loading branch information
yamilmedina authored Mar 9, 2022
1 parent 376c349 commit f14fc7a
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package com.wire.kalium.presentation

import android.graphics.BitmapFactory
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import com.wire.kalium.KaliumApplication
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.configuration.ServerConfig
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.user.SelfUser
import com.wire.kalium.logic.feature.asset.PublicAssetResult
import com.wire.kalium.logic.feature.auth.AuthSession
import com.wire.kalium.logic.feature.auth.AuthenticationResult
import com.wire.kalium.logic.feature.auth.AuthenticationScope
Expand All @@ -31,10 +40,20 @@ class MainActivity : ComponentActivity() {
login(coreLogic.getAuthenticationScope())?.let {
val session = coreLogic.getSessionScope(it)
val conversations = session.conversations.getConversations().first()

// Uploading image code
// val imageContent = applicationContext.assets.open("moon1.jpg").readBytes()
// session.users.uploadUserAvatar("image/jpg", imageContent)

val selfUser = session.users.getSelfUser().first()

val avatarAsset = when (val publicAsset = session.users.getPublicAsset(selfUser.previewPicture.toString())) {
is PublicAssetResult.Success -> publicAsset.asset
else -> null
}

setContent {
MainLayout(conversations, selfUser)
MainLayout(conversations, selfUser, avatarAsset)
}
}
}
Expand All @@ -54,11 +73,26 @@ class MainActivity : ComponentActivity() {
}

@Composable
fun MainLayout(conversations: List<Conversation>, selfUser: SelfUser) {
fun MainLayout(conversations: List<Conversation>, selfUser: SelfUser, avatarAsset: ByteArray?) {
Column {
Text("Conversation count:")
Text("${conversations.size}")
Text("SelfUser:")
Text("$selfUser")

Divider(
modifier = Modifier.fillMaxWidth(),
thickness = 0.5.dp
)

Text(text = "Avatar picture:")

avatarAsset?.let { byteArray ->
Image(
bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)?.asImageBitmap()!!,
contentDescription = "",
modifier = Modifier.size(300.dp)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package com.wire.kalium.logic.data.asset

import com.wire.kalium.cryptography.utils.calcMd5
import com.wire.kalium.logic.data.user.UserAssetId
import com.wire.kalium.network.api.asset.AssetMetadataRequest
import com.wire.kalium.network.api.asset.AssetResponse
import com.wire.kalium.network.api.model.AssetRetentionType
import com.wire.kalium.persistence.dao.asset.AssetEntity
import io.ktor.utils.io.core.toByteArray
import kotlinx.datetime.Clock

interface AssetMapper {
fun toMetadataApiModel(uploadAssetMetadata: UploadAssetData): AssetMetadataRequest
fun fromApiUploadResponseToDomainModel(asset: AssetResponse): UploadedAssetId
fun fromUploadedAssetToDaoModel(uploadAssetData: UploadAssetData, uploadedAssetResponse: AssetResponse): AssetEntity
fun fromUserAssetIdToDaoModel(assetId: UserAssetId): AssetEntity
fun fromUserAssetToDaoModel(assetKey: String, data: ByteArray): AssetEntity
}

class AssetMapperImpl : AssetMapper {
Expand All @@ -34,12 +32,18 @@ class AssetMapperImpl : AssetMapper {
key = uploadedAssetResponse.key,
domain = uploadedAssetResponse.domain,
mimeType = uploadAssetData.mimeType.name,
rawData = uploadAssetData.data, // should use something like byteArray to encrypt aes256cbc
rawData = uploadAssetData.data,
downloadedDate = Clock.System.now().toEpochMilliseconds()
)
}

override fun fromUserAssetIdToDaoModel(assetId: UserAssetId): AssetEntity {
return AssetEntity(assetId.toString(), "", null, "".toByteArray(), null)
override fun fromUserAssetToDaoModel(assetKey: String, data: ByteArray): AssetEntity {
return AssetEntity(
key = assetKey,
domain = "", // is it possible to know this on contacts sync avatars ?
mimeType = "",
rawData = data,
downloadedDate = Clock.System.now().toEpochMilliseconds()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import com.wire.kalium.logic.functional.suspending
import com.wire.kalium.logic.wrapApiRequest
import com.wire.kalium.network.api.asset.AssetApi
import com.wire.kalium.persistence.dao.asset.AssetDAO
import kotlinx.coroutines.flow.firstOrNull

interface AssetRepository {
suspend fun uploadAndPersistPublicAsset(uploadAssetData: UploadAssetData): Either<CoreFailure, UploadedAssetId>
suspend fun saveUserPictureAsset(assetId: List<UserAssetId>): Either<CoreFailure, Unit>
suspend fun downloadPublicAsset(assetKey: String): Either<CoreFailure, ByteArray>
suspend fun downloadUsersPictureAssets(assetId: List<UserAssetId?>): Either<CoreFailure, Unit>
}

internal class AssetDataSource(
Expand All @@ -20,21 +22,36 @@ internal class AssetDataSource(
private val assetDao: AssetDAO
) : AssetRepository {

override suspend fun uploadAndPersistPublicAsset(uploadAssetData: UploadAssetData): Either<NetworkFailure, UploadedAssetId> = suspending {
wrapApiRequest {
assetMapper.toMetadataApiModel(uploadAssetData).let { metaData ->
assetApi.uploadAsset(metaData, uploadAssetData.data)
override suspend fun uploadAndPersistPublicAsset(uploadAssetData: UploadAssetData): Either<NetworkFailure, UploadedAssetId> =
suspending {
wrapApiRequest {
// we should also consider for images, the compression for preview vs complete picture
assetMapper.toMetadataApiModel(uploadAssetData).let { metaData ->
assetApi.uploadAsset(metaData, uploadAssetData.data)
}
}.map { assetResponse ->
val assetEntity = assetMapper.fromUploadedAssetToDaoModel(uploadAssetData, assetResponse)
assetDao.insertAsset(assetEntity)
assetMapper.fromApiUploadResponseToDomainModel(assetResponse)
}
}.map { assetResponse ->
val assetEntity = assetMapper.fromUploadedAssetToDaoModel(uploadAssetData, assetResponse)
assetDao.insertAsset(assetEntity)
assetMapper.fromApiUploadResponseToDomainModel(assetResponse)
}

override suspend fun downloadPublicAsset(assetKey: String): Either<CoreFailure, ByteArray> = suspending {
val persistedAsset = assetDao.getAssetByKey(assetKey).firstOrNull()
if (persistedAsset != null) return@suspending Either.Right(persistedAsset.rawData)

wrapApiRequest {
assetApi.downloadAsset(assetKey, null)
}.map { assetData ->
assetDao.insertAsset(assetMapper.fromUserAssetToDaoModel(assetKey, assetData))
assetData
}
}

override suspend fun saveUserPictureAsset(assetId: List<UserAssetId>): Either<CoreFailure, Unit> = suspending {
// TODO: on next PR we should download immediately the asset data and persist it
assetDao.insertAssets(assetId.map { assetMapper.fromUserAssetIdToDaoModel(it) })
override suspend fun downloadUsersPictureAssets(assetId: List<UserAssetId?>): Either<CoreFailure, Unit> = suspending {
assetId.filterNotNull().forEach {
downloadPublicAsset(it)
}
return@suspending Either.Right(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ data class SelfUser(
val phone: String?,
val accentId: Int,
val team: String?,
val previewPicture: UserAssetId,
val completePicture: UserAssetId
val previewPicture: UserAssetId?,
val completePicture: UserAssetId?
) : User()

typealias UserAssetId = String?
typealias UserAssetId = String
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import com.wire.kalium.network.api.user.self.SelfApi
import com.wire.kalium.network.utils.isSuccessful
import com.wire.kalium.persistence.dao.MetadataDAO
import com.wire.kalium.persistence.dao.UserDAO
import com.wire.kalium.persistence.dao.UserEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
Expand All @@ -30,7 +29,7 @@ interface UserRepository {
suspend fun fetchKnownUsers(): Either<CoreFailure, Unit>
suspend fun fetchUsersByIds(ids: Set<UserId>): Either<CoreFailure, Unit>
suspend fun getSelfUser(): Flow<SelfUser>
suspend fun updateSelfUser(newName: String? = null, newAccent: Int? = null, newAssetId: String? = null): Either<CoreFailure, Unit>
suspend fun updateSelfUser(newName: String? = null, newAccent: Int? = null, newAssetId: String? = null): Either<CoreFailure, SelfUser>
}

class UserDataSource(
Expand All @@ -49,7 +48,7 @@ class UserDataSource(
}.coFold({
Either.Left(it)
}, { user ->
assetRepository.saveUserPictureAsset(listOf(user.previewAssetId, user.completeAssetId))
assetRepository.downloadUsersPictureAssets(listOf(user.previewAssetId, user.completeAssetId))
userDAO.insertUser(user)
metadataDAO.insertValue(Json.encodeToString(user.id), SELF_USER_ID_KEY)
Either.Right(Unit)
Expand All @@ -70,10 +69,7 @@ class UserDataSource(
}.coFold({
Either.Left(it)
}, {
val usersToBePersisted = it.map(userMapper::fromApiModelToDaoModel)
// Save (in case there is no data) a reference to the asset id (profile picture)
assetRepository.saveUserPictureAsset(mapAssetsForUsersToBePersisted(usersToBePersisted))
userDAO.insertUsers(usersToBePersisted)
userDAO.insertUsers(it.map(userMapper::fromApiModelToDaoModel))
Either.Right(Unit)
})
}
Expand All @@ -88,25 +84,17 @@ class UserDataSource(
}
}

private fun mapAssetsForUsersToBePersisted(usersToBePersisted: List<UserEntity>): List<UserAssetId> {
val assetsId = mutableListOf<UserAssetId>()
usersToBePersisted.map {
assetsId.add(it.completeAssetId)
assetsId.add(it.previewAssetId)
}
return assetsId
}

override suspend fun updateSelfUser(newName: String?, newAccent: Int?, newAssetId: String?): Either<CoreFailure, Unit> {
override suspend fun updateSelfUser(newName: String?, newAccent: Int?, newAssetId: String?): Either<CoreFailure, SelfUser> {
val user =
getSelfUser().firstOrNull() ?: return Either.Left(CoreFailure.Unknown(NullPointerException()))

val updateRequest = userMapper.fromModelToUpdateApiModel(user, newName, newAccent, newAssetId)
val updatedSelf = selfApi.updateSelf(updateRequest)

return if (updatedSelf.isSuccessful()) {
userDAO.updateUser(userMapper.fromUpdateRequestToDaoModel(user, updateRequest))
Either.Right(Unit)
val updatedUser = userMapper.fromUpdateRequestToDaoModel(user, updateRequest)
userDAO.updateUser(updatedUser)
Either.Right(userMapper.fromDaoModel(updatedUser))
} else {
Either.Left(CoreFailure.Unknown(IllegalStateException()))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.wire.kalium.logic.feature.asset

import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.data.asset.AssetRepository
import com.wire.kalium.logic.data.user.UserAssetId
import com.wire.kalium.logic.functional.suspending

interface GetPublicAssetUseCase {
/**
* Function that enables downloading a public asset by its asset-key, mostly used for avatar pictures
* Internally, if the asset doesn't exist locally, this will download it first and then return it
*
* @param assetKey the asset identifier
* @return PublicAssetResult with a [ByteArray] in case of success or [CoreFailure] on failure
*/
suspend operator fun invoke(assetKey: UserAssetId): PublicAssetResult
}

internal class GetPublicAssetUseCaseImpl(private val assetDataSource: AssetRepository) : GetPublicAssetUseCase {
override suspend fun invoke(assetKey: UserAssetId): PublicAssetResult = suspending {
assetDataSource.downloadPublicAsset(assetKey).fold({
PublicAssetResult.Failure(it)
}) {
PublicAssetResult.Success(it)
}
}
}

sealed class PublicAssetResult {
class Success(val asset: ByteArray) : PublicAssetResult()
class Failure(val coreFailure: CoreFailure) : PublicAssetResult()
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.wire.kalium.logic.feature.user

import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.data.asset.AssetRepository
import com.wire.kalium.logic.data.asset.ImageAsset
import com.wire.kalium.logic.data.asset.RetentionType
import com.wire.kalium.logic.data.asset.UploadAssetData
import com.wire.kalium.logic.data.user.UserAssetId
import com.wire.kalium.logic.data.user.UserRepository
import com.wire.kalium.logic.functional.suspending

Expand All @@ -14,11 +16,12 @@ interface UploadUserAvatarUseCase {
*
* @param mimeType mimetype of the user picture
* @param imageData binary data of the actual picture
* @return UploadAvatarResult with [UserAssetId] in case of success or [CoreFailure] on failure
*/
suspend operator fun invoke(mimeType: String, imageData: ByteArray): UploadAvatarResult
}

class UploadUserAvatarUseCaseImpl(
internal class UploadUserAvatarUseCaseImpl(
private val userDataSource: UserRepository,
private val assetDataSource: AssetRepository
) : UploadUserAvatarUseCase {
Expand All @@ -29,14 +32,14 @@ class UploadUserAvatarUseCaseImpl(
.flatMap { asset ->
userDataSource.updateSelfUser(newAssetId = asset.key)
}.fold({
UploadAvatarResult.Failure
}) {
UploadAvatarResult.Success
UploadAvatarResult.Failure(it)
}) { updatedUser ->
UploadAvatarResult.Success(updatedUser.completePicture!!)
} // todo: remove old assets, non blocking this response, as will imply deleting locally and remotely
}
}

sealed class UploadAvatarResult {
object Success : UploadAvatarResult()
object Failure : UploadAvatarResult()
class Success(val userAssetId: UserAssetId) : UploadAvatarResult()
class Failure(val coreFailure: CoreFailure) : UploadAvatarResult()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.wire.kalium.logic.feature.user

import com.wire.kalium.logic.data.asset.AssetRepository
import com.wire.kalium.logic.data.user.UserRepository
import com.wire.kalium.logic.feature.asset.GetPublicAssetUseCaseImpl
import com.wire.kalium.logic.feature.asset.GetPublicAssetUseCase
import com.wire.kalium.logic.sync.SyncManager

class UserScope(
Expand All @@ -13,4 +15,5 @@ class UserScope(
val syncSelfUser: SyncSelfUserUseCase get() = SyncSelfUserUseCase(userRepository)
val syncContacts: SyncContactsUseCase get() = SyncContactsUseCaseImpl(userRepository)
val uploadUserAvatar: UploadUserAvatarUseCase get() = UploadUserAvatarUseCaseImpl(userRepository, assetRepository)
val getPublicAsset: GetPublicAssetUseCase get() = GetPublicAssetUseCaseImpl(assetRepository)
}
Loading

0 comments on commit f14fc7a

Please sign in to comment.