From 8e6dece41bc143a39afba15095ff5c7ff76036ff Mon Sep 17 00:00:00 2001 From: winter223 Date: Sun, 17 Apr 2022 12:30:53 +0900 Subject: [PATCH] =?UTF-8?q?[#30]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Firebase SiginIn 작업 --- app/build.gradle | 6 ++ .../moyeorun_android/common/EventLiveData.kt | 65 +++++++++++++++++++ .../common/extension/ActivityExtension.kt | 19 ++++++ .../common/extension/FragmentExtension.kt | 17 +++++ .../moyeorun_android/login/LoginActivity.kt | 28 ++++++-- .../moyeorun_android/login/LoginEvent.kt | 5 ++ .../moyeorun_android/login/LoginModule.kt | 14 ++++ .../moyeorun_android/login/LoginRepository.kt | 59 +++++++++++++++++ .../moyeorun_android/login/LoginViewModel.kt | 39 +++++++++++ 9 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/common/EventLiveData.kt create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/login/LoginEvent.kt create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/login/LoginModule.kt create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/login/LoginRepository.kt create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/login/LoginViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index 0d60182..1fe74b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,12 +80,18 @@ dependencies { implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.activity:activity-ktx:1.4.0' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + // Lifecycle + def lifecycle_version = '2.4.1' + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" + // Firebase implementation platform('com.google.firebase:firebase-bom:29.2.0') implementation 'com.google.firebase:firebase-analytics-ktx' diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/EventLiveData.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/EventLiveData.kt new file mode 100644 index 0000000..ea6e1b5 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/common/EventLiveData.kt @@ -0,0 +1,65 @@ +package com.moyerun.moyeorun_android.common + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer + +typealias EventLiveData = LiveData> + +class MutableEventLiveData : MutableLiveData>() { + + var event: T? + @Deprecated("getter is NOT supported!", level = DeprecationLevel.ERROR) + get() = throw UnsupportedOperationException() + set(value) { + if (value != null) { + setValue(Event(value)) + } + } + + fun postEvent(value: T?) { + if (value != null) { + postValue(Event(value)) + } + } +} + +/** + * Used as a wrapper for data that is exposed via a LiveData that represents an event. + */ +open class Event(private val content: T) { + + var hasBeenHandled = false + private set // Allow external read but not write + + /** + * Returns the content and prevents its use again. + */ + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + /** + * Returns the content, even if it's already been handled. + */ + fun peekContent(): T = content +} + +/** + * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has + * already been handled. + * + * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. + */ +class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { + override fun onChanged(event: Event?) { + event?.getContentIfNotHandled()?.let { value -> + onEventUnhandledContent(value) + } + } +} diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ActivityExtension.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ActivityExtension.kt index 49e089c..afa7533 100644 --- a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ActivityExtension.kt +++ b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ActivityExtension.kt @@ -4,7 +4,15 @@ import android.app.Activity import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity +import androidx.activity.ComponentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.moyerun.moyeorun_android.R +import com.moyerun.moyeorun_android.common.EventLiveData +import com.moyerun.moyeorun_android.common.EventObserver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch fun Activity.toast(msg: String, isShort: Boolean = true) { Toast.makeText(this, msg, if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show() @@ -19,4 +27,15 @@ inline fun FragmentActivity.showAllowingStateLoss( fun Activity.showNetworkErrorToast() { toast(getString(R.string.toast_network_error)) +} + +fun ComponentActivity.repeatOnStart(block: suspend CoroutineScope.() -> Unit) { + lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED, block) } +} + +fun ComponentActivity.observeEvent( + event: EventLiveData, + onEventUnhandledContent: (T) -> Unit +) { + event.observe(this, EventObserver(onEventUnhandledContent)) } \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FragmentExtension.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FragmentExtension.kt index e505621..46be75b 100644 --- a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FragmentExtension.kt +++ b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FragmentExtension.kt @@ -4,7 +4,14 @@ import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.moyerun.moyeorun_android.R +import com.moyerun.moyeorun_android.common.EventLiveData +import com.moyerun.moyeorun_android.common.EventObserver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch fun Fragment.toast(msg: String, isShort: Boolean = false) { Toast.makeText(context, msg, if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show() @@ -24,4 +31,14 @@ inline fun FragmentManager?.showAllowingStateLoss( fun Fragment.showNetworkErrorToast() { toast(getString(R.string.toast_network_error)) +} + +fun Fragment.repeatOnStart(block: suspend CoroutineScope.() -> Unit) { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block) + } +} + +fun Fragment.observeEvent(event: EventLiveData, onEventUnhandledContent: (T) -> Unit) { + event.observe(viewLifecycleOwner, EventObserver(onEventUnhandledContent)) } \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/LoginActivity.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginActivity.kt index 6150ff2..0f21b71 100644 --- a/app/src/main/java/com/moyerun/moyeorun_android/login/LoginActivity.kt +++ b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginActivity.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.provider.Settings import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.Identity @@ -15,13 +16,17 @@ import com.google.android.gms.common.api.CommonStatusCodes import com.moyerun.moyeorun_android.BuildConfig import com.moyerun.moyeorun_android.R import com.moyerun.moyeorun_android.common.Lg +import com.moyerun.moyeorun_android.common.extension.observeEvent import com.moyerun.moyeorun_android.common.extension.showNetworkErrorToast import com.moyerun.moyeorun_android.common.extension.toast import com.moyerun.moyeorun_android.databinding.ActivityLoginBinding +import dagger.hilt.android.AndroidEntryPoint - +@AndroidEntryPoint class LoginActivity : AppCompatActivity() { + private val viewModel: LoginViewModel by viewModels() + private val oneTapClient: SignInClient by lazy { Identity.getSignInClient(this) } private val signInRequest: BeginSignInRequest by lazy { getBeginSignInRequest() } @@ -32,9 +37,8 @@ class LoginActivity : AppCompatActivity() { val credential = oneTapClient.getSignInCredentialFromIntent(result.data) val idToken = credential.googleIdToken if (idToken != null) { - //Todo: 서버에 보내서 인증 @winter223 - // Todo: Firebase crashlytics userId 세팅 - Lg.d("Success. token : $idToken") + Lg.i("Success. token : $idToken") + viewModel.signIn(idToken) } else { showUnknownErrorToast() //Todo: #31 을 rebase 하고 주석 풀기 @@ -62,6 +66,22 @@ class LoginActivity : AppCompatActivity() { val binding = ActivityLoginBinding.inflate(layoutInflater) setContentView(binding.root) + observeEvent(viewModel.loginEvent) { + when (it) { + LoginEvent.Success -> { + // Todo: 메인화면 진입 + Lg.d("Login!") + } + LoginEvent.NewUser -> { + // Todo: 회원가입 + Lg.d("New user!") + } + LoginEvent.Error -> { + showUnknownErrorToast() + } + } + } + binding.buttonLoginGoogle.setOnClickListener { oneTapClient.beginSignIn(signInRequest) .addOnSuccessListener(this) { result -> diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/LoginEvent.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginEvent.kt new file mode 100644 index 0000000..a36af17 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginEvent.kt @@ -0,0 +1,5 @@ +package com.moyerun.moyeorun_android.login + +enum class LoginEvent { + Success, NewUser, Error +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/LoginModule.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginModule.kt new file mode 100644 index 0000000..7e0814b --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginModule.kt @@ -0,0 +1,14 @@ +package com.moyerun.moyeorun_android.login + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class LoginModule { + + @Provides + fun providesLoginRepository(): LoginRepository = LoginRepository() +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/LoginRepository.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginRepository.kt new file mode 100644 index 0000000..6aaaf62 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginRepository.kt @@ -0,0 +1,59 @@ +package com.moyerun.moyeorun_android.login + +import com.google.firebase.auth.FirebaseUser +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.ktx.auth +import com.google.firebase.ktx.Firebase +import kotlinx.coroutines.* +import java.lang.IllegalStateException +import kotlin.coroutines.resume + +class LoginRepository( + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + // Todo: 모여런 서버 붙일 때 반환 타입 조정 + suspend fun signIn(idToken: String): ApiResponse { + val firebaseSignInResult = withContext(coroutineDispatcher) { signInWithFirebaseCredential(idToken) } + if (firebaseSignInResult is ApiResponse.Failure) { + return firebaseSignInResult + } + val moyeorunSignInResult = withContext(coroutineDispatcher) { signInWithMoyeoRun(idToken) } + if (firebaseSignInResult is ApiResponse.Failure) { + Firebase.auth.signOut() + } + return withContext(coroutineDispatcher) { signInWithMoyeoRun(idToken) } + } + + private suspend fun signInWithFirebaseCredential(idToken: String): ApiResponse { + val firebaseAuth = Firebase.auth + return suspendCancellableCoroutine { + val firebaseCredential = GoogleAuthProvider.getCredential(idToken, null) + firebaseAuth.signInWithCredential(firebaseCredential) + .addOnCompleteListener { task -> + val firebaseUser = firebaseAuth.currentUser + if (firebaseUser != null) { + it.resume(ApiResponse.Success(firebaseUser)) + } else { + it.resume( + ApiResponse.Failure( + task.exception + ?: IllegalStateException("FirebaseAuth Failure: FirebaseUser and Task.exception is null") + ) + ) + } + } + } + } + + private suspend fun signInWithMoyeoRun(idToken: String): ApiResponse { + //Todo: 모여런 서버 signIn + return ApiResponse.Success(Unit) + } + + // Todo: 임시. 네트워크 베이스 코드가 Rebase 되면 대체할 것 + sealed class ApiResponse { + data class Success(val data: T) : ApiResponse() + data class Failure(val error: Throwable) : ApiResponse() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/LoginViewModel.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginViewModel.kt new file mode 100644 index 0000000..8c97903 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginViewModel.kt @@ -0,0 +1,39 @@ +package com.moyerun.moyeorun_android.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.moyerun.moyeorun_android.common.EventLiveData +import com.moyerun.moyeorun_android.common.MutableEventLiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val loginRepository: LoginRepository +) : ViewModel() { + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow + get() = _isLoading + + private val _loginEvent = MutableEventLiveData() + val loginEvent: EventLiveData + get() = _loginEvent + + fun signIn(idToken: String) { + viewModelScope.launch { + _isLoading.value = true + val result = loginRepository.signIn(idToken) + if (result is LoginRepository.ApiResponse.Success) { + // Todo: Firebase crashlytics userId 세팅 + _loginEvent.event = LoginEvent.Success + } else { + _loginEvent.event = LoginEvent.Error + } + _isLoading.value = false + } + } +} \ No newline at end of file