Skip to content

Instantly share code, notes, and snippets.

@Dadoufi
Forked from oligazar/CookieJarManager.kt
Created December 27, 2018 07:17
Show Gist options
  • Save Dadoufi/ce4a918cf458d929529f81ae73b5179b to your computer and use it in GitHub Desktop.
Save Dadoufi/ce4a918cf458d929529f81ae73b5179b to your computer and use it in GitHub Desktop.
1. RetrofitException 2. RxErrorHandlingCallAdapterFactory 3. YourRetrofitBuilder 4. RetrofitBuilder 5. RetrofitManager 6. CookieManager 7. SingletonHolder 8. Injector 9. RetrofitHelpers
/**
* Handles cookies for retrofit requests
*
* https://github.com/franmontiel/PersistentCookieJar
* http://codezlab.com/add-cookies-interceptor-using-retrofit-android/
* https://gist.github.com/nikhiljha/52d45ca69a8415c6990d2a63f61184ff
* https://gist.github.com/tsuharesu/cbfd8f02d46498b01f1b
*/
class CookieJarManager {
companion object: SingletonHolder<Context, PersistentCookieJar>(
{ context -> PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context)) }
)
}
/**
* Injector class implements Injection interface and lives in different flawors
*/
object Injector: Injection {
override fun provideOkHttpClient(cache: Cache, config: OkHttpConfigurator): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
return OkHttpClient.Builder()
.addInterceptor(interceptor)
.cache(cache)
.apply { config() }
.build()
}
override fun provideMovieDetailRepository(application: Application): MovieDetailRepository {
val webService = RetrofitManager.createService(application, MovieWebService::class.java)
val movieDao = Room.databaseBuilder(application, MovieDatabase::class.java, "movie-database").build().movieDao()
return MovieDetailRepository(webService, movieDao)
}
}
/**
* Base class that puts together components, which configured in subclass
* in order to create Retrofit instance
*/
abstract class RetrofitBuilder {
fun buildRetrofit(): Retrofit {
return Retrofit.Builder()
.apply { configRetrofit() }
.build()
}
fun buildGson(): Gson {
return GsonBuilder()
.apply { configGson() }
.create()
}
fun buildOkHttpClient(cacheDir: File, factory: (Cache) -> OkHttpClient): OkHttpClient {
val cacheSize = 10 * 1024 * 1024 // 10 MiB
val cache = Cache(cacheDir, cacheSize.toLong())
return factory(cache)
}
abstract fun Retrofit.Builder.configRetrofit()
abstract fun GsonBuilder.configGson()
abstract fun OkHttpClient.Builder.configOkHttpClient()
}
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Response
import retrofit2.Retrofit
import java.io.IOException
class RetrofitException internal constructor(message: String?,
/** The request URL which produced the error. */
val url: String?,
/** Response object containing status code, headers, body, etc. */
val response: Response<*>?,
/** The event kind which triggered this error. */
val kind: Kind,
exception: Throwable?,
/** The Retrofit this request was executed on */
val retrofit: Retrofit?) : RuntimeException(message, exception) {
/** Identifies the event kind which triggered a [RetrofitException]. */
enum class Kind {
/** An [IOException] occurred while communicating to the server. */
NETWORK,
/** A non-200 HTTP status code was received from the server. */
HTTP,
/**
* An internal error occurred while attempting to execute a request. It is best practice to
* re-throw this exception so your application crashes.
*/
UNEXPECTED
}
/**
* HTTP response body converted to specified `type`. `null` if there is no
* response.
*
* @throws IOException if unable to convert the body to the specified `type`.
*/
@Throws(IOException::class)
fun <T> getErrorBodyAs(type: Class<T>): T? {
val errorBody = response?.errorBody()
if (errorBody == null || retrofit == null) {
return null
}
val converter: Converter<ResponseBody, T> = retrofit.responseBodyConverter(type, arrayOfNulls<Annotation>(0))
return converter.convert(errorBody)
}
companion object {
fun httpError(url: String, response: Response<*>, retrofit: Retrofit): RetrofitException {
val message = response.code().toString() + " " + response.message()
return RetrofitException(message, url, response, Kind.HTTP, null, retrofit)
}
fun networkError(exception: IOException): RetrofitException {
return RetrofitException(exception.message, null, null, Kind.NETWORK, exception, null)
}
fun unexpectedError(exception: Throwable): RetrofitException {
return RetrofitException(exception.message, null, null, Kind.UNEXPECTED, exception, null)
}
}
}
fun createGsonConverterFactory(config: GsonBuilder.() -> Unit): GsonConverterFactory = GsonConverterFactory
.create(GsonBuilder()
.apply { config() }
.create())
/**
* Get cache from context for OkHttpClient
*/
fun Context.getCache(sizeMb: Int = 10): Cache {
val cacheSize = sizeMb * 1024 * 1024 // 10 MiB
return Cache(cacheDir, cacheSize.toLong())
}
/**
* Users singleton (backed by SingletonHolder) Retrofit instance
* and helps to instantiate a Retrofit Service from the interface
*/
class RetrofitManager {
companion object: SingletonHolder<Context, Retrofit>({ context ->
TmdbRetrofitBuilder(context).buildRetrofit() }) {
fun <S> createService(context: Context, serviceClass: Class<S>): S =
instance(context).create(serviceClass)
}
}
class RxErrorHandlingCallAdapterFactory private constructor() : CallAdapter.Factory() {
private val originalFactory by lazy {
RxJava2CallAdapterFactory.create()
// RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())
}
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
val wrapped = originalFactory.get(returnType, annotations, retrofit) as CallAdapter<out Any, *>
return RxCallAdapterWrapper(retrofit, wrapped)
}
private class RxCallAdapterWrapper<R>(private val retrofit: Retrofit,
private val wrappedCallAdapter: CallAdapter<R, *>) : CallAdapter<R, Any> {
override fun responseType(): Type {
return wrappedCallAdapter.responseType()
}
@Suppress("UNCHECKED_CAST")
override fun adapt(call: Call<R>): Any {
val obj = wrappedCallAdapter.adapt(call)
return when (obj) {
is Flowable<*> -> obj.onErrorResumeNext { throwable: Throwable ->
Flowable.error(asRetrofitException(throwable))
}
is Single<*> -> obj.onErrorResumeNext { throwable: Throwable ->
Single.error(asRetrofitException(throwable))
}
is Maybe<*> -> obj.onErrorResumeNext { throwable: Throwable ->
Maybe.error(asRetrofitException(throwable))
}
is Completable -> obj.onErrorResumeNext { throwable: Throwable ->
Completable.error(asRetrofitException(throwable))
}
else -> obj
}
}
private fun asRetrofitException(throwable: Throwable): RetrofitException {
// We had non-200 http error
if (throwable is HttpException) {
val response = throwable.response()
return RetrofitException.httpError(response.raw().request().url().toString(), response, retrofit)
}
// A network error happened
if (throwable is IOException) {
return RetrofitException.networkError(throwable)
}
// We don't know what happened. We need to simply convert to an unknown error
return RetrofitException.unexpectedError(throwable)
}
}
companion object {
fun create(): CallAdapter.Factory = RxErrorHandlingCallAdapterFactory()
}
}
/**
* Creation of a singleton that takes an argument.
* Normally, using an object declaration in Kotlin you are guaranteed to get a safe and efficient singleton implementation.
* But it cannot take extra arguments
* https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e
*
* CppContext everywhere:
* https://github.com/LouisCAD/Splitties/tree/master/appctx
*
* Firebase AppContext trick:
* https://firebase.googleblog.com/2016/12/how-does-firebase-initialize-on-android.html
* class Manager private constructor(context: Context) {
* init {
* // Init using context argument
* }
*
* companion object : SingletonHolder<Manager, Context>(::Manager)
* }
*
* or
* class MovieDatabaseHolder {
* companion object: SingletonHolder<Application, MovieDao>(
* { application -> Room.databaseBuilder(application, MovieDatabase::class.java, "movie-database")
* .fallbackToDestructiveMigration().build().movieDao() }
* )
* }
* Usage example: LocalBroadcastManager.instance(context).sendBroadcast(intent)
*/
open class SingletonHolder<in A, out T>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile private var instance: T? = null
fun instance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
class YourRetrofitBuilder(private val context: Context): RetrofitBuilder() {
override fun Retrofit.Builder.configRetrofit() {
baseUrl(us.kostenko.architecturecomponentstmdb.common.api.baseUrl)
// addCallAdapterFactory(CoroutineCallAdapterFactory())
addConverterFactory(GsonConverterFactory.create(buildGson()))
// client(buildOkHttpClient(context.cacheDir) { cache ->
// StethoUtils.getOkHttpClient(cache) {
// apply { configOkHttpClient() }
// }
// })
}
override fun OkHttpClient.Builder.configOkHttpClient() {
addInterceptor(::headerInterceptor)
addInterceptor(::receiveCookieInterceptor)
connectTimeout(30, TimeUnit.SECONDS) // connect timeout
readTimeout(30, TimeUnit.SECONDS)
/**
* https://github.com/franmontiel/PersistentCookieJar
* http://codezlab.com/add-cookies-interceptor-using-retrofit-android/
* https://gist.github.com/nikhiljha/52d45ca69a8415c6990d2a63f61184ff
* https://gist.github.com/tsuharesu/cbfd8f02d46498b01f1b
*/
cookieJar(CookieJarManager.instance(context))
}
/**
* Configure Gson
* allows to provide type adapters etc.
*/
override fun GsonBuilder.configGson() {
// registerTypeAdapter(TicketMenuRoot::class.java, RootItemDeserializer())
setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
}
/**
* Intercepts requests to add values to the headers
*/
private fun headerInterceptor(chain: Interceptor.Chain): Response {
val original = chain.request()
val request = original.newBuilder()
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")
.header("Accept-Charset", "utf-8")
.header("Accept-Language", getLocale())
.method(original.method(), original.body()).build()
return chain.proceed(request)
}
/**
* Custom logic to receive cookies
*/
private fun receiveCookieInterceptor(chain: Interceptor.Chain): Response {
val original = chain.proceed(chain.request())
if (!original.headers("Set-Cookie").isEmpty()) {
val cookies = HashSet<String>()
for (header in original.headers("Set-Cookie")) {
cookies.add(header)
}
Log.v("OkHttp", "receiveCookieInterceptor, cokies: $cookies")
context.saveCookies(cookies)
}
return original
}
}
/**
* Utility function to force use only the two languages
*/
fun getLocale() = when(Locale.getDefault().language) {
"ru" -> "ru"
else -> "en"
}
/**
* Custom cookie handler helper functions
*/
const val cPrefCookies = "prefDateCookies"
fun Context.saveCookies(cookies: HashSet<String>?) {
getPreferences().run {
edit()
.apply {
if (cookies == null) remove(cPrefCookies)
else putStringSet(cPrefCookies, cookies)
}
.apply()
}
}
fun Context.getCookies(): HashSet<String> {
return getPreferences()
.getStringSet(cPrefCookies, HashSet()) as HashSet
}
fun Context.getPreferences(): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment