diff --git a/README.md b/README.md index bd73feb5f..63b0b078e 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,68 @@ -# Show me the code +### Estudo de Caso para uma tela de login de usuario e accesso a uma conta de um banco imaginario. + +#### Empresa R. M. Sistemas Web e Mobiles +###### A pedido de: TCS Mobile hiring + +#### 1. - Analize do problema. +1.1. - preparado a plataforma de trabalho para obter o acesso para a API. +1.1.1. - Instalacao e configuracao do Postman. +1.1.2. - Teste dos diversos snipets de requisicao para a API oferecidos pela Collection Mockada via Postmam. +1.1.3. - Escolhido o Framework OkHttp Client como snipet para as requisicoes junto a API remota. + +#### 2. - Criacao de um prototipo inicial. +2.1. - Escolhido o template "Login Activity" do Android Studio. +2.2. - Escolhido o teste Espresso Unit Test (JUnit) por ja ser embutido no template do Android Studio e por ser + mais facil de manejar diretamente os dados da GUI. +2.3. - Implementado o parser para receber os resultados da requisicao POST para a API. +2.4. - Modificado a tela do template para parecer o maximo possivel ao do design proposto no estudo de caso. +2.5. - Implementado o teste expresso para os diversos complentes clicaveis e editaveis da UI. +2.6. - Implementado o metodo para a receber os dados da requsicao GET para a API. +2.7. - Testado a requisicao remota. +2.8. - Criado o lyout para receber os dados enviados pela API devidamente tratados e formatados. +2.9. - Criado o adapter para exibir os referidos dados em uma lista conforme o design proposto no estudode caso. +2.10. - Testado o envio da requiscao e os controles da UI. +2.11. - Testes finalizados e todos funcionando a contento. + +#### 3. - Implementado e Melhorado a GUI +3.1. - Melhorado a Vizualizacao dos dados na UI. +3.2. - Criado uma versao em portugues do Brasil para as mensagens de tela que por padrao foram configuradas em ingles. +3.3. - Criado uma simples tela de splash screen. +3.4. - projeto postado no github como um pull request numa branch de nome "XYZBankApp". + +O projeto XYZBank foi desenvolvido em Kotlin versao "1.3.72" com Android Studio "4.2 Canary 7" e com a mais +alta versao do gradle "build:gradle:4.2.0-alpha07". + +Estao sendo enviados como pull request, a pasta "XYZBank" contendo: + - arquivos de configuracao do Android Studio, um screencast video do programa em execucao; + + - a pasta "app" contendo: + os fontes "*.kt" do pacote '../src/main/java/com.xyzbank.xyzbankapp/* e os testes + os arquivos de layout e resources de tela "*.xml", na pasta '../src/main/res/*'; + os fontes "*.kt" dos testes nas saus respectivas pastas + + - a pasta gradle para referencia local. + + - a pasta 'outputs/apk/', com um pacote de bytecodes no formato "debug.apk" +... +XYZBank +../app/* +../gradle/* +../outputs/apk/* +../build.gradle +../gradle.properties +../gradlew +../gradlew.bat +../local.properties +../README.md - this file +../settings.gradle +../XYZBank.webm +... + +##### Conforme proposto no estudo de caso, o app foi criado para Android a partir da versao 4.4 (19 - KitKat) +##### Para compilar uma versao desse app: + - atualize o Android Studio para a ultima versao. + - Atualize as entradas do Android SDK desde a versao 19 ate a "major" versao real: 30. + - Atualize o plugin do Kotlin para a ultima versao. + - Atualize o gradle no projeto para a ultima versao. -Esse repositório contem todo o material necessário para realizar o teste: -- A especificação do layout está na pasta 'bank_app_layout' abrindo o index.html, utilizar os Styles do Android - -- Os dados da Api estão mockados, os exemplos e a especificação dos serviços (login e statements) se encontram no arquivo BankApp.postman_collection.json ( é necessário instalar o postman e importar a colection https://www.getpostman.com/apps) - -![Image of Yaktocat](https://github.com/SantanderTecnologia/TesteiOS/blob/new_test/telas.png) - -### # DESAFIO: - -Na primeira tela teremos um formulario de login, o campo user deve aceitar email ou cpf, -o campo password deve validar se a senha tem pelo menos uma letra maiuscula, um caracter especial e um caracter alfanumérico. -Apos a validação, realizar o login no endpoint https://bank-app-test.herokuapp.com/api/login e exibir os dados de retorno na próxima tela. -O ultimo usuário logado deve ser salvo de forma segura localmente, e exibido na tela de login se houver algum salvo. - -Na segunda tela será exibido os dados formatados do retorno do login e será necessário fazer um segundo request para obter os lançamentos do usuário, no endpoint https://bank-app-test.herokuapp.com/api/statements/{idUser} que retornará uma lista de lançamentos - -### # Avaliação - -Você será avaliado pela usabilidade, por respeitar o design e pela arquitetura do app. É esperado que você consiga explicar as decisões que tomou durante o desenvolvimento através de commits. - -Obrigatórios: - -* Java ou Kotlin -* Material Design -* O app deve funcionar a partir do android 4.4 -* Testes unitários, pode usar a ferramenta que você tem mais experiência, só nos explique o que ele tem de bom. -* Arquitetura a ser utilizada: Android Clean Code (https://github.com/kmmraj/android-clean-code && https://medium.com/@kmmraj/android-clean-code-part-1-c66da6551d1) -* Uso do git. - -### # Observações gerais - -Adicione um arquivo [README.md](http://README.md) com os procedimentos para executar o projeto. -Pedimos que trabalhe sozinho e não divulgue o resultado na internet. - -Faça um fork desse desse repositório em seu Github e ao finalizar nos envie um Pull Request com o resultado, por favor informe por qual empresa você esta se candidatando. - -# Importante: não há prazo de entrega, faça com qualidade! - -# BOA SORTE! + {"statementList":[{"title":"Pagamento","desc":"Conta de luz","date":"2018-08-15","value":-50, ...}],"error":{}} diff --git a/XYZBank/.DS_Store b/XYZBank/.DS_Store new file mode 100644 index 000000000..ad7927d4d Binary files /dev/null and b/XYZBank/.DS_Store differ diff --git a/XYZBank/.gradle/6.5/executionHistory/executionHistory.bin b/XYZBank/.gradle/6.5/executionHistory/executionHistory.bin new file mode 100644 index 000000000..87ddb89f5 Binary files /dev/null and b/XYZBank/.gradle/6.5/executionHistory/executionHistory.bin differ diff --git a/XYZBank/.gradle/6.5/executionHistory/executionHistory.lock b/XYZBank/.gradle/6.5/executionHistory/executionHistory.lock new file mode 100644 index 000000000..3fcf7bef4 Binary files /dev/null and b/XYZBank/.gradle/6.5/executionHistory/executionHistory.lock differ diff --git a/XYZBank/.gradle/6.5/fileChanges/last-build.bin b/XYZBank/.gradle/6.5/fileChanges/last-build.bin new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/XYZBank/.gradle/6.5/fileChanges/last-build.bin differ diff --git a/XYZBank/.gradle/6.5/fileContent/fileContent.lock b/XYZBank/.gradle/6.5/fileContent/fileContent.lock new file mode 100644 index 000000000..30a719243 Binary files /dev/null and b/XYZBank/.gradle/6.5/fileContent/fileContent.lock differ diff --git a/XYZBank/.gradle/6.5/fileHashes/fileHashes.bin b/XYZBank/.gradle/6.5/fileHashes/fileHashes.bin new file mode 100644 index 000000000..d2059088e Binary files /dev/null and b/XYZBank/.gradle/6.5/fileHashes/fileHashes.bin differ diff --git a/XYZBank/.gradle/6.5/fileHashes/fileHashes.lock b/XYZBank/.gradle/6.5/fileHashes/fileHashes.lock new file mode 100644 index 000000000..ef5f22e5b Binary files /dev/null and b/XYZBank/.gradle/6.5/fileHashes/fileHashes.lock differ diff --git a/XYZBank/.gradle/6.5/fileHashes/resourceHashesCache.bin b/XYZBank/.gradle/6.5/fileHashes/resourceHashesCache.bin new file mode 100644 index 000000000..19f94f2c8 Binary files /dev/null and b/XYZBank/.gradle/6.5/fileHashes/resourceHashesCache.bin differ diff --git a/XYZBank/.gradle/6.5/gc.properties b/XYZBank/.gradle/6.5/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/XYZBank/.gradle/6.5/javaCompile/classAnalysis.bin b/XYZBank/.gradle/6.5/javaCompile/classAnalysis.bin new file mode 100644 index 000000000..ee3240992 Binary files /dev/null and b/XYZBank/.gradle/6.5/javaCompile/classAnalysis.bin differ diff --git a/XYZBank/.gradle/6.5/javaCompile/jarAnalysis.bin b/XYZBank/.gradle/6.5/javaCompile/jarAnalysis.bin new file mode 100644 index 000000000..88bcc5b63 Binary files /dev/null and b/XYZBank/.gradle/6.5/javaCompile/jarAnalysis.bin differ diff --git a/XYZBank/.gradle/6.5/javaCompile/javaCompile.lock b/XYZBank/.gradle/6.5/javaCompile/javaCompile.lock new file mode 100644 index 000000000..38b659cca Binary files /dev/null and b/XYZBank/.gradle/6.5/javaCompile/javaCompile.lock differ diff --git a/XYZBank/.gradle/6.5/javaCompile/taskHistory.bin b/XYZBank/.gradle/6.5/javaCompile/taskHistory.bin new file mode 100644 index 000000000..280e4500c Binary files /dev/null and b/XYZBank/.gradle/6.5/javaCompile/taskHistory.bin differ diff --git a/XYZBank/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/XYZBank/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 000000000..755957125 Binary files /dev/null and b/XYZBank/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/XYZBank/.gradle/buildOutputCleanup/cache.properties b/XYZBank/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 000000000..e9880746c --- /dev/null +++ b/XYZBank/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Wed Aug 05 13:44:50 BRT 2020 +gradle.version=6.5 diff --git a/XYZBank/.gradle/buildOutputCleanup/outputFiles.bin b/XYZBank/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 000000000..4f819716e Binary files /dev/null and b/XYZBank/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/XYZBank/.gradle/checksums/checksums.lock b/XYZBank/.gradle/checksums/checksums.lock new file mode 100644 index 000000000..8bef2e7fb Binary files /dev/null and b/XYZBank/.gradle/checksums/checksums.lock differ diff --git a/XYZBank/.gradle/vcs-1/gc.properties b/XYZBank/.gradle/vcs-1/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/XYZBank/README.md b/XYZBank/README.md new file mode 100644 index 000000000..117d0f611 --- /dev/null +++ b/XYZBank/README.md @@ -0,0 +1,68 @@ +Estudo de Caso para uma tela de login de usuario e accesso a uma conta de um banco imaginario. + +Empresa R. M. Sistemas Web e Mobiles + +1 - Analize do problema. + +1.1 - preparado a plataforma de trabalho para obter o acesso para a API. +1.1.1 - Instalacao e configuracao do Postman. +1.1.2 - Teste dos diversos snipets de requisicao para a API oferecidos pela Collection Mockada via Postmam. +1.1.3 - Escolhido o Framework OkHttp Client como snipet para as requisicoes junto a API remota. + +2 - Criacao de um prototipo inicial. + +2.1 - Escolhido o template "Login Activity" do Android Studio. +2.2 - Escolhido o teste Espresso Unit Test (JUnit) por ja ser embutido no template do Android Studio e por ser + mais facil de manejar diretamente os dados da GUI. +2.3 - Implementado o parser para receber os resultados da requisicao POST para a API. +2.4 - Modificado a tela do template para parecer o maximo possivel ao do design proposto no estudo de caso. +2.5 - Implementado o teste expresso para os diversos componentes clicaveis e editaveis da UI. +2.6 - Implementado o metodo para a receber os dados da requsicao GET para a API. +2.7 - Testado a requisicao remota. +2.8 - Criado o lyout para receber os dados enviados pela API devidamente tratados e formatados. +2.9 - Criado o adapter para exibir os referidos dados em uma lista conforme o design proposto no estudo de caso. +2.10 - Testado o envio da requiscao e os controles da UI. +2.11 - Testes finalizados e todos funcionando a contento. + +3 - Implementado e Melhorado a GUI + +3.1 - Melhorado a Vizualizacao dos dados na UI. +3.2 - Criado uma versao em portugues do Brasil para as mensagens de tela que por padrao foram configuradas em ingles. +3.3 - Criado uma simples tela de splash screen. +3.4 - projeto postado no github como um pull request numa branch de nome "XYZBankApp". + +O projeto XYZBank foi desenvolvido em Kotlin versao "1.3.72" com Android Studio "4.2 Canary 7" e com a mais +alta versao do gradle "build:gradle:4.2.0-alpha07". + +Estao sendo enviados como pull request, a pasta "XYZBank" contendo: + - arquivos de configuracao do Android Studio, um screencast video do programa em execucao; + + - a pasta "app" contendo: + os fontes "*.kt" do pacote 'app./src/main/java/com.xyzbank.xyzbankapp/* e os testes + os arquivos de layout e resources de tela "*.xml", na pasta 'app/src/main/res/*'; + os fontes "*.kt" dos testes nas saus respectivas pastas + + - a pasta gradle para referencia local. + + - a pasta 'outputs/apk/', com um pacote de bytecodes no formato "debug.apk" + +XYZBank/ +app/* +gradle/* +outputs/apk/* +build.gradle +gradle.properties +gradlew +gradlew.bat +local.properties +README.md - this file +settings.gradle +XYZBank.webm + + +Conforme proposto no estudo de caso, o app foi criado para Android a partir da versao 4.4 (19 - KitKat) +Para compilar uma versao desse app: + - atualize o Android Studio para a ultima versao. + - Atualize as entradas do Android SDK desde a versao 19 ate a "major" versao real: 30. + - Atualize oplugin do Kotlin para a ultima versao. + - Atualize o gradle no projeto para a ultima versao. diff --git a/XYZBank/XYZBank.webm b/XYZBank/XYZBank.webm new file mode 100644 index 000000000..2f6dafc76 Binary files /dev/null and b/XYZBank/XYZBank.webm differ diff --git a/XYZBank/app/build.gradle b/XYZBank/app/build.gradle new file mode 100644 index 000000000..2f2f1c9b6 --- /dev/null +++ b/XYZBank/app/build.gradle @@ -0,0 +1,59 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdkVersion 29 + buildToolsVersion "30.0.1" + + defaultConfig { + applicationId "com.xyzbank.xyzbankapp" + minSdkVersion 18 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + vectorDrawables.useSupportLibrary = true + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.1' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + implementation 'com.squareup.okhttp3:okhttp:3.12.0' + implementation 'com.intuit.ssp:ssp-android:1.0.6' + implementation 'com.intuit.sdp:sdp-android:1.0.6' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.0' + + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test:rules:1.3.0-rc02' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0' +} diff --git a/XYZBank/app/proguard-rules.pro b/XYZBank/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/XYZBank/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/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ExampleInstrumentedTest.kt b/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..8783cb4eb --- /dev/null +++ b/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.xyzbank.xyzbankapp + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * 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("com.xyzbank.xyzbankapp", appContext.packageName) + } +} diff --git a/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/.DS_Store b/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/.DS_Store new file mode 100644 index 000000000..dd91c52c0 Binary files /dev/null and b/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/.DS_Store differ diff --git a/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/login/.DS_Store b/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/login/.DS_Store new file mode 100644 index 000000000..5008ddfcf Binary files /dev/null and b/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/login/.DS_Store differ diff --git a/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/login/LoginActivityUITest.kt b/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/login/LoginActivityUITest.kt new file mode 100644 index 000000000..31494f946 --- /dev/null +++ b/XYZBank/app/src/androidTest/java/com/xyzbank/xyzbankapp/ui/login/LoginActivityUITest.kt @@ -0,0 +1,78 @@ +package com.xyzbank.xyzbankapp.ui.login + + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.rule.ActivityTestRule +import com.xyzbank.xyzbankapp.R +import org.hamcrest.Matchers.allOf +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@LargeTest +@RunWith(AndroidJUnit4::class) +class LoginActivityUITest { + + @Rule + @JvmField + var mActivityTestRule = ActivityTestRule(LoginActivity::class.java) + + @Test + fun loginActivityUITest() { + val editText = onView( + allOf( + withId(R.id.username), withHint("User"), + withParent( + allOf( + withId(R.id.container), + withParent(withId(android.R.id.content)) + ) + ), + isDisplayed() + ) + ) + + val editText2 = onView( + allOf( + withId(R.id.password), + withParent( + allOf( + withId(R.id.container), + withParent(withId(android.R.id.content)) + ) + ), + isDisplayed() + ) + ) + + val button = onView( + allOf( + withId(R.id.login), withText("Login"), + withParent( + allOf( + withId(R.id.container), + withParent(withId(android.R.id.content)) + ) + ), + isDisplayed() + ) + ) + button.check(matches(isDisplayed())) + +// editText.perform(typeText("12345678901"), closeSoftKeyboard()) +// editText.check(matches(withText("12345678901"))) +// editText.perform(clearText(), closeSoftKeyboard()) +// editText.perform(typeText("test_user@msn.com"), closeSoftKeyboard()) +// editText.check(matches(withText("test_user@msn.com"))) + editText2.perform(typeText("Test@1"), closeSoftKeyboard()) + editText2.check(matches(withText("Test@1"))) +// editText.perform(clearText(), closeSoftKeyboard()) + button.perform(click()) + + } +} diff --git a/XYZBank/app/src/main/AndroidManifest.xml b/XYZBank/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f9e68480d --- /dev/null +++ b/XYZBank/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/.DS_Store b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/.DS_Store new file mode 100644 index 000000000..5d47d2b3d Binary files /dev/null and b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/.DS_Store differ diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/commons/BuilderHelper.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/commons/BuilderHelper.kt new file mode 100644 index 000000000..bbaf16310 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/commons/BuilderHelper.kt @@ -0,0 +1,42 @@ +package com.xyzbank.xyzbankapp.commons + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build + +class BuilderHelper() { + // getters and setters + val isNougat: Boolean + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + //val isInternetConected: Boolean = CheckNetwork() + + + fun CheckNetwork(mContext: Context): Boolean { + var result = false + val cm = mContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + if (isNougat) { + cm?.run { + cm.getNetworkCapabilities(cm.activeNetwork)?.run { + result = when { + hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + else -> false + } + } + } + } else { + cm?.run { + cm.activeNetworkInfo?.run { + if (type == ConnectivityManager.TYPE_WIFI) { + result = true + } else if (type == ConnectivityManager.TYPE_MOBILE) { + result = true + } + } + } + } + return result + } +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/commons/temp.html b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/commons/temp.html new file mode 100644 index 000000000..72d9bcc95 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/commons/temp.html @@ -0,0 +1,40 @@ + + + + + + + + + + +
+

