1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-12 14:03:00 +01:00

Merge pull request #1313 from AllanWang/enhancement/deferred

Enhancement/deferred
This commit is contained in:
Allan Wang 2019-01-05 00:26:37 -05:00 committed by GitHub
commit 5c89202f74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 184 additions and 343 deletions

View File

@ -204,7 +204,7 @@ dependencies {
// androidTestImplementation "io.mockk:mockk:${MOCKK}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${COROUTINES}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${kau.coroutines}"
implementation "org.apache.commons:commons-text:${COMMONS_TEXT}"
@ -251,12 +251,6 @@ dependencies {
implementation "com.sothree.slidinguppanel:library:${SLIDING_PANEL}"
//Reactive Libs
implementation "io.reactivex.rxjava2:rxjava:${RX_JAVA}"
implementation "io.reactivex.rxjava2:rxkotlin:${RX_KOTLIN}"
implementation "io.reactivex.rxjava2:rxandroid:${RX_ANDROID}"
implementation "com.github.pwittchen:reactivenetwork-rx2:${RX_NETWORK}"
}
// Validates code and generates apk

View File

@ -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() {

View File

@ -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

View File

@ -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"
)

View File

@ -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()

View File

@ -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()
}
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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,26 +37,10 @@ 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
*/
@ -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

View File

@ -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")

View File

@ -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

View File

@ -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)
}

View File

@ -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()

View File

@ -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"

View File

@ -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)
}

View File

@ -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()
}

View File

@ -1,48 +0,0 @@
/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost
import com.pitchedapps.frost.facebook.requests.zip
import org.junit.Test
import kotlin.test.assertTrue
/**
* Created by Allan Wang on 2017-06-14.
*/
class MiscTest {
/**
* Spin off 15 threads
* Pause each for 1 - 2s
* Ensure that total zipped process does not take over 5s
*/
@Test
fun zip() {
val now = System.currentTimeMillis()
val base = 1
val data: LongArray = (0..15).map { Math.random() + base }
.toTypedArray().zip(List<Long>::toLongArray) {
Thread.sleep((it * 1000).toLong())
System.currentTimeMillis() - now
}.blockingGet()
println(data.contentToString())
assertTrue(
data.all { it >= base * 1000 && it < base * 1000 * 5 },
"zip did not seem to work on different threads"
)
}
}

View File

@ -84,7 +84,7 @@ class FbRequestTest {
val data = AUTH.getMenuData().invoke()
assertNotNull(data)
println(ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(data))
assertTrue(data!!.data.isNotEmpty())
assertTrue(data.data.isNotEmpty())
assertTrue(data.footer.hasContent, "Footer may be badly parsed")
val items = data.flatMapValid()
assertTrue(items.size > 15, "Something may be badly parsed")

View File

@ -22,13 +22,10 @@ import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.facebook.requests.RequestAuth
import com.pitchedapps.frost.facebook.requests.getAuth
import com.pitchedapps.frost.utils.frostJsoup
import io.reactivex.Completable
import org.junit.Assume
import org.junit.Test
import java.io.File
import java.io.FileInputStream
import java.util.Properties
import java.util.concurrent.TimeUnit
import kotlin.reflect.full.starProjectedType
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@ -97,34 +94,3 @@ fun Any.assertComponentsNotEmpty() {
fun <T : Comparable<T>> List<T>.assertDescending(tag: String) {
assertEquals(sortedDescending(), this, "$tag not sorted in descending order")
}
interface CompletableCallback {
fun onComplete()
fun onError(message: String)
}
inline fun concurrentTest(crossinline caller: (callback: CompletableCallback) -> Unit) {
val result = Completable.create { emitter ->
caller(object : CompletableCallback {
override fun onComplete() = emitter.onComplete()
override fun onError(message: String) = emitter.onError(Throwable(message))
})
}.blockingGet(5, TimeUnit.SECONDS)
if (result != null)
throw RuntimeException("Concurrent fail: ${result.message}")
}
class InternalTest {
@Test
fun concurrentTest() = try {
concurrentTest { result ->
Thread().run {
Thread.sleep(100)
result.onError("Intentional fail")
}
}
fail("Did not throw exception")
} catch (e: Exception) {
// pass
}
}

View File

@ -16,8 +16,8 @@
*/
package com.pitchedapps.frost.kotlin
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.rules.Timeout
@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.fail
@ -42,7 +43,7 @@ class FlyweightTest {
@BeforeTest
fun before() {
callCount = AtomicInteger(0)
flyweight = Flyweight(GlobalScope, 100, 200L) {
flyweight = Flyweight(GlobalScope, 200L) {
callCount.incrementAndGet()
when (it) {
LONG_RUNNING_KEY -> Thread.sleep(100000)
@ -54,7 +55,7 @@ class FlyweightTest {
@Test
fun basic() {
assertEquals(2, runBlocking { flyweight.fetch(1) }, "Invalid result")
assertEquals(2, runBlocking { flyweight.fetch(1).await() }, "Invalid result")
assertEquals(1, callCount.get(), "1 call expected")
}
@ -62,9 +63,7 @@ class FlyweightTest {
fun multipleWithOneKey() {
val results: List<Int> = runBlocking {
(0..1000).map {
flyweight.scope.async {
flyweight.fetch(1)
}
flyweight.fetch(1)
}.map { it.await() }
}
assertEquals(1, callCount.get(), "1 call expected")
@ -75,12 +74,12 @@ class FlyweightTest {
@Test
fun consecutiveReuse() {
runBlocking {
flyweight.fetch(1)
flyweight.fetch(1).await()
assertEquals(1, callCount.get(), "1 call expected")
flyweight.fetch(1)
flyweight.fetch(1).await()
assertEquals(1, callCount.get(), "Reuse expected")
Thread.sleep(300)
flyweight.fetch(1)
flyweight.fetch(1).await()
assertEquals(2, callCount.get(), "Refetch expected")
}
}
@ -88,10 +87,10 @@ class FlyweightTest {
@Test
fun invalidate() {
runBlocking {
flyweight.fetch(1)
flyweight.fetch(1).await()
assertEquals(1, callCount.get(), "1 call expected")
flyweight.invalidate(1)
flyweight.fetch(1)
flyweight.fetch(1).await()
assertEquals(2, callCount.get(), "New call expected")
}
}
@ -99,24 +98,19 @@ class FlyweightTest {
@Test
fun destroy() {
runBlocking {
val longRunningResult = async { flyweight.fetch(LONG_RUNNING_KEY) }
flyweight.fetch(1)
val longRunningResult = flyweight.fetch(LONG_RUNNING_KEY)
flyweight.fetch(1).await()
flyweight.cancel()
try {
flyweight.fetch(1)
flyweight.fetch(1).await()
fail("Flyweight should not be fulfilled after it is destroyed")
} catch (e: Exception) {
assertEquals("Flyweight is not active", e.message, "Incorrect error found on fetch after destruction")
} catch (ignore: CancellationException) {
}
try {
assertFalse(longRunningResult.isActive, "Long running result should no longer be active")
longRunningResult.await()
fail("Flyweight should have cancelled previously running requests")
} catch (e: Exception) {
assertEquals(
"Flyweight cancelled",
e.message,
"Incorrect error found on fetch cancelled by destruction"
)
} catch (ignore: CancellationException) {
}
}
}

View File

@ -18,6 +18,7 @@ package com.pitchedapps.frost.utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.BroadcastChannel
@ -31,6 +32,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@ -206,4 +208,39 @@ class CoroutineTest {
)
}
}
/**
* When using [uniqueOnly] for channels with limited capacity,
* the duplicates should not count towards the actual capacity
*/
@Ignore("Not yet working as unique only buffered removes the capacity limitation of the channel")
@Test
fun uniqueOnlyBuffer() {
val channel = Channel<Int>(3)
runBlocking {
val deferred = async {
listen(channel.uniqueOnly(GlobalScope)) {
// Throttle consumer
delay(50)
return@listen false
}
}
listOf(0, 1, 1, 1, 1, 1, 2, 2, 2).forEach {
delay(10)
channel.offer(it)
}
channel.close()
val data = deferred.await()
assertEquals(
listOf(0, 1, 2),
data,
"Unique receiver should not have two consecutive events that are equal"
)
}
}
}

View File

@ -14,7 +14,7 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro
APP_ID=Frost
APP_GROUP=com.pitchedapps
KAU=af43e82
KAU=72d6461
KOTLIN=1.3.11
# https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google
@ -23,14 +23,12 @@ ANDROID_GRADLE=3.2.1
# https://github.com/diffplug/spotless/blob/master/plugin-gradle/CHANGES.md
SPOTLESS=3.17.0
# https://github.com/Kotlin/kotlinx.coroutines/releases
COROUTINES=1.0.1
# https://github.com/bugsnag/bugsnag-android/releases
BUGSNAG=4.9.3
# https://github.com/bugsnag/bugsnag-android-gradle-plugin/releases
BUGSNAG_PLUGIN=3.6.0
# https://github.com/KeepSafe/dexcount-gradle-plugin/releases
DEX_PLUGIN=0.8.4
DEX_PLUGIN=0.8.5
# https://github.com/gladed/gradle-android-git-version/releases
GIT_PLUGIN=0.4.7
# https://mvnrepository.com/artifact/org.apache.commons/commons-text
@ -59,16 +57,6 @@ MATERIAL_DRAWER_KT=2.0.1
OKHTTP=3.12.1
# http://robolectric.org/getting-started/
ROBOELECTRIC=4.1
# https://github.com/ReactiveX/RxAndroid/releases
RX_ANDROID=2.1.0
# https://github.com/JakeWharton/RxBinding/releases
RX_BINDING=2.2.0
# https://github.com/ReactiveX/RxJava/releases
RX_JAVA=2.2.4
# https://github.com/ReactiveX/RxKotlin/releases
RX_KOTLIN=2.3.0
# https://github.com/pwittchen/ReactiveNetwork/releases
RX_NETWORK=2.1.0
# https://github.com/davemorrissey/subsampling-scale-image-view#quick-start
SCALE_IMAGE_VIEW=3.10.0
# https://github.com/umano/AndroidSlidingUpPanel#importing-the-library