Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Android app to use local emulator for development #2010

Merged
merged 21 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ detekt {
}

task checkCode(type: GradleBuild) {
tasks = ['checkstyle', 'lintDebug', 'ktfmtCheck', 'detekt']
tasks = ['checkstyle', 'lintLocalDebug', 'ktfmtCheck', 'detekt']
}

task clean(type: Delete) {
Expand Down
8 changes: 4 additions & 4 deletions cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ steps:
args:
- '-c'
- |
./gradlew -PdisablePreDex assembleStaging assembleStagingUnitTest -PtestBuildType=staging
./gradlew -PdisablePreDex assembleDevStaging assembleDevStagingUnitTest -PtestBuildType=staging

# TODO(#1547): Re-enable once instrumentation tests are fixed.
# if [[ "${_PUSH_TO_MASTER}" ]]; then
# ./gradlew -PdisablePreDex assembleStagingAndroidTest -PtestBuildType=staging
# ./gradlew -PdisablePreDex assembleDevStagingAndroidTest -PtestBuildType=staging
# fi

# Run code quality checks
Expand All @@ -114,11 +114,11 @@ steps:
args:
- '-c'
- |
./gradlew -PdisablePreDex testStagingUnitTest 2> unit-test-logs.txt || echo "fail" > build-status.txt
./gradlew -PdisablePreDex testDevStagingUnitTest 2> unit-test-logs.txt || echo "fail" > build-status.txt
cat unit-test-logs.txt

# TODO: Add a check for _PUSH_TO_MASTER
./gradlew jacocoTestStagingUnitTestReport
./gradlew jacocoTestDevStagingUnitTestReport

- name: 'gcr.io/$PROJECT_ID/android:34'
id: &authenticate_gcloud 'Authorize gcloud'
Expand Down
2 changes: 1 addition & 1 deletion config/lint/lint.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* Runs automatically with every build on Google Cloud Build.
*
* To run manually:
* $ ./gradlew lintDebug
* $ ./gradlew lintLocalDebug
*
* Reports (both html and xml formats) are stored under: ground/build/reports/lint/
*/
Expand Down
3 changes: 3 additions & 0 deletions config/lint/lint.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@
<issue id="UseCompatTextViewDrawableXml" severity="error" />
<issue id="ValidActionsXml" severity="error" />
<issue id="WrongThreadInterprocedural" severity="error" />
<issue id="usesCleartextTraffic">
<ignore path="AndroidManifest.xml"/>
</issue>
</lint>
19 changes: 19 additions & 0 deletions ground/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ android {
multiDexEnabled true
// For rendering vector map markers.
vectorDrawables.useSupportLibrary = true
buildConfigField "String", "EMULATOR_HOST", "\"10.0.2.2\""
buildConfigField "int", "FIRESTORE_EMULATOR_PORT", "8080"
buildConfigField "int", "AUTH_EMULATOR_PORT", "9099"
}

// Use flag -PtestBuildType with desired variant to change default behavior.
Expand Down Expand Up @@ -112,6 +115,22 @@ android {
}
}

flavorDimensions "backend"
productFlavors {
local {
dimension "backend"
versionNameSuffix "-local"
buildConfigField "boolean", "USE_EMULATORS", "true"
manifestPlaceholders.usesCleartextTraffic = true
}
dev {
dimension "backend"
versionNameSuffix "-dev"
buildConfigField "boolean", "USE_EMULATORS", "false"
manifestPlaceholders.usesCleartextTraffic = false
}
}

buildFeatures {
dataBinding true
viewBinding true
Expand Down
3 changes: 2 additions & 1 deletion ground/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme.Launcher">
android:theme="@style/AppTheme.Launcher"
android:usesCleartextTraffic="${usesCleartextTraffic}">

<service
android:name="com.google.android.ground.persistence.remote.firebase.FirebaseMessagingService"
Expand Down
1 change: 0 additions & 1 deletion ground/src/main/java/com/google/android/ground/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ object Config {
const val DB_NAME = "ground.db"

// Firebase Cloud Firestore settings.
const val FIRESTORE_PERSISTENCE_ENABLED = false
const val FIRESTORE_LOGGING_ENABLED = true

// Photos
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ constructor(
val email: String,
val displayName: String,
val photoUrl: String? = null,
val isAnonymous: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/
package com.google.android.ground.persistence.remote

import com.google.android.ground.Config
import com.google.android.ground.BuildConfig.EMULATOR_HOST
import com.google.android.ground.BuildConfig.FIRESTORE_EMULATOR_PORT
import com.google.android.ground.BuildConfig.USE_EMULATORS
import com.google.android.ground.persistence.remote.firebase.FirebaseStorageManager
import com.google.android.ground.persistence.remote.firebase.FirestoreDataStore
import com.google.android.ground.persistence.remote.firebase.FirestoreUuidGenerator
Expand Down Expand Up @@ -51,11 +53,15 @@ abstract class RemotePersistenceModule {

companion object {
@Provides
fun firebaseFirestoreSettings(): FirebaseFirestoreSettings {
return FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(Config.FIRESTORE_PERSISTENCE_ENABLED)
.build()
}
fun firebaseFirestoreSettings(): FirebaseFirestoreSettings =
with(FirebaseFirestoreSettings.Builder()) {
if (USE_EMULATORS) {
host = "$EMULATOR_HOST:$FIRESTORE_EMULATOR_PORT"
isSslEnabled = false
}
isPersistenceEnabled = false
build()
}

/** Returns a reference to the default Storage bucket. */
@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ constructor(
Timber.d("Skipped refreshing user profile as device is offline.")
return
}
remoteDataStore.refreshUserProfile()
if (!authenticationManager.currentUser.isAnonymous) {
remoteDataStore.refreshUserProfile()
}
}

suspend fun getUser(userId: String): User = localUserStore.getUser(userId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.ground.system.auth

import com.google.android.ground.coroutines.ApplicationScope
import com.google.android.ground.model.User
import com.google.android.ground.rx.annotations.Hot
import com.google.firebase.auth.*
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx2.await
import kotlinx.coroutines.tasks.await

private val anonymousUser = User("nobody", "nobody", "Anonymous user ", null, true)

/**
* Forces anonymous sign in with test user used when running against local Firebase Emulator Suite.
*/
class AnonymousAuthenticationManager
@Inject
constructor(
private val firebaseAuth: FirebaseAuth,
@ApplicationScope private val externalScope: CoroutineScope
) : AuthenticationManager {
override val signInState: @Hot(replays = true) Subject<SignInState> = BehaviorSubject.create()

/**
* Returns the current user, blocking until a user logs in. Only call from code where user is
* guaranteed to be authenticated.
*/
override val currentUser: User
get() =
signInState
.filter { it.state == SignInState.State.SIGNED_IN }
.map { it.result.getOrNull()!! }
.blockingFirst() // TODO: Should this be blocking?

override fun init() {
signInState.onNext(
if (firebaseAuth.currentUser == null) SignInState.signedOut()
else SignInState.signedIn(anonymousUser)
)
}

override fun signIn() {
signInState.onNext(SignInState.signingIn())
externalScope.launch {
firebaseAuth.signInAnonymously().await()
signInState.onNext(SignInState.signedIn(anonymousUser))
}
}

override fun signOut() {
firebaseAuth.signOut()
signInState.onNext(SignInState.signedOut())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,35 @@
*/
package com.google.android.ground.system.auth

import dagger.Binds
import com.google.android.ground.BuildConfig.AUTH_EMULATOR_PORT
import com.google.android.ground.BuildConfig.EMULATOR_HOST
import com.google.android.ground.BuildConfig.USE_EMULATORS
import com.google.firebase.auth.FirebaseAuth
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
abstract class AuthenticationModule {
/** Provides the Google implementation of authentication manager. */
@Binds
class AuthenticationModule {
/** Provides appropriate implementation of authentication manager. */
@Provides
@Singleton
abstract fun googleAuthenticationManager(gam: GoogleAuthenticationManager): AuthenticationManager
fun authenticationManager(
anonymousAuthenticationManager: AnonymousAuthenticationManager,
googleAuthenticationManager: GoogleAuthenticationManager
): AuthenticationManager =
if (USE_EMULATORS) anonymousAuthenticationManager else googleAuthenticationManager

@Provides
fun firebaseAuth(): FirebaseAuth {
val auth = FirebaseAuth.getInstance()
if (USE_EMULATORS) {
// Use the auth emulator so we can sign-in anonymously during dev.
auth.useEmulator(EMULATOR_HOST, AUTH_EMULATOR_PORT)
}
return auth
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.google.android.ground.R
import com.google.android.ground.coroutines.ApplicationScope
import com.google.android.ground.coroutines.IoDispatcher
import com.google.android.ground.model.User
import com.google.android.ground.rx.annotations.Hot
import com.google.android.ground.system.ActivityResult
Expand All @@ -33,21 +32,19 @@ import com.google.firebase.auth.*
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx2.asFlow
import kotlinx.coroutines.withContext
import timber.log.Timber

private val SIGN_IN_REQUEST_CODE = AuthenticationManager::class.java.hashCode() and 0xffff
private val signInRequestCode = AuthenticationManager::class.java.hashCode() and 0xffff

class GoogleAuthenticationManager
@Inject
constructor(
resources: Resources,
private val activityStreams: ActivityStreams,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val firebaseAuth: FirebaseAuth,
@ApplicationScope private val externalScope: CoroutineScope
) : AuthenticationManager {

Expand All @@ -62,7 +59,7 @@ constructor(
.build()

externalScope.launch {
activityStreams.getActivityResults(SIGN_IN_REQUEST_CODE).asFlow().collect {
activityStreams.getActivityResults(signInRequestCode).asFlow().collect {
onActivityResult(it)
}
}
Expand Down Expand Up @@ -91,27 +88,28 @@ constructor(

override fun signIn() {
signInState.onNext(SignInState.signingIn())
showSignInDialog()
}

private fun showSignInDialog() =
activityStreams.withActivity {
val signInIntent = getGoogleSignInClient(it).signInIntent
it.startActivityForResult(signInIntent, SIGN_IN_REQUEST_CODE)
it.startActivityForResult(signInIntent, signInRequestCode)
}
}

override fun signOut() {
externalScope.launch {
getFirebaseAuth().signOut()
firebaseAuth.signOut()
signInState.onNext(SignInState.signedOut())
activityStreams.withActivity { getGoogleSignInClient(it).signOut() }
}
}

private suspend fun getFirebaseAuth() = withContext(ioDispatcher) { FirebaseAuth.getInstance() }

private fun getGoogleSignInClient(activity: Activity): GoogleSignInClient =
// TODO: Use app context instead of activity?
GoogleSignIn.getClient(activity, googleSignInOptions)

private suspend fun onActivityResult(activityResult: ActivityResult) {
private fun onActivityResult(activityResult: ActivityResult) {
// The Task returned from getSignedInAccountFromIntent is always completed, so no need to
// attach a listener.
try {
Expand All @@ -123,8 +121,8 @@ constructor(
}
}

private suspend fun onGoogleSignIn(googleAccount: GoogleSignInAccount) =
getFirebaseAuth()
private fun onGoogleSignIn(googleAccount: GoogleSignInAccount) =
firebaseAuth
.signInWithCredential(getFirebaseAuthCredential(googleAccount))
.addOnSuccessListener { authResult: AuthResult -> onFirebaseAuthSuccess(authResult) }
.addOnFailureListener { signInState.onNext(SignInState.error(it)) }
Expand All @@ -135,7 +133,7 @@ constructor(
private fun getFirebaseAuthCredential(googleAccount: GoogleSignInAccount): AuthCredential =
GoogleAuthProvider.getCredential(googleAccount.idToken, null)

private suspend fun getFirebaseUser(): User? = getFirebaseAuth().currentUser?.toUser()
private fun getFirebaseUser(): User? = firebaseAuth.currentUser?.toUser()

private fun FirebaseUser.toUser(): User =
User(uid, email.orEmpty(), displayName.orEmpty(), photoUrl.toString())
Expand Down
9 changes: 9 additions & 0 deletions sharedTest/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ android {
staging {
}
}
flavorDimensions "backend"
productFlavors {
local {
dimension "backend"
}
dev {
dimension "backend"
}
}
}

dependencies {
Expand Down