diff options
Diffstat (limited to 'app/src/main/kotlin')
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() } |