Fix network unsubscription crashes, refactor network methods

This commit is contained in:
len 2016-05-27 16:52:44 +02:00
parent 79bb207a8d
commit efd36388b0
14 changed files with 113 additions and 121 deletions

View File

@ -91,7 +91,6 @@ kapt {
dependencies {
final SUPPORT_LIBRARY_VERSION = '23.4.0'
final DAGGER_VERSION = '2.4'
final OKHTTP_VERSION = '3.2.0'
final RETROFIT_VERSION = '2.0.2'
final NUCLEUS_VERSION = '3.0.0'
final STORIO_VERSION = '1.8.0'
@ -118,7 +117,7 @@ dependencies {
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
// Network client
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp:3.3.0"
// REST
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
@ -141,7 +140,7 @@ dependencies {
compile 'com.jakewharton:disklrucache:2.0.2'
// Parse HTML
compile 'org.jsoup:jsoup:1.9.1'
compile 'org.jsoup:jsoup:1.9.2'
// Changelog
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
@ -176,9 +175,7 @@ dependencies {
compile 'eu.davidea:flexible-adapter:4.2.0'
compile 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e'
compile('com.github.afollestad.material-dialogs:core:0.8.5.5@aar') {
transitive = true
}
compile 'com.afollestad.material-dialogs:core:0.8.5.9'
// Tests
testCompile 'junit:junit:4.12'

View File

@ -28,7 +28,7 @@ class AppGlideModule : GlideModule {
override fun registerComponents(context: Context, glide: Glide) {
App.get(context).component.inject(this)
glide.register(GlideUrl::class.java, InputStream::class.java,
OkHttpUrlLoader.Factory(networkHelper.defaultClient))
OkHttpUrlLoader.Factory(networkHelper.client))
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
}
}

View File

@ -6,8 +6,8 @@ import android.util.Xml
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.*
@ -65,7 +65,8 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
override fun login(username: String, password: String): Observable<Boolean> {
createHeaders(username, password)
return networkService.request(get(getLoginUrl(), headers))
return networkService.request(GET(getLoginUrl(), headers))
.doOnNext { it.close() }
.map { it.code() == 200 }
}
@ -77,7 +78,7 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
}
fun search(query: String): Observable<List<MangaSync>> {
return networkService.request(get(getSearchUrl(query), headers))
return networkService.request(GET(getSearchUrl(query), headers))
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
@ -102,7 +103,7 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
// MAL doesn't support score with decimals
fun getList(): Observable<List<MangaSync>> {
return networkService.request(get(getListUrl(username), headers), networkService.forceCacheClient)
return networkService.request(GET(getListUrl(username), headers), networkService.forceCacheClient)
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
@ -130,7 +131,7 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
}
networkService.request(post(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
networkService.request(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
}
}
@ -144,7 +145,7 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
override fun add(manga: MangaSync): Observable<Response> {
return Observable.defer {
networkService.request(post(getAddUrl(manga), headers, getMangaPostPayload(manga)))
networkService.request(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
}
}

View File

@ -22,7 +22,7 @@ class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interc
// Check if we already solved a challenge
if (response.code() != 502 &&
cookies.get(response.request().url()).find { it.name() == "cf_clearance" } != null) {
cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
return response
}
@ -72,7 +72,7 @@ class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interc
.toString()
val referer = originalRequest.url().toString()
return get(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
} finally {
duktape.close()
}

View File

@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.*
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.subscriptions.Subscriptions
import timber.log.Timber
import java.io.File
import java.io.IOException
class NetworkHelper(context: Context) {
@ -16,74 +16,30 @@ class NetworkHelper(context: Context) {
private val cookieManager = PersistentCookieJar(context)
val defaultClient = OkHttpClient.Builder()
val client = OkHttpClient.Builder()
.cookieJar(cookieManager)
.cache(Cache(cacheDir, cacheSize))
.build()
val forceCacheClient = defaultClient.newBuilder()
.addNetworkInterceptor({ chain ->
val forceCacheClient = client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.header("Cache-Control", "max-age=600")
.build()
})
}
.build()
val cloudflareClient = defaultClient.newBuilder()
val cloudflareClient = client.newBuilder()
.addInterceptor(CloudflareInterceptor(cookies))
.build()
val cookies: PersistentCookieStore
get() = cookieManager.store
fun request(request: Request, client: OkHttpClient = defaultClient): Observable<Response> {
return Observable.create { subscriber ->
val call = client.newCall(request)
subscriber.add(Subscriptions.create {
call.cancel()
Timber.i("Cancel call on thread ${Thread.currentThread().id}")
})
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (!subscriber.isUnsubscribed) {
subscriber.add(Subscriptions.create {
response.body().close()
Timber.i("Close body on thread ${Thread.currentThread().id}")
})
subscriber.onNext(response)
Timber.i("Emit response on thread ${Thread.currentThread().id}")
subscriber.onCompleted()
}
}
override fun onFailure(call: Call, error: IOException) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error)
}
}
})
}
}
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
return Observable.fromCallable { requestBodyProgressBlocking(request, listener) }
}
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
val progressClient = defaultClient.newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
return progressClient.newCall(request).execute()
fun request(request: Request, client: OkHttpClient = this.client): Observable<Response> {
return client.newCall(request).asObservable()
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.data.network
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.subscriptions.Subscriptions
import java.io.IOException
fun Call.asObservable(): Observable<Response> {
return Observable.create { subscriber ->
subscriber.add(Subscriptions.create { cancel() })
try {
val response = execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
}
} catch (error: IOException) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error)
}
}
}
}
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
return progressClient.newCall(request)
}

