Skip to content

Instantly share code, notes, and snippets.

@gold24park
Created January 8, 2023 06:21
Show Gist options
  • Save gold24park/64d1acdc92da7ee0ac408d835fb2903f to your computer and use it in GitHub Desktop.
Save gold24park/64d1acdc92da7ee0ac408d835fb2903f to your computer and use it in GitHub Desktop.
[Android] ETag Interceptor for OkHttp3 Retrofit
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