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" />
+
+
+
+
diff --git a/app/src/main/res/layout/activity_registration.xml b/app/src/main/res/layout/activity_registration.xml
new file mode 100644
index 0000000..3e90039
--- /dev/null
+++ b/app/src/main/res/layout/activity_registration.xml
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9ed56aa..b39f947 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,11 +2,15 @@
Android Base
Login
Email address
- John
- Snow
+ First name
+ Last name
Password
- Username and Password should not be empty
+ Email and Password should not be empty
Logout
+ Sign Up
+ Choose Avatar
+ filename.jpg
+ Confirm Password
Unknown error
@@ -18,4 +22,8 @@
405 Method Not Allowed
409 Conflict
500 Server Error
+ Invalid credentials
+ Something went wrong…
+ Avatar
+
diff --git a/app/src/test/java/com/flatstack/android/ApiTest.kt b/app/src/test/java/com/flatstack/android/ApiTest.kt
new file mode 100644
index 0000000..e69de29
diff --git a/default-detekt-config.yml b/default-detekt-config.yml
index be7726d..1de3eaf 100644
--- a/default-detekt-config.yml
+++ b/default-detekt-config.yml
@@ -1,4 +1,4 @@
-build:
+Lbuild:
maxIssues: 0
weights:
# complexity: 2
@@ -66,7 +66,7 @@ complexity:
active: true
threshold: 60
LongParameterList:
- active: true
+ active: false
functionThreshold: 6
ignoreDefaultParameters: false
MethodOverloading:
diff --git a/deps.gradle b/deps.gradle
index 4291992..60da0e3 100644
--- a/deps.gradle
+++ b/deps.gradle
@@ -2,7 +2,7 @@ ext {
versions = [
BUILD_TOOLS_VERSION: '28.0.3',
MIN_SDK_VERSION : 17,
- TARGET_SDK_VERSION : 28,
+ TARGET_SDK_VERSION : 29,
kotlin : '1.4.10',
apollo : '2.2.3',
detekt : '1.10.0',
@@ -13,10 +13,11 @@ ext {
junit5_runner : '0.2.2',
mockk : '1.9.3',
mockwebserver : '3.14.0',
- retrofit : '2.5.0',
+ retrofit : '2.6.0',
coroutines : '1.1.1',
room : '2.1.0-beta01',
- espressoVersion : '3.1.1'
+ espressoVersion : '3.1.1',
+ glide : '4.11.0'
]
androidx = [
appcompat : 'androidx.appcompat:appcompat:1.0.2',
@@ -24,11 +25,19 @@ ext {
constraintlayout: 'androidx.constraintlayout:constraintlayout:1.1.3',
recyclerview : 'androidx.recyclerview:recyclerview:1.0.0',
cardview : 'androidx.cardview:cardview:1.0.0',
+ viewModelKtx : 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0',
+ liveDataKtx : 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0',
lifecycle : "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle"
]
apollo = [
apollo_runtime : "com.apollographql.apollo:apollo-runtime:$versions.apollo",
- apollo_coroutines : "com.apollographql.apollo:apollo-coroutines-support:$versions.apollo"
+ apollo_coroutines : "com.apollographql.apollo:apollo-coroutines-support:$versions.apollo",
+ okhttp_logging : "com.squareup.okhttp3:logging-interceptor:4.4.1"
+ ]
+ retrofit = [
+ gson : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
+ retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
+ okhttp_urlconnection : "com.squareup.okhttp3:okhttp-urlconnection:4.4.1"
]
room = [
room : "androidx.room:room-runtime:$versions.room",
@@ -57,14 +66,20 @@ ext {
kapt = [
room_compiler: "androidx.room:room-compiler:$versions.room",
lifecycle : "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle",
+ glide : "com.github.bumptech.glide:compiler:$versions.glide"
+ ]
+ glide = [
+ glide : "com.github.bumptech.glide:glide:$versions.glide"
]
androidLibs = androidx.values()
apolloLibs = apollo.values()
+ retrofitLibs = retrofit.values()
coroutinesLibs = coroutines.values()
kodeinLibs = kodein.values()
roomLibs = room.values()
compilerLibs = kapt.values()
unitTestLibs = unitTests.values()
androidTestsLibs = androidTests.values()
+ glideLibs = glide.values()
}