diff --git a/app/build.gradle b/app/build.gradle index b6a859d..bd0fc58 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,6 +95,9 @@ android { apollo { generateKotlinModels.set(true) + customTypeMapping = [ + "Upload" : "com.apollographql.apollo.api.FileUpload" + ] } repositories { @@ -107,10 +110,12 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.6' implementation androidLibs + implementation retrofitLibs implementation apolloLibs implementation coroutinesLibs implementation kodeinLibs implementation roomLibs + implementation glideLibs kapt compilerLibs testImplementation unitTestLibs diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 7a6d45d..93fcf28 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -62,4 +62,7 @@ -keepattributes SourceFile,LineNumberTable +#apollo input types +-keep class * implements com.apollographql.apollo.api.InputType { *; } + diff --git a/app/src/androidTest/java/com/flatstack/android/MainScreenTest.java b/app/src/androidTest/java/com/flatstack/android/MainScreenTest.java index 288ef08..ef3db04 100644 --- a/app/src/androidTest/java/com/flatstack/android/MainScreenTest.java +++ b/app/src/androidTest/java/com/flatstack/android/MainScreenTest.java @@ -30,7 +30,7 @@ public void whenAppLaunch_androidBaseVisible() throws Exception { @Test public void whenButtonClick_startedActivity() { - onView(withId(R.id.button)).perform(click()); + onView(withId(R.id.selectAvatar)).perform(click()); intended(hasComponent(SecondActivity.class.getName())); } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aafc5de..1228c2b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="com.flatstack.android"> + + diff --git a/app/src/main/graphql/com/flatstack/android/graphql/fragment/User.graphql b/app/src/main/graphql/com/flatstack/android/graphql/fragment/User.graphql index a03586a..39a5f08 100644 --- a/app/src/main/graphql/com/flatstack/android/graphql/fragment/User.graphql +++ b/app/src/main/graphql/com/flatstack/android/graphql/fragment/User.graphql @@ -1,4 +1,5 @@ fragment UserGqlFragment on User { firstName lastName + avatarUrl } diff --git a/app/src/main/graphql/com/flatstack/android/graphql/mutation/PresignAvatar.graphql b/app/src/main/graphql/com/flatstack/android/graphql/mutation/PresignAvatar.graphql new file mode 100644 index 0000000..7448ee2 --- /dev/null +++ b/app/src/main/graphql/com/flatstack/android/graphql/mutation/PresignAvatar.graphql @@ -0,0 +1,11 @@ +mutation PresignAvatar( + $input: PresignDataInput! +) { + presignData(input: $input) { + fields { + key + value + } + url + } +} \ No newline at end of file diff --git a/app/src/main/graphql/com/flatstack/android/graphql/mutation/Signin.graphql b/app/src/main/graphql/com/flatstack/android/graphql/mutation/Signin.graphql index 3a1b50e..902475b 100644 --- a/app/src/main/graphql/com/flatstack/android/graphql/mutation/Signin.graphql +++ b/app/src/main/graphql/com/flatstack/android/graphql/mutation/Signin.graphql @@ -1,5 +1,5 @@ -mutation Login($email: String!, $password: String!) { - signin(email: $email, password: $password) { +mutation Login($input: SignInInput!) { + signin(input: $input) { accessToken } } diff --git a/app/src/main/graphql/com/flatstack/android/graphql/mutation/Signup.graphql b/app/src/main/graphql/com/flatstack/android/graphql/mutation/Signup.graphql new file mode 100644 index 0000000..c79ed32 --- /dev/null +++ b/app/src/main/graphql/com/flatstack/android/graphql/mutation/Signup.graphql @@ -0,0 +1,5 @@ +mutation Register($input: SignUpInput!) { + signup(input: $input) { + accessToken + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flatstack/android/Router.kt b/app/src/main/java/com/flatstack/android/Router.kt index ae126e4..b215c10 100644 --- a/app/src/main/java/com/flatstack/android/Router.kt +++ b/app/src/main/java/com/flatstack/android/Router.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import com.flatstack.android.login.LoginActivity import com.flatstack.android.profile.ProfileActivity +import com.flatstack.android.registration.RegistrationActivity class Router( private val appContext: Context @@ -23,4 +24,12 @@ class Router( } }) } + + fun registration(context: Context, clearStack: Boolean = false) { + context.startActivity(Intent(context, RegistrationActivity::class.java).apply { + if (clearStack) { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + }) + } } diff --git a/app/src/main/java/com/flatstack/android/di/kodein.kt b/app/src/main/java/com/flatstack/android/di/kodein.kt index f4ae00c..f916541 100644 --- a/app/src/main/java/com/flatstack/android/di/kodein.kt +++ b/app/src/main/java/com/flatstack/android/di/kodein.kt @@ -13,4 +13,5 @@ fun initKodein(app: Application) = import(viewModelModule) import(netModule) import(repoModule) + import(mapperModule) } diff --git a/app/src/main/java/com/flatstack/android/di/modules/appModule.kt b/app/src/main/java/com/flatstack/android/di/modules/appModule.kt index 5f30e7a..1cd8964 100644 --- a/app/src/main/java/com/flatstack/android/di/modules/appModule.kt +++ b/app/src/main/java/com/flatstack/android/di/modules/appModule.kt @@ -3,6 +3,7 @@ package com.flatstack.android.di.modules import com.flatstack.android.Router import com.flatstack.android.login.LoginMapper import com.flatstack.android.model.network.errors.ErrorHandler +import com.flatstack.android.util.FileUtils import com.flatstack.android.util.StringResource import org.kodein.di.Kodein import org.kodein.di.generic.bind @@ -13,5 +14,6 @@ val appModule = Kodein.Module(name = "appModule") { bind() with singleton { StringResource(instance()) } bind() with singleton { ErrorHandler(instance(), instance(), instance()) } bind() with singleton { Router(instance()) } + bind() with singleton { FileUtils(instance()) } bind() with singleton { LoginMapper() } } diff --git a/app/src/main/java/com/flatstack/android/di/modules/mapperModule.kt b/app/src/main/java/com/flatstack/android/di/modules/mapperModule.kt new file mode 100644 index 0000000..676e0db --- /dev/null +++ b/app/src/main/java/com/flatstack/android/di/modules/mapperModule.kt @@ -0,0 +1,16 @@ +package com.flatstack.android.di.modules + +import com.flatstack.android.registration.SessionFromRegistrationMapper +import com.flatstack.android.registration.mapper.PresignDataFromNetworkMapper +import com.flatstack.android.registration.mapper.PresignDataToNetworkMapper +import com.flatstack.android.registration.mapper.RegisterRequestToNetworkMapper +import org.kodein.di.Kodein +import org.kodein.di.generic.bind +import org.kodein.di.generic.provider + +val mapperModule = Kodein.Module(name = "mapperModule") { + bind() with provider { SessionFromRegistrationMapper() } + bind() with provider { RegisterRequestToNetworkMapper() } + bind() with provider { PresignDataToNetworkMapper() } + bind() with provider { PresignDataFromNetworkMapper() } +} diff --git a/app/src/main/java/com/flatstack/android/di/modules/netModule.kt b/app/src/main/java/com/flatstack/android/di/modules/netModule.kt index d13c1d1..9de9428 100644 --- a/app/src/main/java/com/flatstack/android/di/modules/netModule.kt +++ b/app/src/main/java/com/flatstack/android/di/modules/netModule.kt @@ -2,18 +2,22 @@ package com.flatstack.android.di.modules import com.apollographql.apollo.ApolloClient import com.flatstack.android.BuildConfig +import com.flatstack.android.model.network.NetworkManager import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import org.kodein.di.Kodein import org.kodein.di.generic.bind import org.kodein.di.generic.instance import org.kodein.di.generic.singleton +import org.kodein.di.generic.* val netModule = Kodein.Module(name = "netModule") { bind() with singleton { AuthorizationInterceptor(instance()) } bind() with singleton { OkHttpClient.Builder() - .addInterceptor(instance()) + .addNetworkInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) + .addInterceptor(instance()) .build() } bind() with singleton { @@ -22,4 +26,11 @@ val netModule = Kodein.Module(name = "netModule") { .okHttpClient(instance()) .build() } + + bind("restClient") with singleton { OkHttpClient.Builder() + .addNetworkInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) + .build() + } + + bind() with provider { NetworkManager(instance("restClient")) } } diff --git a/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt b/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt index d3f702f..dd0a321 100644 --- a/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt +++ b/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt @@ -2,6 +2,8 @@ package com.flatstack.android.di.modules import com.flatstack.android.login.LoginRepository import com.flatstack.android.profile.ProfileRepository +import com.flatstack.android.registration.PresignDataRepository +import com.flatstack.android.registration.RegistrationRepository import org.kodein.di.Kodein import org.kodein.di.generic.bind import org.kodein.di.generic.instance @@ -10,4 +12,7 @@ import org.kodein.di.generic.provider val repoModule = Kodein.Module(name = "repoModule") { bind() with provider { LoginRepository(instance(), instance(), instance(), instance()) } bind() with provider { ProfileRepository(instance(), instance(), instance()) } + bind() with provider { RegistrationRepository(instance(), instance(), instance()) } + bind() with provider { + PresignDataRepository(instance(), instance(), instance(), instance()) } } diff --git a/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt b/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt index c69b1d3..d342a28 100644 --- a/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt +++ b/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt @@ -3,6 +3,7 @@ package com.flatstack.android.di.modules import androidx.lifecycle.ViewModelProvider import com.flatstack.android.login.LoginViewModel import com.flatstack.android.profile.ProfileViewModel +import com.flatstack.android.registration.RegistrationViewModel import com.flatstack.android.util.ViewModelFactory import com.flatstack.android.util.bindViewModel import org.kodein.di.Kodein @@ -16,4 +17,6 @@ val viewModelModule = Kodein.Module(name = "viewModelModule") { bindViewModel() with provider { LoginViewModel(instance(), instance()) } bindViewModel() with provider { ProfileViewModel(instance(), instance()) } + bindViewModel() with provider { + RegistrationViewModel(instance(), instance(), instance(), instance(), instance()) } } diff --git a/app/src/main/java/com/flatstack/android/login/LoginActivity.kt b/app/src/main/java/com/flatstack/android/login/LoginActivity.kt index ba1a451..737bc23 100644 --- a/app/src/main/java/com/flatstack/android/login/LoginActivity.kt +++ b/app/src/main/java/com/flatstack/android/login/LoginActivity.kt @@ -40,8 +40,14 @@ class LoginActivity : AppCompatActivity(), KodeinAware { router.profile(context = this, clearStack = true) } + private fun navigateToRegistration() { + val router by kodein.instance() + router.registration(context = this, clearStack = false) + } + private fun initListeners() { bt_login.setOnClickListener { login() } + bt_sign_up.setOnClickListener { navigateToRegistration() } et_password.apply { setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_GO) { @@ -54,7 +60,7 @@ class LoginActivity : AppCompatActivity(), KodeinAware { } private fun login() { - val username = et_login.text.toString() + val username = et_email.text.toString() val password = et_password.text.toString() viewModel.login(username, password) } diff --git a/app/src/main/java/com/flatstack/android/login/LoginRepository.kt b/app/src/main/java/com/flatstack/android/login/LoginRepository.kt index f8ed686..425e305 100644 --- a/app/src/main/java/com/flatstack/android/login/LoginRepository.kt +++ b/app/src/main/java/com/flatstack/android/login/LoginRepository.kt @@ -8,6 +8,7 @@ import com.flatstack.android.model.entities.Session import com.flatstack.android.model.network.NetworkBoundResource import com.flatstack.android.model.network.errors.ErrorHandler import com.flatstack.android.profile.AuthorizationModel +import com.flatstack.android.type.SignInInput import kotlinx.coroutines.* class LoginRepository( @@ -41,5 +42,5 @@ class LoginRepository( } private fun loginMutation(email: String, password: String) = - LoginMutation(email = email, password = password) + LoginMutation(SignInInput(email, password)) } diff --git a/app/src/main/java/com/flatstack/android/model/db/AppDatabase.kt b/app/src/main/java/com/flatstack/android/model/db/AppDatabase.kt index 26e288f..7fe8d37 100644 --- a/app/src/main/java/com/flatstack/android/model/db/AppDatabase.kt +++ b/app/src/main/java/com/flatstack/android/model/db/AppDatabase.kt @@ -9,7 +9,7 @@ import com.flatstack.android.profile.entities.Profile @Database( entities = [Profile::class, Session::class], - version = 1, + version = 3, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/flatstack/android/model/network/NetworkManager.kt b/app/src/main/java/com/flatstack/android/model/network/NetworkManager.kt new file mode 100644 index 0000000..8830d62 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/model/network/NetworkManager.kt @@ -0,0 +1,46 @@ +package com.flatstack.android.model.network + +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.io.IOException + +class NetworkManager(private val okHttpClient: OkHttpClient) { + + fun uploadImageToAws(url: String, fields: Map, file: File) : Boolean { + val requestBody = MultipartBody.Builder().setType(MultipartBody.FORM) + fields.forEach { + requestBody.addFormDataPart(it.key, it.value) + } + requestBody.addFormDataPart( + FORM_DATA_KEY, + file.name, + file.asRequestBody(contentType = REQUEST_BODY_CONTENT_TYPE.toMediaTypeOrNull()) + ) + val request = Request.Builder() + .url(url) + .method(REQUEST_METHOD, requestBody.build()) + .build() + var isSuccessful = false + okHttpClient + .newCall(request) + .enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + isSuccessful = false + } + + override fun onResponse(call: Call, response: Response) { + isSuccessful = true + } + + }) + return isSuccessful + } + + companion object { + const val FORM_DATA_KEY = "file" + const val REQUEST_BODY_CONTENT_TYPE = "application/octet-stream" + const val REQUEST_METHOD = "POST" + } +} diff --git a/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt b/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt index 8fbdfaa..4a656d7 100644 --- a/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt +++ b/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt @@ -8,6 +8,7 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.bumptech.glide.Glide import com.flatstack.android.R import com.flatstack.android.Router import com.flatstack.android.util.observeBy @@ -38,6 +39,10 @@ class ProfileActivity : AppCompatActivity(), KodeinAware, OnRefreshListener { showProfile() showFirstName(it.firstName) showLastName(it.lastName) + if (it.avatarUrl.isEmpty()) { + print("EMPTY AVATAR URL!!!!!!!!!!!!!!!!!!!!!!!!!!") + } + showAvatar(it.avatarUrl) }, onError = ::showError, onLoading = ::visibleProgress) @@ -68,6 +73,12 @@ class ProfileActivity : AppCompatActivity(), KodeinAware, OnRefreshListener { tv_last_name.text = lastName } + private fun showAvatar(avatarUrl: String) { + Glide.with(this) + .load(avatarUrl) + .into(iv_avatar) + } + private fun logout() { viewModel.logout() diff --git a/app/src/main/java/com/flatstack/android/profile/ProfileMapper.kt b/app/src/main/java/com/flatstack/android/profile/ProfileMapper.kt index ca67843..ac9ae0d 100644 --- a/app/src/main/java/com/flatstack/android/profile/ProfileMapper.kt +++ b/app/src/main/java/com/flatstack/android/profile/ProfileMapper.kt @@ -7,7 +7,8 @@ object ProfileMapper { fun mapProfile(me: GetUserQuery.Me?) = me?.fragments?.userGqlFragment.run { Profile( firstName = this?.firstName ?: "", - lastName = this?.lastName ?: "" + lastName = this?.lastName ?: "", + avatarUrl = this?.avatarUrl ?: "" ) } } diff --git a/app/src/main/java/com/flatstack/android/profile/entities/Profile.kt b/app/src/main/java/com/flatstack/android/profile/entities/Profile.kt index 920d76a..a2b9fa1 100644 --- a/app/src/main/java/com/flatstack/android/profile/entities/Profile.kt +++ b/app/src/main/java/com/flatstack/android/profile/entities/Profile.kt @@ -7,6 +7,7 @@ import androidx.room.PrimaryKey data class Profile( val firstName: String, val lastName: String, + val avatarUrl: String, @PrimaryKey(autoGenerate = true) val id: Int = 0 ) diff --git a/app/src/main/java/com/flatstack/android/registration/PresignDataRepository.kt b/app/src/main/java/com/flatstack/android/registration/PresignDataRepository.kt new file mode 100644 index 0000000..3952c48 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/PresignDataRepository.kt @@ -0,0 +1,32 @@ +package com.flatstack.android.registration + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.coroutines.toDeferred +import com.flatstack.android.graphql.mutation.PresignAvatarMutation +import com.flatstack.android.model.network.NetworkManager +import com.flatstack.android.registration.entities.request.PresignDataRequest +import com.flatstack.android.registration.mapper.PresignDataFromNetworkMapper +import com.flatstack.android.registration.mapper.PresignDataToNetworkMapper +import java.io.File + + +class PresignDataRepository( + private val apolloClient: ApolloClient, + private val networkManager: NetworkManager, + private val mapPresignDataToNetwork: PresignDataToNetworkMapper, + private val mapPresignDataFromNetwork: PresignDataFromNetworkMapper +) { + suspend fun presignData(fileName: String, type: String) = + apolloClient.mutate( + PresignAvatarMutation( + PresignDataRequest(fileName, type).run(mapPresignDataToNetwork) + ) + ) + .toDeferred() + .await() + .data?.presignData + ?.run(mapPresignDataFromNetwork) + + fun uploadImageToAws(url: String, fields: Map, file: File) : Boolean = + networkManager.uploadImageToAws(url, fields, file) +} diff --git a/app/src/main/java/com/flatstack/android/registration/RegistrationActivity.kt b/app/src/main/java/com/flatstack/android/registration/RegistrationActivity.kt new file mode 100644 index 0000000..b974318 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/RegistrationActivity.kt @@ -0,0 +1,133 @@ +package com.flatstack.android.registration + +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.inputmethod.EditorInfo +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.lifecycle.observe +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.flatstack.android.R +import com.flatstack.android.registration.entities.request.ImageUploadRequest +import com.flatstack.android.registration.entities.request.MetadataRequest +import com.flatstack.android.util.FileUtils +import com.flatstack.android.util.provideViewModel +import kotlinx.android.synthetic.main.activity_registration.* +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.kodein +import org.kodein.di.generic.instance + + +class RegistrationActivity : AppCompatActivity(), KodeinAware { + + override val kodein: Kodein by kodein() + + private val viewModel: RegistrationViewModel by provideViewModel() + private val fileUtils: FileUtils by instance() + private var avatar: ImageUploadRequest? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_registration) + observeViewModel() + initListeners() + } + + private fun observeViewModel() { + viewModel.errorData.observe(this) { + showError(it) + } + + viewModel.shouldShowProgressData.observe(this) { + setProgress(it) + } + } + + private fun initListeners() { + bt_sign_up.setOnClickListener { register() } + bt_select_avatar.setOnClickListener { pickImage() } + et_confirm_password.apply { + setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_GO) { + register() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + } + } + + private fun register() { + val firstName = et_first_name.text.toString() + val lastName = et_last_name.text.toString() + val email = et_email.text.toString() + val password = et_password.text.toString() + val confirmPassword = et_confirm_password.text.toString() + viewModel.register( + avatar = avatar, + firstName = firstName, + lastName = lastName, + email = email, + password = password, + confirmPassword = confirmPassword, + context = this + ) + } + + private fun pickImage() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + startActivityForResult(intent, PICK_IMAGE_FOR_AVATAR) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == PICK_IMAGE_FOR_AVATAR && resultCode == Activity.RESULT_OK) { + data?.let { intent -> + intent.data?.let { uri -> + + val fileName = fileUtils.getRealFileName(uri) + + avatar = ImageUploadRequest( + id = "", + metadata = MetadataRequest( + size = fileUtils.getFileSizeInBytes(uri), + fileName = fileName, + mimeType = fileUtils.getFileMimeType(uri) + ) + ) + avatar?.file = fileUtils.getFileFromContentUri(uri, fileName) + + Glide.with(this) + .load(uri) + .transform(CenterCrop(), RoundedCorners(PREVIEW_PICTURE_CORNER_RADIUS)) + .into(iv_avatar_preview) + tv_avatar_filename.text = fileName + } + } + } + } + + private fun setProgress(isLoading: Boolean) { + pb_progress.isVisible = isLoading + } + + private fun showError(errorText: String?) { + errorText?.let { + Toast.makeText(this, errorText, Toast.LENGTH_LONG).show() + } + } + + companion object { + const val PICK_IMAGE_FOR_AVATAR = 1 + const val PREVIEW_PICTURE_CORNER_RADIUS = 10 + } +} diff --git a/app/src/main/java/com/flatstack/android/registration/RegistrationRepository.kt b/app/src/main/java/com/flatstack/android/registration/RegistrationRepository.kt new file mode 100644 index 0000000..3483dfc --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/RegistrationRepository.kt @@ -0,0 +1,22 @@ +package com.flatstack.android.registration + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.coroutines.toDeferred +import com.flatstack.android.graphql.mutation.RegisterMutation +import com.flatstack.android.model.entities.Session +import com.flatstack.android.registration.entities.request.RegisterRequest +import com.flatstack.android.registration.mapper.RegisterRequestToNetworkMapper + +class RegistrationRepository( + private val apolloClient: ApolloClient, + private val mapSessionFromRegistration: SessionFromRegistrationMapper, + private val mapRegisterInputToNetwork: RegisterRequestToNetworkMapper +) { + + suspend fun register(request: RegisterRequest) = + apolloClient.mutate(RegisterMutation(mapRegisterInputToNetwork(request))) + .toDeferred() + .await() + .data?.signup + ?.run(mapSessionFromRegistration) ?: Session("") +} diff --git a/app/src/main/java/com/flatstack/android/registration/RegistrationViewModel.kt b/app/src/main/java/com/flatstack/android/registration/RegistrationViewModel.kt new file mode 100644 index 0000000..0c73e64 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/RegistrationViewModel.kt @@ -0,0 +1,110 @@ +package com.flatstack.android.registration + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.flatstack.android.R +import com.flatstack.android.Router +import com.flatstack.android.profile.AuthorizationModel +import com.flatstack.android.registration.entities.request.ImageUploadRequest +import com.flatstack.android.registration.entities.request.RegisterRequest +import com.flatstack.android.registration.entities.response.PresignData +import com.flatstack.android.util.BaseViewModel +import com.flatstack.android.util.StringResource +import kotlinx.coroutines.launch + +class RegistrationViewModel( + private val registrationRepository: RegistrationRepository, + private val stringResource: StringResource, + private val authorizationModel: AuthorizationModel, + private val presignDataRepository: PresignDataRepository, + private val router: Router +) : BaseViewModel() { + + private val errorMutableData = MutableLiveData() + private var presignData : PresignData? = null + + val errorData : LiveData = errorMutableData + + fun register( + avatar: ImageUploadRequest?, + email: String, + firstName: String?, + lastName: String?, + password: String, + confirmPassword: String, + context: Context + ) { + if (email.isNotEmpty() && password.isNotEmpty() && password == confirmPassword) { + var imageUploadRequest: ImageUploadRequest? = null + viewModelScope.launch { + avatar?.let{ avatar -> + presignAvatar(avatar) + presignData?.let { presignData -> + avatar.file?.let { file -> + presignDataRepository.uploadImageToAws(presignData.url, presignData.fields, file) + + imageUploadRequest = avatar.copy( + id = getImageId(presignData.fields) + ) + } + } + } ?: run { + imageUploadRequest = avatar?.copy() + } + signUp(RegisterRequest(imageUploadRequest, email, firstName, lastName, password), context) + } + } else { + showError(stringResource.getString(R.string.invalid_credentials)) + } + } + + private fun getImageId(fields: Map) = + fields[ARG_KEY]?.split("/")?.last() ?: "" + + private suspend fun signUp(registerRequest: RegisterRequest, context: Context) = + runSafe( + operation = { + registrationRepository.register(registerRequest) + }, + onSuccess = { + viewModelScope.launch { + authorizationModel.setSession(it) + } + navigateToProfile(context) + }, + onError = { + showError(it.message) + } + ) + + private suspend fun presignAvatar(avatar: ImageUploadRequest) { + runSafe( + operation = { + presignDataRepository.presignData( + avatar.metadata.fileName, + avatar.metadata.mimeType + ) + }, + onSuccess = { + presignData = it + }, + onError = { + showError(it.message) + } + ) + } + + private fun navigateToProfile(context: Context) { + router.profile(context, clearStack = true) + } + + private fun showError(error: String?) { + errorMutableData.value = error ?: stringResource.getString(R.string.something_went_wrong) + } + + companion object { + const val ARG_KEY = "key" + } +} diff --git a/app/src/main/java/com/flatstack/android/registration/entities/request/ImageUploadRequest.kt b/app/src/main/java/com/flatstack/android/registration/entities/request/ImageUploadRequest.kt new file mode 100644 index 0000000..10a0d83 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/entities/request/ImageUploadRequest.kt @@ -0,0 +1,11 @@ +package com.flatstack.android.registration.entities.request + +import java.io.File + +data class ImageUploadRequest( + val id: String, + val storage: String = "cache", + val metadata: MetadataRequest +) { + var file: File? = null +} diff --git a/app/src/main/java/com/flatstack/android/registration/entities/request/MetadataRequest.kt b/app/src/main/java/com/flatstack/android/registration/entities/request/MetadataRequest.kt new file mode 100644 index 0000000..9876929 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/entities/request/MetadataRequest.kt @@ -0,0 +1,7 @@ +package com.flatstack.android.registration.entities.request + +data class MetadataRequest( + val size: Int, + val fileName: String, + val mimeType: String +) diff --git a/app/src/main/java/com/flatstack/android/registration/entities/request/PresignDataRequest.kt b/app/src/main/java/com/flatstack/android/registration/entities/request/PresignDataRequest.kt new file mode 100644 index 0000000..e1053ab --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/entities/request/PresignDataRequest.kt @@ -0,0 +1,6 @@ +package com.flatstack.android.registration.entities.request + +data class PresignDataRequest( + val fileName: String, + val type: String +) diff --git a/app/src/main/java/com/flatstack/android/registration/entities/request/RegisterRequest.kt b/app/src/main/java/com/flatstack/android/registration/entities/request/RegisterRequest.kt new file mode 100644 index 0000000..2d357ca --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/entities/request/RegisterRequest.kt @@ -0,0 +1,9 @@ +package com.flatstack.android.registration.entities.request + +data class RegisterRequest( + val avatar: ImageUploadRequest?, + val email: String, + val firstName: String?, + val lastName: String?, + val password: String +) diff --git a/app/src/main/java/com/flatstack/android/registration/entities/response/PresignData.kt b/app/src/main/java/com/flatstack/android/registration/entities/response/PresignData.kt new file mode 100644 index 0000000..51b3741 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/entities/response/PresignData.kt @@ -0,0 +1,6 @@ +package com.flatstack.android.registration.entities.response + +data class PresignData( + val fields: Map, + val url: String +) diff --git a/app/src/main/java/com/flatstack/android/registration/mapper/PresignDataFromNetworkMapper.kt b/app/src/main/java/com/flatstack/android/registration/mapper/PresignDataFromNetworkMapper.kt new file mode 100644 index 0000000..4e2a6d5 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/mapper/PresignDataFromNetworkMapper.kt @@ -0,0 +1,14 @@ +package com.flatstack.android.registration.mapper + +import com.flatstack.android.graphql.mutation.PresignAvatarMutation +import com.flatstack.android.registration.entities.response.PresignData + +class PresignDataFromNetworkMapper: (PresignAvatarMutation.PresignData) -> PresignData { + override fun invoke(response: PresignAvatarMutation.PresignData): PresignData = + response.run { + PresignData( + fields = fields.map { it.key to it.value }.toMap(), + url = url + ) + } +} diff --git a/app/src/main/java/com/flatstack/android/registration/mapper/PresignDataToNetworkMapper.kt b/app/src/main/java/com/flatstack/android/registration/mapper/PresignDataToNetworkMapper.kt new file mode 100644 index 0000000..afaf024 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/mapper/PresignDataToNetworkMapper.kt @@ -0,0 +1,14 @@ +package com.flatstack.android.registration.mapper + +import com.flatstack.android.registration.entities.request.PresignDataRequest +import com.flatstack.android.type.PresignDataInput + +class PresignDataToNetworkMapper: (PresignDataRequest) -> PresignDataInput { + override fun invoke(request: PresignDataRequest): PresignDataInput = + request.run { + PresignDataInput( + filename = fileName, + type = type + ) + } +} diff --git a/app/src/main/java/com/flatstack/android/registration/mapper/RegisterRequestToNetworkMapper.kt b/app/src/main/java/com/flatstack/android/registration/mapper/RegisterRequestToNetworkMapper.kt new file mode 100644 index 0000000..e93be25 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/mapper/RegisterRequestToNetworkMapper.kt @@ -0,0 +1,32 @@ +package com.flatstack.android.registration.mapper + +import com.apollographql.apollo.api.Input +import com.flatstack.android.registration.entities.request.RegisterRequest +import com.flatstack.android.type.ImageUploader +import com.flatstack.android.type.ImageUploaderMetadata +import com.flatstack.android.type.SignUpInput + +class RegisterRequestToNetworkMapper : (RegisterRequest) -> SignUpInput { + override fun invoke(request: RegisterRequest): SignUpInput = + request.run { + SignUpInput( + email = email, + password = password, + firstName = Input.optional(firstName), + lastName = Input.optional(lastName), + avatar = Input.optional(avatar?.run { + ImageUploader( + id = id, + storage = Input.optional(storage), + metadata = metadata.run { + ImageUploaderMetadata( + size = size, + filename = fileName, + mimeType = mimeType + ) + } + ) + }) + ) + } +} diff --git a/app/src/main/java/com/flatstack/android/registration/mapper/SessionFromRegistrationMapper.kt b/app/src/main/java/com/flatstack/android/registration/mapper/SessionFromRegistrationMapper.kt new file mode 100644 index 0000000..7988967 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/registration/mapper/SessionFromRegistrationMapper.kt @@ -0,0 +1,9 @@ +package com.flatstack.android.registration + +import com.flatstack.android.graphql.mutation.RegisterMutation +import com.flatstack.android.model.entities.Session + +class SessionFromRegistrationMapper : (RegisterMutation.Signup?) -> Session { + override fun invoke(sigunp: RegisterMutation.Signup?): Session = + Session(accessToken = sigunp?.accessToken ?: "") +} diff --git a/app/src/main/java/com/flatstack/android/util/BaseViewModel.kt b/app/src/main/java/com/flatstack/android/util/BaseViewModel.kt new file mode 100644 index 0000000..6068461 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/util/BaseViewModel.kt @@ -0,0 +1,33 @@ +package com.flatstack.android.util + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +abstract class BaseViewModel : ViewModel() { + + private val shouldShowProgressMutableData = MutableLiveData() + val shouldShowProgressData : LiveData = shouldShowProgressMutableData + + suspend fun runSafe( + operation: suspend () -> T, + onSuccess: (T) -> Unit, + onError: (Throwable) -> Unit + ) { + runCatching { + showProgress() + operation() + } + .onSuccess(onSuccess) + .onFailure(onError) + hideProgress() + } + + private fun showProgress() { + shouldShowProgressMutableData.value = true + } + + private fun hideProgress() { + shouldShowProgressMutableData.value = false + } +} diff --git a/app/src/main/java/com/flatstack/android/util/FileUtils.kt b/app/src/main/java/com/flatstack/android/util/FileUtils.kt new file mode 100644 index 0000000..0911a04 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/util/FileUtils.kt @@ -0,0 +1,66 @@ +package com.flatstack.android.util + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.provider.OpenableColumns +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream + +class FileUtils(private val context: Context) { + + fun getRealFileName(uri: Uri): String { + val cursor = context.contentResolver.query(uri, null, null, null, null) + val nameIndex: Int = cursor!!.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + return cursor.getString(nameIndex).apply { cursor.close() } + } + + fun getFileMimeType(uri: Uri) = + context.contentResolver.getType(uri) ?: "" + + fun getFileSizeInBytes(uri: Uri): Int = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap( + ImageDecoder.createSource( + context.contentResolver, + uri + ) + ).byteCount + } else { + MediaStore.Images.Media.getBitmap(context.contentResolver, uri).byteCount + } + + fun getFileFromContentUri(uri: Uri, filename: String) : File? { + + val file = File(context.cacheDir, filename) + file.createNewFile() + + val bitmap = BitmapFactory.decodeStream(context.contentResolver.openInputStream(uri)) + var outputStream : FileOutputStream? = null + val byteArrayOutputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_COMPRESS_QUALITY, byteArrayOutputStream) + val bitmapData = byteArrayOutputStream.toByteArray() + runCatching { + outputStream = FileOutputStream(file) + }.onSuccess { + runCatching { + outputStream?.write(bitmapData) + outputStream?.flush() + outputStream?.close() + }.onSuccess { + return file + } + } + return null + } + + companion object { + const val JPEG_COMPRESS_QUALITY = 100 + } +} diff --git a/app/src/main/res/drawable/ic_baseline_image_24.xml b/app/src/main/res/drawable/ic_baseline_image_24.xml new file mode 100644 index 0000000..e8883d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_image_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 1684304..bf3eaf0 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -16,7 +16,7 @@ app:layout_constraintGuide_percent="0.5" /> +