Skip to content

Commit

Permalink
Add FirebaseAuth plumbing - show current user icon.
Browse files Browse the repository at this point in the history
Major changes: I had to introduce an `ObservableUseCase` class to handle
situations where a producer will continue to emit events to the
LiveData. The overall design works the same as UseCase.

Added Repository and data classes to interact with firebase.

Added UI for login/logout.

BUG: 74200462
Change-Id: If51891513163614b591a57c8b070ff21c5735841
  • Loading branch information
objcode committed Mar 9, 2018
1 parent 4f911a2 commit efd6167
Show file tree
Hide file tree
Showing 38 changed files with 1,461 additions and 100 deletions.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@ buildscript {
googlePlayServicesClientVersion = "11.8.0"
googleServicesVersion = "3.2.0"
firebaseUiVersion = "3.2.2"
mockitoVersion = "2.8.9"
mockitoKotlinVersion = "1.5.0"
glideVersion = "4.6.1"
}

repositories {
maven {
url "$mdc_repo_location"
}
google()
jcenter()
}
Expand Down
7 changes: 7 additions & 0 deletions mobile/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ dependencies {
kapt "com.google.dagger:dagger-compiler:$rootProject.dagger"
kapt "com.google.dagger:dagger-android-processor:$rootProject.dagger"

// Glide
implementation "com.github.bumptech.glide:glide:$rootProject.glideVersion"
annotationProcessor "com.github.bumptech.glide:compiler:$rootProject.glideVersion"

// Firebase
implementation("com.firebaseui:firebase-ui-auth:$rootProject.firebaseUiVersion") {
exclude group: 'com.android.support'
Expand All @@ -106,4 +110,7 @@ dependencies {

// Local unit tests
testImplementation "junit:junit:$rootProject.junitVersion"
testImplementation "org.mockito:mockito-core:$rootProject.mockitoVersion"
testImplementation "com.nhaarman:mockito-kotlin:$rootProject.mockitoKotlinVersion"
testImplementation "org.hamcrest:hamcrest-library:$rootProject.hamcrestVersion"
}
7 changes: 7 additions & 0 deletions mobile/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@

package com.google.samples.apps.iosched.di

import com.google.samples.apps.iosched.shared.di.SharedModule
import com.google.samples.apps.iosched.MainApplication
import com.google.samples.apps.iosched.shared.di.SharedModule
import com.google.samples.apps.iosched.ui.MainModule
import com.google.samples.apps.iosched.ui.login.LoginViewModelPluginModule
import com.google.samples.apps.iosched.ui.sessiondetail.SessionDetailModule
import com.google.samples.apps.iosched.util.login.LoginModule
import dagger.Component
Expand All @@ -32,17 +33,20 @@ import javax.inject.Singleton
* Whenever a new module is created, it should be added to the list of modules.
*/
@Singleton
@Component(modules = arrayOf(
AndroidSupportInjectionModule::class,
AppModule::class,
ViewModelModule::class,
SharedModule::class,
MainModule::class,
SessionDetailModule::class,
LoginModule::class))
@Component(
modules = arrayOf(
AndroidSupportInjectionModule::class,
AppModule::class,
ViewModelModule::class,
SharedModule::class,
MainModule::class,
SessionDetailModule::class,
LoginModule::class,
LoginViewModelPluginModule::class
)
)
interface AppComponent : AndroidInjector<MainApplication> {

@Component.Builder
abstract class Builder : AndroidInjector.Builder<MainApplication>()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2018 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.samples.apps.iosched.ui.login

import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.net.Uri
import com.google.firebase.auth.FirebaseUser
import com.google.samples.apps.iosched.shared.domain.login.ObservableFirebaseUserUseCase
import com.google.samples.apps.iosched.shared.result.Result
import com.google.samples.apps.iosched.shared.result.Result.Success
import com.google.samples.apps.iosched.shared.util.checkAllMatched
import com.google.samples.apps.iosched.shared.util.map
import com.google.samples.apps.iosched.ui.login.LoginEvent.RequestLogin
import com.google.samples.apps.iosched.ui.login.LoginEvent.RequestLogout
import com.google.samples.apps.iosched.ui.schedule.Event
import javax.inject.Inject

enum class LoginEvent {
RequestLogin, RequestLogout
}

/**
* Interface to implement login functionality in a ViewModel.
*
* You can inject a implementation of this via Dagger2, then use the implementation as an interface
* delegate to add login functionality without writing any code
*
* Example usage
*
* ```
* class MyViewModel @Inject constructor(
* loginViewModelComponent: LoginViewModelPlugin
* ) : ViewModel(), LoginViewModelPlugin by loginViewModelComponent {
* ```
*/
interface LoginViewModelPlugin {
/**
* Live updated value of the current firebase user
*/
val currentFirebaseUser: MutableLiveData<Result<FirebaseUser?>>

/**
* Live updated value of the current firebase users image url
*/
val currentUserImageUri: LiveData<Uri?>

/**
* Emits Events when a login event should be attempted
*/
val performLoginEvent: MutableLiveData<Event<LoginEvent>>

fun isLoggedIn(): Boolean {
val currentUser = currentFirebaseUser.value
return when (currentUser) {
is Success -> currentUser.data != null
else -> false
}.checkAllMatched
}

/**
* Emit an Event on performLoginEvent to request login
*/
fun emitLoginRequest() = performLoginEvent.postValue(Event(RequestLogin))

/**
* Emit an Event on performLoginEvent to request logout
*/
fun emitLogoutRequest() = performLoginEvent.postValue(Event(RequestLogout))
}

/**
* Implementation of LoginViewModel that can be used as an interface delegate.
*/
internal class LoginViewModelPluginImpl @Inject constructor(
observableFirebaseUserUseCase: ObservableFirebaseUserUseCase
) : LoginViewModelPlugin {
override val performLoginEvent = MutableLiveData<Event<LoginEvent>>()
override val currentFirebaseUser = MutableLiveData<Result<FirebaseUser?>>()
override val currentUserImageUri: LiveData<Uri?> = currentFirebaseUser.map { result ->
when (result) {
is Success -> result.data?.photoUrl
else -> null
}
}

init {
observableFirebaseUserUseCase(Unit, currentFirebaseUser)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2018 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.samples.apps.iosched.ui.login

import com.google.samples.apps.iosched.shared.domain.login.ObservableFirebaseUserUseCase
import dagger.Module
import dagger.Provides

@Module
class LoginViewModelPluginModule {

@Provides
fun provideLoginViewModelPlugin(observableFirebaseUserUseCase: ObservableFirebaseUserUseCase):
LoginViewModelPlugin {
return LoginViewModelPluginImpl(observableFirebaseUserUseCase)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import android.graphics.Color.TRANSPARENT
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.StateListDrawable
import android.net.Uri
import android.support.v4.content.ContextCompat
import android.support.v4.view.ViewPager
import android.support.v7.content.res.AppCompatResources
Expand All @@ -30,11 +31,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.bumptech.glide.Glide
import com.google.samples.apps.iosched.R
import com.google.samples.apps.iosched.databinding.ItemSessionTagBinding
import com.google.samples.apps.iosched.shared.model.Tag
import com.google.samples.apps.iosched.util.CircularOutlineProvider
import timber.log.Timber

@BindingAdapter("sessionTags")
fun sessionTags(container: LinearLayout, sessionTags: List<Tag>?) {
Expand All @@ -46,9 +51,9 @@ fun sessionTags(container: LinearLayout, sessionTags: List<Tag>?) {
}

private fun createSessionTagButton(
inflater: LayoutInflater,
container: ViewGroup,
sessionTag: Tag
inflater: LayoutInflater,
container: ViewGroup,
sessionTag: Tag
): Button {
val tagBinding = ItemSessionTagBinding.inflate(inflater, container, false).apply {
tag = sessionTag
Expand Down Expand Up @@ -76,8 +81,10 @@ fun tagColor(textView: TextView, color: Int) {
// for a state (different from transparent tint, which makes the drawable invisible).
val dotOrClear = StateListDrawable().apply {
// clear icon when checked
addState(intArrayOf(android.R.attr.state_checked),
drawableCompat(textView, R.drawable.tag_clear))
addState(
intArrayOf(android.R.attr.state_checked),
drawableCompat(textView, R.drawable.tag_clear)
)
// colored dot by default
addState(StateSet.WILD_CARD,
drawableCompat(textView, R.drawable.tag_dot)?.apply { setTint(tintColor) })
Expand All @@ -92,7 +99,7 @@ fun tagColor(textView: TextView, color: Int) {
addState(StateSet.WILD_CARD, drawableCompat(textView, R.drawable.tag_outline))
}
((textView.background as InsetDrawable).drawable as RippleDrawable)
.setDrawableByLayerId(R.id.tag_fill, tagBg)
.setDrawableByLayerId(R.id.tag_fill, tagBg)
}

fun tagTintOrDefault(color: Int, context: Context): Int {
Expand All @@ -109,3 +116,28 @@ fun drawableCompat(view: View, id: Int) = AppCompatResources.getDrawable(view.co
fun pageMargin(viewPager: ViewPager, pageMargin: Float) {
viewPager.pageMargin = pageMargin.toInt()
}

@BindingAdapter("clipToCircle")
fun clipToCircle(view: View, circleSize: Float) {
view.clipToOutline = true
view.outlineProvider = CircularOutlineProvider(circleSize.toInt())
}

@BindingAdapter("imageUrl")
fun imageUrl(imageView: ImageView, imageUrl: Uri?) {
when (imageUrl) {
null -> {
Timber.d("Unsetting image url")
// TODO: b/74393872 Use an actual placeholder.
Glide.with(imageView)
.load(R.drawable.tag_filled)
.into(imageView)
}
else -> {
Glide.with(imageView)
.load(imageUrl)
.into(imageView)
}
}
}

Loading

0 comments on commit efd6167

Please sign in to comment.