├── settings.gradle.kts ├── app ├── .gitignore ├── src │ ├── test │ │ └── resources │ │ │ └── robolectric.properties │ ├── debug │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ └── colors.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ └── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ └── AndroidManifest.xml │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── values │ │ │ │ ├── ids.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── drawable │ │ │ │ └── app_logo_120dp.xml │ │ │ └── layout │ │ │ │ └── activity_sign_in.xml │ │ ├── kotlin │ │ │ └── net │ │ │ │ └── rafaeltoledo │ │ │ │ └── social │ │ │ │ ├── data │ │ │ │ ├── model │ │ │ │ │ └── User.kt │ │ │ │ ├── auth │ │ │ │ │ ├── AuthManager.kt │ │ │ │ │ ├── DelegatedAuth.kt │ │ │ │ │ ├── GoogleAuth.kt │ │ │ │ │ └── FacebookAuth.kt │ │ │ │ └── firebase │ │ │ │ │ └── FirebaseAuthManager.kt │ │ │ │ ├── di │ │ │ │ ├── FirstModule.kt │ │ │ │ ├── FirebaseModule.kt │ │ │ │ ├── AuthModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ │ ├── ui │ │ │ │ ├── feature │ │ │ │ │ ├── main │ │ │ │ │ │ ├── MainViewModel.kt │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ └── signin │ │ │ │ │ │ ├── SignInActivity.kt │ │ │ │ │ │ └── SignInViewModel.kt │ │ │ │ └── BaseViewModel.kt │ │ │ │ └── SocialApp.kt │ │ └── AndroidManifest.xml │ ├── release │ │ └── res │ │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── ic_launcher_background.xml │ │ │ └── colors.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ └── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ ├── sharedTest │ │ └── kotlin │ │ │ └── net │ │ │ └── rafaeltoledo │ │ │ └── social │ │ │ ├── base │ │ │ └── BaseTest.kt │ │ │ ├── test │ │ │ └── ui │ │ │ │ ├── MainActivityTest.kt │ │ │ │ └── SignInActivityTest.kt │ │ │ ├── data │ │ │ └── auth │ │ │ │ ├── GoogleAuthTest.kt │ │ │ │ └── FacebookAuthTest.kt │ │ │ └── TestSetup.kt │ └── androidTest │ │ └── kotlin │ │ └── net │ │ └── rafaeltoledo │ │ └── social │ │ └── SocialAppTestRunner.kt ├── proguard-rules.pro └── build.gradle ├── distribution ├── debug.keystore └── release.keystore-cipher ├── gradle.properties ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── lint.gradle └── coverage.gradle ├── .gitignore ├── scripts ├── ftl-download-results.sh ├── ftl-run-tests.sh └── ftl-setup.sh ├── .editorconfig ├── README.md ├── gradlew.bat ├── .github └── workflows │ └── ci.yml ├── gradlew └── LICENSE.txt /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":app") 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | google-services.json -------------------------------------------------------------------------------- /app/src/test/resources/robolectric.properties: -------------------------------------------------------------------------------- 1 | # Needed until Robolectric don't support API 29 2 | sdk=28 3 | -------------------------------------------------------------------------------- /distribution/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/distribution/debug.keystore -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | android.enableJetifier=true 3 | kotlin.code.style=official 4 | -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Social Dev. 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/release/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Social 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /distribution/release.keystore-cipher: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/distribution/release.keystore-cipher -------------------------------------------------------------------------------- /app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/data/model/User.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.data.model 2 | 3 | data class User(val id: String) 4 | -------------------------------------------------------------------------------- /app/src/release/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffab00 4 | -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/release/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaeltoledo/social-app/HEAD/app/src/release/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/release/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1DE9B6 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/di/FirstModule.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.di 2 | 3 | import org.koin.dsl.module 4 | 5 | val firstModule = module { 6 | single { "Social App" } 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /scripts/ftl-download-results.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TEST_DIR=$1 4 | 5 | # Create directory for results 6 | mkdir "$TEST_DIR" 7 | 8 | # Pull down test results 9 | gsutil -m cp -r -U "$(gsutil ls gs://cloud-test-social-app-development | tail -1)*" "$TEST_DIR" 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.kt] 12 | max_line_length = 120 13 | continuation_indent_size = 8 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/ui/feature/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.ui.feature.main 2 | 3 | import net.rafaeltoledo.social.ui.BaseViewModel 4 | 5 | class MainViewModel(private val string: String) : BaseViewModel() { 6 | 7 | fun getString() = string 8 | } 9 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/release/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/release/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/di/FirebaseModule.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.di 2 | 3 | import net.rafaeltoledo.social.data.auth.AuthManager 4 | import net.rafaeltoledo.social.data.firebase.FirebaseAuthManager 5 | import org.koin.dsl.module 6 | 7 | val firebaseModule = module { 8 | 9 | single { FirebaseAuthManager() } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/data/auth/AuthManager.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.data.auth 2 | 3 | import net.rafaeltoledo.social.data.model.User 4 | 5 | interface AuthManager { 6 | 7 | suspend fun socialSignIn(token: String, provider: SocialProvider): User 8 | 9 | fun isUserLoggedIn(): Boolean 10 | } 11 | 12 | enum class SocialProvider { 13 | GOOGLE, FACEBOOK 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sign in with Google 4 | Sign in with Facebook 5 | 6 | Failed to perform sign in 7 | Oops… something went wrong! 8 | 9 | -------------------------------------------------------------------------------- /app/src/debug/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffab00 4 | #c67c00 5 | #ffdd4b 6 | 7 | #1de9b6 8 | #00b686 9 | #6effe8 10 | 11 | -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/release/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1de9b6 4 | #00b686 5 | #6effe8 6 | 7 | #ffab00 8 | #c67c00 9 | #ffdd4b 10 | 11 | -------------------------------------------------------------------------------- /scripts/ftl-run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run tests on test lab 4 | gcloud firebase test android run \ 5 | --type instrumentation \ 6 | --app debug-apk/app-debug.apk \ 7 | --test test-apk/app-debug-androidTest.apk \ 8 | --device model=Nexus6P,version=27,locale=en_US,orientation=portrait \ 9 | --timeout 30m \ 10 | --results-bucket cloud-test-social-app-development \ 11 | --no-record-video \ 12 | --no-performance-metrics \ 13 | --use-orchestrator 14 | -------------------------------------------------------------------------------- /scripts/ftl-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$GCLOUD_SERVICE_KEY" = "" ]; then 4 | echo "GCLOUD_SERVICE_KEY env variable is empty. Exiting." 5 | exit 1 6 | fi 7 | 8 | # Export to secrets file 9 | echo $GCLOUD_SERVICE_KEY | base64 -di > client-secret.json 10 | 11 | # Set project ID 12 | gcloud config set project social-app-development 13 | 14 | # Auth account 15 | gcloud auth activate-service-account social-app-ftl@social-app-development.iam.gserviceaccount.com --key-file client-secret.json 16 | 17 | # Delete secret 18 | rm client-secret.json 19 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/di/AuthModule.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.di 2 | 3 | import net.rafaeltoledo.social.data.auth.DelegatedAuth 4 | import net.rafaeltoledo.social.data.auth.FacebookAuth 5 | import net.rafaeltoledo.social.data.auth.GoogleAuth 6 | import net.rafaeltoledo.social.data.auth.SocialProvider 7 | import org.koin.core.qualifier.named 8 | import org.koin.dsl.module 9 | 10 | val authModule = module { 11 | single(named(SocialProvider.GOOGLE.name)) { GoogleAuth() } 12 | single(named(SocialProvider.FACEBOOK.name)) { FacebookAuth() } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/data/auth/DelegatedAuth.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.data.auth 2 | 3 | import android.content.Intent 4 | import androidx.activity.ComponentActivity 5 | 6 | interface DelegatedAuth { 7 | 8 | fun build(activity: ComponentActivity): T 9 | 10 | fun signIn(activity: ComponentActivity) 11 | 12 | fun onResult(requestCode: Int, resultCode: Int, data: Intent?): AuthResult 13 | 14 | fun signOut(callback: (Status) -> Unit) 15 | } 16 | 17 | enum class Status { CANCELED, SUCCESS, FAILURE } 18 | 19 | data class AuthResult(val status: Status, val token: String? = null) 20 | -------------------------------------------------------------------------------- /app/src/sharedTest/kotlin/net/rafaeltoledo/social/base/BaseTest.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.base 2 | 3 | import androidx.test.core.app.ApplicationProvider 4 | import androidx.test.espresso.intent.Intents 5 | import net.rafaeltoledo.social.TestSocialApp 6 | import org.junit.After 7 | import org.junit.Before 8 | 9 | abstract class BaseTest { 10 | 11 | protected val app: TestSocialApp by lazy { ApplicationProvider.getApplicationContext() as TestSocialApp } 12 | 13 | @Before 14 | fun baseSetUp() { 15 | Intents.init() 16 | } 17 | 18 | @After 19 | fun baseTearDown() { 20 | Intents.release() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/app_logo_120dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/di/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.di 2 | 3 | import net.rafaeltoledo.social.data.auth.SocialProvider 4 | import net.rafaeltoledo.social.ui.feature.main.MainViewModel 5 | import net.rafaeltoledo.social.ui.feature.signin.SignInViewModel 6 | import org.koin.androidx.viewmodel.dsl.viewModel 7 | import org.koin.core.qualifier.named 8 | import org.koin.dsl.module 9 | 10 | val viewModelModule = module { 11 | viewModel { MainViewModel(get()) } 12 | viewModel { 13 | SignInViewModel( 14 | get(), 15 | get(named(SocialProvider.GOOGLE.name)), 16 | get(named(SocialProvider.FACEBOOK.name)), 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/net/rafaeltoledo/social/SocialAppTestRunner.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import com.github.tmurakami.dexopener.DexOpener 7 | 8 | /** 9 | * A custom test runner that setups DexOpener and make all our classes non-final on runtime. 10 | * Also, it replaces the App instance with a custom one. 11 | */ 12 | class SocialAppTestRunner : AndroidJUnitRunner() { 13 | 14 | override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { 15 | DexOpener.install(this) 16 | return super.newApplication(cl, TestSocialApp::class.java.name, context) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gradle/lint.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'io.gitlab.arturbosch.detekt' 2 | 3 | detekt { 4 | input = files('src/main/kotlin') 5 | } 6 | 7 | configurations { 8 | ktlint 9 | } 10 | 11 | dependencies { 12 | ktlint 'com.pinterest:ktlint:0.42.1' 13 | } 14 | 15 | task ktlint(type: JavaExec, group: 'verification') { 16 | description 'Check Kotlin code style.' 17 | classpath = configurations.ktlint 18 | main = 'com.pinterest.ktlint.Main' 19 | args 'src/**/*.kt' 20 | } 21 | 22 | check.dependsOn 'ktlint', 'detekt' 23 | 24 | task ktlintFormat(type: JavaExec, group: 'formatting') { 25 | description 'Fix Kotlin code style deviations.' 26 | classpath = configurations.ktlint 27 | main = 'com.pinterest.ktlint.Main' 28 | args '-F', 'src/**/*.kt' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/SocialApp.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social 2 | 3 | import android.app.Application 4 | import net.rafaeltoledo.social.di.authModule 5 | import net.rafaeltoledo.social.di.firebaseModule 6 | import net.rafaeltoledo.social.di.firstModule 7 | import net.rafaeltoledo.social.di.viewModelModule 8 | import org.koin.android.ext.koin.androidContext 9 | import org.koin.core.context.startKoin 10 | 11 | open class SocialApp : Application() { 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | startKoin { 16 | androidContext(applicationContext) 17 | 18 | modules( 19 | listOf( 20 | viewModelModule, 21 | firstModule, 22 | authModule, 23 | firebaseModule 24 | ) 25 | ) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gradle/coverage.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'jacoco' 2 | apply plugin: 'com.github.kt3k.coveralls' 3 | 4 | jacoco.toolVersion versions.jacoco 5 | 6 | tasks.withType(Test) { 7 | jacoco.includeNoLocationClasses = true 8 | jacoco.excludes = ['jdk.internal.*'] 9 | } 10 | 11 | def classes = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug") 12 | def sources = files("$projectDir/src/main/kotlin") 13 | def report = "$buildDir/reports/jacoco/report.xml" 14 | 15 | task createCombinedCoverageReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') { 16 | 17 | sourceDirectories.setFrom(sources) 18 | classDirectories.setFrom(files(classes)) 19 | executionData.setFrom(fileTree(dir: buildDir, includes: ['jacoco/testDebugUnitTest.exec'])) 20 | 21 | reports { 22 | xml.enabled = true 23 | xml.destination file(report) 24 | html.enabled = true 25 | } 26 | } 27 | 28 | coveralls { 29 | sourceDirs = sources.flatten() 30 | jacocoReportPath = report 31 | } 32 | -------------------------------------------------------------------------------- /app/src/sharedTest/kotlin/net/rafaeltoledo/social/test/ui/MainActivityTest.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.test.ui 2 | 3 | import androidx.test.core.app.ActivityScenario 4 | import androidx.test.espresso.Espresso.onView 5 | import androidx.test.espresso.assertion.ViewAssertions.matches 6 | import androidx.test.espresso.matcher.ViewMatchers.withId 7 | import androidx.test.espresso.matcher.ViewMatchers.withText 8 | import androidx.test.ext.junit.runners.AndroidJUnit4 9 | import net.rafaeltoledo.social.R 10 | import net.rafaeltoledo.social.base.BaseTest 11 | import net.rafaeltoledo.social.ui.feature.main.MainActivity 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | 15 | @RunWith(AndroidJUnit4::class) 16 | class MainActivityTest : BaseTest() { 17 | 18 | @Test 19 | fun checkIfActivityIsSuccessfullyCreated() { 20 | // Arrange 21 | val newValue = "Test Social App" 22 | app.stringValue = newValue 23 | 24 | // Act - nothing to do 25 | ActivityScenario.launch(MainActivity::class.java) 26 | 27 | // Assert 28 | onView(withId(R.id.content)).check(matches(withText(newValue))) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/sharedTest/kotlin/net/rafaeltoledo/social/data/auth/GoogleAuthTest.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.data.auth 2 | 3 | import androidx.test.core.app.ActivityScenario 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import com.google.common.truth.Truth.assertThat 6 | import net.rafaeltoledo.social.ui.feature.main.MainActivity 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | @RunWith(AndroidJUnit4::class) 11 | class GoogleAuthTest { 12 | 13 | @Test 14 | fun onBuild_createClientWithSuccess() { 15 | val auth = GoogleAuth() 16 | val activity = ActivityScenario.launch(MainActivity::class.java) 17 | 18 | activity.onActivity { 19 | val result = auth.build(it) 20 | assertThat(result).isNotNull() 21 | } 22 | } 23 | 24 | @Test 25 | fun onBuild_returnsSameInstanceWhenCalledTwice() { 26 | val auth = GoogleAuth() 27 | val activity = ActivityScenario.launch(MainActivity::class.java) 28 | 29 | activity.onActivity { 30 | val first = auth.build(it) 31 | val second = auth.build(it) 32 | assertThat(first).isSameInstanceAs(second) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/ui/feature/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.ui.feature.main 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.widget.TextView 6 | import androidx.appcompat.app.AppCompatActivity 7 | import net.rafaeltoledo.social.R 8 | import net.rafaeltoledo.social.data.auth.AuthManager 9 | import net.rafaeltoledo.social.ui.feature.signin.SignInActivity 10 | import org.koin.android.ext.android.inject 11 | import org.koin.androidx.viewmodel.ext.android.viewModel 12 | 13 | class MainActivity : AppCompatActivity() { 14 | 15 | private val auth: AuthManager by inject() 16 | private val mainViewModel: MainViewModel by viewModel() 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContentView( 21 | TextView(this).apply { 22 | id = R.id.content 23 | text = mainViewModel.getString() 24 | } 25 | ) 26 | } 27 | 28 | override fun onStart() { 29 | super.onStart() 30 | if (!auth.isUserLoggedIn()) { 31 | startActivity(Intent(this, SignInActivity::class.java)) 32 | finish() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/ui/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.ui 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.launch 10 | import net.rafaeltoledo.social.R 11 | import java.io.IOException 12 | 13 | abstract class BaseViewModel : ViewModel() { 14 | 15 | val loading = MutableLiveData() 16 | val error = MutableLiveData() 17 | 18 | init { 19 | loading.postValue(false) 20 | } 21 | 22 | protected fun launchDataLoad(block: suspend CoroutineScope.() -> Unit): Job { 23 | return viewModelScope.launch { 24 | try { 25 | loading.postValue(true) 26 | block() 27 | } catch (error: IOException) { 28 | errorHandler(error) 29 | } finally { 30 | loading.postValue(false) 31 | } 32 | } 33 | } 34 | 35 | private val errorHandler = { e: Throwable -> 36 | Log.e("BaseViewModel", "Failed to execute", e) 37 | loading.postValue(false) 38 | error.postValue(R.string.error_default_message) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Social App 2 | ========== 3 | 4 | [![Build Status](https://github.com/rafaeltoledo/social-app/workflows/CI/badge.svg)](https://github.com/rafaeltoledo/social-app/actions) 5 | [![Coverage Status](https://coveralls.io/repos/github/rafaeltoledo/social-app/badge.svg?branch=develop)](https://coveralls.io/github/rafaeltoledo/social-app?branch=develop) 6 | [![Codebeat Badge](https://codebeat.co/badges/8a7ab7b6-a345-4029-bc9c-206c6b8c31ed)](https://codebeat.co/projects/github-com-rafaeltoledo-social-app-develop) 7 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/3d45f0b680d74b1bba5be7c0af3b45f3)](https://www.codacy.com/app/rafaeltoledo/social-app) 8 | 9 | License 10 | ======= 11 | 12 | Copyright 2021 Rafael Toledo 13 | 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unless required by applicable law or agreed to in writing, software 21 | distributed under the License is distributed on an "AS IS" BASIS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | -------------------------------------------------------------------------------- /app/src/sharedTest/kotlin/net/rafaeltoledo/social/data/auth/FacebookAuthTest.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.data.auth 2 | 3 | import androidx.test.core.app.ActivityScenario 4 | import androidx.test.core.app.ApplicationProvider 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.facebook.FacebookSdk 7 | import com.google.common.truth.Truth.assertThat 8 | import net.rafaeltoledo.social.ui.feature.main.MainActivity 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | 13 | @RunWith(AndroidJUnit4::class) 14 | class FacebookAuthTest { 15 | 16 | @Suppress("DEPRECATION") 17 | @Before 18 | fun setup() { 19 | FacebookSdk.sdkInitialize(ApplicationProvider.getApplicationContext()) 20 | } 21 | 22 | @Test 23 | fun onBuild_createClientWithSuccess() { 24 | val auth = FacebookAuth() 25 | val activity = ActivityScenario.launch(MainActivity::class.java) 26 | 27 | activity.onActivity { 28 | val result = auth.build(it) 29 | assertThat(result).isNotNull() 30 | } 31 | } 32 | 33 | @Test 34 | fun onBuild_returnsSameInstanceWhenCalledTwice() { 35 | val auth = FacebookAuth() 36 | val activity = ActivityScenario.launch(MainActivity::class.java) 37 | 38 | activity.onActivity { 39 | val first = auth.build(it) 40 | val second = auth.build(it) 41 | assertThat(first).isSameInstanceAs(second) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/ui/feature/signin/SignInActivity.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.ui.feature.signin 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.databinding.DataBindingUtil 7 | import com.google.android.material.snackbar.Snackbar 8 | import net.rafaeltoledo.social.R 9 | import net.rafaeltoledo.social.databinding.ActivitySignInBinding 10 | import net.rafaeltoledo.social.ui.feature.main.MainActivity 11 | import org.koin.androidx.viewmodel.ext.android.viewModel 12 | 13 | class SignInActivity : AppCompatActivity() { 14 | 15 | private lateinit var binding: ActivitySignInBinding 16 | private val signInViewModel: SignInViewModel by viewModel() 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | binding = DataBindingUtil.setContentView(this, R.layout.activity_sign_in) 21 | 22 | binding.lifecycleOwner = this 23 | binding.viewModel = signInViewModel 24 | 25 | observeAuthClient() 26 | observeViewState() 27 | } 28 | 29 | private fun observeAuthClient() { 30 | signInViewModel.authClient.observe(this) { 31 | it.signIn(this) 32 | } 33 | 34 | signInViewModel.user.observe(this) { 35 | startActivity(Intent(this, MainActivity::class.java)) 36 | finish() 37 | } 38 | } 39 | 40 | private fun observeViewState() { 41 | signInViewModel.error.observe(this) { 42 | Snackbar.make(binding.root, it, Snackbar.LENGTH_LONG).show() 43 | } 44 | } 45 | 46 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 47 | super.onActivityResult(requestCode, resultCode, data) 48 | signInViewModel.onResult(requestCode, resultCode, data) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/data/firebase/FirebaseAuthManager.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.data.firebase 2 | 3 | import com.google.android.gms.tasks.Task 4 | import com.google.firebase.auth.AuthResult 5 | import com.google.firebase.auth.FacebookAuthProvider 6 | import com.google.firebase.auth.GoogleAuthProvider 7 | import com.google.firebase.auth.ktx.auth 8 | import com.google.firebase.ktx.Firebase 9 | import net.rafaeltoledo.social.data.auth.AuthManager 10 | import net.rafaeltoledo.social.data.auth.SocialProvider 11 | import net.rafaeltoledo.social.data.model.User 12 | import kotlin.coroutines.Continuation 13 | import kotlin.coroutines.resume 14 | import kotlin.coroutines.resumeWithException 15 | import kotlin.coroutines.suspendCoroutine 16 | 17 | class FirebaseAuthManager : AuthManager { 18 | 19 | override suspend fun socialSignIn(token: String, provider: SocialProvider): User = 20 | suspendCoroutine { 21 | Firebase.auth.signInWithCredential( 22 | when (provider) { 23 | SocialProvider.GOOGLE -> GoogleAuthProvider.getCredential(token, null) 24 | SocialProvider.FACEBOOK -> FacebookAuthProvider.getCredential(token) 25 | } 26 | ).addOnCompleteListener { task -> 27 | handleResult(task, it) 28 | } 29 | } 30 | 31 | private fun handleResult(task: Task, continuation: Continuation) { 32 | if (task.isSuccessful.not()) { 33 | continuation.resumeWithException( 34 | task.exception ?: Exception("No exception was thrown by Firebase") 35 | ) 36 | } else { 37 | continuation.resume( 38 | User(task.result?.user?.uid ?: throw IllegalStateException("Expected a user ID")) 39 | ) 40 | } 41 | } 42 | 43 | override fun isUserLoggedIn() = Firebase.auth.currentUser != null 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/data/auth/GoogleAuth.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.data.auth 2 | 3 | import android.content.Intent 4 | import android.util.Log 5 | import androidx.activity.ComponentActivity 6 | import com.google.android.gms.auth.api.signin.GoogleSignIn 7 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 8 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions 9 | import com.google.android.gms.common.api.ApiException 10 | import net.rafaeltoledo.social.BuildConfig 11 | 12 | /** 13 | * Authenticates a user using Google Auth. 14 | */ 15 | class GoogleAuth : DelegatedAuth { 16 | 17 | private var client: GoogleSignInClient? = null 18 | 19 | override fun build(activity: ComponentActivity): T { 20 | if (client == null) { 21 | val options = GoogleSignInOptions.Builder() 22 | .requestIdToken(BuildConfig.GOOGLE_REQUEST_ID_TOKEN) 23 | .requestEmail() 24 | .build() 25 | 26 | client = GoogleSignIn.getClient(activity, options) 27 | } 28 | 29 | @Suppress("UNCHECKED_CAST") return this as T 30 | } 31 | 32 | override fun signIn(activity: ComponentActivity) { 33 | activity.startActivityForResult(client?.signInIntent, RESULT_SIGN_IN) 34 | } 35 | 36 | override fun onResult(requestCode: Int, resultCode: Int, data: Intent?): AuthResult { 37 | val task = GoogleSignIn.getSignedInAccountFromIntent(data) 38 | return try { 39 | val account = task.getResult(ApiException::class.java) 40 | AuthResult(Status.SUCCESS, account!!.idToken) 41 | } catch (e: ApiException) { 42 | Log.w("Google Auth", "Failed to retrieve token", e) 43 | AuthResult(Status.FAILURE) 44 | } 45 | } 46 | 47 | override fun signOut(callback: (Status) -> Unit) { 48 | client?.signOut()?.addOnCompleteListener { callback(if (it.isSuccessful) Status.SUCCESS else Status.FAILURE) } 49 | } 50 | 51 | companion object { 52 | const val RESULT_SIGN_IN = 1441 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/sharedTest/kotlin/net/rafaeltoledo/social/TestSetup.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social 2 | 3 | import com.facebook.FacebookSdk 4 | import net.rafaeltoledo.social.data.auth.AuthManager 5 | import net.rafaeltoledo.social.data.auth.DelegatedAuth 6 | import net.rafaeltoledo.social.data.auth.FacebookAuth 7 | import net.rafaeltoledo.social.data.auth.GoogleAuth 8 | import net.rafaeltoledo.social.data.auth.SocialProvider 9 | import net.rafaeltoledo.social.data.model.User 10 | import org.koin.core.context.loadKoinModules 11 | import org.koin.core.context.stopKoin 12 | import org.koin.core.qualifier.named 13 | import org.koin.dsl.module 14 | 15 | class TestSocialApp : SocialApp() { 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | FacebookSdk.setAutoInitEnabled(false) 20 | loadKoinModules( 21 | listOf( 22 | module { 23 | single(named(SocialProvider.GOOGLE.name)) { googleAuth } 24 | single(named(SocialProvider.FACEBOOK.name)) { fbAuth } 25 | single { authManager } 26 | single { stringValue } 27 | } 28 | ) 29 | ) 30 | } 31 | 32 | override fun onTerminate() { 33 | stopKoin() 34 | super.onTerminate() 35 | } 36 | 37 | var googleAuth: DelegatedAuth = GoogleAuth() 38 | set(value) { 39 | loadKoinModules(module { single(named(SocialProvider.GOOGLE.name)) { value } }) 40 | field = value 41 | } 42 | 43 | var fbAuth: DelegatedAuth = FacebookAuth() 44 | set(value) { 45 | loadKoinModules(module { single(named(SocialProvider.FACEBOOK.name)) { value } }) 46 | field = value 47 | } 48 | 49 | var authManager: AuthManager = noOpAuthManager 50 | set(value) { 51 | loadKoinModules(module { single { value } }) 52 | field = value 53 | } 54 | 55 | var stringValue: String = "Social App" 56 | set(value) { 57 | loadKoinModules(module { single { value } }) 58 | field = value 59 | } 60 | } 61 | 62 | val noOpAuthManager = object : AuthManager { 63 | 64 | override suspend fun socialSignIn(token: String, provider: SocialProvider) = 65 | User("0") 66 | 67 | override fun isUserLoggedIn() = true 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/data/auth/FacebookAuth.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.data.auth 2 | 3 | import android.content.Intent 4 | import android.util.Log 5 | import androidx.activity.ComponentActivity 6 | import com.facebook.CallbackManager 7 | import com.facebook.FacebookCallback 8 | import com.facebook.FacebookException 9 | import com.facebook.login.LoginManager 10 | import com.facebook.login.LoginResult 11 | import java.util.concurrent.CountDownLatch 12 | 13 | /** 14 | * Authenticates a user using the Facebook Login SDK. 15 | */ 16 | class FacebookAuth : DelegatedAuth { 17 | 18 | private lateinit var callbackManager: CallbackManager 19 | private lateinit var status: AuthResult 20 | private lateinit var countDownLatch: CountDownLatch 21 | 22 | private val callback = object : FacebookCallback { 23 | override fun onSuccess(result: LoginResult) { 24 | status = AuthResult(Status.SUCCESS, result.accessToken.token) 25 | countDownLatch.countDown() 26 | } 27 | 28 | override fun onCancel() { 29 | status = AuthResult(Status.CANCELED) 30 | countDownLatch.countDown() 31 | } 32 | 33 | override fun onError(error: FacebookException) { 34 | Log.e("FacebookAuth", "Could not complete Facebook signin", error) 35 | status = AuthResult(Status.FAILURE) 36 | countDownLatch.countDown() 37 | } 38 | } 39 | 40 | override fun build(activity: ComponentActivity): T { 41 | callbackManager = CallbackManager.Factory.create() 42 | LoginManager.getInstance().registerCallback(callbackManager, callback) 43 | @Suppress("UNCHECKED_CAST") return this as T 44 | } 45 | 46 | override fun signIn(activity: ComponentActivity) { 47 | countDownLatch = CountDownLatch(1) 48 | LoginManager.getInstance().logInWithReadPermissions(activity, listOf("email", "public_profile")) 49 | } 50 | 51 | override fun onResult(requestCode: Int, resultCode: Int, data: Intent?): AuthResult { 52 | callbackManager.onActivityResult(requestCode, resultCode, data) 53 | countDownLatch.await() 54 | return status 55 | } 56 | 57 | override fun signOut(callback: (Status) -> Unit) { 58 | LoginManager.getInstance().logOut() 59 | callback(Status.SUCCESS) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/kotlin/net/rafaeltoledo/social/ui/feature/signin/SignInViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.rafaeltoledo.social.ui.feature.signin 2 | 3 | import android.content.Intent 4 | import androidx.activity.ComponentActivity 5 | import androidx.lifecycle.MutableLiveData 6 | import net.rafaeltoledo.social.R 7 | import net.rafaeltoledo.social.data.auth.AuthManager 8 | import net.rafaeltoledo.social.data.auth.DelegatedAuth 9 | import net.rafaeltoledo.social.data.auth.FacebookAuth 10 | import net.rafaeltoledo.social.data.auth.GoogleAuth 11 | import net.rafaeltoledo.social.data.auth.SocialProvider 12 | import net.rafaeltoledo.social.data.auth.Status 13 | import net.rafaeltoledo.social.data.model.User 14 | import net.rafaeltoledo.social.ui.BaseViewModel 15 | 16 | /** 17 | * Handles UI state and orchestrates AuthManager implementations 18 | * based on user selection. 19 | */ 20 | class SignInViewModel( 21 | private val auth: AuthManager, 22 | private val googleAuth: DelegatedAuth, 23 | private val facebookAuth: DelegatedAuth 24 | ) : BaseViewModel() { 25 | 26 | val authClient = MutableLiveData() 27 | val user = MutableLiveData() 28 | 29 | /** 30 | * Triggers the auth flow with Google 31 | * @param activity the Activity object needed for lifecycle 32 | */ 33 | fun googleSignIn(activity: ComponentActivity) { 34 | loading.value = true 35 | 36 | authClient.value = googleAuth.build(activity) 37 | } 38 | 39 | /** 40 | * Triggers the auth flow with Facebook 41 | * @param activity the Activity object needed for lifecycle 42 | */ 43 | fun facebookSignIn(activity: ComponentActivity) { 44 | loading.value = true 45 | 46 | authClient.value = facebookAuth.build(activity) 47 | } 48 | 49 | /** 50 | * Handles the result returned by the native auth SDKs 51 | */ 52 | fun onResult(requestCode: Int, resultCode: Int, data: Intent?) { 53 | launchDataLoad { 54 | val result = authClient.value?.onResult(requestCode, resultCode, data) 55 | if (result?.status == Status.SUCCESS) { 56 | user.postValue( 57 | auth.socialSignIn( 58 | token = result.token ?: throw IllegalStateException("Empty token"), 59 | provider = authClient.value?.provider() ?: throw IllegalStateException("Empty provider") 60 | ) 61 | ) 62 | } else { 63 | error.postValue(R.string.error_sign_in) 64 | } 65 | } 66 | } 67 | 68 | private fun DelegatedAuth.provider() = when (this) { 69 | is GoogleAuth -> SocialProvider.GOOGLE 70 | is FacebookAuth -> SocialProvider.FACEBOOK 71 | else -> throw IllegalArgumentException("unknown social provider") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_sign_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 20 | 21 | 32 | 33 |