Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.teamwable.common.restarter

interface AppReStarter {
fun restartApp()

fun makeToast(message: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.teamwable.common.restarter

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class AppReStarterModule {
@Binds
@Singleton
abstract fun bindsAppRestarter(
appRestarter: DefaultAppReStarter,
): AppReStarter
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.teamwable.common.restarter

import android.content.Context
import android.content.Intent
import android.widget.Toast
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject

class DefaultAppReStarter @Inject constructor(
@ApplicationContext private val context: Context,
) : AppReStarter {
private var currentToast: Toast? = null
private val scope = CoroutineScope(Dispatchers.Main)

override fun restartApp() {
scope.launch {
val restartIntent = context.packageManager
.getLaunchIntentForPackage(context.packageName)
?.component
?.let(Intent::makeRestartActivityTask)

context.startActivity(restartIntent)
}
}
Comment on lines +18 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

앱 재시작 시 null 안전성 개선 필요

getLaunchIntentForPackage()가 null을 반환할 경우 startActivity(null)로 인한 예외가 발생할 수 있습니다.

다음과 같이 null 체크를 추가하세요:

 override fun restartApp() {
     scope.launch {
         val restartIntent = context.packageManager
             .getLaunchIntentForPackage(context.packageName)
             ?.component
             ?.let(Intent::makeRestartActivityTask)
-
-        context.startActivity(restartIntent)
+        
+        restartIntent?.let {
+            context.startActivity(it)
+        } ?: run {
+            // 로그 출력 또는 다른 처리
+        }
     }
 }
🤖 Prompt for AI Agents
In
core/common/src/main/java/com/teamwable/common/restarter/DefaultAppReStarter.kt
around lines 18 to 27, the restartApp method calls startActivity with a
potentially null intent if getLaunchIntentForPackage returns null, which can
cause an exception. To fix this, add a null check before calling startActivity
to ensure the intent is not null, and only call startActivity if the intent is
valid.


override fun makeToast(message: String) {
scope.launch {
currentToast?.cancel()
currentToast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
currentToast?.show()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.teamwable.data.repositoryimpl

import com.teamwable.data.mapper.toModel.toUserModel
import com.teamwable.data.repository.AuthRepository
import com.teamwable.data.util.runHandledCatching
import com.teamwable.model.auth.UserModel
import com.teamwable.network.datasource.AuthService
import com.teamwable.network.dto.request.RequestSocialLoginDto
import com.teamwable.network.util.runHandledCatching
import javax.inject.Inject

internal class DefaultAuthRepository @Inject constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.teamwable.data.repositoryimpl
import com.teamwable.data.mapper.toModel.toCommunityModel
import com.teamwable.data.mapper.toModel.toRequestCommunityDto
import com.teamwable.data.repository.CommunityRepository
import com.teamwable.data.util.runHandledCatching
import com.teamwable.model.community.CommunityModel
import com.teamwable.network.datasource.CommunityService
import com.teamwable.network.util.runHandledCatching
import com.teamwable.network.util.toCustomError
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.teamwable.data.gallery.BitmapFetcher
import com.teamwable.data.gallery.GallerySaver
import com.teamwable.data.gallery.GallerySaver.Companion.FILE_EXTENSION
import com.teamwable.data.repository.FeedImageRepository
import com.teamwable.data.util.runHandledCatching
import com.teamwable.network.util.runHandledCatching
import javax.inject.Inject

internal class DefaultFeedImageRepository @Inject constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import com.teamwable.data.mapper.toModel.toMemberDataModel
import com.teamwable.data.mapper.toModel.toProfile
import com.teamwable.data.repository.ProfileRepository
import com.teamwable.data.util.createImagePart
import com.teamwable.data.util.runHandledCatching
import com.teamwable.model.Profile
import com.teamwable.model.profile.MemberDataModel
import com.teamwable.model.profile.MemberInfoEditModel
import com.teamwable.network.datasource.ProfileService
import com.teamwable.network.dto.request.RequestWithdrawalDto
import com.teamwable.network.util.handleThrowable
import com.teamwable.network.util.runHandledCatching
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
Expand Down
1 change: 1 addition & 0 deletions core/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ android {
dependencies {
implementation(project(":core:model"))
implementation(project(":core:datastore"))
implementation(project(":core:common"))
implementation(libs.paging)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.teamwable.network

import com.teamwable.common.restarter.AppReStarter
import com.teamwable.datastore.datasource.WablePreferencesDataSource
import com.teamwable.network.datasource.AuthService
import com.teamwable.network.util.runSuspendCatching
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import timber.log.Timber
import javax.inject.Inject

class TokenAuthenticator @Inject constructor(
private val dataStore: WablePreferencesDataSource,
private val authService: AuthService,
private val appRestarter: AppReStarter,
) : Authenticator {
private val mutex = Mutex()

override fun authenticate(route: Route?, response: Response): Request? {
if (response.code != 401) return null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토큰 만료만 401로 오나요?! 따로 토큰 만료인지 확인하는 분기가 없어도 되는지 궁금합니다!


if (response.request.header("Authorization-Retry") != null) return null

return runBlocking {
val newAccessToken = refreshToken() ?: return@runBlocking null
response.request
.newBuilder()
.header("Authorization", newAccessToken)
.header("Authorization-Retry", "true")
.build()
}
}

private suspend fun refreshToken(): String? {
return mutex.withLock {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 👍 👍 👍 👍 👍

val accessToken = dataStore.accessToken.first()
val refreshToken = dataStore.refreshToken.first()

runSuspendCatching {
authService.getReissueToken(accessToken, refreshToken)
}.onSuccess {
val newAccess = "Bearer ${it.data.accessToken}"
dataStore.updateAccessToken(newAccess)
dataStore.updateRefreshToken(it.data.refreshToken)
return newAccess
}.onFailure {
Timber.e(it)
dataStore.clear()
notifyReLoginRequired()
}
null
}
}

private fun notifyReLoginRequired() {
appRestarter.makeToast("재 로그인이 필요해요")
appRestarter.restartApp()
}
}
102 changes: 6 additions & 96 deletions core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt
Original file line number Diff line number Diff line change
@@ -1,112 +1,22 @@
package com.teamwable.network

import android.app.Application
import android.content.Intent
import android.widget.Toast
import com.teamwable.datastore.datasource.WablePreferencesDataSource
import com.teamwable.network.datasource.AuthService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class TokenInterceptor @Inject constructor(
private val wablePreferencesDataSource: WablePreferencesDataSource,
private val context: Application,
private val authService: AuthService,
private val dataStore: WablePreferencesDataSource,
) : Interceptor {
private val mutex = Mutex()
private var currentToast: Toast? = null

override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
var response = chain.proceed(originalRequest.newAuthBuilder())

if (response.code == CODE_TOKEN_EXPIRE) {
response.close()
val tokenRefreshed = runBlocking {
refreshTokenIfNeeded()
}
if (tokenRefreshed) {
response = chain.proceed(originalRequest.newAuthBuilder())
} else {
handleFailedTokenReissue()
}
}
return response
}

private suspend fun refreshTokenIfNeeded(): Boolean {
mutex.withLock {
val accessToken = wablePreferencesDataSource.accessToken.first()
val refreshToken = wablePreferencesDataSource.refreshToken.first()

return try {
val tokenResult = runBlocking(Dispatchers.IO) {
authService.getReissueToken(accessToken, refreshToken)
}

when (tokenResult.success) {
true -> {
wablePreferencesDataSource.updateAccessToken(
BEARER + tokenResult.data.accessToken,
)
true
}

false -> false
}
} catch (e: Exception) {
false
}
}
}

private fun handleFailedTokenReissue() = CoroutineScope(Dispatchers.Main).launch {
showToast()
withContext(Dispatchers.IO) {
wablePreferencesDataSource.clear()
}
restartActivity()
}

private fun showToast() {
currentToast?.cancel()
currentToast = Toast.makeText(context, "재 로그인이 필요해요", Toast.LENGTH_SHORT)
currentToast?.show()
}

private suspend fun restartActivity() = with(context) {
mutex.withLock {
startActivity(
Intent.makeRestartActivityTask(
packageManager.getLaunchIntentForPackage(packageName)?.component,
),
)
}
}

private fun Request.newAuthBuilder() = newBuilder()
.addHeader(
name = AUTHORIZATION,
value = runBlocking {
wablePreferencesDataSource.accessToken.first()
},
).build()
val accessToken = runBlocking { dataStore.accessToken.first() }
val authRequest: Request = chain.request().newBuilder()
.addHeader("Authorization", accessToken)
.build()

companion object {
const val CODE_TOKEN_EXPIRE = 401
const val AUTHORIZATION = "Authorization"
const val BEARER = "Bearer "
return chain.proceed(authRequest)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.teamwable.network.di

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.teamwable.network.BuildConfig
import com.teamwable.network.TokenAuthenticator
import com.teamwable.network.TokenInterceptor
import com.teamwable.network.util.isJsonArray
import com.teamwable.network.util.isJsonObject
Expand All @@ -10,7 +11,6 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
Expand All @@ -35,21 +35,18 @@ internal object NetworkModule {
@Singleton
@Provides
fun provideOkHttpClient(
@AccessToken tokenInterceptor: Interceptor,
tokenInterceptor: TokenInterceptor,
loggingInterceptor: HttpLoggingInterceptor,
tokenAuthenticator: TokenAuthenticator,
): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.addInterceptor(tokenInterceptor)
.addInterceptor(loggingInterceptor)
.authenticator(tokenAuthenticator)
.build()
Comment thread
chanubc marked this conversation as resolved.

@Provides
@Singleton
@AccessToken
fun provideAuthInterceptor(interceptor: TokenInterceptor): Interceptor = interceptor

@Singleton
@Provides
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import javax.inject.Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class WableRetrofit

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AccessToken

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class WithoutTokenInterceptor
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.teamwable.data.util
package com.teamwable.network.util

import com.teamwable.network.util.toCustomError
import kotlinx.coroutines.TimeoutCancellationException
import kotlin.coroutines.cancellation.CancellationException

Expand Down
Loading