-
Notifications
You must be signed in to change notification settings - Fork 1
✨[FEAT] #135: Firebase Remote Config 연동 #138
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
base: develop
Are you sure you want to change the base?
Changes from 4 commits
760055d
c4c25d8
dad37b5
5f9f603
1a7e43b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ plugins { | |
| alias(libs.plugins.ksp) | ||
| alias(libs.plugins.hilt.android) | ||
| alias(libs.plugins.compose.compiler) | ||
| alias(libs.plugins.google.services) | ||
| alias(libs.plugins.firebase.crashlytics) | ||
| } | ||
|
|
||
| android { | ||
|
|
@@ -43,6 +45,15 @@ android { | |
|
|
||
| buildTypes { | ||
| debug { | ||
| applicationIdSuffix = ".debug" | ||
| versionNameSuffix = "-debug" | ||
| buildConfigField("String", "BASE_URL", "\"${localProperties["base_url"] ?: ""}\"") | ||
| } | ||
| create("qa") { | ||
| initWith(getByName("debug")) | ||
| applicationIdSuffix = ".qa" | ||
| versionNameSuffix = "-qa" | ||
| matchingFallbacks += listOf("debug") | ||
| buildConfigField("String", "BASE_URL", "\"${localProperties["base_url"] ?: ""}\"") | ||
| } | ||
| release { | ||
|
|
@@ -67,9 +78,6 @@ android { | |
| buildConfig = true | ||
| viewBinding = true | ||
| } | ||
| composeOptions { | ||
| kotlinCompilerExtensionVersion = "1.5.1" | ||
| } | ||
| packaging { | ||
| resources { | ||
| excludes += "/META-INF/{AL2.0,LGPL2.1}" | ||
|
|
@@ -125,6 +133,11 @@ dependencies { | |
| debugImplementation(libs.androidx.ui.tooling) | ||
| debugImplementation(libs.androidx.ui.test.manifest) | ||
|
|
||
| implementation(platform(libs.firebase.bom)) | ||
| implementation(libs.firebase.config.ktx) | ||
| implementation(libs.firebase.analytics.ktx) | ||
| implementation(libs.firebase.crashlytics.ktx) | ||
|
|
||
| implementation(libs.play.services.auth) | ||
| implementation(libs.androidx.credentials) | ||
| implementation(libs.androidx.credentials.play.services.auth) | ||
|
|
@@ -133,4 +146,8 @@ dependencies { | |
| implementation("io.branch.sdk.android:library:5.+") | ||
|
|
||
| implementation(libs.multiplatform.settings) | ||
|
|
||
| // Import the Firebase BoM | ||
| implementation(platform("com.google.firebase:firebase-bom:34.9.0")) | ||
| implementation("com.google.firebase:firebase-analytics") | ||
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,18 +3,43 @@ package com.umc.edison | |
| import android.app.Application | ||
| import android.util.Log | ||
| import androidx.work.Configuration | ||
| import com.google.firebase.ktx.Firebase | ||
| import com.google.firebase.remoteconfig.ktx.remoteConfig | ||
| import com.umc.edison.common.logging.AppLogger | ||
| import com.umc.edison.data.di.EntryPointModule | ||
| import com.umc.edison.data.sync.SyncDataWorkerFactory | ||
| import com.umc.edison.presentation.sync.SyncTrigger | ||
| import com.umc.edison.remote.config.DomainProvider | ||
| import dagger.hilt.EntryPoints | ||
| import dagger.hilt.android.HiltAndroidApp | ||
| import io.branch.referral.Branch | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.SupervisorJob | ||
| import kotlinx.coroutines.delay | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.tasks.await | ||
| import javax.inject.Inject | ||
| import com.umc.edison.common.logging.UserContext | ||
| import com.umc.edison.remote.config.RemoteConfigKeys | ||
|
|
||
| @HiltAndroidApp | ||
| class EdisonApplication : Application(), Configuration.Provider { | ||
| @Inject | ||
| lateinit var domainProvider: DomainProvider | ||
|
|
||
| @Inject | ||
| lateinit var userContext: UserContext | ||
|
|
||
| private val remoteConfigScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) | ||
| private val remoteConfig by lazy { Firebase.remoteConfig } | ||
| private val maxAttempts = 4 | ||
| private val initialBackoffMs = 1_000L | ||
|
|
||
| override fun onCreate() { | ||
| super.onCreate() | ||
| initCrashlyticsContext() | ||
| initRemoteConfig() | ||
|
|
||
| // Branch SDK 초기화 | ||
| Branch.getAutoInstance(this) | ||
|
|
@@ -24,6 +49,61 @@ class EdisonApplication : Application(), Configuration.Provider { | |
| syncTrigger.setupSync() | ||
| } | ||
|
|
||
| private fun initCrashlyticsContext() { | ||
| remoteConfigScope.launch { | ||
| userContext.ensureInstallId() | ||
| userContext.setBuildInfo( | ||
| BuildConfig.BUILD_TYPE, | ||
| BuildConfig.APPLICATION_ID, | ||
| BuildConfig.VERSION_NAME | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private fun initRemoteConfig() { | ||
|
|
||
| val settings = com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings.Builder() | ||
|
||
| .setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 60 * 60 * 12) | ||
| .build() | ||
| remoteConfig.setConfigSettingsAsync(settings) | ||
|
|
||
| remoteConfig.setDefaultsAsync( | ||
| mapOf(RemoteConfigKeys.BASE_URL to BuildConfig.BASE_URL) | ||
| ) | ||
|
|
||
| remoteConfig.getString(RemoteConfigKeys.BASE_URL) | ||
| .takeIf { it.isNotBlank() } | ||
| ?.let(domainProvider::setDomain) | ||
|
|
||
| remoteConfigScope.launch { | ||
| var backoff = initialBackoffMs | ||
| repeat(maxAttempts) { attempt -> | ||
| val activated = try { | ||
| remoteConfig.fetchAndActivate().await() | ||
| } catch (e: Exception) { | ||
| AppLogger.w( | ||
| "EdisonApplication", | ||
| "Remote config fetch failed on attempt ${attempt + 1}", | ||
| e | ||
| ) | ||
| false | ||
| } | ||
|
|
||
| if (activated) { | ||
| remoteConfig.getString(RemoteConfigKeys.BASE_URL) | ||
| .takeIf { it.isNotBlank() } | ||
| ?.let(domainProvider::setDomain) | ||
| return@launch | ||
| } | ||
|
|
||
| if (attempt < maxAttempts - 1) { | ||
| delay(backoff) | ||
| backoff = (backoff * 2).coerceAtMost(8_000L) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override val workManagerConfiguration: Configuration | ||
| get() { | ||
| val syncDataWorkerFactory: SyncDataWorkerFactory = EntryPoints.get( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.umc.edison.common.logging | ||
|
|
||
| import android.util.Log | ||
| import com.google.firebase.crashlytics.ktx.crashlytics | ||
| import com.google.firebase.ktx.Firebase | ||
| import com.umc.edison.BuildConfig | ||
|
|
||
| object AppLogger { | ||
| private const val PREFIX_DEBUG = "D/" | ||
| private const val PREFIX_INFO = "I/" | ||
| private const val PREFIX_WARN = "W/" | ||
| private const val PREFIX_ERROR = "E/" | ||
| private val isDebug = BuildConfig.DEBUG | ||
|
|
||
| fun d(tag: String, message: String) { | ||
| if (isDebug) Log.d(tag, message) | ||
| Firebase.crashlytics.log("$PREFIX_DEBUG$tag: $message") | ||
| } | ||
|
|
||
| fun i(tag: String, message: String) { | ||
| if (isDebug) Log.i(tag, message) | ||
| Firebase.crashlytics.log("$PREFIX_INFO$tag: $message") | ||
| } | ||
|
|
||
| fun w(tag: String, message: String, throwable: Throwable? = null) { | ||
| if (isDebug) Log.w(tag, message, throwable) | ||
| Firebase.crashlytics.log("$PREFIX_WARN$tag: $message") | ||
| throwable?.let { Firebase.crashlytics.recordException(it) } | ||
| } | ||
|
|
||
| fun e(tag: String, message: String, throwable: Throwable? = null) { | ||
| if (isDebug) Log.e(tag, message, throwable) | ||
| Firebase.crashlytics.log("$PREFIX_ERROR$tag: $message") | ||
| throwable?.let { Firebase.crashlytics.recordException(it) } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package com.umc.edison.common.logging | ||
|
|
||
| import com.google.firebase.crashlytics.ktx.crashlytics | ||
| import com.google.firebase.ktx.Firebase | ||
| import com.umc.edison.data.datasources.PrefDataSource | ||
| import java.util.UUID | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| class UserContext @Inject constructor( | ||
| private val prefDataSource: PrefDataSource | ||
| ) { | ||
| companion object { | ||
| private const val KEY_INSTALL_ID = "install_id" | ||
| } | ||
|
|
||
| suspend fun ensureInstallId(): String { | ||
| val existing: String = prefDataSource.get(KEY_INSTALL_ID, "") | ||
| if (existing.isNotBlank()) { | ||
| Firebase.crashlytics.setCustomKey(KEY_INSTALL_ID, existing) | ||
| return existing | ||
| } | ||
| val newId = UUID.randomUUID().toString() | ||
| prefDataSource.set(KEY_INSTALL_ID, newId) | ||
| Firebase.crashlytics.setCustomKey(KEY_INSTALL_ID, newId) | ||
| return newId | ||
| } | ||
|
|
||
| fun setAccountId(accountId: String) { | ||
| Firebase.crashlytics.setUserId(accountId) | ||
| } | ||
|
|
||
| fun setBuildInfo(buildType: String, applicationId: String, versionName: String) { | ||
| Firebase.crashlytics.setCustomKey("build_type", buildType) | ||
| Firebase.crashlytics.setCustomKey("application_id", applicationId) | ||
| Firebase.crashlytics.setCustomKey("version_name", versionName) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.umc.edison.data.token | ||
|
|
||
| class NoRefreshTokenException : IllegalStateException("No refresh token") | ||
| class RefreshFailedException(message: String) : IllegalStateException(message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
qa빌드 타입은initWith(getByName("debug"))를 통해debug빌드 타입의 모든 설정을 상속받습니다. 여기에는BASE_URL에 대한buildConfigField설정도 포함됩니다. 따라서 이 라인에서 다시 설정하는 것은 중복입니다. 코드를 더 깔끔하게 유지하기 위해 이 라인을 제거할 수 있습니다.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gemini-code-assist fixed in 1a7e43b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수정해주셔서 감사합니다. 해당 변경사항이 적용된 것을 확인했습니다.