XYZ Bank

+ +

User

+

Password

+

Login

+

Sign in

+

"Welcome !"

+

Not a valid username

+

Password must be >5 characters + \nAt last one capital letter + \nAt last one digit + \nAt last one symbol

+

"Login failed"

+

Welcome %s!\nGet read to your account

+ +

Hello blank fragment

+

Account

+

Balance

+

Recents

+

Your last successfully login: %s

+

Type your name and pasword to login

+
+ + diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/LoginDataSource.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/LoginDataSource.kt new file mode 100644 index 000000000..59d067e32 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/LoginDataSource.kt @@ -0,0 +1,24 @@ +package com.xyzbank.xyzbankapp.data + +import com.xyzbank.xyzbankapp.data.model.LoggedInUser +import java.io.IOException + +/** + * Class that handles authentication w/ login credentials and retrieves user information. + */ +class LoginDataSource { + + fun login(username: String, password: String): Result { + try { + // TODO: handle loggedInUser authentication + val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "guest") + return Result.Success(fakeUser) + } catch (e: Throwable) { + return Result.Error(IOException("Error logging in", e)) + } + } + + fun logout() { + // TODO: revoke authentication + } +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/LoginRepository.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/LoginRepository.kt new file mode 100644 index 000000000..1f3a18922 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/LoginRepository.kt @@ -0,0 +1,46 @@ +package com.xyzbank.xyzbankapp.data + +import com.xyzbank.xyzbankapp.data.model.LoggedInUser + +/** + * Class that requests authentication and user information from the remote data source and + * maintains an in-memory cache of login status and user credentials information. + */ + +class LoginRepository(val dataSource: LoginDataSource) { + + // in-memory cache of the loggedInUser object + var user: LoggedInUser? = null + private set + + val isLoggedIn: Boolean + get() = user != null + + init { + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + user = null + } + + fun logout() { + user = null + dataSource.logout() + } + + fun login(username: String, password: String): Result { + // handle login + val result = dataSource.login(username, password) + + if (result is Result.Success) { + setLoggedInUser(result.data) + } + + return result + } + + private fun setLoggedInUser(loggedInUser: LoggedInUser) { + this.user = loggedInUser + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + } +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/Result.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/Result.kt new file mode 100644 index 000000000..57a294c37 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/Result.kt @@ -0,0 +1,18 @@ +package com.xyzbank.xyzbankapp.data + +/** + * A generic class that holds a value with its loading status. + * @param + */ +sealed class Result { + + data class Success(val data: T) : Result() + data class Error(val exception: Exception) : Result() + + override fun toString(): String { + return when (this) { + is Success<*> -> "Success[data=$data]" + is Error -> "Error[exception=$exception]" + } + } +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/AccountInfo.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/AccountInfo.kt new file mode 100644 index 000000000..91f0ce9c0 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/AccountInfo.kt @@ -0,0 +1,6 @@ +package com.xyzbank.xyzbankapp.data.model + +class AccountInfo(val title: String, val desc: String, val date: String, val value: Double) { + +} + diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/JSONParser.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/JSONParser.kt new file mode 100644 index 000000000..aac7076da --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/JSONParser.kt @@ -0,0 +1,104 @@ +package com.xyzbank.xyzbankapp.data.model + +import com.xyzbank.bankapp.data.model.UserInfo +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.json.JSONObject +import java.io.IOException + +/* +"statementList": [ + { + "title": "Pagamento", + "desc": "Conta de luz", + "date": "2018-08-15", + "value": -50 + } +], +"error": {} +} +*/ + +/** + * object class that retrieve user information for logged in users from the API + */ + +object JSONParser { + /** + * Function getUserAccount + * + * @param username: validated user name. + * @param password: a validated password. + * @return a filled user account class UserInfo from the above line + * {"userAccount":{"userId":1,"name":"xxx","bankAccount":"NNNN","agency":"NNNNNNNNN","balance":0.000},"error":{}} + */ + @Throws(IOException::class) + fun getUserAccount(username: String, password: String): UserInfo { + val client = OkHttpClient().newBuilder() + .build() + val mediaType = + MediaType.parse("application/x-www-form-urlencoded") + //val body = RequestBody.create(mediaType, "user=test_user&password=Test@1") + val body = RequestBody.create(mediaType, "user=$username&password=$password") + val request = Request.Builder() + .url("https://bank-app-test.herokuapp.com/api/login") + .method("POST", body) + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .build() + val response = client.newCall(request).execute() + + // get JSONObject from JSON file + val obj = JSONObject(response.body()!!.string()) + // fetch JSONObject named userAccount + val ru: JSONObject = obj.getJSONObject("userAccount") + return UserInfo( + ru.getInt("userId"), + ru.getString("name"), + ru.getString("bankAccount"), + ru.getString("agency"), + ru.getDouble("balance") + ) + } + + /** + * Function getAccountentries + * + * @param id: a real user id from the API. + * @return a filled list of each Account Entry, each one in a class: AccountEntries from the above line + * {"statementList":[{"title":"Pagamento","desc":"Conta de luz","date":"2018-08-15","value":-50, ...}],"error":{}} + */ + @Throws(IOException::class) + fun getAccountEntries(id: String): ArrayList { + val client = OkHttpClient().newBuilder() + .build() + val request = Request.Builder() + .url("https://bank-app-test.herokuapp.com/api/statements/$id") + .method("GET", null) + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .build() + val response = client.newCall(request).execute() + + // get JSONObject from JSON file + val obj = JSONObject(response.body()!!.string()) + // fetch JSONObject into a array of accounts entries + val jsonRoot = obj.getJSONArray("statementList") + + val result: ArrayList = ArrayList() + for (i in 0 until jsonRoot.length()) { + val accountData = jsonRoot.getJSONObject(i) + var aci = AccountInfo("",",","",0.00) + for (j in 0 until accountData.length()) { + aci = AccountInfo( + accountData.getString("title"), + accountData.getString("desc"), + accountData.getString("date"), + accountData.getDouble("value") + ) + } + result.add(aci) + } + return result + } +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/LoggedInUser.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/LoggedInUser.kt new file mode 100644 index 000000000..734be77f8 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/LoggedInUser.kt @@ -0,0 +1,9 @@ +package com.xyzbank.xyzbankapp.data.model + +/** + * Data class that captures user information for logged in users retrieved from LoginRepository + */ +data class LoggedInUser( + val userId: String, + val displayName: String +) diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/UserInfo.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/UserInfo.kt new file mode 100644 index 000000000..01b6e41a0 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/data/model/UserInfo.kt @@ -0,0 +1,10 @@ +package com.xyzbank.bankapp.data.model + +class UserInfo( + val userid: Int, + val name: String, + val bankaccount: String, + val agency: String, + val balance: Double) { + +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/.DS_Store b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/.DS_Store new file mode 100644 index 000000000..d3860d201 Binary files /dev/null and b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/.DS_Store differ diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/SplashScreen.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/SplashScreen.kt new file mode 100755 index 000000000..5f1422296 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/SplashScreen.kt @@ -0,0 +1,109 @@ +package com.xyzbank.xyzbankapp.ui + +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.os.CountDownTimer +import android.util.Log +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import com.xyzbank.xyzbankapp.R +import com.xyzbank.xyzbankapp.ui.login.LoginActivity +import java.util.* + + +class SplashScreen : AppCompatActivity() { + + private var animatedImgView: ImageView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.splash_screen) + + val lang = Locale.getDefault().language + animatedImgView = findViewById(R.id.logo) + + + splashScreenUseAsyncTask(lang) + } + + // Show splash screen until network load data complete. + private fun splashScreenUseAsyncTask(lang: String) { + // Create a AsyncTask object. + val retrieveDateTask = RetrieveDateTask() + retrieveDateTask.execute(lang, "", "") + // Create a count down timer object.It will count down every 0.1 seconds and last for milliSeconds milliseconds.. + var t = 0 + var um = 0 + val countDownTimer: CountDownTimer = object : CountDownTimer(9000, 100) { + override fun onTick(l: Long) { + if (um > 1) + when (t) { + in 0..48 -> animatedImgView!!.setImageResource(R.drawable.ic_menino_h) + in 48..62 -> animatedImgView!!.setImageResource(R.drawable.ic_menino_g) + in 62..64 -> animatedImgView!!.setImageResource(R.drawable.ic_menino_f) + in 64..66 -> animatedImgView!!.setImageResource(R.drawable.ic_menino_e) + in 66..68 -> animatedImgView!!.setImageResource(R.drawable.ic_menino_d) + in 68..70 -> animatedImgView!!.setImageResource(R.drawable.ic_menino_c) + in 70..72 -> animatedImgView!!.setImageResource(R.drawable.ic_menino_b) + in 72..89 -> animatedImgView!!.setImageResource(R.drawable.ic_menino_a) + } + t = (l / 100).toInt() + um++ + } + + + override fun onFinish() { + if (!retrieveDateTask.isAsyncTaskComplete) { + start() + } else { + for (i in 0..50) { + animatedImgView!!.setImageResource(R.drawable.ic_menino_g) + animatedImgView!!.setImageResource(R.drawable.ic_menino_g) + animatedImgView!!.setImageResource(R.drawable.ic_menino_g) + animatedImgView!!.setImageResource(R.drawable.ic_menino_h) + animatedImgView!!.setImageResource(R.drawable.ic_menino_g) + animatedImgView!!.setImageResource(R.drawable.ic_menino_h) + } + val intent = Intent(this@SplashScreen, LoginActivity::class.java) + startActivity(intent) + finish() + } + } + } + // Start the count down timer. + countDownTimer.start() + } + + // This is the async task class that get data from network. + private class RetrieveDateTask : + AsyncTask() { + // Indicate whether AsyncTask complete or not. + var isAsyncTaskComplete = false + private set + + // This method will be called before AsyncTask run. + override fun onPreExecute() { + isAsyncTaskComplete = false + } + + // This method will be called when AsyncTask run. + override fun doInBackground(vararg params: String?): String? { + val result: String? + try { + val lang = params[0]!!.substring(0, 2) + Log.d("temp", lang) + } catch (ex: Exception) { + ex.printStackTrace() + } finally { + result = null + } + return result + } + + // This method will be called after AsyncTask run. + override fun onPostExecute(s: String?) { + isAsyncTaskComplete = true + } + } +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/account/AccountActivity.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/account/AccountActivity.kt new file mode 100644 index 000000000..1e357fd89 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/account/AccountActivity.kt @@ -0,0 +1,139 @@ +package com.xyzbank.xyzbankapp.ui.account + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.util.Log +import android.widget.ListView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.xyzbank.bankapp.data.model.UserInfo +import com.xyzbank.xyzbankapp.R +import com.xyzbank.xyzbankapp.commons.BuilderHelper +import com.xyzbank.xyzbankapp.data.model.AccountInfo +import com.xyzbank.xyzbankapp.data.model.JSONParser +import java.text.DecimalFormat +import java.text.SimpleDateFormat +import java.util.* + + +private const val PARAM_ID = "id" +private const val PARAM_NAME = "name" +private const val PARAM_ACCOUNT = "account" +private const val PARAM_AGENCY = "agency" +private const val PARAM_BALANCE = "balance" + +class AccountActivity : AppCompatActivity() { + private lateinit var currency: Currency + private lateinit var locale: Locale + private var mListView: ListView? = null + private lateinit var mAdapter: AcountEntriesAdapter + private lateinit var decFormat: DecimalFormat + private lateinit var dateFormat:SimpleDateFormat + // TODO: Rename and change types of parameters + private var userId: String? = null + private var name: String? = null + private var bankAccount: String? = null + private var agency: String? = null + private var balance: Double = 0.00 + + + @SuppressLint("SetTextI18n") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_account) + val extras = intent.extras + if (extras != null) { + + userId = extras.getString(PARAM_ID) + name = extras.getString(PARAM_NAME) + bankAccount = extras.getString(PARAM_ACCOUNT) + agency = extras.getString(PARAM_AGENCY) + balance = extras.getDouble(PARAM_BALANCE) + } + + locale = Locale.getDefault() + + dateFormat = SimpleDateFormat("yyyy-MM-dd", locale) + currency = Currency.getInstance(locale) + mAdapter = AcountEntriesAdapter(this, locale, currency) + + findViewById(R.id.tv_username).text = name + val al = agency!!.length + val fmtagency = + agency!!.substring(0,2)+"."+ + agency!!.substring(2,al-1) + "-" + + agency!!.substring(al-1,al) + findViewById(R.id.tv_bankaccount).text = "$bankAccount / $fmtagency" + val bal = Formatter(locale).format("%.2f", balance) + findViewById(R.id.tv_balance).text = "${currency.getSymbol()} ${bal}" + mListView = findViewById(R.id.list_entries) + if (BuilderHelper().CheckNetwork(this)) + LoadAccountEntries().execute(userId) + } + + override fun onBackPressed() { + setResult(Activity.RESULT_OK) + finish() + return + } + + /** + * Use this inner calss to send GET request to the API + * this fragment using the provided parameters. + * this does not retun any object, just wait for API result and + * create the GUI + */ + inner class LoadAccountEntries : AsyncTask() { + + private lateinit var userid: String + + override fun onPreExecute() { + Log.d("temp", "temp") + } + + override fun doInBackground(vararg voids: String?): Any { + userid = voids[0]!! + // get the user account + return JSONParser.getAccountEntries(userid) + } + + override fun onPostExecute(results: Any?) { + if (results != null) { + // See Response in Logcat for understand JSON Results and make DeveloperList + var acl = results as ArrayList + for (c in acl) { + mAdapter.add(c) + } + mListView!!.adapter = mAdapter + } + } + } // end of inner class + + companion object { + /** + * Use this factory method to create a new instance of + * this activity using the provided parameters. + * + * @param context: the login activity. + * @param userinfo: the user info for this account entries. + */ + // DONE: Rename and change types and number of parameters + @JvmStatic + fun newInstance(context: Context, userinfo: UserInfo): Intent { + val detailIntent = Intent(context, AccountActivity::class.java) + var arguments = Bundle().apply { + putString(PARAM_ID, userinfo.userid.toString()) + putString(PARAM_NAME, userinfo.name) + putString(PARAM_ACCOUNT, userinfo.bankaccount) + putString(PARAM_AGENCY, userinfo.agency) + putDouble(PARAM_BALANCE, userinfo.balance) + } + detailIntent.putExtras(arguments) + return detailIntent + } + } +} \ No newline at end of file diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/account/AcountEntriesAdapter.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/account/AcountEntriesAdapter.kt new file mode 100644 index 000000000..c7fd49187 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/account/AcountEntriesAdapter.kt @@ -0,0 +1,68 @@ +package com.xyzbank.xyzbankapp.ui.account + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.TextView +import androidx.cardview.widget.CardView +import com.xyzbank.xyzbankapp.R +import com.xyzbank.xyzbankapp.data.model.AccountInfo +import java.util.* +import kotlin.collections.ArrayList + +class AcountEntriesAdapter(val context: Context, val locale: Locale, val currency: Currency) : BaseAdapter() { + private val mItems = ArrayList() + private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + private var itemLayout: CardView? = null + + //private var DB: AnotationsDBHelper = AnotationsDBHelper(context) + + fun add(item: AccountInfo) { + mItems.add(item) + notifyDataSetChanged() + } + + fun clear() { + mItems.clear() + notifyDataSetChanged() + } + + override fun getItem(position: Int): Any { + return mItems[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getCount(): Int { + return mItems.size + } + + @SuppressLint("ViewHolder", "SetTextI18n") + @Suppress("DEPRECATION") + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + val item = getItem(position) as AccountInfo + + context.resources.displayMetrics + + val root: ViewGroup? = null + itemLayout = inflater.inflate(R.layout.account_view, root) as CardView + + itemLayout!!.findViewById(R.id.tv_title).text = item.title + itemLayout!!.findViewById(R.id.tv_date).text = item.date + itemLayout!!.findViewById(R.id.tv_desc).text = item.desc + val tvValue = itemLayout!!.findViewById(R.id.tv_value) + val bal = Formatter(locale).format("%.2f", item.value) + if (item.value < 0.00) + tvValue.setTextColor(Color.parseColor("#A5A5A5")) + tvValue.text = "${currency.symbol} $bal" + + + return itemLayout as CardView + } +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoggedInUserView.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoggedInUserView.kt new file mode 100644 index 000000000..96763555d --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoggedInUserView.kt @@ -0,0 +1,10 @@ +package com.xyzbank.xyzbankapp.ui.login + +import com.xyzbank.bankapp.data.model.UserInfo + +/** + * User details post authentication that is exposed to the UI + */ +data class LoggedInUserView( + val accountInfo: UserInfo +) diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginActivity.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginActivity.kt new file mode 100644 index 000000000..b0baba7a5 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginActivity.kt @@ -0,0 +1,193 @@ +package com.xyzbank.xyzbankapp.ui.login + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.* +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.xyzbank.xyzbankapp.R +import com.xyzbank.xyzbankapp.commons.BuilderHelper +import com.xyzbank.xyzbankapp.ui.account.AccountActivity +import java.text.SimpleDateFormat +import java.util.* + + +class LoginActivity : AppCompatActivity() { + + private lateinit var dateFormat: SimpleDateFormat + private lateinit var welcome: String + private val SUCCESSLOGGEDDATE: String = "successfully_login_date" + private val SUCCESSFULLYLOGGEDIN: String = "successfully_logged_in" + private var prefs: SharedPreferences? = null + private val ACCACT_REQUEST_CODE: Int = 1000 + private var login: Button? = null + private var password: EditText? = null + private var username: EditText? = null + private var tv_message: TextView? = null + private lateinit var myAccount: LoggedInUserView + private lateinit var loginViewModel: LoginViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + prefs = getSharedPreferences("${packageName}.user_settings", Context.MODE_PRIVATE) + + dateFormat = SimpleDateFormat(getString(R.string.date_format_text), Locale.getDefault()) + + username = findViewById(R.id.username) + val lu = prefs!!.getString(SUCCESSFULLYLOGGEDIN, "") + password = findViewById(R.id.password) + if (lu != "") { + username!!.setText(lu) + username!!.setSelection(lu!!.length) + username!!.nextFocusDownId = R.id.password + password!!.requestFocus() + username!!.isEnabled = false + } + login = findViewById(R.id.login) + val loading = findViewById(R.id.loading) + + loginViewModel = ViewModelProvider(this, LoginViewModelFactory()) + .get(LoginViewModel::class.java) + + loginViewModel.loginFormState.observe(this@LoginActivity, Observer { + val loginState = it ?: return@Observer + + // disable login button unless both username / password is valid + login!!.isEnabled = loginState.isDataValid + + if (loginState.usernameError != null) { + username!!.error = getString(loginState.usernameError) + } + if (loginState.passwordError != null) { + password!!.error = getString(loginState.passwordError) + } + }) + + loginViewModel.loginResult.observe(this@LoginActivity, Observer { + val loginResult = it ?: return@Observer + + loading.visibility = View.GONE + if (loginResult.error != null) { + showLoginFailed(loginResult.error) + } + if (loginResult.success != null) { + updateUiWithUser(loginResult.success) + } + + if (savedInstanceState == null) { + val accinf = AccountActivity.newInstance(this, myAccount.accountInfo) + startActivityForResult(accinf, ACCACT_REQUEST_CODE) + } + }) + + tv_message = findViewById(R.id.user_message) + welcome = getString(R.string.user_welcome_msg) + tv_message!!.text = getString(R.string.empty_user_msg) + + username!!.afterTextChanged { + loginViewModel.loginDataChanged( + username!!.text.toString(), + password!!.text.toString() + ) + } + + password?.apply { + afterTextChanged { + loginViewModel.loginDataChanged( + username!!.text.toString(), + password!!.text.toString() + ) + } + + setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + if (loginViewModel.verifyUser(username!!.text.toString(), password!!.text.toString())) { + password!!.requestFocus() + password!!.setSelection(password!!.text.toString().length) + tv_message!!.text = welcome.format(username!!.text.toString()) + } + } + } + false + } + + login!!.setOnClickListener { + loading.visibility = View.VISIBLE + doLogin(username!!.text.toString(), password!!.text.toString()) + } + } + } + + private fun doLogin(username: String, password: String) { + // verify if user name passed is email or number + val nm = if (username.contains("@")) + username.split("@")[0] + else + username + if (BuilderHelper().CheckNetwork(this@LoginActivity)) + loginViewModel.login(nm, password) + } + + private fun updateUiWithUser(model: LoggedInUserView) { + myAccount = model + tv_message!!.text = welcome.format(model.accountInfo.name) + + val editor = prefs!!.edit() + editor.putString(SUCCESSFULLYLOGGEDIN, username!!.text.toString()) + val c = Calendar.getInstance() + val date = c.time + editor.putString(SUCCESSLOGGEDDATE, dateFormat.format(date)) + editor.apply() + + username!!.isEnabled = false + password!!.isEnabled = false + login!!.isEnabled = false + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == ACCACT_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + username!!.isEnabled = true + username!!.setText(prefs!!.getString(SUCCESSFULLYLOGGEDIN, "")) + password!!.isEnabled = true + password!!.setText("") + password!!.requestFocus() + password!!.hasFocus() + tv_message!!.text = getString(R.string.last_successfully_login_msg).format(prefs!!.getString(SUCCESSLOGGEDDATE, "")) + login!!.isEnabled = false + } + } + } + + private fun showLoginFailed(@StringRes errorString: Int) { + Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show() + } +} + +/** + * Extension function to simplify setting an afterTextChanged action to EditText components. + */ +fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { + this.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(editable: Editable?) { + afterTextChanged.invoke(editable.toString()) + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + }) +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginFormState.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginFormState.kt new file mode 100644 index 000000000..0dd2f9736 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginFormState.kt @@ -0,0 +1,8 @@ +package com.xyzbank.xyzbankapp.ui.login + +/** + * Data validation state of the login form. + */ +data class LoginFormState(val usernameError: Int? = null, + val passwordError: Int? = null, + val isDataValid: Boolean = false) diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginResult.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginResult.kt new file mode 100644 index 000000000..93303e9a6 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginResult.kt @@ -0,0 +1,9 @@ +package com.xyzbank.xyzbankapp.ui.login + +/** + * Authentication result : success (user details) or error message. + */ +data class LoginResult( + val success: LoggedInUserView? = null, + val error: Int? = null +) diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginViewModel.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginViewModel.kt new file mode 100644 index 000000000..5716fcb64 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginViewModel.kt @@ -0,0 +1,162 @@ +package com.xyzbank.xyzbankapp.ui.login + +import android.os.AsyncTask +import android.util.Log +import android.util.Patterns +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.xyzbank.bankapp.data.model.UserInfo +import com.xyzbank.xyzbankapp.R +import com.xyzbank.xyzbankapp.data.LoginRepository +import com.xyzbank.xyzbankapp.data.Result +import com.xyzbank.xyzbankapp.data.model.JSONParser +import java.util.regex.Matcher +import java.util.regex.Pattern + +class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { + + private lateinit var myAccount: UserInfo + private val _loginForm = MutableLiveData() + val loginFormState: LiveData = _loginForm + + private val _loginResult = MutableLiveData() + val loginResult: LiveData = _loginResult + + enum class TypeOfTextInName {EMAIL, CPF} + private lateinit var typeName: TypeOfTextInName + + fun verifyUser(username: String, password: String): Boolean { + // First - validate user from UI + val result = loginRepository.login(username, password) + + if (result is Result.Error) { + _loginResult.value = LoginResult(error = R.string.login_failed) + return false + } + return true + } + + fun login(username: String, password: String) { + // First - validate user from UI + val result = loginRepository.login(username, password) + + if (result is Result.Success) { + // load user account + LoadUserAccount().execute("$username|$password") + } else { + _loginResult.value = LoginResult(error = R.string.login_failed) + } + } + + + /** + * Request data from the api using a thread. + * + * @param [String]: the user name and password. + * return true or false. + */ + inner class LoadUserAccount : AsyncTask() { + + private lateinit var password: String + private lateinit var username: String + + override fun onPreExecute() { + Log.d("temp", "temp") + } + + override fun doInBackground(vararg voids: String?): Any { + username = voids[0]!!.split("|")[0] + password = voids[0]!!.split("|")[1] + // get the user account + return JSONParser.getUserAccount(username, password) + } + + override fun onPostExecute(results: Any?) { + if (results != null) { + // See Response in Logcat for understand JSON Results and make DeveloperList + Log.i("onPostExecute: ", (results as UserInfo).userid.toString()) + myAccount = results + _loginResult.value = LoginResult(success = LoggedInUserView(accountInfo = myAccount)) + + } + } + } // end of inner class + + fun loginDataChanged(username: String, password: String) { + if (!isUserNameValid(username)) { + _loginForm.value = LoginFormState(usernameError = R.string.invalid_username) + } else if (!isPasswordValid(password)) { + _loginForm.value = LoginFormState(passwordError = R.string.invalid_password) + } else { + _loginForm.value = LoginFormState(isDataValid = true) + } + } + + // A placeholder username validation check + /** + * Verify the text typed in the name field is cpf or email based. + * + * @param text: the text typed in the name field. + * return true or false. + */ + private fun isUserNameValid(text: String): Boolean { + var result = false + if (text.contains('@')) { + typeName = TypeOfTextInName.EMAIL + result = Patterns.EMAIL_ADDRESS.matcher(text).matches() + } else { + if (verifyCharInName(text)){ + result = true + typeName = TypeOfTextInName.CPF + } + } + return result + } + + /** + * Verify the text typed in the name field is digit based. + * + * @param text: the text typed in the name field. + * return true or false. + */ + private fun verifyCharInName(text: String): Boolean { + val pDig = Pattern.compile("(.*\\d.*)") + val mdg: Matcher = pDig.matcher(text) + return (mdg.matches()) + } + + // A placeholder password validation check + /** + * Verify the text typed in the password field have + * - at last a capital letter + * - at last one digit + * - at lest one symbol + * + * @param text: the text typed in the password field. + * return true or false. + */ + private fun isPasswordValid(password: String): Boolean { + val r = password.length > 5 && verifyCharInPassword(password) + return r + } + + /** + * Verify password typed util. + * + * @param text: the text typed in the password field. + * return true or false. + */ + private fun verifyCharInPassword(text: String): Boolean { + val pLow = Pattern.compile("(.*[a-z].*)") + val pUpp = Pattern.compile("(.*[A-Z].*)") + val pDig = Pattern.compile("(.*\\d.*)") + val pSym = Pattern.compile("(.*[:?!@#$%^&*()].*)") + val mlc: Matcher = pLow.matcher(text) + val muc: Matcher = pUpp.matcher(text) + val mdg: Matcher = pDig.matcher(text) + val msb: Matcher = pSym.matcher(text) + + return mlc.matches() && muc.matches() && mdg.matches() && msb.matches() + } +} diff --git a/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginViewModelFactory.kt b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginViewModelFactory.kt new file mode 100644 index 000000000..b27c49278 --- /dev/null +++ b/XYZBank/app/src/main/java/com/xyzbank/xyzbankapp/ui/login/LoginViewModelFactory.kt @@ -0,0 +1,25 @@ +package com.xyzbank.xyzbankapp.ui.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.xyzbank.xyzbankapp.data.LoginDataSource +import com.xyzbank.xyzbankapp.data.LoginRepository + +/** + * ViewModel provider factory to instantiate LoginViewModel. + * Required given LoginViewModel has a non-empty constructor + */ +class LoginViewModelFactory : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { + return LoginViewModel( + loginRepository = LoginRepository( + dataSource = LoginDataSource() + ) + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/XYZBank/app/src/main/res/.DS_Store b/XYZBank/app/src/main/res/.DS_Store new file mode 100644 index 000000000..46883b49e Binary files /dev/null and b/XYZBank/app/src/main/res/.DS_Store differ diff --git a/XYZBank/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/XYZBank/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/XYZBank/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/XYZBank/app/src/main/res/drawable/.DS_Store b/XYZBank/app/src/main/res/drawable/.DS_Store new file mode 100644 index 000000000..5008ddfcf Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/.DS_Store differ diff --git a/XYZBank/app/src/main/res/drawable/cardview_background.xml b/XYZBank/app/src/main/res/drawable/cardview_background.xml new file mode 100644 index 000000000..23405e73f --- /dev/null +++ b/XYZBank/app/src/main/res/drawable/cardview_background.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/XYZBank/app/src/main/res/drawable/ic_baseline_input_24.xml b/XYZBank/app/src/main/res/drawable/ic_baseline_input_24.xml new file mode 100644 index 000000000..c0ce45c62 --- /dev/null +++ b/XYZBank/app/src/main/res/drawable/ic_baseline_input_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/XYZBank/app/src/main/res/drawable/ic_launcher_background.xml b/XYZBank/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/XYZBank/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XYZBank/app/src/main/res/drawable/ic_menino.png b/XYZBank/app/src/main/res/drawable/ic_menino.png new file mode 100755 index 000000000..b9b9646e8 Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino.png differ diff --git a/XYZBank/app/src/main/res/drawable/ic_menino_a.png b/XYZBank/app/src/main/res/drawable/ic_menino_a.png new file mode 100755 index 000000000..5cf7bf5d3 Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino_a.png differ diff --git a/XYZBank/app/src/main/res/drawable/ic_menino_b.png b/XYZBank/app/src/main/res/drawable/ic_menino_b.png new file mode 100755 index 000000000..1df51986f Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino_b.png differ diff --git a/XYZBank/app/src/main/res/drawable/ic_menino_c.png b/XYZBank/app/src/main/res/drawable/ic_menino_c.png new file mode 100755 index 000000000..a64bbadad Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino_c.png differ diff --git a/XYZBank/app/src/main/res/drawable/ic_menino_d.png b/XYZBank/app/src/main/res/drawable/ic_menino_d.png new file mode 100755 index 000000000..7050d1b45 Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino_d.png differ diff --git a/XYZBank/app/src/main/res/drawable/ic_menino_e.png b/XYZBank/app/src/main/res/drawable/ic_menino_e.png new file mode 100755 index 000000000..5930ad846 Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino_e.png differ diff --git a/XYZBank/app/src/main/res/drawable/ic_menino_f.png b/XYZBank/app/src/main/res/drawable/ic_menino_f.png new file mode 100755 index 000000000..d6b5c910d Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino_f.png differ diff --git a/XYZBank/app/src/main/res/drawable/ic_menino_g.png b/XYZBank/app/src/main/res/drawable/ic_menino_g.png new file mode 100755 index 000000000..36daa824d Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino_g.png differ diff --git a/XYZBank/app/src/main/res/drawable/ic_menino_h.png b/XYZBank/app/src/main/res/drawable/ic_menino_h.png new file mode 100755 index 000000000..b943c46c9 Binary files /dev/null and b/XYZBank/app/src/main/res/drawable/ic_menino_h.png differ diff --git a/XYZBank/app/src/main/res/drawable/view_background.xml b/XYZBank/app/src/main/res/drawable/view_background.xml new file mode 100644 index 000000000..d0f187693 --- /dev/null +++ b/XYZBank/app/src/main/res/drawable/view_background.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/XYZBank/app/src/main/res/layout/account_view.xml b/XYZBank/app/src/main/res/layout/account_view.xml new file mode 100644 index 000000000..bc2aef959 --- /dev/null +++ b/XYZBank/app/src/main/res/layout/account_view.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/XYZBank/app/src/main/res/layout/activity_account.xml b/XYZBank/app/src/main/res/layout/activity_account.xml new file mode 100644 index 000000000..aacb83dc6 --- /dev/null +++ b/XYZBank/app/src/main/res/layout/activity_account.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/XYZBank/app/src/main/res/layout/activity_login.xml b/XYZBank/app/src/main/res/layout/activity_login.xml new file mode 100644 index 000000000..1349aeb79 --- /dev/null +++ b/XYZBank/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + +