Created
January 8, 2023 06:21
-
-
Save gold24park/64d1acdc92da7ee0ac408d835fb2903f to your computer and use it in GitHub Desktop.
[Android] ETag Interceptor for OkHttp3 Retrofit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.jakewharton.disklrucache.DiskLruCache | |
import okhttp3.Interceptor | |
import okhttp3.MediaType.Companion.toMediaTypeOrNull | |
import okhttp3.Request | |
import okhttp3.Response | |
import okhttp3.ResponseBody | |
import okhttp3.ResponseBody.Companion.toResponseBody | |
import java.io.BufferedOutputStream | |
import java.io.File | |
import java.io.IOException | |
import java.io.OutputStream | |
import java.math.BigInteger | |
import java.net.HttpURLConnection.HTTP_NOT_MODIFIED | |
import java.net.HttpURLConnection.HTTP_OK | |
import java.security.MessageDigest | |
// okhttp to network, cache 탐색해서 저장된 etag있는지 확인후 if-none-match에 싣기 | |
class ETagNetworkInterceptor(private val diskCache: DiskCache) : Interceptor { | |
override fun intercept(chain: Interceptor.Chain): Response { | |
val request = chain.request() | |
if (!request.method.equals("GET", true)) { | |
return chain.proceed(request) | |
} | |
val etag = diskCache.getEtag(request) | |
if (etag.isNullOrEmpty()) { | |
return chain.proceed(request) | |
} | |
val modified = diskCache.getModified(request) | |
val reqBuilder = request.newBuilder().apply { | |
addHeader("Host", request.header("Host") ?: request.url.host) | |
addHeader("If-None-Match", etag) | |
if (modified != null) { | |
addHeader("If-Modified-Since", modified) | |
} | |
} | |
val response = chain.proceed(reqBuilder.build()) | |
if (response.code != HTTP_NOT_MODIFIED) { | |
return response | |
} | |
val responseBody = diskCache.getBody(response) ?: return response | |
return response.newBuilder() | |
.code(HTTP_OK) | |
.body(responseBody) | |
.build() | |
} | |
} | |
class ETagInterceptor(private val diskCache: DiskCache): Interceptor { | |
override fun intercept(chain: Interceptor.Chain): Response { | |
val request = chain.request() | |
val response = chain.proceed(chain.request()) | |
if (request.method.equals("GET", true)) { | |
if (response.header("etag").toString().isNotEmpty()) { | |
diskCache.saveEtag(response) | |
} | |
} | |
return response | |
} | |
} | |
class DiskCache(private val cacheDir: File) { | |
private lateinit var diskLruCache: DiskLruCache | |
init { | |
init() | |
} | |
private fun init() { | |
try { | |
diskLruCache = DiskLruCache.open( | |
cacheDir, BuildConfig.VERSION_CODE, 1, 1024 * 1024 * 10 | |
) | |
} catch (e: IOException) { | |
// ignore failed to delete file exception | |
} | |
} | |
fun md5(input: String): String { | |
val md = MessageDigest.getInstance("MD5") | |
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') | |
} | |
private fun getEtagKey(request: Request): String { | |
return md5(request.url.toUrl().toString()) | |
} | |
private fun getModifiedKey(request: Request): String { | |
return md5("${request.url.toUrl()}/modified") | |
} | |
private fun getBodyKey(response: Response): String { | |
return md5("${response.request.url.toUrl()}/body") | |
} | |
fun getEtag(request: Request): String? { | |
return getValue(getEtagKey(request)) | |
} | |
fun getModified(request: Request): String? { | |
return getValue(getModifiedKey(request)) | |
} | |
fun getBody(response: Response): ResponseBody? { | |
return try { | |
val mediaType = response.request.header("content-type")?.toMediaTypeOrNull() | |
getValue(getBodyKey(response))?.toResponseBody(mediaType) | |
} catch (e: Exception) { | |
null | |
} | |
} | |
private fun getValue(key: String): String? { | |
var snapshot: DiskLruCache.Snapshot? = null | |
var value: String? = null | |
try { | |
snapshot = diskLruCache.get(key) | |
value = snapshot.getString(0) | |
} catch (e: Exception) { | |
} finally { | |
snapshot?.close() | |
} | |
return value | |
} | |
fun saveEtag(response: Response) { | |
val etag = response.header("etag") | |
val modified = response.header("If-Modified-Since") | |
val body = response.peekBody(Long.MAX_VALUE).string() | |
setKeyValue(getEtagKey(response.request), etag) | |
setKeyValue(getModifiedKey(response.request), modified) | |
setKeyValue(getBodyKey(response), body) | |
} | |
private fun setKeyValue(key: String, value: String?) { | |
var editor: DiskLruCache.Editor? = null | |
try { | |
editor = diskLruCache.edit(key) | |
if (writeValueToCache(value, editor)) { | |
diskLruCache.flush() | |
editor.commit() | |
} else { | |
editor.abort() | |
} | |
} catch (e: Exception) { | |
editor?.abort() | |
} | |
} | |
fun clearCache() { | |
diskLruCache.delete() | |
init() | |
} | |
private fun writeValueToCache(value: String?, editor: DiskLruCache.Editor?): Boolean { | |
if (value == null || editor == null) { | |
return false | |
} | |
var writeSucceed = true | |
var outputStream: OutputStream? = null | |
try { | |
outputStream = BufferedOutputStream(editor.newOutputStream(0)) | |
outputStream.write(value.toByteArray()) | |
} catch (e: IOException) { | |
writeSucceed = false | |
} finally { | |
outputStream?.close() | |
} | |
return writeSucceed | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment