aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2018-12-31 18:57:28 -0500
committerGitHub <noreply@github.com>2018-12-31 18:57:28 -0500
commit149c6be1bfd4bd84381757940fece1be7b9801aa (patch)
tree85fe10e3ee3ea34ad717f0d61975ca0119dd36d5
parent7661bbfc9b8f34bf9d92dc08a9fcd7cc6ec7cbb3 (diff)
downloadfrost-149c6be1bfd4bd84381757940fece1be7b9801aa.tar.gz
frost-149c6be1bfd4bd84381757940fece1be7b9801aa.tar.bz2
frost-149c6be1bfd4bd84381757940fece1be7b9801aa.zip
Enhancement/coroutines (#1273)
* Convert rest of fbcookie to suspended methods * Replace active checks with yield * Apply spotless * Switch cookie domain to exact url * Optimize imports and enable travis tests again * Update proguard rules * Remove unnecessary yield * Remove unused flyweight * Remove unused disposable and method * Use contexthelper instead of dispatcher main * Convert login activity to coroutines * Use kau helper methods for coroutines * Enhancement/offline site (#1288) * Begin conversion of offline site logic * Fix offline tests and add validation tests * Ignore cookie in jsoup if it is blank * Force load and zip to be in io * Use different zip files to fix tests * Log all test output * Do not log stdout * Allow test skip for fb offline
-rw-r--r--app/build.gradle20
-rw-r--r--app/proguard-rules.pro13
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt6
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/facebook/FbCookieTest.kt18
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt47
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt13
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt29
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt169
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt19
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt187
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt105
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Notifications.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt13
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt (renamed from app/src/main/kotlin/com/pitchedapps/frost/rx/Flyweight.kt)2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt101
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt51
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt21
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt27
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt14
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt24
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt225
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/kotlin/FlyweightTest.kt (renamed from app/src/test/kotlin/com/pitchedapps/frost/rx/FlyweightTest.kt)3
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt73
-rw-r--r--gradle.properties2
34 files changed, 641 insertions, 592 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 23ba05d7..76e96599 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -90,6 +90,8 @@ android {
}
+ def compilerArgs = ["-Xuse-experimental=kotlin.Experimental" /*, "-XXLanguage:+InlineClasses"*/]
+
buildTypes {
debug {
minifyEnabled false
@@ -101,7 +103,7 @@ android {
resValue "string", "frost_web", "Frost Web Debug"
ext.enableBugsnag = false
- kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental", "-XXLanguage:+InlineClasses"]
+ kotlinOptions.freeCompilerArgs += compilerArgs
}
releaseTest {
minifyEnabled true
@@ -138,9 +140,16 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
- testOptions {
- unitTests {
- includeAndroidResources = true
+ testOptions.unitTests {
+ includeAndroidResources = true
+ // Don't throw runtime exceptions for android calls that are not mocked
+ returnDefaultValues = true
+
+ // Always show the result of every unit test, even if it passes.
+ all {
+ testLogging {
+ events 'passed', 'skipped', 'failed', 'standardError'
+ }
}
}
@@ -163,7 +172,6 @@ dependencies {
androidTestImplementation kauDependency.espresso
androidTestImplementation kauDependency.testRules
androidTestImplementation kauDependency.testRunner
- androidTestImplementation "com.squareup.okhttp3:mockwebserver:${OKHTTP}"
testImplementation kauDependency.kotlinTest
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}"
@@ -232,9 +240,9 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:${OKHTTP}"
implementation "com.squareup.okhttp3:logging-interceptor:${OKHTTP}"
+ testImplementation "com.squareup.okhttp3:mockwebserver:${OKHTTP}"
androidTestImplementation "com.squareup.okhttp3:mockwebserver:${OKHTTP}"
-
implementation "co.zsmb:materialdrawer-kt:${MATERIAL_DRAWER_KT}"
implementation "com.bugsnag:bugsnag-android:${BUGSNAG}"
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 75abeece..c05edb53 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -8,11 +8,6 @@
# public static **[] values();
# public static ** valueOf(java.lang.String);
#}
-# Crashlytics
--keepattributes SourceFile,LineNumberTable
--keep public class * extends java.lang.Exception
--keep class com.crashlytics.** { *; }
--dontwarn com.crashlytics.**
# JavaScript Interface
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
@@ -20,8 +15,6 @@
-keepattributes JavascriptInterface
# Jsoup
-keeppackagenames org.jsoup.nodes
-# IAB
--keep class com.android.vending.billing.**
# Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.AppGlideModule
@@ -37,4 +30,10 @@
-keepnames class com.fasterxml.jackson.** { *; }
-keepclassmembers public final enum com.fasterxml.jackson.annotation.JsonAutoDetect$Visibility {
public static final com.fasterxml.jackson.annotation.JsonAutoDetect$Visibility *;
+}
+# Kotlin coroutines
+-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
+-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
+-keepclassmembernames class kotlinx.** {
+ volatile <fields>;
} \ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt
index 23f6dab9..d32b956e 100644
--- a/app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt
@@ -62,14 +62,14 @@ class ImageActivityTest {
}
private val mockServer: MockWebServer by lazy {
- val magentaImg = Buffer()
- magentaImg.writeAll(Okio.source(getResource("bayer-pattern.jpg")))
+ val img = Buffer()
+ img.writeAll(Okio.source(getResource("bayer-pattern.jpg")))
MockWebServer().apply {
setDispatcher(object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse =
when {
request.path.contains("text") -> MockResponse().setResponseCode(200).setBody("Valid mock text response")
- request.path.contains("image") -> MockResponse().setResponseCode(200).setBody(magentaImg)
+ request.path.contains("image") -> MockResponse().setResponseCode(200).setBody(img)
else -> MockResponse().setResponseCode(404).setBody("Error mock response")
}
})
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/facebook/FbCookieTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/facebook/FbCookieTest.kt
index d5c8d2e1..a826391e 100644
--- a/app/src/androidTest/kotlin/com/pitchedapps/frost/facebook/FbCookieTest.kt
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/facebook/FbCookieTest.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.facebook
import android.webkit.CookieManager
@@ -10,4 +26,4 @@ class FbCookieTest {
fun managerAcceptsCookie() {
assertTrue(CookieManager.getInstance().acceptCookie(), "Cookie manager should accept cookie by default")
}
-} \ No newline at end of file
+}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
index 7f3d6b62..fc3751c5 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
@@ -33,7 +33,6 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.pitchedapps.frost.dbflow.CookiesDb
import com.pitchedapps.frost.dbflow.FbTabsDb
import com.pitchedapps.frost.dbflow.NotificationDb
-import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.services.scheduleNotifications
import com.pitchedapps.frost.services.setupNotificationChannels
@@ -93,11 +92,6 @@ class FrostApp : Application() {
KL.shouldLog = { BuildConfig.DEBUG }
Prefs.verboseLogging = false
L.i { "Begin Frost for Facebook" }
- try {
- FbCookie()
- } catch (e: Exception) {
- // no webview found; error will be handled in start activity
- }
FrostPglAdBlock.init(this)
if (Prefs.installDate == -1L) Prefs.installDate = System.currentTimeMillis()
if (Prefs.identifier == -1) Prefs.identifier = Random().nextInt(Int.MAX_VALUE)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
index 14cc579f..3fafa2a6 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
@@ -31,12 +31,15 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.activities.LoginActivity
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SelectorActivity
-import com.pitchedapps.frost.dbflow.loadFbCookiesAsync
+import com.pitchedapps.frost.dbflow.loadFbCookiesSync
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.utils.EXTRA_COOKIES
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.launchNewTask
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.util.ArrayList
import java.util.IllegalFormatException
@@ -52,28 +55,28 @@ class StartActivity : KauBaseActivity() {
showInvalidSdkView()
return
}
-
- try {
- FbCookie.switchBackUser {
- loadFbCookiesAsync {
- val cookies = ArrayList(it)
- L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
- L._d { "Cookies: ${cookies.joinToString("\t")}" }
- if (cookies.isNotEmpty()) {
- if (Prefs.userId != -1L)
- startActivity<MainActivity>(intentBuilder = {
- putParcelableArrayListExtra(EXTRA_COOKIES, cookies)
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or
- Intent.FLAG_ACTIVITY_SINGLE_TOP
- })
- else
- launchNewTask<SelectorActivity>(cookies)
- } else
- launchNewTask<LoginActivity>()
- }
+ launch {
+ try {
+ FbCookie.switchBackUser()
+ val cookies = ArrayList(withContext(Dispatchers.IO) {
+ loadFbCookiesSync()
+ })
+ L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
+ L._d { "Cookies: ${cookies.joinToString("\t")}" }
+ if (cookies.isNotEmpty()) {
+ if (Prefs.userId != -1L)
+ startActivity<MainActivity>(intentBuilder = {
+ putParcelableArrayListExtra(EXTRA_COOKIES, cookies)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or
+ Intent.FLAG_ACTIVITY_SINGLE_TOP
+ })
+ else
+ launchNewTask<SelectorActivity>(cookies)
+ } else
+ launchNewTask<LoginActivity>()
+ } catch (e: Exception) {
+ showInvalidWebView()
}
- } catch (e: Exception) {
- showInvalidWebView()
}
}
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 08b5ab0c..5965e5cf 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
@@ -22,8 +22,6 @@ import ca.allanwang.kau.internal.KauBaseActivity
import ca.allanwang.kau.searchview.SearchViewHolder
import com.pitchedapps.frost.contracts.VideoViewHolder
import com.pitchedapps.frost.utils.setFrostTheme
-import io.reactivex.disposables.CompositeDisposable
-import io.reactivex.disposables.Disposable
/**
* Created by Allan Wang on 2017-06-12.
@@ -35,8 +33,6 @@ abstract class BaseActivity : KauBaseActivity() {
*/
protected open fun backConsumer(): Boolean = false
- private val compositeDisposable = CompositeDisposable()
-
final override fun onBackPressed() {
if (this is SearchViewHolder && searchViewOnBackPress()) return
if (this is VideoViewHolder && videoOnBackPress()) return
@@ -49,15 +45,6 @@ abstract class BaseActivity : KauBaseActivity() {
if (this !is WebOverlayActivityBase) setFrostTheme()
}
- override fun onDestroy() {
- compositeDisposable.dispose()
- super.onDestroy()
- }
-
- fun Disposable.disposeOnDestroy() {
- compositeDisposable.add(this)
- }
-
//
// private var networkDisposable: Disposable? = null
// private var networkConsumer: ((Connectivity) -> Unit)? = null
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
index 7f69cc27..13253bcf 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
@@ -108,6 +108,7 @@ import kotlinx.android.synthetic.main.view_main_fab.*
import kotlinx.android.synthetic.main.view_main_toolbar.*
import kotlinx.android.synthetic.main.view_main_viewpager.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 20/12/17.
@@ -276,7 +277,10 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
val currentCookie = loadFbCookie(Prefs.userId)
if (currentCookie == null) {
toast(R.string.account_not_found)
- FbCookie.reset { launchLogin(cookies(), true) }
+ launch {
+ FbCookie.reset()
+ launchLogin(cookies(), true)
+ }
} else {
materialDialogThemed {
title(R.string.kau_logout)
@@ -288,15 +292,22 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
)
positiveText(R.string.kau_yes)
negativeText(R.string.kau_no)
- onPositive { _, _ -> FbCookie.logout(this@BaseMainActivity) }
+ onPositive { _, _ ->
+ launch {
+ FbCookie.logout(this@BaseMainActivity)
+ }
+ }
}
}
}
-3L -> launchNewTask<LoginActivity>(clearStack = false)
-4L -> launchNewTask<SelectorActivity>(cookies(), false)
else -> {
- FbCookie.switchUser(profile.identifier, this@BaseMainActivity::refreshAll)
- tabsForEachView { _, view -> view.badgeText = null }
+ launch {
+ FbCookie.switchUser(profile.identifier)
+ tabsForEachView { _, view -> view.badgeText = null }
+ refreshAll()
+ }
}
}
false
@@ -456,12 +467,14 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
override fun onResume() {
super.onResume()
- FbCookie.switchBackUser {}
+ val shouldReload = System.currentTimeMillis() - lastAccessTime > MAIN_TIMEOUT_DURATION
+ lastAccessTime = System.currentTimeMillis() // precaution to avoid loops
controlWebview?.resumeTimers()
- if (System.currentTimeMillis() - lastAccessTime > MAIN_TIMEOUT_DURATION) {
- refreshAll()
+ launch {
+ FbCookie.switchBackUser()
+ if (shouldReload)
+ refreshAll()
}
- lastAccessTime = System.currentTimeMillis() // precaution to avoid loops
}
override fun onPause() {
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 8b5fe38d..9540636a 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
@@ -18,7 +18,6 @@ package com.pitchedapps.frost.activities
import android.graphics.drawable.Drawable
import android.os.Bundle
-import android.os.Handler
import android.widget.ImageView
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.Toolbar
@@ -26,6 +25,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.fadeIn
import ca.allanwang.kau.utils.fadeOut
+import ca.allanwang.kau.utils.withMainContext
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
@@ -34,7 +34,7 @@ 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.loadFbCookiesAsync
+import com.pitchedapps.frost.dbflow.loadFbCookiesSuspend
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.profilePictureUrl
import com.pitchedapps.frost.glide.FrostGlide
@@ -46,11 +46,16 @@ import com.pitchedapps.frost.utils.frostEvent
import com.pitchedapps.frost.utils.launchNewTask
import com.pitchedapps.frost.utils.logFrostEvent
import com.pitchedapps.frost.utils.setFrostColors
+import com.pitchedapps.frost.utils.uniqueOnly
import com.pitchedapps.frost.web.LoginWebView
-import io.reactivex.Single
-import io.reactivex.android.schedulers.AndroidSchedulers
-import io.reactivex.functions.BiFunction
-import io.reactivex.subjects.SingleSubject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.resume
/**
* Created by Allan Wang on 2017-06-01.
@@ -63,18 +68,8 @@ class LoginActivity : BaseActivity() {
private val textview: AppCompatTextView by bindView(R.id.textview)
private val profile: ImageView by bindView(R.id.profile)
- private val profileSubject = SingleSubject.create<Boolean>()
- private val usernameSubject = SingleSubject.create<String>()
private lateinit var profileLoader: RequestManager
-
- // Helper to set and enable swipeRefresh
- private var refresh: Boolean
- get() = swipeRefresh.isRefreshing
- set(value) {
- if (value) swipeRefresh.isEnabled = true
- swipeRefresh.isRefreshing = value
- if (!value) swipeRefresh.isEnabled = false
- }
+ private val refreshChannel = Channel<Boolean>(10)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -84,80 +79,96 @@ class LoginActivity : BaseActivity() {
setFrostColors {
toolbar(toolbar)
}
- web.loadLogin({ refresh = it != 100 }) { cookie ->
+ profileLoader = GlideApp.with(profile)
+ launch {
+ for (refreshing in refreshChannel.uniqueOnly(this)) {
+ if (refreshing) swipeRefresh.isEnabled = true
+ swipeRefresh.isRefreshing = refreshing
+ if (!refreshing) swipeRefresh.isEnabled = false
+ }
+ }
+ launch {
+ val cookie = web.loadLogin { refresh(it != 100) }
L.d { "Login found" }
FbCookie.save(cookie.id)
web.fadeOut(onFinish = {
profile.fadeIn()
- loadInfo(cookie)
+ launch { loadInfo(cookie) }
})
}
- profileLoader = GlideApp.with(profile)
}
- private fun loadInfo(cookie: CookieModel) {
- refresh = true
- Single.zip<Boolean, String, Pair<Boolean, String>>(
- profileSubject,
- usernameSubject,
- BiFunction(::Pair)
- )
- .observeOn(AndroidSchedulers.mainThread()).subscribe { (foundImage, name) ->
- refresh = false
- if (!foundImage) {
- L.e { "Could not get profile photo; Invalid userId?" }
- L._i { cookie }
- }
- textview.text = String.format(getString(R.string.welcome), name)
- textview.fadeIn()
- frostEvent("Login", "success" to true)
- /*
- * The user may have logged into an account that is already in the database
- * We will let the db handle duplicates and load it now after the new account has been saved
- */
- loadFbCookiesAsync {
- val cookies = ArrayList(it)
- Handler().postDelayed({
- if (Showcase.intro)
- launchNewTask<IntroActivity>(cookies, true)
- else
- launchNewTask<MainActivity>(cookies, true)
- }, 1000)
- }
- }.disposeOnDestroy()
- loadProfile(cookie.id)
- loadUsername(cookie)
+ private fun refresh(refreshing: Boolean) {
+ refreshChannel.offer(refreshing)
}
- private fun loadProfile(id: Long) {
- profileLoader.load(profilePictureUrl(id))
- .transform(FrostGlide.roundCorner).listener(object : RequestListener<Drawable> {
- override fun onResourceReady(
- resource: Drawable?,
- model: Any?,
- target: Target<Drawable>?,
- dataSource: DataSource?,
- isFirstResource: Boolean
- ): Boolean {
- profileSubject.onSuccess(true)
- return false
- }
-
- override fun onLoadFailed(
- e: GlideException?,
- model: Any?,
- target: Target<Drawable>?,
- isFirstResource: Boolean
- ): Boolean {
- e.logFrostEvent("Profile loading exception")
- profileSubject.onSuccess(false)
- return false
- }
- }).into(profile)
+ private suspend fun loadInfo(cookie: CookieModel): Unit = withMainContext {
+ refresh(true)
+
+ val imageDeferred = async { loadProfile(cookie.id) }
+ val nameDeferred = async { loadUsername(cookie) }
+
+ val foundImage = imageDeferred.await()
+ val name = nameDeferred.await()
+
+ refresh(false)
+
+ if (!foundImage) {
+ L.e { "Could not get profile photo; Invalid userId?" }
+ L._i { cookie }
+ }
+
+ textview.text = String.format(getString(R.string.welcome), name)
+ textview.fadeIn()
+ frostEvent("Login", "success" to true)
+
+ /*
+ * The user may have logged into an account that is already in the database
+ * We will let the db handle duplicates and load it now after the new account has been saved
+ */
+ val cookies = ArrayList(loadFbCookiesSuspend())
+ delay(1000)
+ if (Showcase.intro)
+ launchNewTask<IntroActivity>(cookies, true)
+ else
+ launchNewTask<MainActivity>(cookies, true)
+ }
+
+ private suspend fun loadProfile(id: Long): Boolean = withMainContext {
+ suspendCancellableCoroutine<Boolean> { cont ->
+ profileLoader.load(profilePictureUrl(id))
+ .transform(FrostGlide.roundCorner).listener(object : RequestListener<Drawable> {
+ override fun onResourceReady(
+ resource: Drawable?,
+ model: Any?,
+ target: Target<Drawable>?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean
+ ): Boolean {
+ cont.resume(true)
+ return false
+ }
+
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target<Drawable>?,
+ isFirstResource: Boolean
+ ): Boolean {
+ e.logFrostEvent("Profile loading exception")
+ cont.resume(false)
+ return false
+ }
+ }).into(profile)
+ }
}
- private fun loadUsername(cookie: CookieModel) {
- cookie.fetchUsername(usernameSubject::onSuccess).disposeOnDestroy()
+ private suspend fun loadUsername(cookie: CookieModel): String = withContext(Dispatchers.IO) {
+ suspendCancellableCoroutine<String> { cont ->
+ cookie.fetchUsername {
+ cont.resume(it)
+ }
+ }
}
override fun backConsumer(): Boolean {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
index 90f38e9e..9a0c6270 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
@@ -18,6 +18,7 @@ package com.pitchedapps.frost.activities
import android.os.Bundle
import androidx.viewpager.widget.ViewPager
+import ca.allanwang.kau.utils.withMainContext
import com.google.android.material.tabs.TabLayout
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.utils.L
@@ -27,7 +28,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
@UseExperimental(ExperimentalCoroutinesApi::class)
@@ -97,8 +97,8 @@ class MainActivity : BaseMainActivity() {
.map { "[data-sigil*=$it] [data-sigil=count]" }
.map { doc.select(it) }
.map { e -> e?.getOrNull(0)?.ownText() }
- L._d { "Badges $feed $requests $messages $notifications" }
- withContext(Dispatchers.Main) {
+ L.v { "Badges $feed $requests $messages $notifications" }
+ withMainContext {
tabsForEachView { _, view ->
when (view.iicon) {
FbItem.FEED.icon -> view.badgeText = feed
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt
index 2907bac6..c3224237 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt
@@ -32,6 +32,7 @@ import com.pitchedapps.frost.utils.cookies
import com.pitchedapps.frost.utils.launchNewTask
import com.pitchedapps.frost.utils.setFrostColors
import com.pitchedapps.frost.views.AccountItem
+import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 2017-06-04.
@@ -55,7 +56,10 @@ class SelectorActivity : BaseActivity() {
override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<AccountItem>, item: AccountItem) {
if (item.cookie == null) this@SelectorActivity.launchNewTask<LoginActivity>()
- else FbCookie.switchUser(item.cookie) { launchNewTask<MainActivity>(cookies()) }
+ else launch {
+ FbCookie.switchUser(item.cookie)
+ launchNewTask<MainActivity>(cookies())
+ }
}
})
setFrostColors {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
index 19a1109f..a8c25050 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
@@ -43,6 +43,7 @@ import ca.allanwang.kau.utils.tint
import ca.allanwang.kau.utils.toDrawable
import ca.allanwang.kau.utils.toast
import ca.allanwang.kau.utils.withAlpha
+import ca.allanwang.kau.utils.withMainContext
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
@@ -74,7 +75,6 @@ import com.pitchedapps.frost.views.FrostWebView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
/**
@@ -106,7 +106,7 @@ class FrostWebActivity : WebOverlayActivityBase(false) {
content.scope.launch(Dispatchers.IO) {
refreshReceiver.receive()
refreshReceiver.cancel()
- withContext(Dispatchers.Main) {
+ withMainContext {
materialDialogThemed {
title(R.string.invalid_share_url)
content(R.string.invalid_share_url_desc)
@@ -216,12 +216,15 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
if (forceBasicAgent) //todo check; the webview already adds it dynamically
userAgentString = USER_AGENT_BASIC
Prefs.prevId = Prefs.userId
- if (userId != Prefs.userId) FbCookie.switchUser(userId) { reloadBase(true) }
- else reloadBase(true)
- if (Showcase.firstWebOverlay) {
- coordinator.frostSnackbar(R.string.web_overlay_swipe_hint) {
- duration = BaseTransientBottomBar.LENGTH_INDEFINITE
- setAction(R.string.kau_got_it) { _ -> this.dismiss() }
+ launch {
+ if (userId != Prefs.userId)
+ FbCookie.switchUser(userId)
+ reloadBase(true)
+ if (Showcase.firstWebOverlay) {
+ coordinator.frostSnackbar(R.string.web_overlay_swipe_hint) {
+ duration = BaseTransientBottomBar.LENGTH_INDEFINITE
+ setAction(R.string.kau_got_it) { dismiss() }
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
index 17753ce6..81469ff3 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
@@ -19,6 +19,7 @@ package com.pitchedapps.frost.contracts
import com.mikepenz.iconics.typeface.IIcon
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.fragments.BaseFragment
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
@@ -27,6 +28,7 @@ import kotlinx.coroutines.channels.Channel
*/
interface ActivityContract : FileChooserActivityContract
+@UseExperimental(ExperimentalCoroutinesApi::class)
interface MainActivityContract : ActivityContract, MainFabContract {
val fragmentChannel: BroadcastChannel<Int>
val headerBadgeChannel: Channel<String>
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 8678f997..e8fb5c54 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
@@ -37,6 +37,8 @@ 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
/**
@@ -71,6 +73,11 @@ fun loadFbCookiesAsync(callback: (cookies: List<CookieModel>) -> Unit) {
fun loadFbCookiesSync(): List<CookieModel> =
(select from CookieModel::class).orderBy(CookieModel_Table.name, true).queryList()
+// TODO temp method until dbflow supports coroutines
+suspend fun loadFbCookiesSuspend(): List<CookieModel> = withContext(Dispatchers.IO) {
+ loadFbCookiesSync()
+}
+
inline fun saveFbCookie(cookie: CookieModel, crossinline callback: (() -> Unit) = {}) {
cookie.async save {
L.d { "Fb cookie saved" }
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
index f5009cc5..30c812db 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
@@ -17,19 +17,21 @@
package com.pitchedapps.frost.debugger
import ca.allanwang.kau.logging.KauLoggerExtension
+import ca.allanwang.kau.utils.copyFromInputStream
import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.facebook.requests.call
-import com.pitchedapps.frost.facebook.requests.zip
import com.pitchedapps.frost.utils.createFreshDir
import com.pitchedapps.frost.utils.createFreshFile
import com.pitchedapps.frost.utils.frostJsoup
import com.pitchedapps.frost.utils.unescapeHtml
-import io.reactivex.disposables.CompositeDisposable
-import io.reactivex.rxkotlin.addTo
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
+import okhttp3.HttpUrl
import okhttp3.Request
-import okhttp3.ResponseBody
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@@ -63,13 +65,14 @@ class OfflineWebsite(
/**
* Supplied url without the queries
*/
- private val baseUrl = (baseUrl ?: url.substringBefore("?")
- .substringBefore(".com")).trim('/')
+ private val baseUrl: String = baseUrl ?: run {
+ val url: HttpUrl = HttpUrl.parse(url) ?: throw IllegalArgumentException("Malformed url")
+ return@run "${url.scheme()}://${url.host()}"
+ }
private val mainFile = File(baseDir, "index.html")
private val assetDir = File(baseDir, "assets")
- private var cancelled = false
private val urlMapper = ConcurrentHashMap<String, String>()
private val atomicInt = AtomicInteger()
@@ -91,35 +94,33 @@ class OfflineWebsite(
.get()
.call()
- private val compositeDisposable = CompositeDisposable()
-
/**
* Caller to bind callbacks and start the load
* Callback is guaranteed to be called unless the load is cancelled
*/
- fun load(progress: (Int) -> Unit = {}, callback: (Boolean) -> Unit) {
+ suspend fun load(progress: (Int) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
reset()
L.v { "Saving $url to ${baseDir.absolutePath}" }
- if (!baseDir.exists() && !baseDir.mkdirs()) {
+ if (!baseDir.isDirectory && !baseDir.mkdirs()) {
L.e { "Could not make directory" }
- return callback(false)
+ return@withContext false
}
if (!mainFile.createNewFile()) {
L.e { "Could not create ${mainFile.absolutePath}" }
- return callback(false)
+ return@withContext false
}
if (!assetDir.createFreshDir()) {
L.e { "Could not create ${assetDir.absolutePath}" }
- return callback(false)
+ return@withContext false
}
progress(10)
- if (cancelled) return
+ yield()
val doc: Document
if (html == null || html.length < 100) {
@@ -132,10 +133,10 @@ class OfflineWebsite(
doc.outputSettings().escapeMode(Entities.EscapeMode.extended)
if (doc.childNodeSize() == 0) {
L.e { "No content found" }
- return callback(false)
+ return@withContext false
}
- if (cancelled) return
+ yield()
progress(35)
@@ -151,32 +152,41 @@ class OfflineWebsite(
it.attr("href", absLink)
}
- if (cancelled) return
+ yield()
mainFile.writeText(doc.html())
progress(50)
- downloadCss().subscribe { cssLinks, cssThrowable ->
+ fun partialProgress(from: Int, to: Int, steps: Int): (Int) -> Unit {
+ if (steps == 0) return { progress(to) }
+ val section = (to - from) / steps
+ return { progress(from + it * section) }
+ }
- if (cssThrowable != null) {
- L.e { "CSS parsing failed: ${cssThrowable.message} $cssThrowable" }
- callback(false)
- return@subscribe
- }
+ val cssProgress = partialProgress(50, 70, cssQueue.size)
- progress(70)
+ cssQueue.clean().forEachIndexed { index, url ->
+ yield()
+ cssProgress(index)
+ val newUrls = downloadCss(url)
+ fileQueue.addAll(newUrls)
+ }
- fileQueue.addAll(cssLinks)
+ progress(70)
- if (cancelled) return@subscribe
+ val fileProgress = partialProgress(70, 100, fileQueue.size)
- downloadFiles().subscribe { success, throwable ->
- L.v { "All files downloaded: $success with throwable $throwable" }
- progress(100)
- callback(true)
- }
- }.addTo(compositeDisposable)
+ fileQueue.clean().forEachIndexed { index, url ->
+ yield()
+ fileProgress(index)
+ if (!downloadFile(url))
+ return@withContext false
+ }
+
+ yield()
+ progress(100)
+ return@withContext true
}
fun zip(name: String): Boolean {
@@ -198,11 +208,12 @@ class OfflineWebsite(
out.closeEntry()
delete()
}
+ baseDir.listFiles { file -> file != zip }
+ .forEach { it.zip() }
+ assetDir.listFiles()
+ .forEach { it.zip("assets/${it.name}") }
- baseDir.listFiles { _, n -> n != "$name.zip" }.forEach { it.zip() }
- assetDir.listFiles().forEach {
- it.zip("assets/${it.name}")
- }
+ assetDir.delete()
}
return true
} catch (e: Exception) {
@@ -211,76 +222,55 @@ class OfflineWebsite(
}
}
- fun loadAndZip(name: String, progress: (Int) -> Unit = {}, callback: (Boolean) -> Unit) {
-
- load({ progress((it * 0.85f).toInt()) }) {
- if (cancelled) return@load
- if (!it) callback(false)
- else {
- val result = zip(name)
- progress(100)
- callback(result)
- }
+ suspend fun loadAndZip(name: String, progress: (Int) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
+ coroutineScope {
+ val success = load { progress((it * 0.85f).toInt()) }
+ if (!success) return@coroutineScope false
+ val result = zip(name)
+ progress(100)
+ return@coroutineScope result
}
}
- private fun downloadFiles() = fileQueue.clean().toTypedArray().zip<String, Boolean, Boolean>({
- it.all { self -> self }
- }, {
- it.downloadUrl({ false }) { file, body ->
- body.byteStream().use { input ->
- file.outputStream().use { output ->
- input.copyTo(output)
- return@downloadUrl true
- }
- }
+ private fun downloadFile(url: String): Boolean {
+ return try {
+ val file = File(assetDir, fileName(url))
+ file.createNewFile()
+ val stream = request(url).execute().body()?.byteStream()
+ ?: throw IllegalArgumentException("Response body not found for $url")
+ file.copyFromInputStream(stream)
+ true
+ } catch (e: Exception) {
+ L.e(e) { "Download file failed" }
+ false
}
- })
+ }
+
+ private fun downloadCss(url: String): Set<String> {
+ return try {
+ val file = File(assetDir, fileName(url))
+ file.createNewFile()
- private fun downloadCss() = cssQueue.clean().toTypedArray().zip<String, Set<String>, Set<String>>({
- it.flatMap { l -> l }.toSet()
- }, { cssUrl ->
- cssUrl.downloadUrl({ emptySet() }) { file, body ->
- var content = body.string()
+ var content = request(url).execute().body()?.string()
+ ?: throw IllegalArgumentException("Response body not found for $url")
val links = FB_CSS_URL_MATCHER.findAll(content).mapNotNull { it[1] }
val absLinks = links.mapNotNull {
- val url = when {
+ val newUrl = when {
it.startsWith("http") -> it
it.startsWith("/") -> "$baseUrl$it"
else -> return@mapNotNull null
}
// css files are already in the asset folder,
// so the url does not point to another subfolder
- content = content.replace(it, url.fileName())
- url
+ content = content.replace(it, fileName(newUrl))
+ newUrl
}.toSet()
- L.v { "Abs links $absLinks" }
-
file.writeText(content)
- return@downloadUrl absLinks
- }
- })
-
- private inline fun <T> String.downloadUrl(
- fallback: () -> T,
- action: (file: File, body: ResponseBody) -> T
- ): T {
-
- val file = File(assetDir, fileName())
- if (!file.createNewFile()) {
- L.e { "Could not create path for ${file.absolutePath}" }
- return fallback()
- }
-
- val body = request(this).execute().body() ?: return fallback()
-
- try {
- body.use {
- return action(file, it)
- }
+ absLinks
} catch (e: Exception) {
- return fallback()
+ L.e(e) { "Download css failed" }
+ emptySet()
}
}
@@ -291,7 +281,7 @@ class OfflineWebsite(
val absLink = it.attr("abs:$key")
if (!absLink.isValid) return@forEach
collector.add(absLink)
- it.attr(key, "assets/${absLink.fileName()}")
+ it.attr(key, "assets/${fileName(absLink)}")
}
}
@@ -303,11 +293,11 @@ class OfflineWebsite(
* or create a new one
* This is thread-safe
*/
- private fun String.fileName(): String {
- val mapped = urlMapper[this]
+ private fun fileName(url: String): String {
+ val mapped = urlMapper[url]
if (mapped != null) return mapped
- val candidate = substringBefore("?").trim('/')
+ val candidate = url.substringBefore("?").trim('/')
.substringAfterLast("/").shorten()
val index = atomicInt.getAndIncrement()
@@ -321,7 +311,7 @@ class OfflineWebsite(
if (newUrl.endsWith(".js"))
newUrl = "$newUrl.txt"
- urlMapper[this] = newUrl
+ urlMapper[url] = newUrl
return newUrl
}
@@ -332,16 +322,9 @@ class OfflineWebsite(
filter(String::isNotBlank).filter { it.startsWith("http") }
private fun reset() {
- cancelled = false
urlMapper.clear()
atomicInt.set(0)
fileQueue.clear()
cssQueue.clear()
}
-
- fun cancel() {
- cancelled = true
- compositeDisposable.dispose()
- L.v { "Request cancelled" }
- }
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
index 2eb37ba4..5683526a 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
@@ -23,19 +23,14 @@ import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.dbflow.loadFbCookie
import com.pitchedapps.frost.dbflow.removeCookie
import com.pitchedapps.frost.dbflow.saveFbCookie
-import com.pitchedapps.frost.facebook.FbCookie.webCookie
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.cookies
import com.pitchedapps.frost.utils.launchLogin
-import io.reactivex.Observable
-import io.reactivex.android.schedulers.AndroidSchedulers
-import io.reactivex.subjects.SingleSubject
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.yield
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -46,7 +41,7 @@ import kotlin.coroutines.suspendCoroutine
*/
object FbCookie {
- const val COOKIE_DOMAIN = FACEBOOK_COM
+ const val COOKIE_DOMAIN = FB_URL_BASE
/**
* Retrieves the facebook cookie if it exists
@@ -55,49 +50,29 @@ object FbCookie {
inline val webCookie: String?
get() = CookieManager.getInstance().getCookie(COOKIE_DOMAIN)
- private fun CookieManager.setWebCookie(cookie: String?, callback: (() -> Unit)?) {
- removeAllCookies { _ ->
- if (cookie == null) {
- callback?.invoke()
- return@removeAllCookies
- }
- L.d { "Setting cookie" }
- val cookies = cookie.split(";").map { Pair(it, SingleSubject.create<Boolean>()) }
- cookies.forEach { (cookie, callback) -> setCookie(COOKIE_DOMAIN, cookie) { callback.onSuccess(it) } }
- Observable.zip<Boolean, Unit>(cookies.map { (_, callback) -> callback.toObservable() }) {}
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe {
- callback?.invoke()
- L.d { "Cookies set" }
- L._d { cookie }
- flush()
- }
- }
- }
-
private suspend fun CookieManager.suspendSetWebCookie(cookie: String?): Boolean {
cookie ?: return true
- L.test { "Orig $webCookie" }
- removeAllCookies()
- L.test { "Save $cookie" }
- // Save all cookies regardless of result, then check if all succeeded
- val result = cookie.split(";").map { setSingleWebCookie(it) }.all { it }
- L.test { "AAAA $webCookie" }
- flush()
- L.test { "SSSS $webCookie" }
- return result
+ return withContext(NonCancellable) {
+ removeAllCookies()
+ // Save all cookies regardless of result, then check if all succeeded
+ val result = cookie.split(";")
+ .map { async { setSingleWebCookie(it) } }
+ .awaitAll().all { it }
+ flush()
+ L.d { "Cookies set" }
+ L._d { "Set $cookie\n\tResult $webCookie" }
+ result
+ }
}
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
removeAllCookies {
- L.test { "Removed all cookies $webCookie" }
cont.resume(it)
}
}
private suspend fun CookieManager.setSingleWebCookie(cookie: String): Boolean = suspendCoroutine { cont ->
setCookie(COOKIE_DOMAIN, cookie.trim()) {
- L.test { "Save single $cookie\n\n\t$webCookie" }
cont.resume(it)
}
}
@@ -110,67 +85,63 @@ object FbCookie {
saveFbCookie(cookie)
}
- fun reset(callback: () -> Unit) {
+ suspend fun reset() {
Prefs.userId = -1L
with(CookieManager.getInstance()) {
- removeAllCookies {
- flush()
- callback()
- }
+ removeAllCookies()
+ flush()
}
}
- fun switchUser(id: Long, callback: () -> Unit) = switchUser(loadFbCookie(id), callback)
+ suspend fun switchUser(id: Long) = switchUser(loadFbCookie(id))
- fun switchUser(name: String, callback: () -> Unit) = switchUser(loadFbCookie(name), callback)
+ suspend fun switchUser(name: String) = switchUser(loadFbCookie(name))
- fun switchUser(cookie: CookieModel?, callback: () -> Unit) {
+ suspend fun switchUser(cookie: CookieModel?) {
if (cookie == null) {
L.d { "Switching User; null cookie" }
- callback()
return
}
- L.d { "Switching User" }
- Prefs.userId = cookie.id
- CookieManager.getInstance().setWebCookie(cookie.cookie, callback)
+ withContext(NonCancellable) {
+ L.d { "Switching User" }
+ Prefs.userId = cookie.id
+ CookieManager.getInstance().suspendSetWebCookie(cookie.cookie)
+ }
}
/**
* Helper function to remove the current cookies
* and launch the proper login page
*/
- fun logout(context: Context) {
+ suspend fun logout(context: Context) {
val cookies = arrayListOf<CookieModel>()
if (context is Activity)
cookies.addAll(context.cookies().filter { it.id != Prefs.userId })
- logout(Prefs.userId) {
- context.launchLogin(cookies, true)
- }
+ logout(Prefs.userId)
+ context.launchLogin(cookies, true)
}
/**
* Clear the cookies of the given id
*/
- fun logout(id: Long, callback: () -> Unit) {
+ suspend fun logout(id: Long) {
L.d { "Logging out user" }
removeCookie(id)
- reset(callback)
+ reset()
}
/**
* Notifications may come from different accounts, and we need to switch the cookies to load them
* When coming back to the main app, switch back to our original account before continuing
*/
- fun switchBackUser(callback: () -> Unit) {
- if (Prefs.prevId == -1L) return callback()
+ suspend fun switchBackUser() {
+ if (Prefs.prevId == -1L) return
val prevId = Prefs.prevId
Prefs.prevId = -1L
if (prevId != Prefs.userId) {
- switchUser(prevId) {
- L.d { "Switch back user" }
- L._d { "${Prefs.userId} to $prevId" }
- callback()
- }
- } else callback()
+ switchUser(prevId)
+ L.d { "Switch back user" }
+ L._d { "${Prefs.userId} to $prevId" }
+ }
}
}
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 50da367d..53ea6e67 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
@@ -24,7 +24,7 @@ import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.facebook.get
-import com.pitchedapps.frost.rx.Flyweight
+import com.pitchedapps.frost.kotlin.Flyweight
import com.pitchedapps.frost.utils.L
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Notifications.kt
index 43815b67..a098b6c0 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Notifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Notifications.kt
@@ -35,9 +35,4 @@ fun RequestAuth.markNotificationRead(notifId: Long): FrostRequest<Boolean> {
url("${FB_URL_BASE}a/jewel_notifications_log.php")
post(body.toForm())
}
-}
-
-fun RequestAuth.markNotificationsRead(vararg notifId: Long) =
- notifId.toTypedArray().zip<Long, Boolean, Boolean>(
- { it.all { self -> self } },
- { markNotificationRead(it).invoke() })
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
index 72150ddd..ea549026 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
@@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
+import ca.allanwang.kau.utils.ContextHelper
import ca.allanwang.kau.utils.fadeScaleTransition
import ca.allanwang.kau.utils.setIcon
import ca.allanwang.kau.utils.withArguments
@@ -39,7 +40,6 @@ import com.pitchedapps.frost.utils.REQUEST_REFRESH
import com.pitchedapps.frost.utils.REQUEST_TEXT_ZOOM
import com.pitchedapps.frost.utils.frostEvent
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
@@ -80,7 +80,7 @@ abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, Dyna
open lateinit var job: Job
override val coroutineContext: CoroutineContext
- get() = Dispatchers.Main + job
+ get() = ContextHelper.dispatcher + job
override val baseUrl: String by lazy { arguments!!.getString(ARG_URL) }
override val baseEnum: FbItem by lazy { FbItem[arguments]!! }
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt
index 7a8309ff..00d04a3e 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt
@@ -17,6 +17,7 @@
package com.pitchedapps.frost.fragments
import ca.allanwang.kau.adapters.fastAdapter
+import ca.allanwang.kau.utils.withMainContext
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.IItem
import com.mikepenz.fastadapter.adapters.ItemAdapter
@@ -29,7 +30,6 @@ import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.frostJsoup
import com.pitchedapps.frost.views.FrostRecyclerView
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
/**
@@ -49,22 +49,20 @@ abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), Recycle
}
}
- final override suspend fun reload(progress: (Int) -> Unit): Boolean {
+ final override suspend fun reload(progress: (Int) -> Unit): Boolean = withContext(Dispatchers.IO) {
val data = try {
reloadImpl(progress)
} catch (e: Exception) {
L.e(e) { "Recycler reload fail" }
null
}
- if (!isActive)
- return false
- return withContext(Dispatchers.Main) {
+ withMainContext {
if (data == null) {
valid = false
- return@withContext false
+ false
} else {
adapter.setNewList(data)
- return@withContext true
+ true
}
}
}
@@ -134,6 +132,7 @@ abstract class FrostParserFragment<T : Any, Item : IItem<*, *>> : RecyclerFragme
val response = parser.parse(cookie, doc)
if (response == null) {
L.i { "RecyclerFragment failed for ${baseEnum.name}" }
+ L._d { "Cookie used: $cookie" }
return@withContext null
}
progress(80)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/rx/Flyweight.kt b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt
index 8bba4c3c..7ac80147 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/rx/Flyweight.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt
@@ -14,7 +14,7 @@
* 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.rx
+package com.pitchedapps.frost.kotlin
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt b/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt
deleted file mode 100644
index 25f6d6aa..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt
+++ /dev/null
@@ -1,101 +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.rx
-
-import io.reactivex.Single
-import io.reactivex.schedulers.Schedulers
-import java.util.concurrent.TimeUnit
-
-/**
- * Created by Allan Wang on 07/01/18.
- *
- * Reactive flyweight to help deal with prolonged executions
- * Each call will output a [Single], which may be new if none exist or the old one is invalidated,
- * or reused if an old one is still valid
- *
- * Types:
- * T input argument for caller
- * C condition condition to check against for validity
- * R response response within reactive output
- */
-abstract class RxFlyweight<in T : Any, C : Any, R : Any> {
-
- /**
- * Given an input emit the desired response
- * This will be executed in a separate thread
- */
- protected abstract fun call(input: T): R
-
- /**
- * Given an input and condition, check if
- * we may used cache data or if we need to make a new request
- * Return [true] to use cache, [false] otherwise
- */
- protected abstract fun validate(input: T, cond: C): Boolean
-
- /**
- * Given an input, create a new condition to be used
- * for future requests
- */
- protected abstract fun cache(input: T): C
-
- private val conditionals = mutableMapOf<T, C>()
- private val sources = mutableMapOf<T, Single<R>>()
-
- private val lock = Any()
-
- /**
- * Entry point to give an input a receive a [Single]
- * Note that the observer is not bound to any particular thread,
- * as it is dependent on [createNewSource]
- */
- operator fun invoke(input: T): Single<R> {
- synchronized(lock) {
- val source = sources[input]
-
- // update condition and retrieve old one
- val condition = conditionals.put(input, cache(input))
-
- // check to reuse observable
- if (source != null && condition != null && validate(input, condition))
- return source
-
- val newSource = createNewSource(input).cache().doOnError { sources.remove(input) }
-
- sources[input] = newSource
- return newSource
- }
- }
-
- /**
- * Open source creator
- * Result will then be created with [Single.cache]
- * If you don't have a need for cache,
- * you likely won't have a need for flyweights
- */
- protected open fun createNewSource(input: T): Single<R> =
- Single.fromCallable { call(input) }
- .timeout(15, TimeUnit.SECONDS)
- .subscribeOn(Schedulers.io())
-
- fun reset() {
- synchronized(lock) {
- sources.clear()
- conditionals.clear()
- }
- }
-}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt
index 3cc7deaf..0db08d0f 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt
@@ -19,8 +19,8 @@ package com.pitchedapps.frost.services
import android.app.job.JobParameters
import android.app.job.JobService
import androidx.annotation.CallSuper
+import ca.allanwang.kau.utils.ContextHelper
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
@@ -28,7 +28,7 @@ abstract class BaseJobService : JobService(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
- get() = Dispatchers.Main + job
+ get() = ContextHelper.dispatcher + job
protected val startTime = System.currentTimeMillis()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
index 7360c191..b1e0ac8c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
@@ -30,6 +30,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
/**
* Created by Allan Wang on 2017-06-14.
@@ -81,11 +82,11 @@ class NotificationService : BaseJobService() {
private suspend fun sendNotifications(params: JobParameters?): Unit = withContext(Dispatchers.Default) {
val currentId = Prefs.userId
val cookies = loadFbCookiesSync()
- if (!isActive) return@withContext
+ yield()
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
var notifCount = 0
for (cookie in cookies) {
- if (!isActive) break
+ yield()
val current = cookie.id == currentId
if (Prefs.notificationsGeneral &&
(current || Prefs.notificationAllAccounts)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
index ece1f677..08f13a10 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
@@ -18,10 +18,12 @@ package com.pitchedapps.frost.settings
import android.content.Context
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
+import ca.allanwang.kau.utils.launchMain
import ca.allanwang.kau.utils.materialDialog
import ca.allanwang.kau.utils.startActivityForResult
import ca.allanwang.kau.utils.string
import ca.allanwang.kau.utils.toast
+import ca.allanwang.kau.utils.withMainContext
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.DebugActivity
import com.pitchedapps.frost.activities.SettingsActivity
@@ -39,9 +41,7 @@ import com.pitchedapps.frost.utils.sendFrostEmail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import java.io.File
/**
@@ -87,9 +87,7 @@ fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
attempt = launch(Dispatchers.IO) {
try {
val data = parser.parse(FbCookie.webCookie)
- withContext(Dispatchers.Main) {
- if (!isActive)
- return@withContext
+ withMainContext {
loading.dismiss()
createEmail(parser, data?.data)
}
@@ -120,43 +118,44 @@ fun SettingsActivity.sendDebug(url: String, html: String?) {
baseDir = DebugActivity.baseDir(this)
)
+ val job = Job()
+
val md = materialDialog {
title(R.string.parsing_data)
progress(false, 100)
negativeText(R.string.kau_cancel)
onNegative { dialog, _ -> dialog.dismiss() }
canceledOnTouchOutside(false)
- dismissListener { downloader.cancel() }
+ dismissListener { job.cancel() }
}
val progressChannel = Channel<Int>(10)
- launch(Dispatchers.Main) {
+ launchMain {
for (p in progressChannel) {
md.setProgress(p)
}
}
- launch(Dispatchers.IO) {
- downloader.loadAndZip(ZIP_NAME, { progressChannel.offer(it) }) { success ->
- launch(Dispatchers.Main) {
- if (!isActive) return@launch
- md.dismiss()
- if (success) {
- val zipUri = frostUriFromFile(
- File(downloader.baseDir, "$ZIP_NAME.zip")
- )
- L.i { "Sending debug zip with uri $zipUri" }
- sendFrostEmail(R.string.debug_report_email_title) {
- addItem("Url", url)
- addAttachment(zipUri)
- extras = {
- type = "application/zip"
- }
- }
- } else {
- toast(R.string.error_generic)
+
+ launchMain {
+ val success = downloader.loadAndZip(ZIP_NAME) {
+ progressChannel.offer(it)
+ }
+ md.dismiss()
+ if (success) {
+ val zipUri = frostUriFromFile(
+ File(downloader.baseDir, "$ZIP_NAME.zip")
+ )
+ L.i { "Sending debug zip with uri $zipUri" }
+ sendFrostEmail(R.string.debug_report_email_title) {
+ addItem("Url", url)
+ addAttachment(zipUri)
+ extras = {
+ type = "application/zip"
}
}
+ } else {
+ toast(R.string.error_generic)
}
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
index cca7ace0..3f92c41d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
@@ -70,6 +70,9 @@ import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.FbUrlFormatter.Companion.VIDEO_REDIRECT
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.facebook.formattedFbUrl
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
@@ -88,6 +91,14 @@ const val ARG_IMAGE_URL = "arg_image_url"
const val ARG_TEXT = "arg_text"
const val ARG_COOKIE = "arg_cookie"
+/**
+ * Most context items implement [CoroutineScope] by default.
+ * We will add a fallback just in case.
+ * It is expected that the scope returned always has the Android main dispatcher as part of the context.
+ */
+internal inline val Context.ctxCoroutine: CoroutineScope
+ get() = this as? CoroutineScope ?: GlobalScope
+
inline fun <reified T : Activity> Context.launchNewTask(
cookieList: ArrayList<CookieModel> = arrayListOf(),
clearStack: Boolean = false
@@ -116,7 +127,9 @@ private inline fun <reified T : WebOverlayActivityBase> Context.launchWebOverlay
L.v { "Launch received: $url\nLaunch web overlay: $argUrl" }
if (argUrl.isFacebookUrl && argUrl.contains("/logout.php")) {
L.d { "Logout php found" }
- FbCookie.logout(this)
+ ctxCoroutine.launch {
+ FbCookie.logout(this@launchWebOverlayImpl)
+ }
} else if (!(Prefs.linksInDefaultApp && resolveActivityForUri(Uri.parse(argUrl))))
startActivity<T>(false, intentBuilder = {
putExtra(ARG_URL, argUrl)
@@ -375,10 +388,8 @@ fun frostJsoup(url: String) = frostJsoup(FbCookie.webCookie, url)
fun frostJsoup(cookie: String?, url: String) =
Jsoup.connect(url).run {
- if (cookie != null) cookie(
- FACEBOOK_COM,
- cookie
- ) else this
+ if (cookie.isNullOrBlank()) this
+ else cookie(FACEBOOK_COM, cookie)
}.userAgent(USER_AGENT_BASIC).get()!!
fun Element.first(vararg select: String): Element? {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt
index 9619eecc..72d8803c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt
@@ -29,6 +29,7 @@ import ca.allanwang.kau.utils.fadeIn
import ca.allanwang.kau.utils.fadeOut
import ca.allanwang.kau.utils.invisibleIf
import ca.allanwang.kau.utils.isVisible
+import ca.allanwang.kau.utils.launchMain
import ca.allanwang.kau.utils.tint
import ca.allanwang.kau.utils.withAlpha
import com.pitchedapps.frost.R
@@ -39,14 +40,11 @@ import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.WEB_LOAD_DELAY
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
-import io.reactivex.disposables.Disposable
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
class FrostContentWeb @JvmOverloads constructor(
context: Context,
@@ -128,24 +126,20 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
val refreshReceiver = refreshChannel.openSubscription()
val progressReceiver = progressChannel.openSubscription()
- scope.launch(Dispatchers.Default) {
+ scope.launchMain {
launch {
for (r in refreshReceiver) {
- withContext(Dispatchers.Main) {
- refresh.isRefreshing = r
- refresh.isEnabled = true
- }
+ refresh.isRefreshing = r
+ refresh.isEnabled = true
}
}
launch {
for (p in progressReceiver) {
- withContext(Dispatchers.Main) {
- progress.invisibleIf(p == 100)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
- progress.setProgress(p, true)
- else
- progress.progress = p
- }
+ progress.invisibleIf(p == 100)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ progress.setProgress(p, true)
+ else
+ progress.progress = p
}
}
}
@@ -177,7 +171,6 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
core.destroy()
}
- private var dispose: Disposable? = null
private var transitionStart: Long = -1
private var refreshReceiver: ReceiveChannel<Boolean>? = null
@@ -194,7 +187,7 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
L.v { "Registered transition" }
with(coreView) {
refreshReceiver = refreshChannel.openSubscription().also { receiver ->
- scope.launch(Dispatchers.Main) {
+ scope.launchMain {
var loading = false
for (r in receiver) {
if (r) {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt
index f7cb2214..860bf36c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt
@@ -28,12 +28,14 @@ import com.pitchedapps.frost.contracts.FrostContentCore
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.fragments.RecyclerContentContract
import com.pitchedapps.frost.utils.Prefs
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 2017-05-29.
*
*/
+@UseExperimental(ExperimentalCoroutinesApi::class)
class FrostRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
index c8b54e7a..19d16e87 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
@@ -16,31 +16,35 @@
*/
package com.pitchedapps.frost.web
+import android.content.Context
import android.webkit.JavascriptInterface
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.contracts.MainActivityContract
import com.pitchedapps.frost.contracts.VideoViewHolder
+import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.WebContext
import com.pitchedapps.frost.utils.cookies
+import com.pitchedapps.frost.utils.ctxCoroutine
import com.pitchedapps.frost.utils.isIndependent
import com.pitchedapps.frost.utils.launchImageActivity
import com.pitchedapps.frost.utils.showWebContextMenu
import com.pitchedapps.frost.views.FrostWebView
import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 2017-06-01.
*/
class FrostJSI(val web: FrostWebView) {
- private val context = web.context
- private val activity = context as? MainActivity
+ private val context: Context = web.context
+ private val activity: MainActivity? = context as? MainActivity
private val header: SendChannel<String>? = activity?.headerBadgeChannel
private val refresh: SendChannel<Boolean> = web.parent.refreshChannel
- private val cookies = activity?.cookies() ?: arrayListOf()
+ private val cookies: List<CookieModel> = activity?.cookies() ?: arrayListOf()
/**
* Attempts to load the url in an overlay
@@ -103,7 +107,9 @@ class FrostJSI(val web: FrostWebView) {
@JavascriptInterface
fun loadLogin() {
L.d { "Sign up button found; load login" }
- FbCookie.logout(context)
+ context.ctxCoroutine.launch {
+ FbCookie.logout(context)
+ }
}
/**
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 2fe78f02..8132382a 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
@@ -28,6 +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 com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.FB_LOGIN_URL
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
@@ -39,6 +40,10 @@ 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.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
/**
* Created by Allan Wang on 2017-05-29.
@@ -60,13 +65,18 @@ class LoginWebView @JvmOverloads constructor(
webChromeClient = LoginChromeClient()
}
- fun loadLogin(progressCallback: (Int) -> Unit, loginCallback: (CookieModel) -> Unit) {
- this.progressCallback = progressCallback
- this.loginCallback = loginCallback
- L.d { "Begin loading login" }
- FbCookie.reset {
- setupWebview()
- loadUrl(FB_LOGIN_URL)
+ 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)
+ }
+ }
}
}
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
index f7dad4d3..07c92fbf 100644
--- a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
+++ b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
@@ -18,24 +18,231 @@ package com.pitchedapps.frost.debugger
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.internal.COOKIE
+import kotlinx.coroutines.runBlocking
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
+import org.junit.Assume.assumeTrue
import org.junit.Test
import java.io.File
-import java.util.concurrent.CountDownLatch
+import java.util.zip.ZipFile
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
/**
* Created by Allan Wang on 05/01/18.
*/
class OfflineWebsiteTest {
+ lateinit var server: MockWebServer
+ lateinit var baseDir: File
+
+ @BeforeTest
+ fun before() {
+ val buildPath = if (File("").absoluteFile.name == "app") "build/offline_test" else "app/build/offline_test"
+ baseDir = File(buildPath)
+ assertTrue(baseDir.deleteRecursively(), "Failed to clean base dir")
+ server = MockWebServer()
+ server.start()
+ }
+
+ @AfterTest
+ fun after() {
+ server.shutdown()
+ }
+
+ private fun zipAndFetch(url: String = server.url("/").toString(), cookie: String = ""): ZipFile {
+ val name = "test${System.currentTimeMillis()}"
+ runBlocking {
+ val success = OfflineWebsite(url, cookie, baseDir = baseDir)
+ .loadAndZip(name)
+ assertTrue(success, "An error occurred")
+ }
+
+ return ZipFile(File(baseDir, "$name.zip"))
+ }
+
+ private val tagWhitespaceRegex = Regex(">\\s+<", setOf(RegexOption.MULTILINE))
+
+ private fun ZipFile.assertContentEquals(path: String, content: String) {
+ val entry = getEntry(path)
+ assertNotNull(entry, "Entry $path not found")
+ val actualContent = getInputStream(entry).bufferedReader().use { it.readText() }
+ assertEquals(
+ content.replace(tagWhitespaceRegex, "><").toLowerCase(),
+ actualContent.replace(tagWhitespaceRegex, "><").toLowerCase(), "Content mismatch for $path"
+ )
+ }
+
+ @Test
+ fun fbOffline() {
+ // Not really a test. Skip in CI
+ assumeTrue(COOKIE.isNotEmpty())
+ zipAndFetch(FB_URL_BASE)
+ }
+
+ @Test
+ fun basicSingleFile() {
+ val content = """
+ <!DOCTYPE html>
+ <html>
+ <head></head>
+ <body>
+ <h1>Single File Test</h1>
+ </body>
+ </html>
+ """.trimIndent()
+
+ server.enqueue(MockResponse().setBody(content))
+
+ val zip = zipAndFetch()
+
+ assertEquals(1, zip.size(), "1 file expected")
+ zip.assertContentEquals("index.html", content)
+ }
+
@Test
- fun basic() {
- val countdown = CountDownLatch(1)
- val buildPath = if (File(".").parentFile?.name == "app") "build/offline_test" else "app/build/offline_test"
- OfflineWebsite(FB_URL_BASE, COOKIE, baseDir = File(buildPath))
- .loadAndZip("test") {
- println("Outcome $it")
- countdown.countDown()
+ fun withCssAsset() {
+ val cssUrl = server.url("1.css")
+
+ val content = """
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <link rel="stylesheet" href="$cssUrl">
+ </head>
+ <body>
+ <h1>Css File Test</h1>
+ </body>
+ </html>
+ """.trimIndent()
+
+ val css1 = """
+ .hello {
+ display: none;
}
- countdown.await()
+ """.trimIndent()
+
+ server.setDispatcher(object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse =
+ when {
+ request.path.contains(cssUrl.encodedPath()) -> MockResponse().setBody(css1)
+ else -> MockResponse().setBody(content)
+ }
+ })
+
+ val zip = zipAndFetch()
+
+ assertEquals(2, zip.size(), "2 files expected")
+ zip.assertContentEquals("index.html", content.replace(cssUrl.toString(), "assets/a0_1.css"))
+ zip.assertContentEquals("assets/a0_1.css", css1)
+ }
+
+ @Test
+ fun withJsAsset() {
+ val jsUrl = server.url("1.js")
+
+ val content = """
+ <!DOCTYPE html>
+ <html>
+ <head></head>
+ <body>
+ <h1>Js File Test</h1>
+ <script type="text/javascript" src="$jsUrl"></script>
+ </body>
+ </html>
+ """.trimIndent()
+
+ val js1 = """
+ console.log('hello');
+ """.trimIndent()
+
+ server.setDispatcher(object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse =
+ when {
+ request.path.contains(jsUrl.encodedPath()) -> MockResponse().setBody(js1)
+ else -> MockResponse().setBody(content)
+ }
+ })
+
+ val zip = zipAndFetch()
+
+ assertEquals(2, zip.size(), "2 files expected")
+ zip.assertContentEquals("index.html", content.replace(jsUrl.toString(), "assets/a0_1.js.txt"))
+ zip.assertContentEquals("assets/a0_1.js.txt", js1)
+ }
+
+ @Test
+ fun fullTest() {
+ val css1Url = server.url("1.css")
+ val css2Url = server.url("2.css")
+ val js1Url = server.url("1.js")
+ val js2Url = server.url("2.js")
+
+ val content = """
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <link rel="stylesheet" href="$css1Url">
+ <link rel="stylesheet" href="$css2Url">
+ </head>
+ <body>
+ <h1>Multi File Test</h1>
+ <script type="text/javascript" src="$js1Url"></script>
+ <script type="text/javascript" src="$js2Url"></script>
+ </body>
+ </html>
+ """.trimIndent()
+
+ val css1 = """
+ .hello {
+ display: none;
+ }
+ """.trimIndent()
+
+ val css2 = """
+ .world {
+ display: none;
+ }
+ """.trimIndent()
+
+ val js1 = """
+ console.log('hello');
+ """.trimIndent()
+
+ val js2 = """
+ console.log('world');
+ """.trimIndent()
+
+ server.setDispatcher(object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse =
+ when {
+ request.path.contains(css1Url.encodedPath()) -> MockResponse().setBody(css1)
+ request.path.contains(css2Url.encodedPath()) -> MockResponse().setBody(css2)
+ request.path.contains(js1Url.encodedPath()) -> MockResponse().setBody(js1)
+ request.path.contains(js2Url.encodedPath()) -> MockResponse().setBody(js2)
+ else -> MockResponse().setBody(content)
+ }
+ })
+
+ val zip = zipAndFetch()
+
+ assertEquals(5, zip.size(), "2 files expected")
+ zip.assertContentEquals(
+ "index.html", content
+ .replace(css1Url.toString(), "assets/a0_1.css")
+ .replace(css2Url.toString(), "assets/a1_2.css")
+ .replace(js1Url.toString(), "assets/a2_1.js.txt")
+ .replace(js2Url.toString(), "assets/a3_2.js.txt")
+ )
+
+ zip.assertContentEquals("assets/a0_1.css", css1)
+ zip.assertContentEquals("assets/a1_2.css", css2)
+ zip.assertContentEquals("assets/a2_1.js.txt", js1)
+ zip.assertContentEquals("assets/a3_2.js.txt", js2)
}
}
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/rx/FlyweightTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/kotlin/FlyweightTest.kt
index b58878cb..0eee530e 100644
--- a/app/src/test/kotlin/com/pitchedapps/frost/rx/FlyweightTest.kt
+++ b/app/src/test/kotlin/com/pitchedapps/frost/kotlin/FlyweightTest.kt
@@ -14,7 +14,7 @@
* 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.rx
+package com.pitchedapps.frost.kotlin
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
@@ -118,7 +118,6 @@ class FlyweightTest {
"Incorrect error found on fetch cancelled by destruction"
)
}
- println("Done")
}
}
}
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt
deleted file mode 100644
index 26a5a8de..00000000
--- a/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt
+++ /dev/null
@@ -1,73 +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.rx
-
-import com.pitchedapps.frost.internal.concurrentTest
-import org.junit.Before
-import org.junit.Test
-
-/**
- * Created by Allan Wang on 07/01/18.
- */
-private inline val threadId
- get() = Thread.currentThread().id
-
-class ResettableFlyweightTest {
-
- class IntFlyweight : RxFlyweight<Int, Long, Long>() {
- override fun call(input: Int): Long {
- println("Call for $input on thread $threadId")
- Thread.sleep(20)
- return System.currentTimeMillis()
- }
-
- override fun validate(input: Int, cond: Long) = System.currentTimeMillis() - cond < 500
-
- override fun cache(input: Int): Long = System.currentTimeMillis()
- }
-
- private lateinit var flyweight: IntFlyweight
-
- @Before
- fun init() {
- flyweight = IntFlyweight()
- }
-
- @Test
- fun testCache() = concurrentTest { result ->
- flyweight(1).subscribe { i, _ ->
- flyweight(1).subscribe { j, _ ->
- if (i != null && i == j)
- result.onComplete()
- else
- result.onError("Did not use cache during calls")
- }
- }
- }
-
- @Test
- fun testNoCache() = concurrentTest { result ->
- flyweight(1).subscribe { i, _ ->
- flyweight(2).subscribe { j, _ ->
- if (i != null && i != j)
- result.onComplete()
- else
- result.onError("Should not use cache for calls with different keys")
- }
- }
- }
-}
diff --git a/gradle.properties b/gradle.properties
index 62b16ce4..11127a3e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,7 +14,7 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro
APP_ID=Frost
APP_GROUP=com.pitchedapps
-KAU=d850474
+KAU=af43e82
KOTLIN=1.3.11
# https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google