diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..61342e3b3 Binary files /dev/null and b/.DS_Store differ diff --git a/TesteAndroid/.gitignore b/TesteAndroid/.gitignore new file mode 100644 index 000000000..603b14077 --- /dev/null +++ b/TesteAndroid/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/TesteAndroid/.idea/$CACHE_FILE$ b/TesteAndroid/.idea/$CACHE_FILE$ new file mode 100644 index 000000000..f9784d3c7 --- /dev/null +++ b/TesteAndroid/.idea/$CACHE_FILE$ @@ -0,0 +1,17 @@ + + + + + + + + + + + Android + + + + + + \ No newline at end of file diff --git a/TesteAndroid/.idea/.name b/TesteAndroid/.idea/.name new file mode 100644 index 000000000..e8020f579 --- /dev/null +++ b/TesteAndroid/.idea/.name @@ -0,0 +1 @@ +Teste Android \ No newline at end of file diff --git a/TesteAndroid/.idea/codeStyles/Project.xml b/TesteAndroid/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..88ea3aa1e --- /dev/null +++ b/TesteAndroid/.idea/codeStyles/Project.xml @@ -0,0 +1,122 @@ + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/TesteAndroid/.idea/codeStyles/codeStyleConfig.xml b/TesteAndroid/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/TesteAndroid/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/TesteAndroid/.idea/dictionaries b/TesteAndroid/.idea/dictionaries new file mode 100644 index 000000000..2ada05a92 --- /dev/null +++ b/TesteAndroid/.idea/dictionaries @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/TesteAndroid/.idea/gradle.xml b/TesteAndroid/.idea/gradle.xml new file mode 100644 index 000000000..ac6b0aec6 --- /dev/null +++ b/TesteAndroid/.idea/gradle.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/.idea/jarRepositories.xml b/TesteAndroid/.idea/jarRepositories.xml new file mode 100644 index 000000000..a5f05cd8c --- /dev/null +++ b/TesteAndroid/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/.idea/misc.xml b/TesteAndroid/.idea/misc.xml new file mode 100644 index 000000000..37a750962 --- /dev/null +++ b/TesteAndroid/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/.idea/runConfigurations.xml b/TesteAndroid/.idea/runConfigurations.xml new file mode 100644 index 000000000..7f68460d8 --- /dev/null +++ b/TesteAndroid/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/TesteAndroid/.idea/vcs.xml b/TesteAndroid/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/TesteAndroid/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/TesteAndroid/READ.md b/TesteAndroid/READ.md new file mode 100644 index 000000000..34b64a67c --- /dev/null +++ b/TesteAndroid/READ.md @@ -0,0 +1,33 @@ +# Overview sobre o projeto + +Seguem algumas considerações sobre a arquitetura utilizada no projeto: + +Utilizei MVVMClean como arquitetura, separando as camadas em Fragment, ViewModel, Handler e Presenter. + +Segue um resumo da responsabilidade de cada camada: +- No fragment ficam basicamente os observers dos objetos LiveData adicionados nas ViewModels e nos ViewPresenters, atualizando os dados da +interface diretamente no xml, utilizando databinding. +- Na ViewModel ficam os objetos LiveData, os eventos de click de botão e as funções que controlam as ações do usuário, de acordo com ação de cada view. +Também ficam na ViewModel as requisições ao serviço, por meio da classe Handler. +- Por sua vez, a classe Handler é responsável por trabalhar com os dados requisitados pelo usuário. Sejam eles por meio de input de dados ou ações/requisições. + - É nela onde são feitas as chamadas à classe Service, que por sua vez busca/envia os dados usando a classe Repository. + - Também é nela onde enviam-se os dados trabalhados para objetos na classe Presenter, que por sua vez guarda dados de input, + estados das Views (selected, enabled), label texts, etc. e gerencia estados de componentes através de objetos LiveData. + +Utilizei injeção de dependência com a biblioteca Koin, que também foi utilizada nas rotinas de teste, utilizando a classe KoinTest. +Para as reqisições à API utilizei Retrofit. + +### # Observações gerais + +Na tela de login, há a validação de usuário e senha, onde para habilitar o botão de login, é necessário digitar os dados de acordo com a validação +sugerida (nome de usuário utilizando email ou cpf, e senha com ao menos 1 número, 1 caractere especial e uma letra maiúscula). +Fiz a validação desta maneira e não após o toque no botão de Sign in pois acredito que neste cenário entende-se que o usuário á está cadastrado e sabe +das regras de cadastro de nome de usuário e senha. + +*Para executar o projeto basta abrir no Android Studio, aguardar o gradle sincronizar as dependências e executar o mesmo. + +# OBRIGADO PELA OPORTUNIDADE! + +#MARLON + +#Estou me candidatando através da empresa HProjekt. Recrutador/Contato: Luiz Pontes. diff --git a/TesteAndroid/app/.gitignore b/TesteAndroid/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/TesteAndroid/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/TesteAndroid/app/build.gradle b/TesteAndroid/app/build.gradle new file mode 100644 index 000000000..d7f909b17 --- /dev/null +++ b/TesteAndroid/app/build.gradle @@ -0,0 +1,62 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: "androidx.navigation.safeargs.kotlin" + +android { + compileSdkVersion 29 + + buildFeatures { + dataBinding = true + } + + defaultConfig { + applicationId "br.com.mdr.testeandroid" + minSdkVersion 19 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.0' + + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + + // Koin + implementation "org.koin:koin-androidx-scope:2.1.6" + implementation "org.koin:koin-androidx-viewmodel:2.1.1" + testImplementation "org.koin:koin-test:2.1.6" + + // Retrofit + implementation 'com.squareup.retrofit2:retrofit:2.6.2' + implementation 'com.squareup.retrofit2:converter-gson:2.6.2' + + // OkHttp + implementation "com.squareup.okhttp3:logging-interceptor:3.12.1" + + testImplementation 'android.arch.core:core-testing:1.1.1' + testImplementation 'junit:junit:4.12' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.7' + + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} \ No newline at end of file diff --git a/TesteAndroid/app/proguard-rules.pro b/TesteAndroid/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/TesteAndroid/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/TesteAndroid/app/src/androidTest/java/br/com/mdr/testeandroid/ExampleInstrumentedTest.kt b/TesteAndroid/app/src/androidTest/java/br/com/mdr/testeandroid/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..6b6879391 --- /dev/null +++ b/TesteAndroid/app/src/androidTest/java/br/com/mdr/testeandroid/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package br.com.mdr.testeandroid + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("br.com.mdr.testeandroid", appContext.packageName) + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/AndroidManifest.xml b/TesteAndroid/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..59ae6b4f9 --- /dev/null +++ b/TesteAndroid/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/App.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/App.kt new file mode 100644 index 000000000..b09e83b43 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/App.kt @@ -0,0 +1,38 @@ +package br.com.mdr.testeandroid + +import android.app.Application +import br.com.mdr.testeandroid.di.* +import org.koin.android.ext.koin.androidContext + +import org.koin.core.context.startKoin + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ +class App: Application() { + + override fun onCreate() { + super.onCreate() + setupKoin() + } + + private fun setupKoin() { + startKoin { + androidContext(this@App) + modules( + listOf( + apiModule, + modelModule, + networkModule, + repositoryModule, + serviceModule, + viewModelModule, + preferencesModule, + adapterModule, + presenterModule + ) + ) + } + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/adapter/AdapterItemsContract.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/adapter/AdapterItemsContract.kt new file mode 100644 index 000000000..b5b74b4ff --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/adapter/AdapterItemsContract.kt @@ -0,0 +1,8 @@ +package br.com.mdr.testeandroid.adapter + + +interface AdapterItemsContract { + + fun replaceItens(list: List<*>) + +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/adapter/StatementAdapter.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/adapter/StatementAdapter.kt new file mode 100644 index 000000000..ae6c72bf0 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/adapter/StatementAdapter.kt @@ -0,0 +1,41 @@ +package br.com.mdr.testeandroid.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import br.com.mdr.testeandroid.databinding.DashboardStatementItemBinding +import br.com.mdr.testeandroid.model.business.Statement + +/** + * @author Marlon D. Rocha + * @since 07/07/20 + */ +class StatementAdapter: RecyclerView.Adapter(), AdapterItemsContract { + + private var itens: MutableList? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatementViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = DashboardStatementItemBinding.inflate(inflater, parent, false) + return StatementViewHolder(binding) + } + + override fun onBindViewHolder(holder: StatementViewHolder, position: Int) { + val statement = itens!![position] + holder.bind(statement) + } + + override fun getItemCount() = if (itens != null) itens!!.size else 0 + + @Suppress("UNCHECKED_CAST") + override fun replaceItens(list: List<*>) { + this.itens = list as MutableList + notifyDataSetChanged() + } + + class StatementViewHolder(private val binding: DashboardStatementItemBinding): RecyclerView.ViewHolder(binding.root) { + fun bind(statement: Statement) { + binding.statement = statement + } + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/api/DashboardApi.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/api/DashboardApi.kt new file mode 100644 index 000000000..bf11f1b3b --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/api/DashboardApi.kt @@ -0,0 +1,20 @@ +package br.com.mdr.testeandroid.api + +import br.com.mdr.testeandroid.model.api.DashboardApiModel +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +/** + * @author Marlon D. Rocha + * @since 07/07/20 + */ +interface DashboardApi { + companion object { + private const val USER_ID_PARAM = "id" + private const val STATEMENTS_PATH = "statements/{$USER_ID_PARAM}" + } + + @GET(STATEMENTS_PATH) + suspend fun getStatements(@Path(USER_ID_PARAM) userId: String): Response +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/api/SignInApi.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/api/SignInApi.kt new file mode 100644 index 000000000..2e3bc8831 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/api/SignInApi.kt @@ -0,0 +1,20 @@ +package br.com.mdr.testeandroid.api + +import br.com.mdr.testeandroid.model.api.SignInApiModel +import br.com.mdr.testeandroid.model.api.UserApiModel +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ +interface SignInApi { + companion object { + private const val SIGN_IN_PATH = "login" + } + + @POST(SIGN_IN_PATH) + suspend fun signInUser(@Body user: SignInApiModel): Response +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ApiModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ApiModule.kt new file mode 100644 index 000000000..9f02c9b8f --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ApiModule.kt @@ -0,0 +1,16 @@ +package br.com.mdr.testeandroid.di + +import br.com.mdr.testeandroid.api.DashboardApi +import br.com.mdr.testeandroid.api.SignInApi +import org.koin.dsl.module +import retrofit2.Retrofit + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ + +val apiModule = module { + single { get().create(SignInApi::class.java) } + single { get().create(DashboardApi::class.java) } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ModelModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ModelModule.kt new file mode 100644 index 000000000..ba5018eb4 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ModelModule.kt @@ -0,0 +1,21 @@ +package br.com.mdr.testeandroid.di + +import br.com.mdr.testeandroid.model.api.UserApiModel +import br.com.mdr.testeandroid.model.business.Statement +import br.com.mdr.testeandroid.model.business.User +import org.koin.dsl.module + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ + +val modelModule = module { + + // Business + factory { User() } + single { Statement() } + + // Api + factory { UserApiModel() } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/NetworkModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/NetworkModule.kt new file mode 100644 index 000000000..f3f158abd --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/NetworkModule.kt @@ -0,0 +1,70 @@ +package br.com.mdr.testeandroid.di + +import br.com.mdr.testeandroid.util.Constants.Companion.BASE_URL +import com.google.gson.GsonBuilder +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.dsl.module +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + + +/** + * @author Marlon D. Rocha + * @since 04/07/2020 + */ + +val networkModule = module { + + // Retrofit + single { + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(get()) + .client(get()) + .build() + } + + // OkHttp Client + single { + OkHttpClient.Builder() + .addInterceptor(get()) + .addInterceptor(get()) + .build() + } + + // Http Logging Interceptor + single { + HttpLoggingInterceptor( + HttpLoggingInterceptor.Logger {} + ).apply { + level = HttpLoggingInterceptor.Level.BODY + } + } + + // Interceptor + single { + Interceptor { chain -> + chain.request().run { + newBuilder() + .addHeader("Accept", "application/json") + .addHeader("Content-type", "application/json") + .method(this.method(), this.body()) + .build() + .let(chain::proceed) + } + } + } + + // Gson + single { + GsonBuilder().create() + } + + // GsonConverterFactory + single { + GsonConverterFactory.create(get()) as Converter.Factory + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/PreferencesModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/PreferencesModule.kt new file mode 100644 index 000000000..3534b6701 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/PreferencesModule.kt @@ -0,0 +1,18 @@ +package br.com.mdr.testeandroid.di + +import android.app.Application +import android.content.SharedPreferences +import br.com.mdr.testeandroid.util.Constants.Companion.PREFERENCES_FILE_KEY +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module + +val preferencesModule = module { + single { getSharedPrefs(androidApplication()) } + single { + getSharedPrefs(androidApplication()).edit() + } +} + +private fun getSharedPrefs(app: Application): SharedPreferences { + return app.getSharedPreferences(PREFERENCES_FILE_KEY, android.content.Context.MODE_PRIVATE) +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/PresenterModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/PresenterModule.kt new file mode 100644 index 000000000..22b7836dc --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/PresenterModule.kt @@ -0,0 +1,17 @@ +package br.com.mdr.testeandroid.di + +import br.com.mdr.testeandroid.flow.dashboard.DashboardHandler +import br.com.mdr.testeandroid.flow.signin.SignInHandler +import br.com.mdr.testeandroid.flow.signin.SignInViewPresenter +import org.koin.dsl.module + +val presenterModule = module { + + //SignIn + single { SignInHandler(get(), get()) } + single { SignInViewPresenter() } + + //Dashboard + single { DashboardHandler(get()) } + +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/RepositoryModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/RepositoryModule.kt new file mode 100644 index 000000000..a64c55ae2 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/RepositoryModule.kt @@ -0,0 +1,15 @@ +package br.com.mdr.testeandroid.di + +import br.com.mdr.testeandroid.repository.DashboardRepository +import br.com.mdr.testeandroid.repository.SignInRepository +import org.koin.dsl.module + +/** + * @author Marlon D. Rocha + * @since 05/07/2020 + */ + +val repositoryModule = module { + single { SignInRepository(get(), get()) } + single { DashboardRepository(get()) } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ServiceModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ServiceModule.kt new file mode 100644 index 000000000..6d2322254 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ServiceModule.kt @@ -0,0 +1,15 @@ +package br.com.mdr.testeandroid.di + +import br.com.mdr.testeandroid.service.DashboardService +import br.com.mdr.testeandroid.service.SignInService +import org.koin.dsl.module + +/** + * @author Marlon D. Rocha + * @since 05/07/2020 + */ + +val serviceModule = module { + single { SignInService(get()) } + single { DashboardService(get()) } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ViewModelModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ViewModelModule.kt new file mode 100644 index 000000000..1d2dd8105 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/ViewModelModule.kt @@ -0,0 +1,16 @@ +package br.com.mdr.testeandroid.di + +import br.com.mdr.testeandroid.flow.dashboard.DashboardViewModel +import br.com.mdr.testeandroid.flow.signin.SignInViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +/** + * @author Marlon D. Rocha + * @since 05/07/2020 + */ + +val viewModelModule = module { + viewModel { SignInViewModel(signInHandler = get()) } + viewModel { DashboardViewModel(get()) } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/adapterModule.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/adapterModule.kt new file mode 100644 index 000000000..1478e36ca --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/di/adapterModule.kt @@ -0,0 +1,13 @@ +package br.com.mdr.testeandroid.di + +import br.com.mdr.testeandroid.adapter.StatementAdapter +import org.koin.dsl.module + +/** + * @author Marlon D. Rocha + * @since 07/07/20 + */ + +val adapterModule = module { + single { StatementAdapter() } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/ActivityExtensions.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/ActivityExtensions.kt new file mode 100644 index 000000000..9897f40c1 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/ActivityExtensions.kt @@ -0,0 +1,21 @@ +package br.com.mdr.testeandroid.extensions + +import android.app.Activity +import android.os.Build +import android.view.View +import androidx.core.content.ContextCompat + +/** + * @author Marlon D. Rocha + * @since 07/07/20 + */ + +fun Activity.setLightStatusBar(isLight: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + window?.decorView?.systemUiVisibility = if (isLight) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0 +} + +fun Activity.setStatusBarColor(color: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + window?.statusBarColor = ContextCompat.getColor(this, color) +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/ButtonExtensions.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/ButtonExtensions.kt new file mode 100644 index 000000000..594cef4e5 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/ButtonExtensions.kt @@ -0,0 +1,17 @@ +package br.com.mdr.testeandroid.extensions + +import android.content.Context +import android.content.res.ColorStateList +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import com.google.android.material.button.MaterialButton + +/** + * @author Marlon D. Rocha + * @since 05/07/2020 + */ + +fun MaterialButton.changeBackgroundColor(@ColorRes color: Int, context: Context) { + val buttonColor = ContextCompat.getColor(context, color) + backgroundTintList = ColorStateList.valueOf(buttonColor) +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/DoubleExtensions.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/DoubleExtensions.kt new file mode 100644 index 000000000..4e87bb1e0 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/DoubleExtensions.kt @@ -0,0 +1,13 @@ +package br.com.mdr.testeandroid.extensions + +import java.text.NumberFormat +import java.util.* + +/** + * @author Marlon D. Rocha + * @since 07/07/20 + */ + +fun Double.formattedCurrency(): String { + return NumberFormat.getCurrencyInstance(Locale("pt", "BR")).format(this) +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/FragmentExtensions.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/FragmentExtensions.kt new file mode 100644 index 000000000..c3658e2a4 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/FragmentExtensions.kt @@ -0,0 +1,32 @@ +package br.com.mdr.testeandroid.extensions + +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import br.com.mdr.testeandroid.R +import com.google.android.material.snackbar.Snackbar + +/** + * @author Marlon D. Rocha + * @since 08/07/20 + */ + +fun Fragment.showErrorSnack(snackMessage: String) { + val snackBar = Snackbar.make(this.requireView(), snackMessage, Snackbar.LENGTH_LONG) + val textId = com.google.android.material.R.id.snackbar_text + val snackView = snackBar.view + val txtSnack = snackView.findViewById(textId) + txtSnack.maxLines = 5 + val params = snackView.layoutParams as FrameLayout.LayoutParams + val sideMargin = 16 + params.setMargins(params.leftMargin + sideMargin, + params.topMargin, + params.rightMargin + sideMargin, + params.bottomMargin + sideMargin) + snackView.layoutParams = params + snackView.setBackgroundResource(R.drawable.error_snack_corner) + + txtSnack.setTextColor(ContextCompat.getColor(requireContext(), R.color.colorPrimary)) + snackBar.show() +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/LiveDataExtensions.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/LiveDataExtensions.kt new file mode 100644 index 000000000..7d5d55024 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/LiveDataExtensions.kt @@ -0,0 +1,39 @@ +package br.com.mdr.testeandroid.extensions + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** + * @author Marlon D. Rocha + * @since 09/07/20 + */ + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS, + afterObserve: () -> Unit = {} +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + this.observeForever(observer) + + afterObserve.invoke() + + if (!latch.await(time, timeUnit)) { + this.removeObserver(observer) + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/StringExtensions.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/StringExtensions.kt new file mode 100644 index 000000000..b9fdc93de --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/extensions/StringExtensions.kt @@ -0,0 +1,75 @@ +package br.com.mdr.testeandroid.extensions + +import android.text.TextUtils +import br.com.mdr.testeandroid.util.Constants.Companion.REGEX_SPECIAL_CHARS +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern + +fun String.hasDigit(): Boolean { + var hasDigit = false + for (letter in this) { + if (!hasDigit) + hasDigit = letter.isDigit() + } + return hasDigit +} + +fun String.hasUppercasedLetter(): Boolean { + + for (letter in this) { + if (letter.isUpperCase()) + return true + } + return false +} + +fun String.isEmail(): Boolean { + return !TextUtils.isEmpty(this) && android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches() +} + +fun String.hasSpecialCharacters(): Boolean { + val pattern: Pattern = Pattern.compile(REGEX_SPECIAL_CHARS) + val matcher: Matcher = pattern.matcher(this) + + return !matcher.matches() +} + +fun String.isCPF(): Boolean { + if (TextUtils.isEmpty(this)) return false + + val numbers = arrayListOf() + + this.filter { it.isDigit() }.forEach { + numbers.add(it.toString().toInt()) + } + + if (numbers.size != 11) return false + + //repeticao + (0..9).forEach { n -> + val digits = arrayListOf() + (0..10).forEach { digits.add(n) } + if (numbers == digits) return false + } + + //digito 1 + val dv1 = ((0..8).sumBy { (it + 1) * numbers[it] }).rem(11).let { + if (it >= 10) 0 else it + } + + val dv2 = ((0..8).sumBy { it * numbers[it] }.let { (it + (dv1 * 9)).rem(11) }).let { + if (it >= 10) 0 else it + } + + return numbers[9] == dv1 && numbers[10] == dv2 +} + +fun String.formatDateBr(): String { + val deFaultformatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val convertedDate = deFaultformatter.parse(this) + + val brFormatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + return brFormatter.format(convertedDate) +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardFragment.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardFragment.kt new file mode 100644 index 000000000..7c9ff7cea --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardFragment.kt @@ -0,0 +1,83 @@ +package br.com.mdr.testeandroid.flow.dashboard + +import android.content.SharedPreferences +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import br.com.mdr.testeandroid.R +import br.com.mdr.testeandroid.adapter.StatementAdapter +import br.com.mdr.testeandroid.databinding.DashboardFragmentBinding +import br.com.mdr.testeandroid.extensions.setLightStatusBar +import br.com.mdr.testeandroid.extensions.setStatusBarColor +import br.com.mdr.testeandroid.extensions.showErrorSnack +import br.com.mdr.testeandroid.util.Constants.Companion.USER_KEY +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel + +class DashboardFragment : Fragment() { + private val viewModel: DashboardViewModel by viewModel() + private val adapter: StatementAdapter by inject() + private var binding: DashboardFragmentBinding? = null + private val preferencesEditor: SharedPreferences.Editor by inject() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DashboardFragmentBinding.inflate(inflater) + binding?.let { + it.recyclerStatements.adapter = adapter + setupListeners(it) + } + return binding?.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity?.setStatusBarColor(R.color.colorAccent) + activity?.setLightStatusBar(false) + + setupObservables() + if (arguments != null) { + arguments?.let { + val user = DashboardFragmentArgs.fromBundle(it).usuario + binding?.user = user + viewModel.fetchUserStatements(user) + } + } + } + + private fun setupObservables() { + viewModel.statementsLive.observe(viewLifecycleOwner, Observer { statements -> + statements?.let { + adapter.replaceItens(it) + } + }) + + viewModel.errorLive.observe( viewLifecycleOwner, Observer { error -> + if (error?.code != 0) + error?.message?.let { showErrorSnack(it) } + }) + + viewModel.isLoading.observe(viewLifecycleOwner, Observer { + binding?.apply { showLoading = it } + }) + } + + private fun setupListeners(binding: DashboardFragmentBinding) { + binding.apply { + btnSignOut.setOnClickListener{ + signOutUser() + viewModel.clickListener() + } + } + } + + private fun signOutUser() { + preferencesEditor.remove(USER_KEY) + preferencesEditor.apply() + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardHandler.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardHandler.kt new file mode 100644 index 000000000..38f88239c --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardHandler.kt @@ -0,0 +1,19 @@ +package br.com.mdr.testeandroid.flow.dashboard + +import br.com.mdr.testeandroid.model.api.DashboardApiModel +import br.com.mdr.testeandroid.service.DashboardService + +class DashboardHandler( + override val service: DashboardService +) : IDashboardHandler { + + override suspend fun fetchStatements(userId: Int): DashboardApiModel { + var apiResult = DashboardApiModel() + + service.getStatements(userId)?.let { + apiResult = it + } + + return apiResult + } +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardViewModel.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardViewModel.kt new file mode 100644 index 000000000..e7bef1452 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/DashboardViewModel.kt @@ -0,0 +1,47 @@ +package br.com.mdr.testeandroid.flow.dashboard + +import android.view.View +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.navigation.findNavController +import br.com.mdr.testeandroid.R +import br.com.mdr.testeandroid.flow.main.BaseViewModel +import br.com.mdr.testeandroid.model.api.DashboardApiModel +import br.com.mdr.testeandroid.model.business.Error +import br.com.mdr.testeandroid.model.business.Statement +import br.com.mdr.testeandroid.model.business.User +import kotlinx.coroutines.launch + +class DashboardViewModel( + private val dashboardHandler: DashboardHandler +) : BaseViewModel() { + private lateinit var _user: User + val statementsLive: MutableLiveData?> = MutableLiveData() + var errorLive: MutableLiveData = MutableLiveData() + + fun clickListener() = View.OnClickListener { + when (it.id) { + R.id.btnSignOut -> { signOutUser(it)} + } + } + + private fun signOutUser(view: View) { + val direction = DashboardFragmentDirections.actionDashboardFragmentToSignInFragment() + view.findNavController().navigate(direction) + } + + fun fetchUserStatements(user: User) { + isLoading.value = true + _user = user + viewModelScope.launch { + val apiResult = dashboardHandler.fetchStatements(_user.userId!!) + fetchApiResult(apiResult) + } + } + + private fun fetchApiResult(result: DashboardApiModel) { + result.statementList?.let { statementsLive.value = it } + result.error?.let { errorLive.value = it } + isLoading.postValue(false) + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/IDashboardHandler.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/IDashboardHandler.kt new file mode 100644 index 000000000..f7b3a096d --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/dashboard/IDashboardHandler.kt @@ -0,0 +1,9 @@ +package br.com.mdr.testeandroid.flow.dashboard + +import br.com.mdr.testeandroid.model.api.DashboardApiModel +import br.com.mdr.testeandroid.service.IDashboardService + +interface IDashboardHandler { + val service: IDashboardService + suspend fun fetchStatements(userId: Int): DashboardApiModel +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/main/BaseViewModel.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/main/BaseViewModel.kt new file mode 100644 index 000000000..7c360d8e4 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/main/BaseViewModel.kt @@ -0,0 +1,12 @@ +package br.com.mdr.testeandroid.flow.main + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +/** + * @author Marlon D. Rocha + * @since 10/07/20 + */ +open class BaseViewModel: ViewModel() { + var isLoading: MutableLiveData = MutableLiveData(false) +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/main/MainActivity.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/main/MainActivity.kt new file mode 100644 index 000000000..0e03e967c --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/main/MainActivity.kt @@ -0,0 +1,26 @@ +package br.com.mdr.testeandroid.flow.main + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import br.com.mdr.testeandroid.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + //private val activityPresenter: LoadingPresenter by inject() + private lateinit var mainBinding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mainBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(mainBinding.root) + //setupObservables() + } + +// private fun setupObservables() { +// activityPresenter.isLoading.observe(this, Observer { +// GlobalScope.launch { +// withContext(Dispatchers.Main) { mainBinding.showLoading = it } +// } +// }) +// } + +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/ISignInHandler.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/ISignInHandler.kt new file mode 100644 index 000000000..40d19116d --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/ISignInHandler.kt @@ -0,0 +1,10 @@ +package br.com.mdr.testeandroid.flow.signin + +import br.com.mdr.testeandroid.service.ISignInService + +interface ISignInHandler { + val signInPresenter: ISignInViewPresenter + val service: ISignInService + fun onUserNameChanged(userName: CharSequence) + fun onPasswordChanged(password: CharSequence) +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/ISignInViewPresenter.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/ISignInViewPresenter.kt new file mode 100644 index 000000000..721455231 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/ISignInViewPresenter.kt @@ -0,0 +1,12 @@ +package br.com.mdr.testeandroid.flow.signin + +import androidx.lifecycle.MutableLiveData + +interface ISignInViewPresenter { + var buttonEnabledLive: MutableLiveData + var userName: String + var password: String + var maskedUserName: MutableLiveData + + fun handleButtonState() +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInFragment.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInFragment.kt new file mode 100644 index 000000000..336bfe039 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInFragment.kt @@ -0,0 +1,116 @@ +package br.com.mdr.testeandroid.flow.signin + +import android.content.SharedPreferences +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import br.com.mdr.testeandroid.R +import br.com.mdr.testeandroid.databinding.SignInFragmentBinding +import br.com.mdr.testeandroid.extensions.* +import br.com.mdr.testeandroid.model.business.User +import br.com.mdr.testeandroid.util.Constants.Companion.USER_KEY +import com.google.gson.Gson +import kotlinx.android.synthetic.main.sign_in_fragment.* +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel + +class SignInFragment : Fragment() { + private var binding: SignInFragmentBinding? = null + //Caso a viewmodel venha a ser utilizada por mais de um Fragment, pode ser utilizado o escopo sharedViewModel() + private val viewModel: SignInViewModel by viewModel() + private val preferences: SharedPreferences by inject() + private val preferencesEditor: SharedPreferences.Editor by inject() + private val gson: Gson by inject() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = SignInFragmentBinding.inflate(inflater) + binding?.let { + it.handler = viewModel.signInHandler + setupListeners(it) + } + return binding?.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupObservables() + checkLoggedUser() + activity?.setStatusBarColor(R.color.white) + activity?.setLightStatusBar(true) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + + private fun setupObservables() { + viewModel.errorLive.observe( viewLifecycleOwner, Observer { error -> + if (error?.code != 0) + error?.message?.let { showErrorSnack(it) } + }) + + viewModel.signInHandler.signInPresenter.buttonEnabledLive.observe(viewLifecycleOwner, Observer { isEnabled -> + binding.apply { + btnSignIn?.isEnabled = isEnabled + val buttonColor = + if (isEnabled) + R.color.colorAccent + else + R.color.disabled_background + btnSignIn?.changeBackgroundColor(buttonColor, requireContext()) + } + }) + + viewModel.signInHandler.signInPresenter.maskedUserName.observe(viewLifecycleOwner, Observer { maskedUserName -> + binding?.let { edtUserName.setText(maskedUserName) } + }) + + viewModel.showUserInfo.observe(viewLifecycleOwner, Observer { + binding?.showUserAccount = it + }) + + viewModel.userLive.observe(viewLifecycleOwner, Observer { user -> + binding?.let { + if (viewModel.isUserLogged) { + it.user = user + it.showUserAccount = user?.userId != null + val welComeString = "${resources.getString(R.string.hello_user)} ${user?.name}" + it.welcomeLabel = welComeString + } else { + saveUser(user) + it.btnSignInUser.callOnClick() + } + } + }) + + viewModel.isLoading.observe(viewLifecycleOwner, Observer { + binding?.apply { showLoading = it } + }) + } + + private fun saveUser(user: User) { + val strUser = gson.toJson(user) + preferencesEditor.putString(USER_KEY, strUser) + preferencesEditor.apply() + } + + private fun checkLoggedUser() { + val strUser = preferences.getString(USER_KEY, "") + val user = gson.fromJson(strUser, User::class.java) + viewModel.setUser(user) + } + + private fun setupListeners(binding: SignInFragmentBinding) { + binding.apply { + clickListener = viewModel.manageOnClick() + } + } + +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInHandler.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInHandler.kt new file mode 100644 index 000000000..5872e8dc4 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInHandler.kt @@ -0,0 +1,53 @@ +package br.com.mdr.testeandroid.flow.signin + +import br.com.mdr.testeandroid.extensions.isCPF +import br.com.mdr.testeandroid.model.api.SignInApiModel +import br.com.mdr.testeandroid.model.api.UserApiModel +import br.com.mdr.testeandroid.service.SignInService +import br.com.mdr.testeandroid.util.MaskUtil + +class SignInHandler( + override val signInPresenter: SignInViewPresenter, + override val service: SignInService +) : ISignInHandler { + private var maskedCpf = false + + override fun onUserNameChanged(userName: CharSequence) { + var userNameString = userName.toString() + + if (userNameString.length == 14 && maskedCpf) { + userNameString = MaskUtil.removeMask(userName.toString()) + } + + if (userNameString.isCPF() && !maskedCpf) { + maskedCpf = true + signInPresenter.maskedUserName.value = MaskUtil.getCpfMask(userNameString) + } else { + maskedCpf = false + } + + signInPresenter.userName = userNameString + handleButtonState() + } + + override fun onPasswordChanged(password: CharSequence) { + signInPresenter.password = password.toString() + handleButtonState() + } + + private fun handleButtonState() { + signInPresenter.handleButtonState() + } + + suspend fun callSignInUser(): UserApiModel { + var apiResult = UserApiModel() + + val signInApiModel = SignInApiModel(signInPresenter.userName, signInPresenter.password) + service.loginUser(signInApiModel)?.let { + signInPresenter.userName = "" + signInPresenter.password = "" + apiResult = it + } + return apiResult + } +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInViewModel.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInViewModel.kt new file mode 100644 index 000000000..0fa7e2861 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInViewModel.kt @@ -0,0 +1,60 @@ +package br.com.mdr.testeandroid.flow.signin + +import android.view.View +import androidx.lifecycle.* +import androidx.navigation.findNavController +import br.com.mdr.testeandroid.R +import br.com.mdr.testeandroid.flow.main.BaseViewModel +import br.com.mdr.testeandroid.model.api.UserApiModel +import br.com.mdr.testeandroid.model.business.Error +import br.com.mdr.testeandroid.model.business.User +import kotlinx.coroutines.launch + +class SignInViewModel( + val signInHandler: SignInHandler) : BaseViewModel() { + var showUserInfo: MutableLiveData = MutableLiveData(false) + var isUserLogged: Boolean = false + var userLive: MutableLiveData = MutableLiveData() + var errorLive: MutableLiveData = MutableLiveData() + + + fun manageOnClick() = View.OnClickListener { + when (it.id) { + R.id.btnSignIn -> { + callSignIn() + } + R.id.btnSignInAnother -> { + showUserInfo.value = false + } + + R.id.btnSignInUser -> { + val userSaved = userLive.value + userSaved?.let { user -> + val direction = SignInFragmentDirections.actionSignInFragmentToDashboardFragment(usuario = user) + it.findNavController().navigate(direction) + } + } + } + } + + fun setUser(user: User?) { + user?.let{ + isUserLogged = true + userLive.value = it + } + } + + fun callSignIn() { + isLoading.value = true + viewModelScope.launch { + val apiResult = signInHandler.callSignInUser() + fetchApiResult(apiResult) + } + } + + private fun fetchApiResult(result: UserApiModel) { + result.userAccount?.let { userLive.value = it } + result.error?.let { errorLive.value = it } + isLoading.postValue(false) + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInViewPresenter.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInViewPresenter.kt new file mode 100644 index 000000000..4dda6f495 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/flow/signin/SignInViewPresenter.kt @@ -0,0 +1,17 @@ +package br.com.mdr.testeandroid.flow.signin + +import androidx.lifecycle.MutableLiveData +import br.com.mdr.testeandroid.extensions.* + +class SignInViewPresenter( + override var buttonEnabledLive: MutableLiveData = MutableLiveData(false), + override var userName: String = "", + override var password: String = "", + override var maskedUserName: MutableLiveData = MutableLiveData("") +) : ISignInViewPresenter { + + override fun handleButtonState() { + buttonEnabledLive.value = (userName.isCPF() || userName.isEmail()) && + (password.hasUppercasedLetter() && password.hasDigit() && password.hasSpecialCharacters()) + } +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/DashboardApiModel.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/DashboardApiModel.kt new file mode 100644 index 000000000..5d711b115 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/DashboardApiModel.kt @@ -0,0 +1,14 @@ +package br.com.mdr.testeandroid.model.api + +import br.com.mdr.testeandroid.model.business.Error +import br.com.mdr.testeandroid.model.business.Statement + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ + +data class DashboardApiModel( + var statementList: MutableList? = null, + var error: Error? = null +) diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/SignInApiModel.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/SignInApiModel.kt new file mode 100644 index 000000000..6399395ed --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/SignInApiModel.kt @@ -0,0 +1,9 @@ +package br.com.mdr.testeandroid.model.api + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ +class SignInApiModel ( + val user: String, + val password: String) \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/UserApiModel.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/UserApiModel.kt new file mode 100644 index 000000000..83209beef --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/api/UserApiModel.kt @@ -0,0 +1,14 @@ +package br.com.mdr.testeandroid.model.api + +import br.com.mdr.testeandroid.model.business.Error +import br.com.mdr.testeandroid.model.business.User + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ + +data class UserApiModel( + var userAccount: User? = null, + var error: Error? = null +) diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/Error.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/Error.kt new file mode 100644 index 000000000..1ee9bbcac --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/Error.kt @@ -0,0 +1,6 @@ +package br.com.mdr.testeandroid.model.business + +data class Error( + val code: Int = 0, + val message: String = "" +) \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/Statement.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/Statement.kt new file mode 100644 index 000000000..90875283a --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/Statement.kt @@ -0,0 +1,25 @@ +package br.com.mdr.testeandroid.model.business + +import br.com.mdr.testeandroid.extensions.formatDateBr +import java.text.NumberFormat +import java.util.* + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ +data class Statement( + var title: String = "", + var desc: String = "", + var date: String = "", + var value: Double? = 0.0 +) { + fun getFormatedValue(): String { + return NumberFormat.getCurrencyInstance(Locale("pt", "BR")) + .format(value) + } + + fun getFormatedDate(): String? { + return date.formatDateBr() + } +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/User.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/User.kt new file mode 100644 index 000000000..e2399473a --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/model/business/User.kt @@ -0,0 +1,26 @@ +package br.com.mdr.testeandroid.model.business + +import br.com.mdr.testeandroid.extensions.formattedCurrency +import java.io.Serializable + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ +data class User( + var userId: Int? = null, + var name: String? = null, + var bankAccount: String? = null, + var agency: String? = null, + var balance: Double? = null +): Serializable { + + fun getFullAccount(): String { + return "$agency / $bankAccount" + } + + fun getFormattedBalance(): String { + balance?.let { return it.formattedCurrency() } + return "" + } +} diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/BaseRepository.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/BaseRepository.kt new file mode 100644 index 000000000..833080b3e --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/BaseRepository.kt @@ -0,0 +1,10 @@ +package br.com.mdr.testeandroid.repository + +import retrofit2.Response + +abstract class BaseRepository { + + protected fun handleResponse(response: Response): T? { + return response.body() + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/DashboardRepository.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/DashboardRepository.kt new file mode 100644 index 000000000..e188d1954 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/DashboardRepository.kt @@ -0,0 +1,17 @@ +package br.com.mdr.testeandroid.repository + +import br.com.mdr.testeandroid.api.DashboardApi +import br.com.mdr.testeandroid.model.api.DashboardApiModel + +/** + * @author Marlon D. Rocha + * @since 07/07/20 + */ +class DashboardRepository( + private val dashboardApi: DashboardApi +) : BaseRepository(), IDashboardRepository { + + override suspend fun getStatements(userId: Int): DashboardApiModel? { + return handleResponse(dashboardApi.getStatements(userId.toString())) + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/IDashboardRepository.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/IDashboardRepository.kt new file mode 100644 index 000000000..16814d411 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/IDashboardRepository.kt @@ -0,0 +1,7 @@ +package br.com.mdr.testeandroid.repository + +import br.com.mdr.testeandroid.model.api.DashboardApiModel + +interface IDashboardRepository { + suspend fun getStatements(userId: Int): DashboardApiModel? +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/ISignInRepository.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/ISignInRepository.kt new file mode 100644 index 000000000..8c919a44e --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/ISignInRepository.kt @@ -0,0 +1,8 @@ +package br.com.mdr.testeandroid.repository + +import br.com.mdr.testeandroid.model.api.SignInApiModel +import br.com.mdr.testeandroid.model.api.UserApiModel + +interface ISignInRepository { + suspend fun signInUser(user: SignInApiModel): UserApiModel? +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/SignInRepository.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/SignInRepository.kt new file mode 100644 index 000000000..f8048c3c8 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/repository/SignInRepository.kt @@ -0,0 +1,19 @@ +package br.com.mdr.testeandroid.repository + +import br.com.mdr.testeandroid.api.SignInApi +import br.com.mdr.testeandroid.model.api.SignInApiModel +import br.com.mdr.testeandroid.model.api.UserApiModel +import com.google.gson.Gson + +/** + * @author Marlon D. Rocha + * @since 04/07/20 + */ +class SignInRepository( + private val signInApi: SignInApi +) : BaseRepository(), ISignInRepository { + + override suspend fun signInUser(user: SignInApiModel): UserApiModel? { + return handleResponse(signInApi.signInUser(user)) + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/DashboardService.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/DashboardService.kt new file mode 100644 index 000000000..15329ee49 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/DashboardService.kt @@ -0,0 +1,14 @@ +package br.com.mdr.testeandroid.service + +import br.com.mdr.testeandroid.model.api.DashboardApiModel +import br.com.mdr.testeandroid.repository.DashboardRepository + + +class DashboardService( + private val dashboardRepository: DashboardRepository +) : IDashboardService { + + override suspend fun getStatements(userId: Int): DashboardApiModel? { + return dashboardRepository.getStatements(userId) + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/IDashboardService.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/IDashboardService.kt new file mode 100644 index 000000000..9a2392b69 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/IDashboardService.kt @@ -0,0 +1,14 @@ +package br.com.mdr.testeandroid.service + +import br.com.mdr.testeandroid.model.api.DashboardApiModel + +interface IDashboardService { + + /** + * Get user statements + * @param userId The id of logged user + * @return List of user statements + * @throws when data is not valid for creation + */ + suspend fun getStatements(userId: Int): DashboardApiModel? +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/ISignInService.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/ISignInService.kt new file mode 100644 index 000000000..1e2ce31c2 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/ISignInService.kt @@ -0,0 +1,15 @@ +package br.com.mdr.testeandroid.service + +import br.com.mdr.testeandroid.model.api.SignInApiModel +import br.com.mdr.testeandroid.model.api.UserApiModel + +interface ISignInService { + + /** + * Sign in user to app + * @param signInUser The user that will be created + * @return The authenticated user + * @throws when user data is not valid for creation + */ + suspend fun loginUser(signInUser: SignInApiModel): UserApiModel? +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/SignInService.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/SignInService.kt new file mode 100644 index 000000000..1d68d9c8d --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/service/SignInService.kt @@ -0,0 +1,15 @@ +package br.com.mdr.testeandroid.service + +import br.com.mdr.testeandroid.model.api.SignInApiModel +import br.com.mdr.testeandroid.model.api.UserApiModel +import br.com.mdr.testeandroid.repository.SignInRepository + + +class SignInService( + private val signInRepository: SignInRepository +) : ISignInService { + + override suspend fun loginUser(signInUser: SignInApiModel): UserApiModel? { + return signInRepository.signInUser(signInUser) + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/util/Constants.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/util/Constants.kt new file mode 100644 index 000000000..8dbfe41a0 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/util/Constants.kt @@ -0,0 +1,10 @@ +package br.com.mdr.testeandroid.util + +class Constants { + companion object { + const val BASE_URL = "https://bank-app-test.herokuapp.com/api/" + const val REGEX_SPECIAL_CHARS = "[a-zA-Z0-9]*" + const val USER_KEY = "logged_user" + const val PREFERENCES_FILE_KEY = "br.com.mdr.testeandroid" + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/util/Mask.kt b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/util/Mask.kt new file mode 100644 index 000000000..e64351056 --- /dev/null +++ b/TesteAndroid/app/src/main/java/br/com/mdr/testeandroid/util/Mask.kt @@ -0,0 +1,39 @@ +package br.com.mdr.testeandroid.util + +import android.util.Log + + +class MaskUtil{ + + companion object { + private const val maskCPF = "###.###.###-##" + + fun removeMask(cpfFull : String) : String{ + return cpfFull.replace(".", "").replace("-", "") + .replace("(", "").replace(")", "") + .replace("/", "").replace(" ", "") + .replace("*", "") + } + + fun getCpfMask(_cpf: String): String { + val mask = maskCPF + var mascara = "" + + var i = 0 + for (m : Char in mask.toCharArray()){ + if ((m != '#') && _cpf.length != i){ + mascara += m + continue + } + try { + mascara += _cpf[i] + }catch (e : Exception){ + break + } + i++ + } + Log.i("TesteAndroid", mascara) + return mascara + } + } +} \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/TesteAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/TesteAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/drawable/error_snack_corner.xml b/TesteAndroid/app/src/main/res/drawable/error_snack_corner.xml new file mode 100644 index 000000000..3d798bf28 --- /dev/null +++ b/TesteAndroid/app/src/main/res/drawable/error_snack_corner.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/drawable/ic_launcher_background.xml b/TesteAndroid/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/TesteAndroid/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TesteAndroid/app/src/main/res/drawable/ic_logout.png b/TesteAndroid/app/src/main/res/drawable/ic_logout.png new file mode 100644 index 000000000..de1e4ae3c Binary files /dev/null and b/TesteAndroid/app/src/main/res/drawable/ic_logout.png differ diff --git a/TesteAndroid/app/src/main/res/drawable/img_logo.png b/TesteAndroid/app/src/main/res/drawable/img_logo.png new file mode 100644 index 000000000..66bdc8d5d Binary files /dev/null and b/TesteAndroid/app/src/main/res/drawable/img_logo.png differ diff --git a/TesteAndroid/app/src/main/res/layout/activity_main.xml b/TesteAndroid/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..bc2477502 --- /dev/null +++ b/TesteAndroid/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/layout/dashboard_fragment.xml b/TesteAndroid/app/src/main/res/layout/dashboard_fragment.xml new file mode 100644 index 000000000..cdd1c3ae7 --- /dev/null +++ b/TesteAndroid/app/src/main/res/layout/dashboard_fragment.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/layout/dashboard_statement_item.xml b/TesteAndroid/app/src/main/res/layout/dashboard_statement_item.xml new file mode 100644 index 000000000..849b65214 --- /dev/null +++ b/TesteAndroid/app/src/main/res/layout/dashboard_statement_item.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/layout/sign_in_fragment.xml b/TesteAndroid/app/src/main/res/layout/sign_in_fragment.xml new file mode 100644 index 000000000..7d24ec81f --- /dev/null +++ b/TesteAndroid/app/src/main/res/layout/sign_in_fragment.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/TesteAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/TesteAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/TesteAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/TesteAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png b/TesteAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..a571e6009 Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/TesteAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..61da551c5 Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png b/TesteAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..c41dd2853 Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/TesteAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..db5080a75 Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/TesteAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..6dba46dab Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/TesteAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..da31a871c Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/TesteAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..15ac68172 Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/TesteAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..b216f2d31 Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/TesteAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..f25a41974 Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/TesteAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/TesteAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..e96783ccc Binary files /dev/null and b/TesteAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/TesteAndroid/app/src/main/res/navigation/main_nav_graph.xml b/TesteAndroid/app/src/main/res/navigation/main_nav_graph.xml new file mode 100644 index 000000000..5e2e15a65 --- /dev/null +++ b/TesteAndroid/app/src/main/res/navigation/main_nav_graph.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/values/colors.xml b/TesteAndroid/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..16f97dd07 --- /dev/null +++ b/TesteAndroid/app/src/main/res/values/colors.xml @@ -0,0 +1,13 @@ + + + #FFFFFF + #F6F6F6 + #3B48EE + + + #FFFFFF + #E43342 + #C5CCD4 + #485465 + #A8B4C4 + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/values/dimens.xml b/TesteAndroid/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..7398bb774 --- /dev/null +++ b/TesteAndroid/app/src/main/res/values/dimens.xml @@ -0,0 +1,20 @@ + + + + 4dp + 8dp + 16dp + 21dp + 28dp + 32dp + + + 16sp + 12sp + 20sp + 25sp + + + 8dp + 2dp + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/values/strings.xml b/TesteAndroid/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..5c6b180d7 --- /dev/null +++ b/TesteAndroid/app/src/main/res/values/strings.xml @@ -0,0 +1,24 @@ + + Teste Android + Imagem + + + Login + Entrar com outra conta + Acessar conta + + + User + Password + + + Recents + Saldo + Conta + Agência: + Olá, + Conta corrente: + Bank + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/values/styles.xml b/TesteAndroid/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..8af263db9 --- /dev/null +++ b/TesteAndroid/app/src/main/res/values/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/TesteAndroid/app/src/main/res/xml/backup_descriptor.xml b/TesteAndroid/app/src/main/res/xml/backup_descriptor.xml new file mode 100644 index 000000000..6fd6103a4 --- /dev/null +++ b/TesteAndroid/app/src/main/res/xml/backup_descriptor.xml @@ -0,0 +1,4 @@ + + + + diff --git a/TesteAndroid/app/src/test/java/br/com/mdr/testeandroid/DashboardViewModelTest.kt b/TesteAndroid/app/src/test/java/br/com/mdr/testeandroid/DashboardViewModelTest.kt new file mode 100644 index 000000000..f9e25901f --- /dev/null +++ b/TesteAndroid/app/src/test/java/br/com/mdr/testeandroid/DashboardViewModelTest.kt @@ -0,0 +1,83 @@ +package br.com.mdr.testeandroid + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import br.com.mdr.testeandroid.di.* +import br.com.mdr.testeandroid.extensions.getOrAwaitValue +import br.com.mdr.testeandroid.flow.dashboard.DashboardViewModel +import br.com.mdr.testeandroid.model.business.User +import kotlinx.coroutines.* +import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.setMain +import org.junit.* +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.test.KoinTest +import org.koin.test.inject + + +/** + * @author Marlon D. Rocha + * @since 10/07/20 + */ + +@RunWith(JUnit4::class) +class DashboardViewModelTest: KoinTest { + private val viewModel: DashboardViewModel by inject() + + @get:Rule + val rule = InstantTaskExecutorRule() + + @ExperimentalCoroutinesApi + @Before + fun setup() { + Dispatchers.setMain(Dispatchers.Unconfined) + startKoin { + modules( + listOf( + apiModule, + modelModule, + networkModule, + repositoryModule, + serviceModule, + viewModelModule, + presenterModule + ) + ) + } + } + + @After + fun removeModules() { + stopKoin() + } + + @ExperimentalCoroutinesApi + @Test + fun `Verify if statements request returns a not empty list`() = runBlockingTest { + val user = User(1) + viewModel.fetchUserStatements(user) + + Assert.assertEquals(true, viewModel.statementsLive.getOrAwaitValue()?.isNotEmpty()) + } + + @ExperimentalCoroutinesApi + @Test + fun `Verify if statements request returns a empty list`() = runBlockingTest { + val user = User(5) + viewModel.fetchUserStatements(user) + + Assert.assertNotEquals(true, viewModel.statementsLive.getOrAwaitValue()?.isEmpty()) + } + + @ExperimentalCoroutinesApi + @Test + fun `Verify if statements request returns not null object`() = runBlockingTest { + val user = User(2) + viewModel.fetchUserStatements(user) + + Assert.assertNotNull(viewModel.statementsLive.getOrAwaitValue()) + } + +} \ No newline at end of file diff --git a/TesteAndroid/app/src/test/java/br/com/mdr/testeandroid/SignInViewModelTest.kt b/TesteAndroid/app/src/test/java/br/com/mdr/testeandroid/SignInViewModelTest.kt new file mode 100644 index 000000000..d7b3a651d --- /dev/null +++ b/TesteAndroid/app/src/test/java/br/com/mdr/testeandroid/SignInViewModelTest.kt @@ -0,0 +1,75 @@ +package br.com.mdr.testeandroid + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import br.com.mdr.testeandroid.di.* +import br.com.mdr.testeandroid.extensions.getOrAwaitValue +import br.com.mdr.testeandroid.flow.signin.* +import kotlinx.coroutines.* +import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.setMain +import org.junit.* +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.test.KoinTest +import org.koin.test.inject + + +/** + * @author Marlon D. Rocha + * @since 09/07/20 + */ + +@RunWith(JUnit4::class) +class SignInViewModelTest: KoinTest { + private val viewModel: SignInViewModel by inject() + + @get:Rule + val rule = InstantTaskExecutorRule() + + @ExperimentalCoroutinesApi + @Before + fun setup() { + Dispatchers.setMain(Dispatchers.Unconfined) + startKoin { + modules( + listOf( + apiModule, + modelModule, + networkModule, + repositoryModule, + serviceModule, + viewModelModule, + presenterModule + ) + ) + } + } + + @After + fun removeModules() { + stopKoin() + } + + @ExperimentalCoroutinesApi + @Test + fun `Verify if sign in request returns user`() = runBlockingTest { + viewModel.signInHandler.signInPresenter.userName = "email@email.com" + viewModel.signInHandler.signInPresenter.password = "M1a." + viewModel.callSignIn() + + Assert.assertEquals(true, viewModel.userLive.getOrAwaitValue() != null) + } + + @ExperimentalCoroutinesApi + @Test + fun `Verify if sign in request returns error`() = runBlockingTest { + viewModel.signInHandler.signInPresenter.userName = "" + viewModel.signInHandler.signInPresenter.password = "" + viewModel.callSignIn() + + Assert.assertEquals(true, viewModel.errorLive.getOrAwaitValue() != null) + } + +} \ No newline at end of file diff --git a/TesteAndroid/build.gradle b/TesteAndroid/build.gradle new file mode 100644 index 000000000..6e9f3793f --- /dev/null +++ b/TesteAndroid/build.gradle @@ -0,0 +1,24 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = "1.3.72" + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.0.0" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/TesteAndroid/gradle.properties b/TesteAndroid/gradle.properties new file mode 100644 index 000000000..4d15d015f --- /dev/null +++ b/TesteAndroid/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/TesteAndroid/gradle/wrapper/gradle-wrapper.jar b/TesteAndroid/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..f6b961fd5 Binary files /dev/null and b/TesteAndroid/gradle/wrapper/gradle-wrapper.jar differ diff --git a/TesteAndroid/gradle/wrapper/gradle-wrapper.properties b/TesteAndroid/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..d3f56fc8d --- /dev/null +++ b/TesteAndroid/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jul 04 16:35:14 BRT 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/TesteAndroid/gradlew b/TesteAndroid/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/TesteAndroid/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/TesteAndroid/gradlew.bat b/TesteAndroid/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/TesteAndroid/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/TesteAndroid/settings.gradle b/TesteAndroid/settings.gradle new file mode 100644 index 000000000..671ac59fb --- /dev/null +++ b/TesteAndroid/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name = "Teste Android" \ No newline at end of file diff --git a/bank_app_layout/.DS_Store b/bank_app_layout/.DS_Store new file mode 100644 index 000000000..db7fed8b7 Binary files /dev/null and b/bank_app_layout/.DS_Store differ