View File

@ -70,6 +70,6 @@ class PersistentCookieStore(context: Context) {
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
}
fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
}

View File

@ -7,8 +7,7 @@ private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).b
private val DEFAULT_HEADERS = Headers.Builder().build()
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
@JvmOverloads
fun get(url: String,
fun GET(url: String,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
@ -19,8 +18,7 @@ fun get(url: String,
.build()
}
@JvmOverloads
fun post(url: String,
fun POST(url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {

View File

@ -5,8 +5,10 @@ import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.Source
@ -57,7 +59,7 @@ abstract class OnlineSource(context: Context) : Source {
* Default network client for doing requests.
*/
open val client: OkHttpClient
get() = network.defaultClient
get() = network.client
init {
// Inject dependencies.
@ -114,7 +116,7 @@ abstract class OnlineSource(context: Context) : Source {
if (page.page == 1) {
page.url = popularMangaInitialUrl()
}
return get(page.url, headers)
return GET(page.url, headers)
}
/**
@ -159,7 +161,7 @@ abstract class OnlineSource(context: Context) : Source {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
}
return get(page.url, headers)
return GET(page.url, headers)
}
/**
@ -201,7 +203,7 @@ abstract class OnlineSource(context: Context) : Source {
* @param manga the manga to be updated.
*/
open protected fun mangaDetailsRequest(manga: Manga): Request {
return get(baseUrl + manga.url, headers)
return GET(baseUrl + manga.url, headers)
}
/**
@ -236,7 +238,7 @@ abstract class OnlineSource(context: Context) : Source {
* @param manga the manga to look for chapters.
*/
open protected fun chapterListRequest(manga: Manga): Request {
return get(baseUrl + manga.url, headers)
return GET(baseUrl + manga.url, headers)
}
/**
@ -281,7 +283,7 @@ abstract class OnlineSource(context: Context) : Source {
* @param chapter the chapter whose page list has to be fetched
*/
open protected fun pageListRequest(chapter: Chapter): Request {
return get(baseUrl + chapter.url, headers)
return GET(baseUrl + chapter.url, headers)
}
/**
@ -321,7 +323,7 @@ abstract class OnlineSource(context: Context) : Source {
* @param page the chapter whose page list has to be fetched
*/
open protected fun imageUrlRequest(page: Page): Request {
return get(page.url, headers)
return GET(page.url, headers)
}
/**
@ -347,11 +349,12 @@ abstract class OnlineSource(context: Context) : Source {
*
* @param page the page whose source image has to be downloaded.
*/
fun imageResponse(page: Page): Observable<Response> = network
.requestBodyProgress(imageRequest(page), page)
fun imageResponse(page: Page): Observable<Response> = client
.newCallWithProgress(imageRequest(page), page)
.asObservable()
.doOnNext {
if (!it.isSuccessful) {
it.body().close()
it.close()
throw RuntimeException("Not a valid response")
}
}
@ -363,7 +366,7 @@ abstract class OnlineSource(context: Context) : Source {
* @param page the chapter whose page list has to be fetched
*/
open protected fun imageRequest(page: Page): Request {
return get(page.imageUrl, headers)
return GET(page.imageUrl, headers)
}
/**

View File

@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.data.source.online
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.getLanguages
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
@ -32,7 +32,7 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
override val client = when(map.client) {
"cloudflare" -> network.cloudflareClient
else -> network.defaultClient
else -> network.client
}
override val id = map.id.let {
@ -44,8 +44,8 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
page.url = popularMangaInitialUrl()
}
return when (map.popular.method?.toLowerCase()) {
"post" -> post(page.url, headers, map.popular.createForm())
else -> get(page.url, headers)
"post" -> POST(page.url, headers, map.popular.createForm())
else -> GET(page.url, headers)
}
}
@ -74,8 +74,8 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
page.url = searchMangaInitialUrl(query)
}
return when (map.search.method?.toLowerCase()) {
"post" -> post(page.url, headers, map.search.createForm())
else -> get(page.url, headers)
"post" -> POST(page.url, headers, map.search.createForm())
else -> GET(page.url, headers)
}
}

View File

@ -5,8 +5,8 @@ import android.net.Uri
import android.text.Html
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.MangasPage
@ -20,7 +20,6 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.net.URI
import java.net.URISyntaxException
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
@ -58,12 +57,12 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun mangaDetailsRequest(manga: Manga): Request {
val mangaId = manga.url.substringAfterLast("r")
return get("$baseUrl/comic_pop?id=$mangaId", headers)
return GET("$baseUrl/comic_pop?id=$mangaId", headers)
}
override fun pageListRequest(chapter: Chapter): Request {
val id = chapter.url.substringAfterLast("#")
return get("$baseUrl/areader?id=$id&p=1", headers)
return GET("$baseUrl/areader?id=$id&p=1", headers)
}
override fun imageUrlRequest(page: Page): Request {
@ -71,7 +70,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
val start = pageUrl.indexOf("#") + 1
val end = pageUrl.indexOf("_", start)
val id = pageUrl.substring(start, end)
return get("$baseUrl/areader?id=$id&p=${pageUrl.substring(end+1)}", headers)
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end+1)}", headers)
}
override fun popularMangaParse(response: Response, page: MangasPage) {
@ -216,9 +215,8 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
}
override fun login(username: String, password: String) =
network.request(get("$baseUrl/forums/index.php?app=core&module=global&section=login", headers))
.map { it.body().string() }
.flatMap { doLogin(it, username, password) }
network.request(GET("$baseUrl/forums/index.php?app=core&module=global&section=login", headers))
.flatMap { doLogin(it.body().string(), username, password) }
.map { isAuthenticationSuccessful(it) }
private fun doLogin(response: String, username: String, password: String): Observable<Response> {
@ -235,7 +233,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
add("rememberMe", "1")
}.build()
return network.request(post(url, headers, payload))
return network.request(POST(url, headers, payload))
}
override fun isLoginRequired() = true
@ -244,12 +242,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
response.priorResponse() != null && response.priorResponse().code() == 302
override fun isLogged(): Boolean {
try {
return network.cookies.get(URI(baseUrl)).find { it.name() == "pass_hash" } != null
} catch (e: URISyntaxException) {
// Ignore
}
return false
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
}
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> {

View File

@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.MangasPage
@ -54,7 +54,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
add("genres", "")
}.build()
return post(page.url, headers, form)
return POST(page.url, headers, form)
}
override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch"
@ -95,7 +95,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
} ?: 0
}
override fun pageListRequest(chapter: Chapter) = post(baseUrl + chapter.url, headers)
override fun pageListRequest(chapter: Chapter) = POST(baseUrl + chapter.url, headers)
override fun pageListParse(response: Response, pages: MutableList<Page>) {
//language=RegExp
@ -111,7 +111,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
// Not used
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
override fun imageUrlRequest(page: Page) = get(page.url)
override fun imageUrlRequest(page: Page) = GET(page.url)
override fun imageUrlParse(document: Document) = ""

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.model.MangasPage
@ -47,7 +47,7 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
var builder = okhttp3.FormBody.Builder()
builder.add("query", query)
return post(page.url, headers, builder.build())
return POST(page.url, headers, builder.build())
}
override fun searchMangaSelector() = "div.content-list > div.style-list > div.box"

View File

@ -11,9 +11,10 @@ import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.ProgressListener
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.saveTo
import timber.log.Timber
@ -100,12 +101,14 @@ class UpdateDownloader(private val context: Context) :
try {
// Make the request and download the file
val response = network.requestBodyProgressBlocking(get(result.url), progressListener)
val response = network.client.newCallWithProgress(GET(result.url), progressListener).execute()
if (response.isSuccessful) {
response.body().source().saveTo(apkFile)
// Set download successful
result.successful = true
} else {
response.close()
}
} catch (e: Exception) {
Timber.e(e, e.message)