diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt index fd87edc..5cca6c7 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt @@ -1,10 +1,24 @@ package com.hoc081098.refreshtokensample.data.local +import java.io.IOException import kotlinx.coroutines.flow.Flow interface UserLocalSource { fun user(): Flow + /** + * Updates the data transactionally in an atomic read-modify-write operation. All operations + * are serialized, and the [transform] itself is a coroutine so it can perform heavy work + * such as RPCs. + * + * The coroutine completes when the data has been persisted durably to disk (after which + * [user] will reflect the update). If the transform or write to disk fails, the + * transaction is aborted and an exception is thrown. + * + * @return the snapshot returned by the transform + * @throws IOException when an exception is encountered when writing data to disk + * @throws Exception when thrown by the transform function + */ suspend fun update(transform: suspend (current: UserLocal?) -> UserLocal?): UserLocal? } diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt index 27165fd..712633c 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt @@ -13,8 +13,6 @@ import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response @@ -25,70 +23,73 @@ class AuthInterceptor @Inject constructor( private val apiService: Provider, private val appDispatchers: AppDispatchers, ) : Interceptor { - private val mutex = Mutex() - override fun intercept(chain: Interceptor.Chain): Response { - val req = chain.request().also { debug("[1] $it") } + val request = chain.request().also { debug("[1] START url=${it.url}") } - if (NO_AUTH in req.headers.values(CUSTOM_HEADER)) { - return chain.proceedWithToken(req, null) + if (NO_AUTH in request.headers.values(CUSTOM_HEADER)) { + return chain.proceedWithToken(request, null) } val token = runBlocking(appDispatchers.io) { userLocalSource.user().first() } ?.token - .also { debug("[2] $req $it") } - val res = chain.proceedWithToken(req, token) + .also { debug("[2] READ TOKEN token=${it.firstTwoCharactersAndLastTwoCharacters()}, url=${request.url}") } + val response = chain.proceedWithToken(request, token) - if (res.code != HTTP_UNAUTHORIZED || token == null) { - return res + if (response.code != HTTP_UNAUTHORIZED || token == null) { + return response } - debug("[3] $req") + val newToken = executeTokenRefreshing(request, token) + + return if (newToken !== null && newToken != token) { + chain.proceedWithToken(request, newToken) + } else { + response + } + } + + private fun executeTokenRefreshing(request: Request, token: String?): String? { + val requestUrl = request.url + debug("[3] BEFORE REFRESHING token=${token.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") - val newToken: String? = runBlocking(appDispatchers.io) { - mutex.withLock { - val user = - userLocalSource.user().first().also { debug("[4] $req $it") } - val maybeUpdatedToken = user?.token + return runBlocking(appDispatchers.io) { + userLocalSource.update { currentUserLocal -> + val maybeUpdatedToken = currentUserLocal?.token + debug("[4] INSIDE REFRESHING maybeUpdatedToken=${maybeUpdatedToken.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") when { - user == null || maybeUpdatedToken == null -> null.also { debug("[5-1] $req") } // already logged out! - maybeUpdatedToken != token -> maybeUpdatedToken.also { debug("[5-2] $req") } // refreshed by another request + maybeUpdatedToken == null -> + null + .also { debug("[5-1] LOGGED OUT url=$requestUrl") } // already logged out! + maybeUpdatedToken != token -> + currentUserLocal + .also { debug("[5-2] REFRESHED BY ANOTHER url=$requestUrl") } // refreshed by another request else -> { - debug("[5-3] $req") + debug("[5-3] START REFRESHING REQUEST url=$requestUrl") - val refreshTokenRes = apiService.get() - .refreshToken(user.toRefreshTokenBody()) - .also { debug("[6] $req $it") } + val refreshTokenRes = apiService + .get() + .refreshToken(currentUserLocal.toRefreshTokenBody()) - when (refreshTokenRes.code()) { + when (val code = refreshTokenRes.code()) { HTTP_OK -> { - debug("[7-1] $req") - refreshTokenRes.body()!!.token.also { updatedToken -> - userLocalSource.update { - (it ?: return@update null) - .toBuilder() - .setToken(updatedToken) - .build() - } - } + currentUserLocal + .toBuilder() + .setToken(refreshTokenRes.body()!!.token) + .build() + .also { debug("[6-1] REFRESH SUCCESSFULLY newToken=${it.token.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") } } - HTTP_UNAUTHORIZED -> { - debug("[7-2] $req") - userLocalSource.update { null } - null + HTTP_UNAUTHORIZED -> null.also { + debug("[6-2] REFRESH FAILED HTTP_UNAUTHORIZED url=$requestUrl") } - else -> { - debug("[7-3] $req") - null + else -> currentUserLocal.also { + debug("[6-3] REFRESH FAILED code=$code, url=$requestUrl") } } } } - } + }?.token } - - return if (newToken !== null) chain.proceedWithToken(req, newToken) else res } private fun Interceptor.Chain.proceedWithToken(req: Request, token: String?): Response = @@ -111,3 +112,9 @@ class AuthInterceptor @Inject constructor( } private fun UserLocal.toRefreshTokenBody() = RefreshTokenBody(refreshToken, username) + +@Suppress("NOTHING_TO_INLINE") +private inline fun String?.firstTwoCharactersAndLastTwoCharacters(): String? { + this ?: return null + return if (length <= 4) this else "${take(2)}...${takeLast(2)}" +}