Skip to content

Commit 954aee7

Browse files
authored
Merge pull request #117 from FindYou-Kuit/feat/firebase-performance
[Feat] Firebase Performance Monitoring 추가
2 parents 336100f + 3aa96da commit 954aee7

14 files changed

Lines changed: 213 additions & 29 deletions

File tree

app/build.gradle.kts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ plugins {
1010
alias(libs.plugins.navigationSafeArgs)
1111
alias(libs.plugins.googleServices)
1212
alias(libs.plugins.firebaseCrashlytics)
13+
alias(libs.plugins.firebasePerf)
1314
}
1415

1516
val properties = Properties().apply {
@@ -24,8 +25,8 @@ android {
2425
applicationId = "com.kuit.findu"
2526
minSdk = 28
2627
targetSdk = 35
27-
versionCode = 19
28-
versionName = "1.1.4"
28+
versionCode = 21
29+
versionName = "1.1.6"
2930

3031
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3132
buildConfigField("String", "GPT_KEY", properties["GPT_KEY"].toString())
@@ -171,6 +172,7 @@ dependencies {
171172
implementation(libs.firebase.analytics.ktx)
172173
implementation(libs.firebase.config.ktx)
173174
implementation(libs.firebase.crashlytics)
175+
implementation(libs.firebase.perf)
174176

175177
// AdMob
176178
implementation("com.google.android.gms:play-services-ads:23.1.0")

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
1212
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- CameraX -->
1313
<uses-permission android:name="android.permission.CAMERA" />
14-
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
14+
<uses-permission
15+
android:name="com.google.android.gms.permission.AD_ID"
1516
tools:ignore="AdvertisingIdPolicy" />
1617

1718
<queries>
@@ -47,6 +48,11 @@
4748
android:name="com.google.android.gms.ads.APPLICATION_ID"
4849
android:value="ca-app-pub-7675272869453438~5374193050" />
4950

51+
<!-- Performance -->
52+
<meta-data
53+
android:name="firebase_performance_logcat_enabled"
54+
android:value="true" />
55+
5056
<!-- Kakao Login -->
5157
<activity
5258
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"

app/src/main/java/com/kuit/findu/FindUApp.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
package com.kuit.findu
22

3+
import android.app.Activity
34
import android.app.Application
5+
import android.content.Intent
6+
import android.os.Bundle
47
import androidx.appcompat.app.AppCompatDelegate
58
import com.google.android.gms.ads.MobileAds
69
import com.kakao.sdk.common.KakaoSdk
10+
import com.kuit.findu.data.dataremote.util.SessionExpiredEventManager
11+
import com.kuit.findu.presentation.ui.login.LoginActivity
12+
import com.kuit.findu.presentation.ui.splash.SplashActivity
713
import com.naver.maps.map.NaverMapSdk
814
import dagger.hilt.android.HiltAndroidApp
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.SupervisorJob
18+
import kotlinx.coroutines.launch
19+
import java.lang.ref.WeakReference
20+
import javax.inject.Inject
921

1022
@HiltAndroidApp
1123
class FindUApp : Application() {
24+
25+
@Inject
26+
lateinit var sessionExpiredEventManager: SessionExpiredEventManager
27+
28+
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
29+
private var currentActivity: WeakReference<Activity>? = null
30+
1231
override fun onCreate() {
1332
super.onCreate()
1433

@@ -21,5 +40,35 @@ class FindUApp : Application() {
2140

2241
// AdMob 초기화
2342
MobileAds.initialize(this)
43+
44+
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
45+
override fun onActivityResumed(activity: Activity) {
46+
currentActivity = WeakReference(activity)
47+
}
48+
49+
override fun onActivityPaused(activity: Activity) {
50+
if (currentActivity?.get() === activity) {
51+
currentActivity = null
52+
}
53+
}
54+
55+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
56+
override fun onActivityStarted(activity: Activity) {}
57+
override fun onActivityStopped(activity: Activity) {}
58+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
59+
override fun onActivityDestroyed(activity: Activity) {}
60+
})
61+
62+
applicationScope.launch {
63+
sessionExpiredEventManager.sessionExpiredEvent.collect {
64+
val activity = currentActivity?.get() ?: return@collect
65+
if (activity is LoginActivity || activity is SplashActivity) return@collect
66+
67+
val intent = Intent(activity, LoginActivity::class.java).apply {
68+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
69+
}
70+
activity.startActivity(intent)
71+
}
72+
}
2473
}
2574
}

app/src/main/java/com/kuit/findu/data/dataremote/util/AuthAuthenticator.kt

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,50 @@ import javax.inject.Inject
1313
class AuthAuthenticator @Inject constructor(
1414
private val tokenLocalDataSource: TokenLocalDataSource,
1515
private val reissueService: ReissueService,
16+
private val sessionExpiredEventManager: SessionExpiredEventManager,
1617
) : Authenticator {
1718
override fun authenticate(route: Route?, response: Response): Request? {
18-
// 401 Unauthorized 에러 감지
19-
if (response.code == 401) {
19+
val staleToken = tokenLocalDataSource.accessToken
20+
21+
synchronized(LOCK) {
22+
val currentToken = tokenLocalDataSource.accessToken
23+
24+
if (currentToken != staleToken) {
25+
return response.request.newBuilder()
26+
.header("Authorization", "Bearer $currentToken")
27+
.build()
28+
}
29+
2030
return try {
2131
val refreshToken = tokenLocalDataSource.refreshToken
2232

23-
// 토큰 재발급 요청
2433
val reissueResponse = runBlocking {
2534
reissueService.postReissueToken(
2635
TokenReissueRequestDto(refreshToken)
2736
)
2837
}
2938

3039
if (reissueResponse.success) {
31-
// 새로운 토큰으로 저장
3240
tokenLocalDataSource.accessToken = reissueResponse.data.accessToken
3341
tokenLocalDataSource.refreshToken = reissueResponse.data.refreshToken
3442

35-
// 새로운 토큰으로 원래 요청 재시도
3643
response.request.newBuilder()
3744
.header("Authorization", "Bearer ${reissueResponse.data.accessToken}")
3845
.build()
3946
} else {
40-
// 토큰 재발급 실패 - 로그인 화면으로 이동
4147
tokenLocalDataSource.clearToken()
48+
sessionExpiredEventManager.notifySessionExpired()
4249
null
4350
}
4451
} catch (e: Exception) {
45-
// 토큰 재발급 중 오류 발생 - 로그인 화면으로 이동
4652
tokenLocalDataSource.clearToken()
53+
sessionExpiredEventManager.notifySessionExpired()
4754
null
4855
}
4956
}
57+
}
5058

51-
return null
59+
companion object {
60+
private val LOCK = Any()
5261
}
5362
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.kuit.findu.data.dataremote.util
2+
3+
import com.kuit.findu.data.datalocal.datasource.TokenLocalDataSource
4+
import okhttp3.Interceptor
5+
import okhttp3.Response
6+
import javax.inject.Inject
7+
8+
class AuthErrorInterceptor @Inject constructor(
9+
private val tokenLocalDataSource: TokenLocalDataSource,
10+
private val sessionExpiredEventManager: SessionExpiredEventManager,
11+
) : Interceptor {
12+
override fun intercept(chain: Interceptor.Chain): Response {
13+
val response = chain.proceed(chain.request())
14+
15+
if (response.code == 403) {
16+
tokenLocalDataSource.clearToken()
17+
sessionExpiredEventManager.notifySessionExpired()
18+
}
19+
20+
return response
21+
}
22+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package com.kuit.findu.data.dataremote.util
22

3-
class AuthenticationException: Exception()
3+
class AuthenticationException(message: String = "Session expired") : Exception(message)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.kuit.findu.data.dataremote.util
2+
3+
import kotlinx.coroutines.flow.MutableSharedFlow
4+
import kotlinx.coroutines.flow.SharedFlow
5+
import kotlinx.coroutines.flow.asSharedFlow
6+
import javax.inject.Inject
7+
import javax.inject.Singleton
8+
9+
@Singleton
10+
class SessionExpiredEventManager @Inject constructor() {
11+
private val _sessionExpiredEvent = MutableSharedFlow<Unit>(
12+
replay = 0,
13+
extraBufferCapacity = 1,
14+
onBufferOverflow = kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
15+
)
16+
val sessionExpiredEvent: SharedFlow<Unit> = _sessionExpiredEvent.asSharedFlow()
17+
18+
fun notifySessionExpired() {
19+
_sessionExpiredEvent.tryEmit(Unit)
20+
}
21+
}

app/src/main/java/com/kuit/findu/di/NetworkModule.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import com.kuit.findu.BuildConfig.DEBUG
77
import com.kuit.findu.data.datalocal.datasource.TokenLocalDataSource
88
import com.kuit.findu.data.dataremote.service.ReissueService
99
import com.kuit.findu.data.dataremote.util.AuthAuthenticator
10+
import com.kuit.findu.data.dataremote.util.AuthErrorInterceptor
1011
import com.kuit.findu.data.dataremote.util.AuthInterceptor
1112
import com.kuit.findu.data.dataremote.util.DiscordLogger
1213
import com.kuit.findu.data.dataremote.util.ErrorTrackingInterceptor
14+
import com.kuit.findu.data.dataremote.util.SessionExpiredEventManager
1315
import com.kuit.findu.di.qualifier.ReissueRetrofit
1416
import dagger.Module
1517
import dagger.Provides
@@ -45,13 +47,15 @@ object NetworkModule {
4547
loggingInterceptor: HttpLoggingInterceptor,
4648
authInterceptor: AuthInterceptor,
4749
authAuthenticator: AuthAuthenticator,
50+
authErrorInterceptor: AuthErrorInterceptor,
4851
errorTrackingInterceptor: ErrorTrackingInterceptor,
4952
): OkHttpClient =
5053
OkHttpClient.Builder().apply {
5154
connectTimeout(10, TimeUnit.SECONDS)
5255
writeTimeout(10, TimeUnit.SECONDS)
5356
readTimeout(10, TimeUnit.SECONDS)
5457
addInterceptor(authInterceptor)
58+
addInterceptor(authErrorInterceptor)
5559
if (DEBUG) addInterceptor(loggingInterceptor)
5660
else addInterceptor(errorTrackingInterceptor)
5761
authenticator(authAuthenticator)
@@ -75,8 +79,18 @@ object NetworkModule {
7579
fun provideAuthAuthenticator(
7680
tokenLocalDataSource: TokenLocalDataSource,
7781
reissueService: ReissueService,
82+
sessionExpiredEventManager: SessionExpiredEventManager,
7883
): AuthAuthenticator {
79-
return AuthAuthenticator(tokenLocalDataSource, reissueService)
84+
return AuthAuthenticator(tokenLocalDataSource, reissueService, sessionExpiredEventManager)
85+
}
86+
87+
@Provides
88+
@Singleton
89+
fun provideAuthErrorInterceptor(
90+
tokenLocalDataSource: TokenLocalDataSource,
91+
sessionExpiredEventManager: SessionExpiredEventManager,
92+
): AuthErrorInterceptor {
93+
return AuthErrorInterceptor(tokenLocalDataSource, sessionExpiredEventManager)
8094
}
8195

8296
@Provides

app/src/main/java/com/kuit/findu/presentation/ui/home/HomeFragment.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import com.kuit.findu.presentation.ui.home.composeview.HomeScreen
2424
import com.kuit.findu.presentation.ui.home.viewmodel.HomeUiEffect
2525
import com.kuit.findu.presentation.ui.home.viewmodel.HomeUiEvent
2626
import com.kuit.findu.presentation.ui.home.viewmodel.HomeViewModel
27-
import com.kuit.findu.presentation.ui.login.LoginActivity
2827
import com.kuit.findu.presentation.util.permission.LocationPermissionManager.hasLocationPermission
2928
import dagger.hilt.android.AndroidEntryPoint
3029

@@ -96,7 +95,6 @@ class HomeFragment : Fragment() {
9695
}
9796

9897
is HomeUiEffect.Dial -> call120()
99-
is HomeUiEffect.NavigateToLogin -> startLoginActivity()
10098
}
10199
}
102100
}
@@ -176,12 +174,6 @@ class HomeFragment : Fragment() {
176174
)
177175
}
178176

179-
private fun startLoginActivity() {
180-
val intent = Intent(requireContext(), LoginActivity::class.java)
181-
startActivity(intent)
182-
requireActivity().finish()
183-
}
184-
185177
private fun navigateToProtectDetail(id: String, tag: String, name: String) {
186178
when (tag) {
187179
AnimalStateType.PROTECT.state -> {

app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,21 @@ import androidx.compose.foundation.layout.width
1515
import androidx.compose.foundation.shape.RoundedCornerShape
1616
import androidx.compose.material.Text
1717
import androidx.compose.runtime.Composable
18+
import androidx.compose.runtime.remember
1819
import androidx.compose.ui.Alignment
1920
import androidx.compose.ui.Modifier
2021
import androidx.compose.ui.draw.clip
2122
import androidx.compose.ui.layout.ContentScale
2223
import androidx.compose.ui.res.painterResource
2324
import androidx.compose.ui.text.style.TextOverflow
2425
import androidx.compose.ui.tooling.preview.Preview
26+
import androidx.compose.ui.platform.LocalContext
2527
import androidx.compose.ui.unit.dp
28+
import android.os.SystemClock
29+
import android.util.Log
2630
import coil.compose.AsyncImage
31+
import coil.request.ImageRequest
32+
import com.google.firebase.perf.FirebasePerformance
2733
import com.kuit.findu.R
2834
import com.kuit.findu.domain.model.ProtectAnimal
2935
import com.kuit.findu.presentation.type.AnimalStateType
@@ -48,8 +54,33 @@ fun HomeProtectAnimalCard(
4854
Box(
4955
modifier = Modifier.size(height = 100.dp, width = 120.dp),
5056
) {
57+
val context = LocalContext.current
58+
val imageRequest = remember(animal.thumbnailImageUrl) {
59+
val trace = FirebasePerformance.getInstance().newTrace("home_protect_image_load")
60+
var startTime = 0L
61+
ImageRequest.Builder(context)
62+
.data(animal.thumbnailImageUrl)
63+
.listener(
64+
onStart = {
65+
startTime = SystemClock.elapsedRealtime()
66+
trace.start()
67+
},
68+
onSuccess = { _, _ ->
69+
val duration = SystemClock.elapsedRealtime() - startTime
70+
Log.d("ImagePerf", "보호동물 이미지 로딩 완료: ${duration}ms | ${animal.thumbnailImageUrl}")
71+
trace.stop()
72+
},
73+
onError = { _, _ ->
74+
val duration = SystemClock.elapsedRealtime() - startTime
75+
Log.e("ImagePerf", "보호동물 이미지 로딩 실패: ${duration}ms | ${animal.thumbnailImageUrl}")
76+
trace.putAttribute("status", "error")
77+
trace.stop()
78+
}
79+
)
80+
.build()
81+
}
5182
AsyncImage(
52-
model = animal.thumbnailImageUrl,
83+
model = imageRequest,
5384
contentDescription = "Animal Image",
5485
contentScale = ContentScale.Crop,
5586
modifier = Modifier

0 commit comments

Comments
 (0)