aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2019-01-05 00:26:37 -0500
committerGitHub <noreply@github.com>2019-01-05 00:26:37 -0500
commit5c89202f74f68ee6f273296014b5fff837520246 (patch)
tree08245d02eb04045ec2c5d475ce6db4efe481a412 /app/src/main/kotlin/com
parent8c77e02e89dfec7bff04a397dfc82613ccd1242a (diff)
parent635bdddebbc52ec67cfb157830c3fc8b32f9a6e7 (diff)
downloadfrost-5c89202f74f68ee6f273296014b5fff837520246.tar.gz
frost-5c89202f74f68ee6f273296014b5fff837520246.tar.bz2
frost-5c89202f74f68ee6f273296014b5fff837520246.zip
Merge pull request #1313 from AllanWang/enhancement/deferred
Enhancement/deferred
Diffstat (limited to 'app/src/main/kotlin/com')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt12
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt38
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt59
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt25
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt37
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt40
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt75
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt13
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt30
15 files changed, 127 insertions, 217 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
index 20ad46d9..5b62afad 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
@@ -44,9 +44,6 @@ import com.raizlabs.android.dbflow.config.DatabaseConfig
import com.raizlabs.android.dbflow.config.FlowConfig
import com.raizlabs.android.dbflow.config.FlowManager
import com.raizlabs.android.dbflow.runtime.ContentResolverNotifier
-import io.reactivex.exceptions.UndeliverableException
-import io.reactivex.plugins.RxJavaPlugins
-import java.net.SocketTimeoutException
import java.util.Random
import kotlin.reflect.KClass
@@ -135,15 +132,6 @@ class FrostApp : Application() {
L.d { "Activity ${activity.localClassName} created" }
}
})
-
- RxJavaPlugins.setErrorHandler {
- when (it) {
- is SocketTimeoutException, is UndeliverableException ->
- L.e { "RxJava common error ${it.message}" }
- else ->
- L.e(it) { "RxJava error" }
- }
- }
}
private fun initBugsnag() {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
index aa6a0130..7622ff5c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
@@ -73,8 +73,8 @@ class StartActivity : KauBaseActivity() {
loadFbCookiesSync()
})
L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
- L._d { "Cookies: ${cookies.joinToString("\t")}" }
- loadAssets()
+ L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieModel::toSensitiveString)}" }
+ loadAssets()
when {
cookies.isEmpty() -> launchNewTask<LoginActivity>()
// Has cookies but no selected account
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
index 283477d7..8d849bff 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
@@ -66,9 +66,7 @@ class AboutActivity : AboutActivityBase(null, {
val include = arrayOf(
"AboutLibraries",
"AndroidIconics",
- "androidin_appbillingv3",
"androidslidinguppanel",
- "Crashlytics",
"dbflow",
"fastadapter",
"glide",
@@ -77,7 +75,6 @@ class AboutActivity : AboutActivityBase(null, {
"kotterknife",
"materialdialogs",
"materialdrawer",
- "rxjava",
"subsamplingscaleimageview"
)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
index 5965e5cf..e514fa14 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
@@ -45,44 +45,6 @@ abstract class BaseActivity : KauBaseActivity() {
if (this !is WebOverlayActivityBase) setFrostTheme()
}
- //
-// private var networkDisposable: Disposable? = null
-// private var networkConsumer: ((Connectivity) -> Unit)? = null
-//
-// fun setNetworkObserver(consumer: (connectivity: Connectivity) -> Unit) {
-// this.networkConsumer = consumer
-// }
-//
-// private fun observeNetworkConnectivity() {
-// val consumer = networkConsumer ?: return
-// networkDisposable = ReactiveNetwork.observeNetworkConnectivity(applicationContext)
-// .subscribeOn(Schedulers.io())
-// .observeOn(AndroidSchedulers.mainThread())
-// .subscribe { connectivity: Connectivity ->
-// connectivity.apply {
-// L.d{"Network connectivity changed: isAvailable: $isAvailable isRoaming: $isRoaming"}
-// consumer(connectivity)
-// }
-// }
-// }
-//
-// private fun disposeNetworkConnectivity() {
-// if (networkDisposable?.isDisposed == false)
-// networkDisposable?.dispose()
-// networkDisposable = null
-// }
-//
-// override fun onResume() {
-// super.onResume()
-//// disposeNetworkConnectivity()
-//// observeNetworkConnectivity()
-// }
-//
-// override fun onPause() {
-// super.onPause()
-//// disposeNetworkConnectivity()
-// }
-
override fun onStop() {
if (this is VideoViewHolder) videoOnStop()
super.onStop()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt
index 6257e6f1..a1b41830 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt
@@ -22,6 +22,7 @@ import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import ca.allanwang.kau.internal.KauBaseActivity
+import ca.allanwang.kau.utils.launchMain
import ca.allanwang.kau.utils.setIcon
import ca.allanwang.kau.utils.visible
import com.mikepenz.google_material_typeface_library.GoogleMaterial
@@ -32,12 +33,12 @@ import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.createFreshDir
import com.pitchedapps.frost.utils.setFrostColors
-import io.reactivex.Single
-import io.reactivex.android.schedulers.AndroidSchedulers
-import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_debug.*
import kotlinx.android.synthetic.main.view_main_fab.*
+import kotlinx.coroutines.CoroutineExceptionHandler
import java.io.File
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
/**
* Created by Allan Wang on 05/01/18.
@@ -74,36 +75,32 @@ class DebugActivity : KauBaseActivity() {
fab.setOnClickListener { _ ->
fab.hide()
- val parent = baseDir(this)
- parent.createFreshDir()
- val rxScreenshot = Single.fromCallable {
- debug_webview.getScreenshot(File(parent, "screenshot.png"))
- }.subscribeOn(Schedulers.io())
- val rxBody = Single.create<String> { emitter ->
- debug_webview.evaluateJavascript(JsActions.RETURN_BODY.function) {
- emitter.onSuccess(it)
- }
- }.subscribeOn(AndroidSchedulers.mainThread())
- Single.zip(listOf(rxScreenshot, rxBody)) {
- val screenshot = it[0] == true
- val body = it[1] as? String
- screenshot to body
- }.observeOn(AndroidSchedulers.mainThread())
- .subscribe { (screenshot, body), err ->
- if (err != null) {
- L.e { "DebugActivity error ${err.message}" }
- setResult(Activity.RESULT_CANCELED)
- finish()
- return@subscribe
+ val errorHandler = CoroutineExceptionHandler { _, throwable ->
+ L.e { "DebugActivity error ${throwable.message}" }
+ setResult(Activity.RESULT_CANCELED)
+ finish()
+ }
+
+ launchMain(errorHandler) {
+ val parent = baseDir(this@DebugActivity)
+ parent.createFreshDir()
+
+ val body: String? = suspendCoroutine { cont ->
+ debug_webview.evaluateJavascript(JsActions.RETURN_BODY.function) {
+ cont.resume(it)
}
- val intent = Intent()
- intent.putExtra(RESULT_URL, debug_webview.url)
- intent.putExtra(RESULT_SCREENSHOT, screenshot)
- if (body != null)
- intent.putExtra(RESULT_BODY, body)
- setResult(Activity.RESULT_OK, intent)
- finish()
}
+
+ val hasScreenshot: Boolean = debug_webview.getScreenshot(File(parent, "screenshot.png"))
+
+ val intent = Intent()
+ intent.putExtra(RESULT_URL, debug_webview.url)
+ intent.putExtra(RESULT_SCREENSHOT, hasScreenshot)
+ if (body != null)
+ intent.putExtra(RESULT_BODY, body)
+ setResult(Activity.RESULT_OK, intent)
+ finish()
+ }
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
index 150d29f4..f3eb8fe6 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
@@ -33,9 +33,10 @@ import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.pitchedapps.frost.R
import com.pitchedapps.frost.dbflow.CookieModel
-import com.pitchedapps.frost.dbflow.fetchUsername
import com.pitchedapps.frost.dbflow.loadFbCookiesSuspend
+import com.pitchedapps.frost.dbflow.saveFbCookie
import com.pitchedapps.frost.facebook.FbCookie
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.profilePictureUrl
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.GlideApp
@@ -43,6 +44,7 @@ import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Showcase
import com.pitchedapps.frost.utils.frostEvent
+import com.pitchedapps.frost.utils.frostJsoup
import com.pitchedapps.frost.utils.launchNewTask
import com.pitchedapps.frost.utils.logFrostEvent
import com.pitchedapps.frost.utils.setFrostColors
@@ -55,6 +57,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import java.net.UnknownHostException
import kotlin.coroutines.resume
/**
@@ -88,7 +92,7 @@ class LoginActivity : BaseActivity() {
}
}
launch {
- val cookie = web.loadLogin { refresh(it != 100) }
+ val cookie = web.loadLogin { refresh(it != 100) }.await()
L.d { "Login found" }
FbCookie.save(cookie.id)
webFadeOut()
@@ -168,11 +172,22 @@ class LoginActivity : BaseActivity() {
}
private suspend fun loadUsername(cookie: CookieModel): String = withContext(Dispatchers.IO) {
- suspendCancellableCoroutine<String> { cont ->
- cookie.fetchUsername {
- cont.resume(it)
+ val result: String = try {
+ withTimeout(5000) {
+ frostJsoup(cookie.cookie, FbItem.PROFILE.url).title()
}
+ } catch (e: Exception) {
+ if (e !is UnknownHostException)
+ e.logFrostEvent("Fetch username failed")
+ ""
}
+
+ if (cookie.name?.isNotBlank() == false && result != cookie.name) {
+ cookie.name = result
+ saveFbCookie(cookie)
+ }
+
+ cookie.name ?: ""
}
override fun backConsumer(): Boolean {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
index e8fb5c54..67953144 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
@@ -17,11 +17,7 @@
package com.pitchedapps.frost.dbflow
import android.os.Parcelable
-import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
-import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.utils.L
-import com.pitchedapps.frost.utils.frostJsoup
-import com.pitchedapps.frost.utils.logFrostEvent
import com.raizlabs.android.dbflow.annotation.ConflictAction
import com.raizlabs.android.dbflow.annotation.Database
import com.raizlabs.android.dbflow.annotation.PrimaryKey
@@ -34,12 +30,9 @@ import com.raizlabs.android.dbflow.kotlinextensions.save
import com.raizlabs.android.dbflow.kotlinextensions.select
import com.raizlabs.android.dbflow.kotlinextensions.where
import com.raizlabs.android.dbflow.structure.BaseModel
-import io.reactivex.disposables.Disposable
-import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import java.net.UnknownHostException
/**
* Created by Allan Wang on 2017-05-30.
@@ -54,7 +47,12 @@ object CookiesDb {
@Parcelize
@Table(database = CookiesDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, var cookie: String? = null) :
- BaseModel(), Parcelable
+ BaseModel(), Parcelable {
+
+ override fun toString(): String = "CookieModel(${hashCode()})"
+
+ fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)"
+}
fun loadFbCookie(id: Long): CookieModel? =
(select from CookieModel::class where (CookieModel_Table.id eq id)).querySingle()
@@ -92,26 +90,3 @@ fun removeCookie(id: Long) {
L._d { id }
}
}
-
-inline fun CookieModel.fetchUsername(crossinline callback: (String) -> Unit): Disposable =
- ReactiveNetwork.checkInternetConnectivity().subscribeOn(Schedulers.io()).subscribe { yes, _ ->
- if (!yes) return@subscribe callback("")
- var result = ""
- try {
- result = frostJsoup(cookie, FbItem.PROFILE.url).title()
- L.d { "Fetch username found" }
- } catch (e: Exception) {
- if (e !is UnknownHostException)
- e.logFrostEvent("Fetch username failed")
- } finally {
- if (result.isBlank() && (name?.isNotBlank() == true)) {
- callback(name!!)
- return@subscribe
- }
- if (name != result) {
- name = result
- saveFbCookie(this@fetchUsername)
- }
- callback(result)
- }
- }
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
index 53ea6e67..b49fd970 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
@@ -26,10 +26,7 @@ import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.kotlin.Flyweight
import com.pitchedapps.frost.utils.L
-import io.reactivex.Single
-import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.runBlocking
import okhttp3.Call
import okhttp3.FormBody
import okhttp3.OkHttpClient
@@ -40,27 +37,11 @@ import org.apache.commons.text.StringEscapeUtils
/**
* Created by Allan Wang on 21/12/17.
*/
-val fbAuth = Flyweight<String, RequestAuth>(GlobalScope, 100, 3600000 /* an hour */) {
+val fbAuth = Flyweight<String, RequestAuth>(GlobalScope, 3600000 /* an hour */) {
it.getAuth()
}
/**
- * Synchronously fetch [RequestAuth] from cookie
- * [action] will only be called if a valid auth is found.
- * Otherwise, [fail] will be called
- */
-fun String?.fbRequest(fail: () -> Unit = {}, action: RequestAuth.() -> Unit) {
- if (this == null) return fail()
- try {
- val auth = runBlocking { fbAuth.fetch(this@fbRequest) }
- auth.action()
- } catch (e: Exception) {
- L.e { "Failed auth for ${hashCode()}: ${e.message}" }
- fail()
- }
-}
-
-/**
* Underlying container for all fb requests
*/
data class RequestAuth(
@@ -136,7 +117,11 @@ fun String.getAuth(): RequestAuth {
.call()
call.execute().body()?.charStream()?.useLines { lines ->
lines.forEach {
- val text = StringEscapeUtils.unescapeEcmaScript(it)
+ val text = try {
+ StringEscapeUtils.unescapeEcmaScript(it)
+ } catch (ignore: Exception) {
+ return@forEach
+ }
val fb_dtsg = FB_DTSG_MATCHER.find(text)[1]
if (fb_dtsg != null) {
auth = auth.copy(fb_dtsg = fb_dtsg)
@@ -154,19 +139,6 @@ fun String.getAuth(): RequestAuth {
return auth
}
-inline fun <T, reified R : Any, O> Array<T>.zip(
- crossinline mapper: (List<R>) -> O,
- crossinline caller: (T) -> R
-): Single<O> {
- if (isEmpty())
- return Single.just(mapper(emptyList()))
- val singles = map { Single.fromCallable { caller(it) }.subscribeOn(Schedulers.io()) }
- return Single.zip(singles) {
- val results = it.mapNotNull { it as? R }
- mapper(results)
- }
-}
-
/**
* Execute the call and attempt to check validity
* Valid = not blank & no "error" instance
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt
index ee18e15e..36dff6ff 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt
@@ -133,7 +133,7 @@ class HdImageFetcher(private val model: HdImageMaybe) : DataFetcher<InputStream>
val result: Result<InputStream?> = runCatching {
runBlocking {
withTimeout(20000L) {
- val auth = fbAuth.fetch(model.cookie)
+ val auth = fbAuth.fetch(model.cookie).await()
if (cancelled) throw RuntimeException("Cancelled")
val url = auth.getFullSizedImage(model.id).invoke() ?: throw RuntimeException("Null url")
if (cancelled) throw RuntimeException("Cancelled")
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt
index f7ed9937..d9d518b1 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt
@@ -74,7 +74,7 @@ class MenuFragment : GenericRecyclerFragment<MenuItemData, IItem<*, *>>() {
override suspend fun reloadImpl(progress: (Int) -> Unit): List<MenuItemData>? = withContext(Dispatchers.IO) {
val cookie = FbCookie.webCookie ?: return@withContext null
progress(10)
- val auth = fbAuth.fetch(cookie)
+ val auth = fbAuth.fetch(cookie).await()
progress(30)
val data = auth.getMenuData().invoke() ?: return@withContext null
if (data.data.isEmpty()) return@withContext null
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt
index 0c471c63..870e2ccd 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt
@@ -27,7 +27,6 @@ import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import com.pitchedapps.frost.facebook.FbCookie
-import com.pitchedapps.frost.utils.L
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
@@ -70,7 +69,6 @@ class FrostCookieInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val origRequest = chain.request()
val cookie = FbCookie.webCookie ?: return chain.proceed(origRequest)
- L.v { "Add cookie to req $cookie" }
val request = origRequest.newBuilder().addHeader("Cookie", cookie).build()
return chain.proceed(request)
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt
index 7ac80147..914ce151 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt
@@ -17,6 +17,7 @@
package com.pitchedapps.frost.kotlin
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -25,9 +26,6 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import java.util.concurrent.ConcurrentHashMap
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
/**
* Flyweight to keep track of values so long as they are valid.
@@ -38,19 +36,16 @@ import kotlin.coroutines.suspendCoroutine
*/
class Flyweight<K, V>(
val scope: CoroutineScope,
- capacity: Int,
val maxAge: Long,
private val fetcher: suspend (K) -> V
) {
// Receives a key and a pending request
- private val actionChannel = Channel<Pair<K, Continuation<V>>>(capacity)
+ private val actionChannel = Channel<Pair<K, CompletableDeferred<V>>>(Channel.UNLIMITED)
// Receives a key to invalidate the associated value
- private val invalidatorChannel = Channel<K>(capacity)
- // Receives a key to fetch the value
- private val requesterChannel = Channel<K>(capacity)
+ private val invalidatorChannel = Channel<K>(Channel.UNLIMITED)
// Receives a key and the resulting value
- private val receiverChannel = Channel<Pair<K, Result<V>>>(capacity)
+ private val receiverChannel = Channel<Pair<K, Result<V>>>(Channel.UNLIMITED)
// Keeps track of keys and associated update times
private val conditionMap: MutableMap<K, Long> = mutableMapOf()
@@ -58,10 +53,17 @@ class Flyweight<K, V>(
private val resultMap: MutableMap<K, Result<V>> = mutableMapOf()
// Keeps track of unfulfilled actions
// Note that the explicit type is very important here. See https://youtrack.jetbrains.net/issue/KT-18053
- private val pendingMap: MutableMap<K, MutableList<Continuation<V>>> = ConcurrentHashMap()
+ private val pendingMap: MutableMap<K, MutableList<CompletableDeferred<V>>> = ConcurrentHashMap()
private val job: Job
+ private fun CompletableDeferred<V>.completeWith(result: Result<V>) {
+ if (result.isSuccess)
+ complete(result.getOrNull()!!)
+ else
+ completeExceptionally(result.exceptionOrNull()!!)
+ }
+
init {
job = scope.launch(Dispatchers.IO) {
launch {
@@ -70,17 +72,17 @@ class Flyweight<K, V>(
/*
* New request received. Continuation should be fulfilled eventually
*/
- actionChannel.onReceive { (key, continuation) ->
+ actionChannel.onReceive { (key, completable) ->
val lastUpdate = conditionMap[key]
val lastResult = resultMap[key]
// Valid value, retrieved within acceptable time
if (lastResult != null && lastUpdate != null && System.currentTimeMillis() - lastUpdate < maxAge) {
- continuation.resumeWith(lastResult)
+ completable.completeWith(lastResult)
} else {
val valueRequestPending = key in pendingMap
- pendingMap.getOrPut(key) { mutableListOf() }.add(continuation)
+ pendingMap.getOrPut(key) { mutableListOf() }.add(completable)
if (!valueRequestPending)
- requesterChannel.send(key)
+ fulfill(key)
}
}
/*
@@ -97,7 +99,7 @@ class Flyweight<K, V>(
resultMap.remove(key)
if (pendingMap[key]?.isNotEmpty() == true)
// Refetch value for pending requests
- requesterChannel.send(key)
+ fulfill(key)
}
/*
* Value request fulfilled. Should now fulfill pending requests
@@ -106,33 +108,41 @@ class Flyweight<K, V>(
conditionMap[key] = System.currentTimeMillis()
resultMap[key] = result
pendingMap.remove(key)?.forEach {
- it.resumeWith(result)
+ it.completeWith(result)
}
}
}
}
}
- launch {
- /*
- * Value request received. Should fetch new value using supplied fetcher
- */
- for (key in requesterChannel) {
- val result = runCatching {
- fetcher(key)
- }
- receiverChannel.send(key to result)
- }
- }
}
}
- suspend fun fetch(key: K): V = suspendCoroutine {
- if (!job.isActive) it.resumeWithException(IllegalStateException("Flyweight is not active"))
- else scope.launch {
- actionChannel.send(key to it)
+ /*
+ * Value request received. Should fetch new value using supplied fetcher
+ */
+ private fun fulfill(key: K) {
+ scope.launch {
+ val result = runCatching {
+ fetcher(key)
+ }
+ receiverChannel.send(key to result)
}
}
+ /**
+ * Queues the request, and returns a completable once it is sent to a channel.
+ * The fetcher will only be suspended if the channels are full.
+ *
+ * Note that if the job is already inactive, a cancellation exception will be thrown.
+ * The message may default to the message for all completables under a cancelled job
+ */
+ fun fetch(key: K): CompletableDeferred<V> {
+ val completable = CompletableDeferred<V>(job)
+ if (!job.isActive) completable.completeExceptionally(CancellationException("Flyweight is not active"))
+ else actionChannel.offer(key to completable)
+ return completable
+ }
+
suspend fun invalidate(key: K) {
invalidatorChannel.send(key)
}
@@ -141,12 +151,11 @@ class Flyweight<K, V>(
job.cancel()
if (pendingMap.isNotEmpty()) {
val error = CancellationException("Flyweight cancelled")
- pendingMap.values.flatten().forEach { it.resumeWithException(error) }
+ pendingMap.values.flatten().forEach { it.completeExceptionally(error) }
pendingMap.clear()
}
actionChannel.close()
invalidatorChannel.close()
- requesterChannel.close()
receiverChannel.close()
conditionMap.clear()
resultMap.clear()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt
index c88f3946..a8ecb27d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt
@@ -179,7 +179,7 @@ class FrostRequestService : BaseJobService() {
}
launch(Dispatchers.IO) {
try {
- val auth = fbAuth.fetch(cookie)
+ val auth = fbAuth.fetch(cookie).await()
command.invoke(auth, bundle)
L.d {
"Finished frost service for ${command.name} in ${System.currentTimeMillis() - startTime} ms"
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
index 22668309..d2b53ab5 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
@@ -23,7 +23,6 @@ import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import android.webkit.WebView
-import androidx.annotation.WorkerThread
import ca.allanwang.kau.utils.withAlpha
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.injectors.CssAssets
@@ -33,6 +32,8 @@ import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.createFreshFile
import com.pitchedapps.frost.utils.isFacebookUrl
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import java.io.File
/**
@@ -61,14 +62,16 @@ class DebugWebView @JvmOverloads constructor(
isDrawingCacheEnabled = true
}
- @WorkerThread
- fun getScreenshot(output: File): Boolean {
+ /**
+ * Fetches a screenshot of the current webview, returning true if successful, false otherwise.
+ */
+ suspend fun getScreenshot(output: File): Boolean = withContext(Dispatchers.IO) {
if (!output.createFreshFile()) {
L.e { "Failed to create ${output.absolutePath} for debug screenshot" }
- return false
+ return@withContext false
}
- return try {
+ try {
output.outputStream().use {
drawingCache.compress(Bitmap.CompressFormat.PNG, 100, it)
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
index 8132382a..c21ce93b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
@@ -28,7 +28,7 @@ import android.webkit.WebResourceRequest
import android.webkit.WebView
import ca.allanwang.kau.utils.fadeIn
import ca.allanwang.kau.utils.isVisible
-import ca.allanwang.kau.utils.withMainContext
+import ca.allanwang.kau.utils.launchMain
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.FB_LOGIN_URL
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
@@ -40,10 +40,8 @@ import com.pitchedapps.frost.injectors.jsInject
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.isFacebookUrl
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlin.coroutines.resume
/**
* Created by Allan Wang on 2017-05-29.
@@ -54,7 +52,7 @@ class LoginWebView @JvmOverloads constructor(
defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr) {
- private lateinit var loginCallback: (CookieModel) -> Unit
+ private val completable: CompletableDeferred<CookieModel> = CompletableDeferred()
private lateinit var progressCallback: (Int) -> Unit
@SuppressLint("SetJavaScriptEnabled")
@@ -65,19 +63,15 @@ class LoginWebView @JvmOverloads constructor(
webChromeClient = LoginChromeClient()
}
- suspend fun loadLogin(progressCallback: (Int) -> Unit): CookieModel = withMainContext {
- coroutineScope {
- suspendCancellableCoroutine<CookieModel> { cont ->
- this@LoginWebView.progressCallback = progressCallback
- this@LoginWebView.loginCallback = { cont.resume(it) }
- L.d { "Begin loading login" }
- launch {
- FbCookie.reset()
- setupWebview()
- loadUrl(FB_LOGIN_URL)
- }
- }
+ suspend fun loadLogin(progressCallback: (Int) -> Unit): CompletableDeferred<CookieModel> = coroutineScope {
+ this@LoginWebView.progressCallback = progressCallback
+ L.d { "Begin loading login" }
+ launchMain {
+ FbCookie.reset()
+ setupWebview()
+ loadUrl(FB_LOGIN_URL)
}
+ completable
}
private inner class LoginClient : BaseWebViewClient() {
@@ -86,7 +80,7 @@ class LoginWebView @JvmOverloads constructor(
super.onPageFinished(view, url)
val cookieModel = checkForLogin(url)
if (cookieModel != null)
- loginCallback(cookieModel)
+ completable.complete(cookieModel)
if (!view.isVisible) view.fadeIn()
}