From a4070d4e7e4bff26f8cf3a09ce5f284c658a6a52 Mon Sep 17 00:00:00 2001 From: winter223 Date: Wed, 13 Apr 2022 19:11:07 +0900 Subject: [PATCH 01/20] =?UTF-8?q?[#30]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=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 - UI 작업 --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 ++ .../moyeorun_android/login/LoginActivity.kt | 17 ++++++++ .../main/res/drawable/login_google_logo.xml | 18 ++++++++ app/src/main/res/drawable/login_logo.xml | 42 +++++++++++++++++++ app/src/main/res/layout/activity_login.xml | 35 ++++++++++++++++ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 3 ++ 8 files changed, 120 insertions(+) create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/login/LoginActivity.kt create mode 100644 app/src/main/res/drawable/login_google_logo.xml create mode 100644 app/src/main/res/drawable/login_logo.xml create mode 100644 app/src/main/res/layout/activity_login.xml diff --git a/app/build.gradle b/app/build.gradle index 41d5050..30bd6ea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,6 +65,7 @@ android { kotlinOptions { jvmTarget = '1.8' } + buildFeatures { viewBinding true } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19ebc43..6faa503 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,9 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.MoyeoRunandroid"> + 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 new file mode 100644 index 0000000..26df5b8 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/login/LoginActivity.kt @@ -0,0 +1,17 @@ +package com.moyerun.moyeorun_android.login + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import com.moyerun.moyeorun_android.databinding.ActivityLoginBinding + +class LoginActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.buttonLoginGoogle.setOnClickListener { + //Todo: 구글 로그인 작업 @winter223 + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/login_google_logo.xml b/app/src/main/res/drawable/login_google_logo.xml new file mode 100644 index 0000000..5051ec3 --- /dev/null +++ b/app/src/main/res/drawable/login_google_logo.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/login_logo.xml b/app/src/main/res/drawable/login_logo.xml new file mode 100644 index 0000000..f901f5d --- /dev/null +++ b/app/src/main/res/drawable/login_logo.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..615a8aa --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ab23f44..64dc310 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -14,6 +14,7 @@ #333333 #828282 #A9A9A9 + #686868 #EBECEF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 09e4982..bc81fd2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,7 @@ 취소 확인 + + 시작하기 + 구글 계정으로 로그인 \ No newline at end of file From ad6d58b64f0be818a185e560b04c79b14955c321 Mon Sep 17 00:00:00 2001 From: winter223 Date: Sun, 17 Apr 2022 09:28:04 +0900 Subject: [PATCH 02/20] =?UTF-8?q?[#30]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=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 - Google OneTabSiginin 작업 --- app/build.gradle | 3 + .../common/extension/ActivityExtension.kt | 5 + .../common/extension/FragmentExtension.kt | 5 + .../moyeorun_android/login/LoginActivity.kt | 101 +++++++++++++++++- app/src/main/res/values/strings.xml | 2 + 5 files changed, 114 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 30bd6ea..0d60182 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,6 +22,7 @@ android { def localProperties = new Properties() localProperties.load(rootProject.file('./local.properties').newDataInputStream()) buildConfigField("String", "BASE_URL", localProperties['baseUrl']) + buildConfigField("String", "WEB_CLIENT_ID", localProperties["webClientId"]) } signingConfigs { @@ -89,6 +90,8 @@ dependencies { implementation platform('com.google.firebase:firebase-bom:29.2.0') implementation 'com.google.firebase:firebase-analytics-ktx' implementation 'com.google.firebase:firebase-crashlytics-ktx' + implementation 'com.google.firebase:firebase-auth-ktx' + implementation 'com.google.android.gms:play-services-auth:20.1.0' // Hilt implementation "com.google.dagger:hilt-android:$hilt_version" 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 af4d5e4..49e089c 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,6 +4,7 @@ import android.app.Activity import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity +import com.moyerun.moyeorun_android.R fun Activity.toast(msg: String, isShort: Boolean = true) { Toast.makeText(this, msg, if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show() @@ -14,4 +15,8 @@ inline fun FragmentActivity.showAllowingStateLoss( dialogFragmentFactory: () -> DialogFragment ) { supportFragmentManager.showAllowingStateLoss(tag, dialogFragmentFactory) +} + +fun Activity.showNetworkErrorToast() { + toast(getString(R.string.toast_network_error)) } \ 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 e36ee18..e505621 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,6 +4,7 @@ import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import com.moyerun.moyeorun_android.R fun Fragment.toast(msg: String, isShort: Boolean = false) { Toast.makeText(context, msg, if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show() @@ -19,4 +20,8 @@ inline fun FragmentManager?.showAllowingStateLoss( val transaction = beginTransaction() transaction.add(dialogFragmentFactory(), tag) transaction.commitAllowingStateLoss() +} + +fun Fragment.showNetworkErrorToast() { + toast(getString(R.string.toast_network_error)) } \ 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 26df5b8..6150ff2 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 @@ -1,17 +1,114 @@ package com.moyerun.moyeorun_android.login -import androidx.appcompat.app.AppCompatActivity +import android.content.Intent +import android.content.IntentSender import android.os.Bundle +import android.provider.Settings +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import com.google.android.gms.auth.api.identity.BeginSignInRequest +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.auth.api.identity.SignInClient +import com.google.android.gms.common.api.ApiException +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.showNetworkErrorToast +import com.moyerun.moyeorun_android.common.extension.toast import com.moyerun.moyeorun_android.databinding.ActivityLoginBinding + class LoginActivity : AppCompatActivity() { + + private val oneTapClient: SignInClient by lazy { Identity.getSignInClient(this) } + private val signInRequest: BeginSignInRequest by lazy { getBeginSignInRequest() } + + private val beginSignInResultLauncher = + registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> + if (result != null) { + try { + val credential = oneTapClient.getSignInCredentialFromIntent(result.data) + val idToken = credential.googleIdToken + if (idToken != null) { + //Todo: 서버에 보내서 인증 @winter223 + // Todo: Firebase crashlytics userId 세팅 + Lg.d("Success. token : $idToken") + } else { + showUnknownErrorToast() + //Todo: #31 을 rebase 하고 주석 풀기 +// Lg.fe("No ID token") + } + } catch (e: ApiException) { + when (e.statusCode) { + CommonStatusCodes.CANCELED -> { /*Doing nothing*/ } + CommonStatusCodes.NETWORK_ERROR -> { + showNetworkErrorToast() + Lg.e("One-tap encountered a network error. $e") + } + else -> { + showUnknownErrorToast() + //Todo: #31 을 rebase 하고 주석 풀기 +// Lg.fe("Couldn't get credential from result.", e) + } + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityLoginBinding.inflate(layoutInflater) setContentView(binding.root) binding.buttonLoginGoogle.setOnClickListener { - //Todo: 구글 로그인 작업 @winter223 + oneTapClient.beginSignIn(signInRequest) + .addOnSuccessListener(this) { result -> + try { + val intentSenderRequest = + IntentSenderRequest.Builder(result.pendingIntent.intentSender).build() + beginSignInResultLauncher.launch(intentSenderRequest) + } catch (e: IntentSender.SendIntentException) { + showUnknownErrorToast() +// Lg.fe("Couldn't start One Tab UI", e) + } + } + .addOnFailureListener(this) { + // 기기에 등록된 계정이 없는 경우 호출 + startDeviceGoogleSignInActivity() + //Todo: #31 을 rebase 하고 주석 풀기 + // 간혹 등록된 계정이 있는데도 해당 콜백을 타는 경우가 있어서 로깅 +// Lg.fe("No Google Accounts found", it) + } } } + + private fun startDeviceGoogleSignInActivity() { + startActivity(Intent(Settings.ACTION_ADD_ACCOUNT).apply { + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google")) + }) + } + + private fun getBeginSignInRequest(): BeginSignInRequest { + return BeginSignInRequest.builder() + .setGoogleIdTokenRequestOptions( + BeginSignInRequest.GoogleIdTokenRequestOptions.builder() + .setSupported(true) + .setServerClientId(BuildConfig.WEB_CLIENT_ID) + // false 로 설정해서 앱에 로그인한 적이 있는 계정뿐만 아니라 + // 기기에 등록된 구글 계정을 모두 보여준다 + .setFilterByAuthorizedAccounts(false) + .build() + ) + // 하나의 계정만 있다면 자동으로 선택 + .setAutoSelectEnabled(true) + .build() + } + + private fun showUnknownErrorToast() { + toast(getString(R.string.login_toast_unknown_error)) + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc81fd2..d3e208c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,8 +4,10 @@ 취소 확인 + 네트워크 연결 상태를 확인해주세요. 시작하기 구글 계정으로 로그인 + 로그인에 실패하였습니다. \ No newline at end of file From 8e6dece41bc143a39afba15095ff5c7ff76036ff Mon Sep 17 00:00:00 2001 From: winter223 Date: Sun, 17 Apr 2022 12:30:53 +0900 Subject: [PATCH 03/20] =?UTF-8?q?[#30]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=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 From 0d8b2606954fd3346b08d1bf74fc6f9f1585365f Mon Sep 17 00:00:00 2001 From: winter223 Date: Tue, 19 Apr 2022 14:44:52 +0900 Subject: [PATCH 04/20] =?UTF-8?q?[#30]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=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 - ProfileEditActivity 추가 - 이름과 닉네임 입력이 가능하고, ViewModel 의 UiModel 을 뷰에 반영시키도록 함 --- app/build.gradle | 2 + .../common/extension/ViewExtension.kt | 13 +++ .../moyeorun_android/login/LoginActivity.kt | 4 +- .../profile/ProfileEditActivity.kt | 105 ++++++++++++++++++ .../profile/ProfileEditViewModel.kt | 42 +++++++ .../profile/ProfileUiModel.kt | 11 ++ .../views/BadgeRoundImageView.kt | 6 +- app/src/main/res/drawable/ic_check.xml | 13 +++ app/src/main/res/drawable/ic_toolbar_back.xml | 20 ++++ app/src/main/res/layout/activity_profile.xml | 90 +++++++++++++++ app/src/main/res/values/strings.xml | 5 + app/src/main/res/values/styles_edittext.xml | 11 ++ app/src/main/res/values/styles_toolbar.xml | 14 +++ build.gradle | 2 +- 14 files changed, 333 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileEditActivity.kt create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileEditViewModel.kt create mode 100644 app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileUiModel.kt create mode 100644 app/src/main/res/drawable/ic_check.xml create mode 100644 app/src/main/res/drawable/ic_toolbar_back.xml create mode 100644 app/src/main/res/layout/activity_profile.xml create mode 100644 app/src/main/res/values/styles_toolbar.xml diff --git a/app/build.gradle b/app/build.gradle index 1fe74b5..9c885cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' + id 'kotlin-parcelize' id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'dagger.hilt.android.plugin' @@ -83,6 +84,7 @@ dependencies { implementation 'androidx.activity:activity-ktx:1.4.0' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ViewExtension.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ViewExtension.kt index bb0e66e..8a31097 100644 --- a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ViewExtension.kt +++ b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ViewExtension.kt @@ -1,6 +1,9 @@ package com.moyerun.moyeorun_android.common.extension +import android.text.TextUtils import android.view.View +import android.widget.TextView +import androidx.annotation.DrawableRes fun View.setOnDebounceClickListener(interval: Long = 1000L, action: (View?) -> Unit) { val debounceClickListener = object : View.OnClickListener { @@ -16,4 +19,14 @@ fun View.setOnDebounceClickListener(interval: Long = 1000L, action: (View?) -> U } } setOnClickListener(debounceClickListener) +} + +fun TextView.setTextIfNew(text: CharSequence?) { + if (TextUtils.equals(this.text, text).not()) { + setText(text) + } +} + +fun TextView.setDrawableEnd(@DrawableRes resId: Int?) { + setCompoundDrawablesWithIntrinsicBounds(0, 0, resId ?: 0, 0) } \ 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 0f21b71..ebe0ad6 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 @@ -20,6 +20,7 @@ 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 com.moyerun.moyeorun_android.profile.ProfileEditActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -73,8 +74,7 @@ class LoginActivity : AppCompatActivity() { Lg.d("Login!") } LoginEvent.NewUser -> { - // Todo: 회원가입 - Lg.d("New user!") + ProfileEditActivity.startActivity(this) } LoginEvent.Error -> { showUnknownErrorToast() diff --git a/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileEditActivity.kt b/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileEditActivity.kt new file mode 100644 index 0000000..d3b5f50 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileEditActivity.kt @@ -0,0 +1,105 @@ +package com.moyerun.moyeorun_android.profile + +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.widget.EditText +import androidx.activity.viewModels +import androidx.core.widget.doAfterTextChanged +import com.moyerun.moyeorun_android.R +import com.moyerun.moyeorun_android.common.extension.repeatOnStart +import com.moyerun.moyeorun_android.common.extension.setDrawableEnd +import com.moyerun.moyeorun_android.common.extension.setTextIfNew +import com.moyerun.moyeorun_android.databinding.ActivityProfileBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ProfileEditActivity : AppCompatActivity() { + + private val viewModel: ProfileEditViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = ActivityProfileBinding.inflate(layoutInflater) + setContentView(binding.root) + + val originalProfile: ProfileUiModel? = intent.getParcelableExtra(EXTRA_PROFILE_UI_MODEL) + val isNewProfile = originalProfile == null + + if (isNewProfile) { + binding.textviewProfileTitle.text = "기본 정보" + binding.buttonProfileConfirm.text = "다음" + } else { + binding.textviewProfileTitle.text = "프로필" + binding.buttonProfileConfirm.text = "완료" + } + + viewModel.updateProfile(originalProfile) + + binding.edittextProfileName.doAfterTextChanged { + viewModel.onNameChanged(it?.toString().orEmpty()) + } + + binding.edittextProfileNickname.doAfterTextChanged { + viewModel.onNicknameChanged(it?.toString().orEmpty()) + } + + repeatOnStart { + launch { + viewModel.profileUiModel + .map { it.name } + .distinctUntilChanged() + .collect { + binding.edittextProfileName.setTextAndCheckIcon(it) + } + } + launch { + viewModel.profileUiModel + .map { it.nickname } + .distinctUntilChanged() + .collect { + binding.edittextProfileNickname.setTextAndCheckIcon(it) + } + } + launch { + viewModel.profileUiModel + .map { it.imageUrl } + .distinctUntilChanged() + .collect { + binding.badgeimageviewProfileImage.setBigCircleImgSrc(it) + } + } + } + } + + private fun isValidText(text: String): Boolean { + // Todo: 유효성 검사 조건 추가 (ex. 정규 표현식, 글자 제한) + return text.isNotEmpty() + } + + private fun EditText.setTextAndCheckIcon(text: String) { + val isValid = isValidText(text) + val resId = if (isValid) { + R.drawable.ic_check + } else { + null + } + setDrawableEnd(resId) + setTextIfNew(text) + } + + companion object { + private const val EXTRA_PROFILE_UI_MODEL = "profileUiModel" + + fun startActivity(context: Context, profileUiModel: ProfileUiModel? = null) { + context.startActivity(Intent(context, ProfileEditActivity::class.java).apply { + putExtra(EXTRA_PROFILE_UI_MODEL, profileUiModel) + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileEditViewModel.kt b/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileEditViewModel.kt new file mode 100644 index 0000000..2694743 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileEditViewModel.kt @@ -0,0 +1,42 @@ +package com.moyerun.moyeorun_android.profile + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class ProfileEditViewModel: ViewModel() { + + private val _profileUiModel = MutableStateFlow(ProfileUiModel()) + val profileUiModel: StateFlow + get() = _profileUiModel + + private var oldProfileUiModel: ProfileUiModel? = null + + private var isNewPost = true + + fun updateProfile(profileUiModel: ProfileUiModel?) { + if (profileUiModel == null) return + _profileUiModel.update { profileUiModel } + oldProfileUiModel = profileUiModel + isNewPost = false + } + + fun onNameChanged(name: String) { + _profileUiModel.update { + it.copy(name = name) + } + } + + fun onNicknameChanged(nickname: String) { + _profileUiModel.update { + it.copy(nickname = nickname) + } + } + + fun onImageUrlChanged(imageUrl: String) { + _profileUiModel.update { + it.copy(imageUrl = imageUrl) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileUiModel.kt b/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileUiModel.kt new file mode 100644 index 0000000..5a35e90 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/profile/ProfileUiModel.kt @@ -0,0 +1,11 @@ +package com.moyerun.moyeorun_android.profile + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ProfileUiModel( + val imageUrl: String = "", + val name: String = "", + val nickname: String = "" +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/views/BadgeRoundImageView.kt b/app/src/main/java/com/moyerun/moyeorun_android/views/BadgeRoundImageView.kt index 9108791..525530d 100644 --- a/app/src/main/java/com/moyerun/moyeorun_android/views/BadgeRoundImageView.kt +++ b/app/src/main/java/com/moyerun/moyeorun_android/views/BadgeRoundImageView.kt @@ -88,9 +88,11 @@ class BadgeRoundImageView @JvmOverloads constructor( @RequiresPermission(Manifest.permission.INTERNET) fun setBigCircleImgSrc(imgUrl: String, isImageCropped: Boolean = false) { if (isImageCropped) { - Glide.with(context).load(imgUrl).centerCrop().into(binding.imgBigCircle) + Glide.with(context).load(imgUrl).centerCrop() + .placeholder(R.drawable.user_profile_image_default_112dp).into(binding.imgBigCircle) } else - Glide.with(context).load(imgUrl).into(binding.imgBigCircle) + Glide.with(context).load(imgUrl) + .placeholder(R.drawable.user_profile_image_default_112dp).into(binding.imgBigCircle) } fun setBigCircleImageBg(@ColorRes bgResId: Int) { diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..49fd67d --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_toolbar_back.xml b/app/src/main/res/drawable/ic_toolbar_back.xml new file mode 100644 index 0000000..128ab24 --- /dev/null +++ b/app/src/main/res/drawable/ic_toolbar_back.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml new file mode 100644 index 0000000..8cb96a0 --- /dev/null +++ b/app/src/main/res/layout/activity_profile.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + +