mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
Feature/save avatar image from middleware
This commit is contained in:
parent
654f56e77e
commit
90da3e6149
39 changed files with 535 additions and 80 deletions
|
@ -4,6 +4,7 @@ apply plugin: 'kotlin-android-extensions'
|
|||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'io.fabric'
|
||||
|
||||
|
||||
apply from: "$rootDir/versioning.gradle"
|
||||
|
||||
android {
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".app.AndroidApplication"
|
||||
|
|
|
@ -3,6 +3,8 @@ package com.agileburo.anytype.di.feature
|
|||
import com.agileburo.anytype.core_utils.di.scope.PerScreen
|
||||
import com.agileburo.anytype.domain.auth.repo.AuthRepository
|
||||
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
|
||||
import com.agileburo.anytype.domain.image.ImageLoader
|
||||
import com.agileburo.anytype.domain.image.LoadImage
|
||||
import com.agileburo.anytype.presentation.desktop.DesktopViewModelFactory
|
||||
import com.agileburo.anytype.ui.desktop.DesktopFragment
|
||||
import dagger.Module
|
||||
|
@ -31,9 +33,11 @@ class DesktopModule {
|
|||
@Provides
|
||||
@PerScreen
|
||||
fun provideDesktopViewModelFactory(
|
||||
getAccount: GetAccount
|
||||
getAccount: GetAccount,
|
||||
loadImage: LoadImage
|
||||
): DesktopViewModelFactory = DesktopViewModelFactory(
|
||||
getAccount = getAccount
|
||||
getAccount = getAccount,
|
||||
loadImage = loadImage
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
@ -43,4 +47,12 @@ class DesktopModule {
|
|||
): GetAccount = GetAccount(
|
||||
repository = repository
|
||||
)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideLoadImageUseCase(
|
||||
loader: ImageLoader
|
||||
): LoadImage = LoadImage(
|
||||
loader = loader
|
||||
)
|
||||
}
|
|
@ -3,6 +3,9 @@ package com.agileburo.anytype.di.feature
|
|||
import com.agileburo.anytype.core_utils.di.scope.PerScreen
|
||||
import com.agileburo.anytype.domain.auth.interactor.Logout
|
||||
import com.agileburo.anytype.domain.auth.repo.AuthRepository
|
||||
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
|
||||
import com.agileburo.anytype.domain.image.ImageLoader
|
||||
import com.agileburo.anytype.domain.image.LoadImage
|
||||
import com.agileburo.anytype.presentation.profile.ProfileViewModelFactory
|
||||
import com.agileburo.anytype.ui.profile.ProfileFragment
|
||||
import dagger.Module
|
||||
|
@ -31,16 +34,34 @@ class ProfileModule {
|
|||
@Provides
|
||||
@PerScreen
|
||||
fun provideProfileViewModelFactory(
|
||||
logout: Logout
|
||||
): ProfileViewModelFactory {
|
||||
return ProfileViewModelFactory(
|
||||
logout = logout
|
||||
)
|
||||
}
|
||||
logout: Logout,
|
||||
loadImage: LoadImage,
|
||||
getAccount: GetAccount
|
||||
): ProfileViewModelFactory = ProfileViewModelFactory(
|
||||
logout = logout,
|
||||
loadImage = loadImage,
|
||||
getAccount = getAccount
|
||||
)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideLogoutUseCase(repository: AuthRepository): Logout {
|
||||
return Logout(repository)
|
||||
}
|
||||
fun provideLogoutUseCase(
|
||||
repository: AuthRepository
|
||||
): Logout = Logout(repository)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideLoadImageUseCase(
|
||||
loader: ImageLoader
|
||||
): LoadImage = LoadImage(
|
||||
loader = loader
|
||||
)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideGetAccountUseCase(
|
||||
authRepository: AuthRepository
|
||||
): GetAccount = GetAccount(
|
||||
repository = authRepository
|
||||
)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.agileburo.anytype.di.main
|
||||
|
||||
import com.agileburo.anytype.data.auth.other.ImageDataLoader
|
||||
import com.agileburo.anytype.data.auth.other.ImageLoaderRemote
|
||||
import com.agileburo.anytype.domain.image.ImageLoader
|
||||
import com.agileburo.anytype.middleware.interactor.Middleware
|
||||
import com.agileburo.anytype.middleware.interactor.MiddlewareImageLoader
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class ImageModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideImageLoader(remote: ImageLoaderRemote): ImageLoader {
|
||||
return ImageDataLoader(
|
||||
remote = remote
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideImageLoaderRemote(middleware: Middleware): ImageLoaderRemote {
|
||||
return MiddlewareImageLoader(
|
||||
middleware = middleware
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,10 +1,6 @@
|
|||
package com.agileburo.anytype.di.main
|
||||
|
||||
import com.agileburo.anytype.di.feature.AuthSubComponent
|
||||
import com.agileburo.anytype.di.feature.DesktopSubComponent
|
||||
import com.agileburo.anytype.di.feature.KeychainPhraseSubComponent
|
||||
import com.agileburo.anytype.di.feature.ProfileSubComponent
|
||||
import com.agileburo.anytype.di.feature.SplashSubComponent
|
||||
import com.agileburo.anytype.di.feature.*
|
||||
import dagger.Component
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -12,7 +8,8 @@ import javax.inject.Singleton
|
|||
@Component(
|
||||
modules = [
|
||||
ContextModule::class,
|
||||
DataModule::class
|
||||
DataModule::class,
|
||||
ImageModule::class
|
||||
]
|
||||
)
|
||||
interface MainComponent {
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
package com.agileburo.anytype.ui.auth.account
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI
|
||||
import android.view.View
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_utils.ext.hideKeyboard
|
||||
import com.agileburo.anytype.core_utils.ext.invisible
|
||||
import com.agileburo.anytype.core_utils.ext.parsePath
|
||||
import com.agileburo.anytype.core_utils.ext.visible
|
||||
import com.agileburo.anytype.core_utils.ext.*
|
||||
import com.agileburo.anytype.di.common.componentManager
|
||||
import com.agileburo.anytype.presentation.auth.account.CreateAccountViewModel
|
||||
import com.agileburo.anytype.presentation.auth.account.CreateAccountViewModelFactory
|
||||
|
@ -69,6 +70,22 @@ class CreateAccountFragment : NavigationFragment(R.layout.fragment_create_accoun
|
|||
|
||||
profileIconPlaceholder.invisible()
|
||||
|
||||
val writeExternalStoragePermission = ContextCompat.checkSelfPermission(
|
||||
requireActivity(),
|
||||
WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
if (writeExternalStoragePermission != PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
profileIcon.showSnackbar("Do not have permission")
|
||||
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
arrayOf(WRITE_EXTERNAL_STORAGE),
|
||||
REQUEST_PERMISSION_CODE
|
||||
)
|
||||
}
|
||||
|
||||
vm.onAvatarSet(uri.parsePath(requireContext()))
|
||||
}
|
||||
}
|
||||
|
@ -97,5 +114,6 @@ class CreateAccountFragment : NavigationFragment(R.layout.fragment_create_accoun
|
|||
|
||||
companion object {
|
||||
const val SELECT_IMAGE_CODE = 1
|
||||
const val REQUEST_PERMISSION_CODE = 2
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.agileburo.anytype.ui.desktop
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Observer
|
||||
|
@ -13,9 +14,12 @@ import com.agileburo.anytype.presentation.desktop.DesktopViewModel
|
|||
import com.agileburo.anytype.presentation.desktop.DesktopViewModelFactory
|
||||
import com.agileburo.anytype.presentation.profile.ProfileView
|
||||
import com.agileburo.anytype.ui.base.ViewStateFragment
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.android.synthetic.main.fragment_desktop.*
|
||||
import java.io.ByteArrayInputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class DesktopFragment : ViewStateFragment<ViewState<List<DesktopView>>>(R.layout.fragment_desktop) {
|
||||
|
||||
private val profileObserver = Observer<ProfileView> { profile ->
|
||||
|
@ -44,6 +48,20 @@ class DesktopFragment : ViewStateFragment<ViewState<List<DesktopView>>>(R.layout
|
|||
vm.navigation.observe(this, navObserver)
|
||||
vm.profile.observe(this, profileObserver)
|
||||
vm.onViewCreated()
|
||||
|
||||
vm.image.observe(this, Observer { blob ->
|
||||
|
||||
val stream = ByteArrayInputStream(blob)
|
||||
|
||||
Glide
|
||||
.with(this)
|
||||
.load(BitmapFactory.decodeStream(stream))
|
||||
.centerInside()
|
||||
.circleCrop()
|
||||
.into(profileImage)
|
||||
|
||||
stream.close()
|
||||
})
|
||||
}
|
||||
|
||||
override fun render(state: ViewState<List<DesktopView>>) {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package com.agileburo.anytype.ui.profile
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_utils.ui.ViewState
|
||||
|
@ -10,7 +12,9 @@ import com.agileburo.anytype.presentation.profile.ProfileView
|
|||
import com.agileburo.anytype.presentation.profile.ProfileViewModel
|
||||
import com.agileburo.anytype.presentation.profile.ProfileViewModelFactory
|
||||
import com.agileburo.anytype.ui.base.ViewStateFragment
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.android.synthetic.main.fragment_profile.*
|
||||
import java.io.ByteArrayInputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
class ProfileFragment : ViewStateFragment<ViewState<ProfileView>>(R.layout.fragment_profile) {
|
||||
|
@ -29,6 +33,21 @@ class ProfileFragment : ViewStateFragment<ViewState<ProfileView>>(R.layout.fragm
|
|||
vm.state.observe(this, this)
|
||||
vm.navigation.observe(this, navObserver)
|
||||
vm.onViewCreated()
|
||||
|
||||
vm.image.observe(this, Observer { blob ->
|
||||
|
||||
val stream = ByteArrayInputStream(blob)
|
||||
|
||||
Glide
|
||||
.with(this)
|
||||
.load(BitmapFactory.decodeStream(stream))
|
||||
.centerInside()
|
||||
.circleCrop()
|
||||
.into(pic)
|
||||
|
||||
stream.close()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
override fun render(state: ViewState<ProfileView>) {
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
app:enterAnim="@anim/slide_in_right"
|
||||
app:exitAnim="@anim/slide_out_left"
|
||||
app:popEnterAnim="@anim/slide_in_left"
|
||||
app:popExitAnim="@anim/slide_out_right"
|
||||
android:id="@+id/action_open_profile"
|
||||
app:destination="@id/profileScreen" />
|
||||
app:destination="@id/profileScreen"
|
||||
app:popExitAnim="@anim/slide_out_right" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
@ -56,7 +56,7 @@
|
|||
</fragment>
|
||||
<navigation
|
||||
android:id="@+id/login_nav"
|
||||
app:startDestination="@id/startLoginScreen" >
|
||||
app:startDestination="@id/startLoginScreen">
|
||||
<fragment
|
||||
android:id="@+id/selectAccountScreen"
|
||||
android:name="com.agileburo.anytype.ui.auth.account.SelectAccountFragment"
|
||||
|
@ -119,7 +119,7 @@
|
|||
android:id="@+id/congratulationScreen"
|
||||
android:name="com.agileburo.anytype.ui.auth.CongratulationFragment"
|
||||
android:label="StartLoginFragment"
|
||||
tools:layout="@layout/fragment_congratulation"/>
|
||||
tools:layout="@layout/fragment_congratulation" />
|
||||
<fragment
|
||||
android:id="@+id/setupSelectedAccountScreen"
|
||||
android:name="com.agileburo.anytype.ui.auth.account.SetupSelectedAccountFragment"
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
package com.agileburo.anytype.data.auth.mapper
|
||||
|
||||
import com.agileburo.anytype.data.auth.model.AccountEntity
|
||||
import com.agileburo.anytype.data.auth.model.ImageEntity
|
||||
import com.agileburo.anytype.data.auth.model.WalletEntity
|
||||
import com.agileburo.anytype.domain.auth.model.Account
|
||||
import com.agileburo.anytype.domain.auth.model.Image
|
||||
import com.agileburo.anytype.domain.auth.model.Wallet
|
||||
|
||||
fun AccountEntity.toDomain(): Account {
|
||||
return Account(
|
||||
id = id,
|
||||
name = name
|
||||
name = name,
|
||||
avatar = avatar?.toDomain()
|
||||
)
|
||||
}
|
||||
|
||||
fun Account.toEntity(): AccountEntity {
|
||||
return AccountEntity(
|
||||
id = id,
|
||||
name = name
|
||||
name = name,
|
||||
avatar = avatar?.toEntity()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -23,4 +27,34 @@ fun WalletEntity.toDomain(): Wallet {
|
|||
return Wallet(
|
||||
mnemonic = mnemonic
|
||||
)
|
||||
}
|
||||
|
||||
fun ImageEntity.toDomain(): Image {
|
||||
return Image(
|
||||
id = id,
|
||||
sizes = sizes.map { it.toDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
fun Image.toEntity(): ImageEntity {
|
||||
return ImageEntity(
|
||||
id = id,
|
||||
sizes = sizes.map { it.toEntity() }
|
||||
)
|
||||
}
|
||||
|
||||
fun ImageEntity.Size.toDomain(): Image.Size {
|
||||
return when (this) {
|
||||
ImageEntity.Size.THUMB -> Image.Size.THUMB
|
||||
ImageEntity.Size.LARGE -> Image.Size.LARGE
|
||||
ImageEntity.Size.SMALL -> Image.Size.SMALL
|
||||
}
|
||||
}
|
||||
|
||||
fun Image.Size.toEntity(): ImageEntity.Size {
|
||||
return when (this) {
|
||||
Image.Size.THUMB -> ImageEntity.Size.THUMB
|
||||
Image.Size.SMALL -> ImageEntity.Size.SMALL
|
||||
Image.Size.LARGE -> ImageEntity.Size.LARGE
|
||||
}
|
||||
}
|
|
@ -2,5 +2,6 @@ package com.agileburo.anytype.data.auth.model
|
|||
|
||||
data class AccountEntity(
|
||||
val id: String,
|
||||
val name: String
|
||||
val name: String,
|
||||
val avatar: ImageEntity?
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package com.agileburo.anytype.data.auth.model
|
||||
|
||||
data class ImageEntity(
|
||||
val id: String,
|
||||
val sizes: List<Size>
|
||||
) {
|
||||
enum class Size { SMALL, LARGE, THUMB }
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.agileburo.anytype.data.auth.other
|
||||
|
||||
import com.agileburo.anytype.data.auth.mapper.toEntity
|
||||
import com.agileburo.anytype.domain.auth.model.Image
|
||||
import com.agileburo.anytype.domain.image.ImageLoader
|
||||
|
||||
class ImageDataLoader(
|
||||
private val remote: ImageLoaderRemote
|
||||
) : ImageLoader {
|
||||
|
||||
override suspend fun load(
|
||||
id: String, size: Image.Size
|
||||
): ByteArray = remote.load(
|
||||
id = id,
|
||||
size = size.toEntity()
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.agileburo.anytype.data.auth.other
|
||||
|
||||
import com.agileburo.anytype.data.auth.model.ImageEntity
|
||||
|
||||
interface ImageLoaderRemote {
|
||||
suspend fun load(id: String, size: ImageEntity.Size): ByteArray
|
||||
}
|
|
@ -30,7 +30,7 @@ ext {
|
|||
okhttp_logging_interceptor_version = '3.8.1'
|
||||
rxjava2_version = '2.1.1'
|
||||
moshi_version = '1.8.0'
|
||||
gson_version = '2.8.5'
|
||||
gson_version = '2.8.6'
|
||||
rxrelay_version = '2.1.0'
|
||||
better_link_method_version = '2.2.0'
|
||||
|
||||
|
|
|
@ -2,5 +2,6 @@ package com.agileburo.anytype.domain.auth.model
|
|||
|
||||
data class Account(
|
||||
val id: String,
|
||||
val name: String
|
||||
val name: String,
|
||||
val avatar: Image?
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
package com.agileburo.anytype.domain.image
|
||||
|
||||
import com.agileburo.anytype.domain.auth.model.Image
|
||||
|
||||
|
||||
interface ImageLoader {
|
||||
/**
|
||||
* @param id id of the image
|
||||
* @param size requested image size
|
||||
*/
|
||||
suspend fun load(id: String, size: Image.Size): ByteArray
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.agileburo.anytype.domain.image
|
||||
|
||||
import com.agileburo.anytype.domain.auth.model.Image
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.base.Either
|
||||
|
||||
class LoadImage(private val loader: ImageLoader) : BaseUseCase<ByteArray, LoadImage.Param>() {
|
||||
|
||||
override suspend fun run(params: Param) = try {
|
||||
loader.load(
|
||||
id = params.id,
|
||||
size = params.size
|
||||
).let {
|
||||
Either.Right(it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Either.Left(e)
|
||||
}
|
||||
|
||||
class Param(val id: String, val size: Image.Size = Image.Size.SMALL)
|
||||
|
||||
}
|
|
@ -36,20 +36,26 @@ class CreateAccountTest {
|
|||
|
||||
val name = DataFactory.randomString()
|
||||
|
||||
val path = null
|
||||
|
||||
val account = Account(
|
||||
id = DataFactory.randomUuid(),
|
||||
name = DataFactory.randomString()
|
||||
name = DataFactory.randomString(),
|
||||
avatar = null
|
||||
)
|
||||
|
||||
val param = CreateAccount.Params(name)
|
||||
val param = CreateAccount.Params(
|
||||
name = name,
|
||||
avatarPath = path
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking { createAccount(name) } doReturn account
|
||||
onBlocking { createAccount(name, path) } doReturn account
|
||||
}
|
||||
|
||||
createAccount.run(param)
|
||||
|
||||
verify(repo, times(1)).createAccount(name)
|
||||
verify(repo, times(1)).createAccount(name, path)
|
||||
verify(repo, times(1)).saveAccount(any())
|
||||
verifyNoMoreInteractions(repo)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.agileburo.anytype.middleware
|
||||
|
||||
import anytype.Models
|
||||
import com.agileburo.anytype.data.auth.model.ImageEntity
|
||||
|
||||
fun Models.Image.toEntity(): ImageEntity? {
|
||||
return if (id.isNullOrBlank())
|
||||
null
|
||||
else
|
||||
ImageEntity(
|
||||
id = id,
|
||||
sizes = sizesList.map { size -> size.toEntity() }
|
||||
)
|
||||
}
|
||||
|
||||
fun ImageEntity.Size.toMiddleware(): Models.ImageSize = when (this) {
|
||||
ImageEntity.Size.SMALL -> Models.ImageSize.SMALL
|
||||
ImageEntity.Size.LARGE -> Models.ImageSize.LARGE
|
||||
ImageEntity.Size.THUMB -> Models.ImageSize.THUMB
|
||||
}
|
||||
|
||||
fun Models.ImageSize.toEntity(): ImageEntity.Size = when (this) {
|
||||
Models.ImageSize.SMALL -> ImageEntity.Size.SMALL
|
||||
Models.ImageSize.LARGE -> ImageEntity.Size.LARGE
|
||||
Models.ImageSize.THUMB -> ImageEntity.Size.THUMB
|
||||
else -> throw IllegalStateException("Unexpected image size from middleware")
|
||||
}
|
|
@ -6,8 +6,11 @@ import com.agileburo.anytype.data.auth.repo.AuthRemote
|
|||
import com.agileburo.anytype.middleware.Event
|
||||
import com.agileburo.anytype.middleware.EventProxy
|
||||
import com.agileburo.anytype.middleware.interactor.Middleware
|
||||
import com.agileburo.anytype.middleware.toEntity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AuthMiddleware(
|
||||
private val middleware: Middleware,
|
||||
|
@ -19,18 +22,23 @@ class AuthMiddleware(
|
|||
) = middleware.selectAccount(id, path).let { response ->
|
||||
AccountEntity(
|
||||
id = response.id,
|
||||
name = response.name
|
||||
name = response.name,
|
||||
avatar = null
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createAccount(
|
||||
name: String,
|
||||
avatarPath: String?
|
||||
) = middleware.createAccount(name, avatarPath).let { response ->
|
||||
AccountEntity(
|
||||
id = response.id,
|
||||
name = response.name
|
||||
)
|
||||
) = withContext(Dispatchers.IO) {
|
||||
middleware.createAccount(name, avatarPath).let { response ->
|
||||
AccountEntity(
|
||||
id = response.id,
|
||||
name = response.name,
|
||||
avatar = response.avatar.toEntity()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override suspend fun recoverAccount() {
|
||||
|
@ -44,7 +52,8 @@ class AuthMiddleware(
|
|||
.map { event ->
|
||||
AccountEntity(
|
||||
id = event.id,
|
||||
name = event.name
|
||||
name = event.name,
|
||||
avatar = null
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,10 +10,13 @@ import anytype.Commands.AccountRecoverRequest;
|
|||
import anytype.Commands.AccountRecoverResponse;
|
||||
import anytype.Commands.AccountSelectRequest;
|
||||
import anytype.Commands.AccountSelectResponse;
|
||||
import anytype.Commands.ImageGetBlobRequest;
|
||||
import anytype.Commands.ImageGetBlobResponse;
|
||||
import anytype.Commands.WalletCreateRequest;
|
||||
import anytype.Commands.WalletCreateResponse;
|
||||
import anytype.Commands.WalletRecoverRequest;
|
||||
import anytype.Commands.WalletRecoverResponse;
|
||||
import anytype.Models;
|
||||
import lib.Lib;
|
||||
|
||||
public class Middleware {
|
||||
|
@ -64,7 +67,8 @@ public class Middleware {
|
|||
} else {
|
||||
return new CreateAccountResponse(
|
||||
response.getAccount().getId(),
|
||||
response.getAccount().getName()
|
||||
response.getAccount().getName(),
|
||||
response.getAccount().getAvatar()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -136,4 +140,25 @@ public class Middleware {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] loadImage(String id, Models.ImageSize size) throws Exception {
|
||||
|
||||
ImageGetBlobRequest request = ImageGetBlobRequest
|
||||
.newBuilder()
|
||||
.setId(id)
|
||||
.setSize(size)
|
||||
.build();
|
||||
|
||||
byte[] encodedRequest = request.toByteArray();
|
||||
|
||||
byte[] encodedResponse = Lib.imageGetBlob(encodedRequest);
|
||||
|
||||
ImageGetBlobResponse response = ImageGetBlobResponse.parseFrom(encodedResponse);
|
||||
|
||||
if (response.getError() != null && response.getError().getCode() != ImageGetBlobResponse.Error.Code.NULL) {
|
||||
throw new Exception(response.getError().getDescription());
|
||||
} else {
|
||||
return response.getBlob().toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package com.agileburo.anytype.middleware.interactor
|
||||
|
||||
import com.agileburo.anytype.data.auth.model.ImageEntity
|
||||
import com.agileburo.anytype.data.auth.other.ImageLoaderRemote
|
||||
import com.agileburo.anytype.middleware.toMiddleware
|
||||
|
||||
class MiddlewareImageLoader(private val middleware: Middleware) : ImageLoaderRemote {
|
||||
|
||||
override suspend fun load(
|
||||
id: String, size: ImageEntity.Size
|
||||
): ByteArray = middleware.loadImage(id, size.toMiddleware())
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package com.agileburo.anytype.middleware.model
|
||||
|
||||
import anytype.Models
|
||||
|
||||
class CreateAccountResponse(
|
||||
val id: String,
|
||||
val name: String
|
||||
val name: String,
|
||||
val avatar: Models.Image
|
||||
)
|
|
@ -32,6 +32,7 @@ dependencies {
|
|||
|
||||
implementation applicationDependencies.kotlin
|
||||
implementation applicationDependencies.coroutines
|
||||
implementation applicationDependencies.gson
|
||||
|
||||
implementation databaseDependencies.room
|
||||
implementation databaseDependencies.roomKtx
|
||||
|
|
|
@ -4,16 +4,19 @@ import android.content.Context
|
|||
import androidx.room.Database
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.agileburo.anytype.common.Config
|
||||
import com.agileburo.anytype.common.Provider
|
||||
import com.agileburo.anytype.dao.AccountDao
|
||||
import com.agileburo.anytype.model.AccountTable
|
||||
import com.agileburo.anytype.util.Converters
|
||||
|
||||
@Database(
|
||||
entities = [AccountTable::class],
|
||||
exportSchema = false,
|
||||
version = 1
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AnytypeDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun accountDao(): AccountDao
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.agileburo.anytype.mapper
|
||||
|
||||
import com.agileburo.anytype.data.auth.model.AccountEntity
|
||||
import com.agileburo.anytype.data.auth.model.ImageEntity
|
||||
import com.agileburo.anytype.model.AccountTable
|
||||
|
||||
fun AccountTable.toEntity(): AccountEntity {
|
||||
return AccountEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
avatar = avatar?.let { cached ->
|
||||
ImageEntity(
|
||||
id = cached.avatarId,
|
||||
sizes = cached.sizes.map { it.toEntity() }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun ImageEntity.Size.toTable(): AccountTable.Size = when (this) {
|
||||
ImageEntity.Size.SMALL -> AccountTable.Size.SMALL
|
||||
ImageEntity.Size.THUMB -> AccountTable.Size.THUMB
|
||||
ImageEntity.Size.LARGE -> AccountTable.Size.LARGE
|
||||
}
|
||||
|
||||
fun AccountTable.Size.toEntity(): ImageEntity.Size = when (this) {
|
||||
AccountTable.Size.SMALL -> ImageEntity.Size.SMALL
|
||||
AccountTable.Size.LARGE -> ImageEntity.Size.LARGE
|
||||
AccountTable.Size.THUMB -> ImageEntity.Size.THUMB
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package com.agileburo.anytype.mapper
|
||||
|
||||
import com.agileburo.anytype.data.auth.model.AccountEntity
|
||||
import com.agileburo.anytype.model.AccountTable
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 2019-10-22.
|
||||
*/
|
||||
|
||||
fun AccountTable.toEntity(): AccountEntity =
|
||||
AccountEntity(
|
||||
id = this.id,
|
||||
name = this.name
|
||||
)
|
||||
|
||||
//Todo исправить timestamp
|
||||
fun AccountEntity.toTable(): AccountTable =
|
||||
AccountTable(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
timestamp = 0
|
||||
)
|
|
@ -1,12 +1,26 @@
|
|||
package com.agileburo.anytype.model
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import com.agileburo.anytype.common.Config
|
||||
import com.agileburo.anytype.util.Converters
|
||||
|
||||
@Entity(tableName = Config.ACCOUNT_TABLE_NAME)
|
||||
data class AccountTable(
|
||||
@PrimaryKey val id: String,
|
||||
val name: String,
|
||||
val timestamp: Long
|
||||
)
|
||||
val timestamp: Long,
|
||||
@Embedded val avatar: Avatar? = null
|
||||
) {
|
||||
|
||||
data class Avatar(
|
||||
val avatarId: String,
|
||||
val sizes: List<Size>
|
||||
)
|
||||
|
||||
@TypeConverters(Converters::class)
|
||||
enum class Size { SMALL, LARGE, THUMB }
|
||||
|
||||
}
|
|
@ -5,6 +5,7 @@ import com.agileburo.anytype.data.auth.model.AccountEntity
|
|||
import com.agileburo.anytype.data.auth.repo.AuthCache
|
||||
import com.agileburo.anytype.db.AnytypeDatabase
|
||||
import com.agileburo.anytype.mapper.toEntity
|
||||
import com.agileburo.anytype.mapper.toTable
|
||||
import com.agileburo.anytype.model.AccountTable
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -18,7 +19,13 @@ class DefaultAuthCache(
|
|||
AccountTable(
|
||||
id = account.id,
|
||||
name = account.name,
|
||||
timestamp = System.currentTimeMillis()
|
||||
timestamp = System.currentTimeMillis(),
|
||||
avatar = account.avatar?.let { avatar ->
|
||||
AccountTable.Avatar(
|
||||
avatarId = avatar.id,
|
||||
sizes = avatar.sizes.map { it.toTable() }
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package com.agileburo.anytype.util
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.agileburo.anytype.model.AccountTable
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
|
||||
object Converters {
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun fromImageSizeList(sizes: List<AccountTable.Size>): String {
|
||||
return Gson().toJson(sizes)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun toImageSizeList(string: String): List<AccountTable.Size> {
|
||||
val type = object : TypeToken<List<AccountTable.Size>>() {}.type
|
||||
return Gson().fromJson(string, type)
|
||||
}
|
||||
|
||||
}
|
|
@ -65,4 +65,25 @@ class AccountDaoTest {
|
|||
assertTrue { result.size == 1 }
|
||||
assertTrue { result.first() == secondAccount }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should save account with its avatar`() = runBlocking {
|
||||
|
||||
val account = AccountTable(
|
||||
id = MockDataFactory.randomString(),
|
||||
name = MockDataFactory.randomString(),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
avatar = AccountTable.Avatar(
|
||||
avatarId = MockDataFactory.randomString(),
|
||||
sizes = listOf(AccountTable.Size.LARGE, AccountTable.Size.SMALL)
|
||||
)
|
||||
)
|
||||
|
||||
database.accountDao().insert(account)
|
||||
|
||||
val result = database.accountDao().lastAccount()
|
||||
|
||||
assertTrue { result.size == 1 }
|
||||
assertTrue { result.first() == account }
|
||||
}
|
||||
}
|
|
@ -29,10 +29,14 @@ class SetupNewAccountViewModel(
|
|||
}
|
||||
|
||||
private fun proceedWithCreatingAccount() {
|
||||
|
||||
Timber.d("Starting setting up new account")
|
||||
|
||||
createAccount.invoke(
|
||||
scope = viewModelScope,
|
||||
params = CreateAccount.Params(
|
||||
name = session.name ?: throw IllegalStateException("Name not set")
|
||||
name = session.name ?: throw IllegalStateException("Name not set"),
|
||||
avatarPath = session.avatarPath
|
||||
)
|
||||
) { result ->
|
||||
result.either(
|
||||
|
|
|
@ -6,14 +6,17 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.agileburo.anytype.core_utils.common.Event
|
||||
import com.agileburo.anytype.core_utils.ui.ViewState
|
||||
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
|
||||
import com.agileburo.anytype.domain.auth.model.Account
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
|
||||
import com.agileburo.anytype.domain.image.LoadImage
|
||||
import com.agileburo.anytype.presentation.navigation.AppNavigation
|
||||
import com.agileburo.anytype.presentation.navigation.SupportNavigation
|
||||
import com.agileburo.anytype.presentation.profile.ProfileView
|
||||
import timber.log.Timber
|
||||
|
||||
class DesktopViewModel(
|
||||
private val loadImage: LoadImage,
|
||||
private val getAccount: GetAccount
|
||||
) : ViewStateViewModel<ViewState<List<DesktopView>>>(),
|
||||
SupportNavigation<Event<AppNavigation.Command>> {
|
||||
|
@ -22,9 +25,10 @@ class DesktopViewModel(
|
|||
val profile: LiveData<ProfileView>
|
||||
get() = _profile
|
||||
|
||||
init {
|
||||
proceedWithGettingAccount()
|
||||
}
|
||||
|
||||
private val _image = MutableLiveData<ByteArray>()
|
||||
val image: LiveData<ByteArray>
|
||||
get() = _image
|
||||
|
||||
override val navigation = MutableLiveData<Event<AppNavigation.Command>>()
|
||||
|
||||
|
@ -32,13 +36,33 @@ class DesktopViewModel(
|
|||
getAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
|
||||
result.either(
|
||||
fnL = { e -> Timber.e(e, "Error while getting account") },
|
||||
fnR = { account -> _profile.postValue(ProfileView(name = account.name)) }
|
||||
fnR = { account ->
|
||||
_profile.postValue(ProfileView(name = account.name))
|
||||
loadAvatarImage(account)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAvatarImage(account: Account) {
|
||||
account.avatar?.let { image ->
|
||||
loadImage.invoke(
|
||||
scope = viewModelScope,
|
||||
params = LoadImage.Param(
|
||||
id = image.id
|
||||
)
|
||||
) { result ->
|
||||
result.either(
|
||||
fnL = { e -> Timber.e(e, "Error while loading image") },
|
||||
fnR = { blob -> _image.postValue(blob) }
|
||||
)
|
||||
}
|
||||
} ?: Timber.d("Avatar not loaded: null value")
|
||||
}
|
||||
|
||||
fun onViewCreated() {
|
||||
stateData.postValue(ViewState.Init)
|
||||
proceedWithGettingAccount()
|
||||
}
|
||||
|
||||
fun onAddNewDocumentClicked() {
|
||||
|
|
|
@ -3,15 +3,18 @@ package com.agileburo.anytype.presentation.desktop
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
|
||||
import com.agileburo.anytype.domain.image.LoadImage
|
||||
|
||||
class DesktopViewModelFactory(
|
||||
private val getAccount: GetAccount
|
||||
private val getAccount: GetAccount,
|
||||
private val loadImage: LoadImage
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return DesktopViewModel(
|
||||
getAccount = getAccount
|
||||
getAccount = getAccount,
|
||||
loadImage = loadImage
|
||||
) as T
|
||||
}
|
||||
}
|
|
@ -1,30 +1,69 @@
|
|||
package com.agileburo.anytype.presentation.profile
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.agileburo.anytype.core_utils.common.Event
|
||||
import com.agileburo.anytype.core_utils.ui.ViewState
|
||||
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
|
||||
import com.agileburo.anytype.domain.auth.interactor.Logout
|
||||
import com.agileburo.anytype.domain.auth.model.Account
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
|
||||
import com.agileburo.anytype.domain.image.LoadImage
|
||||
import com.agileburo.anytype.presentation.navigation.AppNavigation
|
||||
import com.agileburo.anytype.presentation.navigation.SupportNavigation
|
||||
import timber.log.Timber
|
||||
|
||||
class ProfileViewModel(
|
||||
private val getAccount: GetAccount,
|
||||
private val loadImage: LoadImage,
|
||||
private val logout: Logout
|
||||
) : ViewStateViewModel<ViewState<ProfileView>>(), SupportNavigation<Event<AppNavigation.Command>> {
|
||||
|
||||
private val _image = MutableLiveData<ByteArray>()
|
||||
val image: LiveData<ByteArray>
|
||||
get() = _image
|
||||
|
||||
override val navigation: MutableLiveData<Event<AppNavigation.Command>> = MutableLiveData()
|
||||
|
||||
fun onViewCreated() {
|
||||
stateData.postValue(ViewState.Init)
|
||||
proceedWithGettingAccount()
|
||||
}
|
||||
|
||||
fun onBackButtonClicked() {
|
||||
// TODO dispatch navigation command
|
||||
}
|
||||
|
||||
private fun proceedWithGettingAccount() {
|
||||
getAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
|
||||
result.either(
|
||||
fnL = { e -> Timber.e(e, "Error while getting account") },
|
||||
fnR = { account ->
|
||||
stateData.postValue(ViewState.Success(ProfileView(name = account.name)))
|
||||
loadAvatarImage(account)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAvatarImage(account: Account) {
|
||||
account.avatar?.let { image ->
|
||||
loadImage.invoke(
|
||||
scope = viewModelScope,
|
||||
params = LoadImage.Param(
|
||||
id = image.id
|
||||
)
|
||||
) { result ->
|
||||
result.either(
|
||||
fnL = { e -> Timber.e(e, "Error while loading image") },
|
||||
fnR = { blob -> _image.postValue(blob) }
|
||||
)
|
||||
}
|
||||
} ?: Timber.d("Avatar not loaded: null value")
|
||||
}
|
||||
|
||||
fun onLogoutClicked() {
|
||||
logout.invoke(viewModelScope, BaseUseCase.None) { result ->
|
||||
result.either(
|
||||
|
|
|
@ -3,15 +3,21 @@ package com.agileburo.anytype.presentation.profile
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.agileburo.anytype.domain.auth.interactor.Logout
|
||||
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
|
||||
import com.agileburo.anytype.domain.image.LoadImage
|
||||
|
||||
class ProfileViewModelFactory(
|
||||
private val logout: Logout
|
||||
private val logout: Logout,
|
||||
private val getAccount: GetAccount,
|
||||
private val loadImage: LoadImage
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return ProfileViewModel(
|
||||
logout = logout
|
||||
logout = logout,
|
||||
getAccount = getAccount,
|
||||
loadImage = loadImage
|
||||
) as T
|
||||
}
|
||||
}
|
|
@ -14,7 +14,8 @@ import timber.log.Timber
|
|||
* email : ki@agileburo.com
|
||||
* on 2019-10-21.
|
||||
*/
|
||||
class SplashViewModel(private val checkAuthorizationStatus: CheckAuthorizationStatus) : ViewModel() {
|
||||
class SplashViewModel(private val checkAuthorizationStatus: CheckAuthorizationStatus) :
|
||||
ViewModel() {
|
||||
|
||||
val navigation: MutableLiveData<Event<AppNavigation.Command>> = MutableLiveData()
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue