From 1dd7be9174f1740aa1cae29f6d62d6f83f5917ba Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 22 Nov 2021 22:24:17 -0800 Subject: Migrate refresh channel to flow --- app/build.gradle | 2 + .../frost/activities/WebOverlayActivity.kt | 6 +- .../frost/contracts/FrostContentContract.kt | 12 +- .../pitchedapps/frost/views/FrostContentView.kt | 79 ++++++--- .../pitchedapps/frost/views/FrostRecyclerView.kt | 7 +- .../com/pitchedapps/frost/views/FrostWebView.kt | 56 ++---- .../pitchedapps/frost/web/FrostChromeClients.kt | 5 +- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 8 +- .../kotlin/com/pitchedapps/frost/web/FrostWeb.kt | 84 +++++++++ .../pitchedapps/frost/web/FrostWebViewClients.kt | 14 +- .../pitchedapps/frost/web/FrostWebViewClients2.kt | 196 +++++++++++++++++++++ 11 files changed, 380 insertions(+), 89 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients2.kt diff --git a/app/build.gradle b/app/build.gradle index 2655f695..7e064152 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -270,6 +270,8 @@ dependencies { implementation kau.Dependencies.kau('searchview', KAU) implementation kau.Dependencies.coreKtx + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0" + implementation kau.Dependencies.swipeRefreshLayout implementation "androidx.biometric:biometric:${Versions.andxBiometric}" 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 ef7579a8..171583ed 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -68,6 +68,8 @@ import com.pitchedapps.frost.views.FrostWebView import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import javax.inject.Inject @@ -97,10 +99,8 @@ class FrostWebActivity : WebOverlayActivityBase() { * We will subscribe to the load cycle once, * and pop a dialog giving the user the option to copy the shared text */ - val refreshReceiver = content.refreshChannel.openSubscription() content.scope.launch(Dispatchers.IO) { - refreshReceiver.receive() - refreshReceiver.cancel() + content.refreshFlow.take(1).collect() withMainContext { materialDialog { title(R.string.invalid_share_url) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt index b8d0d86f..1d429138 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt @@ -18,9 +18,11 @@ package com.pitchedapps.frost.contracts import android.view.View import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.web.FrostEmitter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.flow.SharedFlow /** * Created by Allan Wang on 20/12/17. @@ -56,7 +58,9 @@ interface FrostContentParent : DynamicUiContract { /** * Observable to get data on whether view is refreshing or not */ - val refreshChannel: BroadcastChannel + val refreshFlow: SharedFlow + + val refreshEmit: FrostEmitter /** * Observable to get data on refresh progress, with range [0, 100] @@ -124,17 +128,15 @@ interface FrostContentCore : DynamicUiContract { * Reference to parent * Bound through calling [FrostContentParent.bind] */ - var parent: FrostContentParent + val parent: FrostContentParent /** * Initializes view through given [container] * * The content may be free to extract other data from * the container if necessary - * - * [parent] must be bounded before calling this! */ - fun bind(container: FrostContentContainer): View + fun bind(parent: FrostContentParent, container: FrostContentContainer): View /** * Call to reload wrapped data 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 c30ee199..1891a786 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -42,12 +42,23 @@ import com.pitchedapps.frost.injectors.ThemeProvider import com.pitchedapps.frost.kotlin.subscribeDuringJob import com.pitchedapps.frost.prefs.Prefs import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.web.FrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.flow.transformWhile import javax.inject.Inject class FrostContentWeb @JvmOverloads constructor( @@ -119,11 +130,22 @@ abstract class FrostContentViewBase( private val refresh: SwipeRefreshLayout by bindView(R.id.content_refresh) private val progress: ProgressBar by bindView(R.id.content_progress) + private val coreView: View by bindView(R.id.content_core) + /** * While this can be conflated, there exist situations where we wish to watch refresh cycles. * Here, we'd need to make sure we don't skip events */ - override val refreshChannel: BroadcastChannel = BroadcastChannel(10) + private val refreshMutableFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + override val refreshFlow: SharedFlow = refreshMutableFlow.asSharedFlow() + + override val refreshEmit: FrostEmitter = + FrostEmitter { refreshMutableFlow.tryEmit(it) } + override val progressChannel: BroadcastChannel = ConflatedBroadcastChannel() override val titleChannel: BroadcastChannel = ConflatedBroadcastChannel() @@ -160,7 +182,6 @@ abstract class FrostContentViewBase( */ protected fun init() { inflate(context, layoutRes, this) - core.parent = this reloadThemeSelf() } @@ -169,15 +190,15 @@ abstract class FrostContentViewBase( baseEnum = container.baseEnum init() scope = container - core.bind(container) + core.bind(this, container) refresh.setOnRefreshListener { core.reload(true) } - refreshChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { r -> + refreshFlow.distinctUntilChanged().onEach { r -> L.v { "Refreshing $r" } refresh.isRefreshing = r - } + }.launchIn(scope) progressChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { p -> progress.invisibleIf(p == 100) @@ -220,33 +241,37 @@ abstract class FrostContentViewBase( * The cycle only starts on the first load since there may have been another process when this is registered */ override fun registerTransition(urlChanged: Boolean, animate: Boolean): Boolean { - if (!urlChanged && refreshReceiver != null) { + if (!urlChanged && transitionStart != -1L) { L.v { "Consuming url load" } return false // still in progress; do not bother with load } + coreView.transition(animate) + return true + } + + private fun View.transition(animate: Boolean) { L.v { "Registered transition" } - with(core) { - refreshReceiver = refreshChannel.openSubscription().also { receiver -> - scope.launchMain { - var loading = false - for (r in receiver) { - if (r) { - loading = true - transitionStart = System.currentTimeMillis() - clearAnimation() - if (isVisible) - fadeOut(duration = 200L) - } else if (loading) { - if (animate && prefs.animate) circularReveal(offset = WEB_LOAD_DELAY) - else fadeIn(duration = 200L, offset = WEB_LOAD_DELAY) - L.v { "Transition loaded in ${System.currentTimeMillis() - transitionStart} ms" } - receiver.cancel() - refreshReceiver = null - } + transitionStart = 0L // Marker for pending transition + scope.launchMain { + refreshFlow.distinctUntilChanged() + // Pseudo windowed mode + .runningFold(false to false) { (_, prev), curr -> prev to curr } + // Take until prev was loading and current is not loading + // Unlike takeWhile, we include the last state (first non matching) + .transformWhile { emit(it); it != (true to false) } + .onEach { (prev, curr) -> + if (curr) { + transitionStart = System.currentTimeMillis() + clearAnimation() + if (isVisible) + fadeOut(duration = 200L) + } else if (prev) { // prev && !curr + if (animate && prefs.animate) circularReveal(offset = WEB_LOAD_DELAY) + else fadeIn(duration = 200L, offset = WEB_LOAD_DELAY) + L.v { "Transition loaded in ${System.currentTimeMillis() - transitionStart} ms" } } - } - } + }.collect() + transitionStart = -1L } - return true } } 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 2ab00916..a2b71572 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -61,7 +61,8 @@ class FrostRecyclerView @JvmOverloads constructor( layoutManager = LinearLayoutManager(context) } - override fun bind(container: FrostContentContainer): View { + override fun bind(parent: FrostContentParent, container: FrostContentContainer): View { + this.parent = parent if (container !is RecyclerContentContract) throw IllegalStateException("FrostRecyclerView must bind to a container that is a RecyclerContentContract") this.recyclerContract = container @@ -78,10 +79,10 @@ class FrostRecyclerView @JvmOverloads constructor( override fun reloadBase(animate: Boolean) { if (prefs.animate) fadeOut(onFinish = onReloadClear) scope.launch { - parent.refreshChannel.offer(true) + parent.refreshEmit(true) recyclerContract.reload { parent.progressChannel.offer(it) } parent.progressChannel.offer(100) - parent.refreshChannel.offer(false) + parent.refreshEmit(false) if (prefs.animate) circularReveal() } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt index f384d134..140b4901 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt @@ -35,25 +35,23 @@ import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.currentCookie import com.pitchedapps.frost.facebook.FB_HOME_URL import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.USER_AGENT -import com.pitchedapps.frost.fragments.WebFragment import com.pitchedapps.frost.injectors.ThemeProvider import com.pitchedapps.frost.prefs.Prefs import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostDownload import com.pitchedapps.frost.web.FrostChromeClient -import com.pitchedapps.frost.web.FrostJSI +import com.pitchedapps.frost.web.FrostWebClientEntryPoint +import com.pitchedapps.frost.web.FrostWebComponentBuilder +import com.pitchedapps.frost.web.FrostWebEntryPoint import com.pitchedapps.frost.web.FrostWebViewClient +import com.pitchedapps.frost.web.FrostWebViewClientMenu +import com.pitchedapps.frost.web.FrostWebViewClientMessenger import com.pitchedapps.frost.web.NestedWebView -import dagger.BindsInstance -import dagger.hilt.DefineComponent -import dagger.hilt.EntryPoint import dagger.hilt.EntryPoints -import dagger.hilt.InstallIn import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.components.ViewComponent import javax.inject.Inject -import javax.inject.Scope import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -103,9 +101,11 @@ class FrostWebView @JvmOverloads constructor( get() = url ?: "" @SuppressLint("SetJavaScriptEnabled") - override fun bind(container: FrostContentContainer): View { - val component = frostWebComponentBuilder.frostWebView(this).build() - val entryPoint = EntryPoints.get(component, FrostWebEntryPoint::class.java) + override fun bind(parent: FrostContentParent, container: FrostContentContainer): View { + this.parent = parent + val component = frostWebComponentBuilder.frostParent(parent).frostWebView(this).build() + val webEntryPoint = EntryPoints.get(component, FrostWebEntryPoint::class.java) + val clientEntryPoint = EntryPoints.get(component, FrostWebClientEntryPoint::class.java) userAgentString = USER_AGENT with(settings) { javaScriptEnabled = true @@ -116,10 +116,14 @@ class FrostWebView @JvmOverloads constructor( } setLayerType(LAYER_TYPE_HARDWARE, null) // attempt to get custom client; otherwise fallback to original - frostWebClient = (container as? WebFragment)?.client(this) ?: FrostWebViewClient(this) + frostWebClient = when (parent.baseEnum) { + FbItem.MESSENGER -> FrostWebViewClientMessenger(this) + FbItem.MENU -> FrostWebViewClientMenu(this) + else -> clientEntryPoint.webClient() + } webViewClient = frostWebClient webChromeClient = FrostChromeClient(this, themeProvider, webFileChooser) - addJavascriptInterface(entryPoint.frostJsi(), "Frost") + addJavascriptInterface(webEntryPoint.frostJsi(), "Frost") setBackgroundColor(Color.TRANSPARENT) setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> context.ctxCoroutine.launchMain { @@ -251,29 +255,3 @@ class FrostWebView @JvmOverloads constructor( super.destroy() } } - -@Scope -@Retention(AnnotationRetention.BINARY) -@Target( - AnnotationTarget.FUNCTION, - AnnotationTarget.TYPE, - AnnotationTarget.CLASS -) -annotation class FrostWebScoped - -@FrostWebScoped -@DefineComponent(parent = ViewComponent::class) -interface FrostWebComponent - -@DefineComponent.Builder -interface FrostWebComponentBuilder { - fun frostWebView(@BindsInstance web: FrostWebView): FrostWebComponentBuilder - fun build(): FrostWebComponent -} - -@EntryPoint -@InstallIn(FrostWebComponent::class) -interface FrostWebEntryPoint { - @FrostWebScoped - fun frostJsi(): FrostJSI -} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt index 61a76e70..372a7bad 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt @@ -51,7 +51,8 @@ class FrostChromeClient( private val webFileChooser: WebFileChooser, ) : WebChromeClient() { - private val refresh: SendChannel = web.parent.refreshChannel +// private val refresh: SendChannel = web.parent.refreshChannel + private val refreshEmit = web.parent.refreshEmit private val progress: SendChannel = web.parent.progressChannel private val title: SendChannel = web.parent.titleChannel private val context = web.context!! @@ -87,7 +88,7 @@ class FrostChromeClient( private fun JsResult.frostCancel() { cancel() - refresh.offer(false) + refreshEmit(false) progress.offer(100) } 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 3ead78f4..f43f3b81 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -39,17 +39,18 @@ import javax.inject.Inject /** * Created by Allan Wang on 2017-06-01. */ +@FrostWebScoped class FrostJSI @Inject internal constructor( val web: FrostWebView, private val activity: Activity, private val fbCookie: FbCookie, - private val prefs: Prefs + private val prefs: Prefs, + @FrostRefresh private val refreshEmit: FrostEmitter ) { private val mainActivity: MainActivity? = activity as? MainActivity private val webActivity: WebOverlayActivityBase? = activity as? WebOverlayActivityBase private val header: SendChannel? = mainActivity?.headerBadgeChannel - private val refresh: SendChannel = web.parent.refreshChannel private val cookies: List = activity.cookies() /** @@ -144,7 +145,8 @@ class FrostJSI @Inject internal constructor( @JavascriptInterface fun isReady() { if (web.frostWebClient !is FrostWebViewClientMenu) { - refresh.offer(false) + L.v { "JSI is ready" } + refreshEmit(false) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt new file mode 100644 index 00000000..30845a79 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2021 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 . + */ +package com.pitchedapps.frost.web + +import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.views.FrostWebView +import dagger.BindsInstance +import dagger.Module +import dagger.Provides +import dagger.hilt.DefineComponent +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewComponent +import kotlinx.coroutines.flow.SharedFlow +import javax.inject.Qualifier +import javax.inject.Scope + +/** + * Defines a new scope for Frost web related content. + * + * This is a subset of [dagger.hilt.android.scopes.ViewScoped] + */ +@Scope +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPE, + AnnotationTarget.CLASS +) +annotation class FrostWebScoped + +@FrostWebScoped +@DefineComponent(parent = ViewComponent::class) +interface FrostWebComponent + +@DefineComponent.Builder +interface FrostWebComponentBuilder { + fun frostParent(@BindsInstance parent: FrostContentParent): FrostWebComponentBuilder + fun frostWebView(@BindsInstance web: FrostWebView): FrostWebComponentBuilder + fun build(): FrostWebComponent +} + +@EntryPoint +@InstallIn(FrostWebComponent::class) +interface FrostWebEntryPoint { + fun frostJsi(): FrostJSI +} + +fun interface FrostEmitter : (T) -> Unit + +@Module +@InstallIn(FrostWebComponent::class) +object FrostWebFlowModule { + @Provides + @FrostWebScoped + @FrostRefresh + fun refreshFlow(parent: FrostContentParent): SharedFlow = parent.refreshFlow + + @Provides + @FrostWebScoped + @FrostRefresh + fun refreshEmit(parent: FrostContentParent): FrostEmitter = parent.refreshEmit +} + +/** + * Observable to get data on whether view is refreshing or not + */ +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class FrostRefresh diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt index 3b332199..ba19989d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -53,7 +53,6 @@ import com.pitchedapps.frost.utils.isMessengerUrl import com.pitchedapps.frost.utils.launchImageActivity import com.pitchedapps.frost.utils.startActivityForUri import com.pitchedapps.frost.views.FrostWebView -import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.launch /** @@ -83,7 +82,7 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { protected val fbCookie: FbCookie get() = web.fbCookie protected val prefs: Prefs get() = web.prefs protected val themeProvider: ThemeProvider get() = web.themeProvider - protected val refresh: SendChannel = web.parent.refreshChannel +// protected val refresh: SendChannel = web.parent.refreshChannel protected val isMain = web.parent.baseEnum != null /** @@ -156,7 +155,7 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { super.onPageStarted(view, url, favicon) if (url == null) return v { "loading $url ${web.settings.userAgentString}" } - refresh.offer(true) +// refresh.offer(true) } private fun injectBackgroundColor() { @@ -182,7 +181,7 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { view.messengerJsInject() } else -> { - refresh.offer(false) +// refresh.offer(false) } } } @@ -191,7 +190,7 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { url ?: return v { "finished $url" } if (!url.isFacebookUrl && !url.isMessengerUrl) { - refresh.offer(false) +// refresh.offer(false) return } onPageFinishedActions(url) @@ -204,9 +203,10 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { injectAndFinish() } - internal fun injectAndFinish() { + // Temp open + internal open fun injectAndFinish() { v { "page finished reveal" } - refresh.offer(false) +// refresh.offer(false) injectBackgroundColor() when { web.url.isFacebookUrl -> { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients2.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients2.kt new file mode 100644 index 00000000..008b1197 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients2.kt @@ -0,0 +1,196 @@ +/* + * 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 . + */ +package com.pitchedapps.frost.web + +import android.graphics.Bitmap +import android.graphics.Color +import android.webkit.WebResourceRequest +import android.webkit.WebView +import ca.allanwang.kau.utils.withAlpha +import com.pitchedapps.frost.enums.ThemeCategory +import com.pitchedapps.frost.injectors.JsActions +import com.pitchedapps.frost.injectors.JsAssets +import com.pitchedapps.frost.injectors.jsInject +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.isFacebookUrl +import com.pitchedapps.frost.utils.isMessengerUrl +import com.pitchedapps.frost.utils.launchImageActivity +import com.pitchedapps.frost.views.FrostWebView +import dagger.Binds +import dagger.Module +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import javax.inject.Inject +import javax.inject.Qualifier + +/** + * Created by Allan Wang on 2017-05-31. + * + * Collection of webview clients + */ + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class FrostWebClient + +@EntryPoint +@InstallIn(FrostWebComponent::class) +interface FrostWebClientEntryPoint { + + @FrostWebScoped + @FrostWebClient + fun webClient(): FrostWebViewClient +} + +@Module +@InstallIn(FrostWebComponent::class) +interface FrostWebViewClientModule { + @Binds + @FrostWebClient + fun webClient(binds: FrostWebViewClient2): FrostWebViewClient +} + +/** + * The default webview client + */ +open class FrostWebViewClient2 @Inject constructor( + web: FrostWebView, + @FrostRefresh private val refreshEmit: FrostEmitter +) : FrostWebViewClient(web) { + + init { + L.i { "Refresh web client 2" } + } + + override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) { + super.doUpdateVisitedHistory(view, url, isReload) + urlSupportsRefresh = urlSupportsRefresh(url) + web.parent.swipeAllowedByPage = urlSupportsRefresh + view.jsInject( + JsAssets.AUTO_RESIZE_TEXTAREA.maybe(prefs.autoExpandTextBox), + prefs = prefs + ) + v { "History $url; refresh $urlSupportsRefresh" } + } + + private fun urlSupportsRefresh(url: String?): Boolean { + if (url == null) return false + if (url.isMessengerUrl) return false + if (!url.isFacebookUrl) return true + if (url.contains("soft=composer")) return false + if (url.contains("sharer.php") || url.contains("sharer-dialog.php")) return false + return true + } + + private fun WebView.facebookJsInject() { + jsInject(*facebookJsInjectors.toTypedArray(), prefs = prefs) + } + + private fun WebView.messengerJsInject() { + jsInject( + themeProvider.injector(ThemeCategory.MESSENGER), + prefs = prefs + ) + } + + override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + if (url == null) return + v { "loading $url ${web.settings.userAgentString}" } + refreshEmit(true) + } + + private fun injectBackgroundColor() { + web.setBackgroundColor( + when { + isMain -> Color.TRANSPARENT + web.url.isFacebookUrl -> themeProvider.bgColor.withAlpha(255) + else -> Color.WHITE + } + ) + } + + override fun onPageCommitVisible(view: WebView, url: String?) { + super.onPageCommitVisible(view, url) + injectBackgroundColor() + when { + url.isFacebookUrl -> { + v { "FB Page commit visible" } + view.facebookJsInject() + } + url.isMessengerUrl -> { + v { "Messenger Page commit visible" } + view.messengerJsInject() + } + else -> { + refreshEmit(false) + } + } + } + + override fun onPageFinished(view: WebView, url: String?) { + url ?: return + v { "finished $url" } + if (!url.isFacebookUrl && !url.isMessengerUrl) { + refreshEmit(false) + return + } + onPageFinishedActions(url) + } + + internal override fun injectAndFinish() { + v { "page finished reveal" } + refreshEmit(false) + injectBackgroundColor() + when { + web.url.isFacebookUrl -> { + web.jsInject( + JsActions.LOGIN_CHECK, + JsAssets.TEXTAREA_LISTENER, + JsAssets.HEADER_BADGES.maybe(isMain), + prefs = prefs + ) + web.facebookJsInject() + } + web.url.isMessengerUrl -> { + web.messengerJsInject() + } + } + } + + /** + * Helper to format the request and launch it + * returns true to override the url + * returns false if we are already in an overlaying activity + */ + private fun launchRequest(request: WebResourceRequest): Boolean { + v { "Launching url: ${request.url}" } + return web.requestWebOverlay(request.url.toString()) + } + + private fun launchImage(url: String, text: String? = null, cookie: String? = null): Boolean { + v { "Launching image: $url" } + web.context.launchImageActivity(url, text, cookie) + if (web.canGoBack()) web.goBack() + return true + } +} + +private const val EMIT_THEME = 0b1 +private const val EMIT_ID = 0b10 +private const val EMIT_COMPLETE = EMIT_THEME or EMIT_ID +private const val EMIT_FINISH = 0 -- cgit v1.2.3 From 779ec08188f4bda736b3e0f2940570f1f7eb49e1 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 22 Nov 2021 22:53:00 -0800 Subject: Migrate progress channel to flow --- .../pitchedapps/frost/contracts/FrostContentContract.kt | 4 +++- .../com/pitchedapps/frost/views/FrostContentView.kt | 15 ++++++++++----- .../com/pitchedapps/frost/views/FrostRecyclerView.kt | 4 ++-- .../com/pitchedapps/frost/web/FrostChromeClients.kt | 6 +++--- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt index 1d429138..8ebf7af7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt @@ -65,7 +65,9 @@ interface FrostContentParent : DynamicUiContract { /** * Observable to get data on refresh progress, with range [0, 100] */ - val progressChannel: BroadcastChannel + val progressFlow: SharedFlow + + val progressEmit: FrostEmitter /** * Observable to get new title data (unique values only) 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 1891a786..3ec80f36 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -22,7 +22,6 @@ import android.util.AttributeSet import android.view.View import android.widget.FrameLayout import android.widget.ProgressBar -import ca.allanwang.kau.utils.ContextHelper import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.circularReveal import ca.allanwang.kau.utils.fadeIn @@ -39,7 +38,6 @@ import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.WEB_LOAD_DELAY import com.pitchedapps.frost.injectors.ThemeProvider -import com.pitchedapps.frost.kotlin.subscribeDuringJob import com.pitchedapps.frost.prefs.Prefs import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.web.FrostEmitter @@ -51,6 +49,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collect @@ -146,7 +145,13 @@ abstract class FrostContentViewBase( override val refreshEmit: FrostEmitter = FrostEmitter { refreshMutableFlow.tryEmit(it) } - override val progressChannel: BroadcastChannel = ConflatedBroadcastChannel() + private val progressMutableFlow = MutableStateFlow(0) + + override val progressFlow: SharedFlow = progressMutableFlow.asSharedFlow() + + override val progressEmit: FrostEmitter = + FrostEmitter { progressMutableFlow.tryEmit(it) } + override val titleChannel: BroadcastChannel = ConflatedBroadcastChannel() override lateinit var scope: CoroutineScope @@ -200,13 +205,13 @@ abstract class FrostContentViewBase( refresh.isRefreshing = r }.launchIn(scope) - progressChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { p -> + progressFlow.onEach { p -> progress.invisibleIf(p == 100) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progress.setProgress(p, true) else progress.progress = p - } + }.launchIn(scope) } override fun reloadTheme() { 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 a2b71572..9e21ede8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -80,8 +80,8 @@ class FrostRecyclerView @JvmOverloads constructor( if (prefs.animate) fadeOut(onFinish = onReloadClear) scope.launch { parent.refreshEmit(true) - recyclerContract.reload { parent.progressChannel.offer(it) } - parent.progressChannel.offer(100) + recyclerContract.reload { parent.progressEmit(it) } + parent.progressEmit(100) parent.refreshEmit(false) if (prefs.animate) circularReveal() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt index 372a7bad..9f2437b0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt @@ -53,7 +53,7 @@ class FrostChromeClient( // private val refresh: SendChannel = web.parent.refreshChannel private val refreshEmit = web.parent.refreshEmit - private val progress: SendChannel = web.parent.progressChannel + private val progressEmit = web.parent.progressEmit private val title: SendChannel = web.parent.titleChannel private val context = web.context!! @@ -74,7 +74,7 @@ class FrostChromeClient( override fun onProgressChanged(view: WebView, newProgress: Int) { super.onProgressChanged(view, newProgress) - progress.offer(newProgress) + progressEmit(newProgress) } override fun onShowFileChooser( @@ -89,7 +89,7 @@ class FrostChromeClient( private fun JsResult.frostCancel() { cancel() refreshEmit(false) - progress.offer(100) + progressEmit(100) } override fun onJsAlert( -- cgit v1.2.3 From bbfac885b89a79af2c85f5f0df7635770b49a07a Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 22 Nov 2021 22:57:55 -0800 Subject: Convert title channel to flow --- .../kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt | 8 +++----- .../com/pitchedapps/frost/contracts/FrostContentContract.kt | 6 +++--- .../main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt | 8 ++++++-- .../main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt | 5 ++--- 4 files changed, 14 insertions(+), 13 deletions(-) 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 171583ed..ae8d442f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -27,7 +27,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import ca.allanwang.kau.swipe.SwipeBackContract import ca.allanwang.kau.swipe.kauSwipeOnCreate import ca.allanwang.kau.swipe.kauSwipeOnDestroy -import ca.allanwang.kau.utils.ContextHelper import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.copyToClipboard import ca.allanwang.kau.utils.darken @@ -56,7 +55,6 @@ import com.pitchedapps.frost.facebook.USER_AGENT import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP_CONST import com.pitchedapps.frost.facebook.USER_AGENT_MOBILE_CONST import com.pitchedapps.frost.facebook.formattedFbUrl -import com.pitchedapps.frost.kotlin.subscribeDuringJob import com.pitchedapps.frost.utils.ARG_URL import com.pitchedapps.frost.utils.ARG_USER_ID import com.pitchedapps.frost.utils.BiometricUtils @@ -69,6 +67,8 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @@ -215,9 +215,7 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT content.bind(this) - content.titleChannel.subscribeDuringJob(this, ContextHelper.coroutineContext) { - toolbar.title = it - } + content.titleFlow.onEach { toolbar.title = it }.launchIn(this) with(web) { userAgentString = userAgent diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt index 8ebf7af7..d32cb873 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt @@ -21,7 +21,6 @@ import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.web.FrostEmitter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.flow.SharedFlow /** @@ -72,8 +71,9 @@ interface FrostContentParent : DynamicUiContract { /** * Observable to get new title data (unique values only) */ - // todo note that this should be like a behavior subject vs publish subject - val titleChannel: BroadcastChannel + val titleFlow: SharedFlow + + val titleEmit: FrostEmitter var baseUrl: String 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 3ec80f36..b76f6e39 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -80,7 +80,6 @@ class FrostContentRecycler @JvmOverloads constructor( override val layoutRes: Int = R.layout.view_content_base_recycler } -@UseExperimental(ExperimentalCoroutinesApi::class) abstract class FrostContentView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -152,7 +151,12 @@ abstract class FrostContentViewBase( override val progressEmit: FrostEmitter = FrostEmitter { progressMutableFlow.tryEmit(it) } - override val titleChannel: BroadcastChannel = ConflatedBroadcastChannel() + private val titleMutableFlow = MutableStateFlow("") + + override val titleFlow: SharedFlow = titleMutableFlow.asSharedFlow() + + override val titleEmit: FrostEmitter = + FrostEmitter { titleMutableFlow.tryEmit(it) } override lateinit var scope: CoroutineScope diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt index 9f2437b0..90345aa2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt @@ -34,7 +34,6 @@ import com.pitchedapps.frost.contracts.WebFileChooser import com.pitchedapps.frost.injectors.ThemeProvider import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.views.FrostWebView -import kotlinx.coroutines.channels.SendChannel /** * Created by Allan Wang on 2017-05-31. @@ -54,7 +53,7 @@ class FrostChromeClient( // private val refresh: SendChannel = web.parent.refreshChannel private val refreshEmit = web.parent.refreshEmit private val progressEmit = web.parent.progressEmit - private val title: SendChannel = web.parent.titleChannel + private val titleEmit = web.parent.titleEmit private val context = web.context!! override fun getDefaultVideoPoster(): Bitmap? = @@ -69,7 +68,7 @@ class FrostChromeClient( override fun onReceivedTitle(view: WebView, title: String) { super.onReceivedTitle(view, title) if (title.startsWith("http")) return - this.title.offer(title) + titleEmit(title) } override fun onProgressChanged(view: WebView, newProgress: Int) { -- cgit v1.2.3 From dcd0db9282d92beacd35b3418d924ff3c607dead Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 22 Nov 2021 23:21:34 -0800 Subject: Convert header channel to flow --- .../pitchedapps/frost/activities/MainActivity.kt | 39 ++++++++++++++++------ .../frost/contracts/ActivityContract.kt | 7 +++- .../pitchedapps/frost/views/FrostContentView.kt | 2 -- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 5 ++- 4 files changed, 36 insertions(+), 17 deletions(-) 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 755064cd..4b18088c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -18,23 +18,33 @@ 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.facebook.parsers.BadgeParser -import com.pitchedapps.frost.kotlin.subscribeDuringJob import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.views.BadgedIcon +import com.pitchedapps.frost.web.FrostEmitter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach @UseExperimental(ExperimentalCoroutinesApi::class) class MainActivity : BaseMainActivity() { override val fragmentChannel = BroadcastChannel(10) - override val headerBadgeChannel = BroadcastChannel(Channel.CONFLATED) + + private val headerMutableFlow = MutableStateFlow("") + override val headerFlow: SharedFlow = headerMutableFlow.asSharedFlow() + override val headerEmit: FrostEmitter = FrostEmitter { headerMutableFlow.tryEmit(it) } override fun onNestedCreate(savedInstanceState: Bundle?) { with(contentBinding) { @@ -90,12 +100,18 @@ class MainActivity : BaseMainActivity() { (tab.customView as BadgedIcon).badgeText = null } }) - headerBadgeChannel.subscribeDuringJob(this@MainActivity, Dispatchers.IO) { html -> - val data = - BadgeParser.parseFromData(cookie = fbCookie.webCookie, text = html)?.data - ?: return@subscribeDuringJob - L.v { "Badges $data" } - withMainContext { + headerFlow + .filter { it.isNotBlank() } + .mapNotNull { html -> + BadgeParser.parseFromData( + cookie = fbCookie.webCookie, + text = html + )?.data + } + .distinctUntilChanged() + .flowOn(Dispatchers.IO) + .onEach { data -> + L.v { "Badges $data" } tabsForEachView { _, view -> when (view.iicon) { FbItem.FEED.icon -> view.badgeText = data.feed @@ -105,6 +121,7 @@ class MainActivity : BaseMainActivity() { } } } - } + .flowOn(Dispatchers.Main) + .launchIn(this@MainActivity) } } 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 756b1f3d..83110417 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt @@ -18,13 +18,18 @@ package com.pitchedapps.frost.contracts import com.mikepenz.iconics.typeface.IIcon import com.pitchedapps.frost.fragments.BaseFragment +import com.pitchedapps.frost.web.FrostEmitter import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.flow.SharedFlow @UseExperimental(ExperimentalCoroutinesApi::class) interface MainActivityContract : MainFabContract { val fragmentChannel: BroadcastChannel - val headerBadgeChannel: BroadcastChannel + + val headerFlow: SharedFlow + val headerEmit: FrostEmitter + fun setTitle(res: Int) fun setTitle(text: CharSequence) 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 b76f6e39..75d1ffe4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -44,9 +44,7 @@ import com.pitchedapps.frost.web.FrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow 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 f43f3b81..4d92e8c2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -32,7 +32,6 @@ 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 import javax.inject.Inject @@ -50,7 +49,7 @@ class FrostJSI @Inject internal constructor( private val mainActivity: MainActivity? = activity as? MainActivity private val webActivity: WebOverlayActivityBase? = activity as? WebOverlayActivityBase - private val header: SendChannel? = mainActivity?.headerBadgeChannel + private val headerEmit: FrostEmitter? = mainActivity?.headerEmit private val cookies: List = activity.cookies() /** @@ -159,7 +158,7 @@ class FrostJSI @Inject internal constructor( @JavascriptInterface fun handleHeader(html: String?) { html ?: return - header?.offer(html) + headerEmit?.invoke(html) } @JavascriptInterface -- cgit v1.2.3 From 30d6fd9d33e17110726a299749b058416ed77ecf Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 23 Nov 2021 11:46:10 -0800 Subject: Convert fragment channel to flow --- .../frost/activities/BaseMainActivity.kt | 19 ++++++++-------- .../pitchedapps/frost/activities/MainActivity.kt | 12 +++++++---- .../frost/contracts/ActivityContract.kt | 3 ++- .../pitchedapps/frost/fragments/BaseFragment.kt | 25 ++++++++-------------- .../frost/fragments/FragmentContract.kt | 10 +-------- 5 files changed, 30 insertions(+), 39 deletions(-) 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 84352cb4..8585f68b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -498,7 +498,10 @@ abstract class BaseMainActivity : ) positiveButton(R.string.kau_yes) { this@BaseMainActivity.launch { - fbCookie.logout(this@BaseMainActivity, deleteCookie = true) + fbCookie.logout( + this@BaseMainActivity, + deleteCookie = true + ) } } negativeButton(R.string.kau_no) @@ -637,7 +640,7 @@ abstract class BaseMainActivity : private fun refreshAll() { L.d { "Refresh all" } - fragmentChannel.offer(REQUEST_REFRESH) + fragmentEmit(REQUEST_REFRESH) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -737,19 +740,19 @@ abstract class BaseMainActivity : * These results can be stacked */ if (hasRequest(REQUEST_REFRESH)) { - fragmentChannel.offer(REQUEST_REFRESH) + fragmentEmit(REQUEST_REFRESH) } if (hasRequest(REQUEST_NAV)) { frostNavigationBar(prefs, themeProvider) } if (hasRequest(REQUEST_TEXT_ZOOM)) { - fragmentChannel.offer(REQUEST_TEXT_ZOOM) + fragmentEmit(REQUEST_TEXT_ZOOM) } if (hasRequest(REQUEST_SEARCH)) { invalidateOptionsMenu() } if (hasRequest(REQUEST_FAB)) { - fragmentChannel.offer(lastPosition) + fragmentEmit(lastPosition) } if (hasRequest(REQUEST_NOTIFICATION)) { scheduleNotificationsFromPrefs(prefs) @@ -792,7 +795,6 @@ abstract class BaseMainActivity : override fun onDestroy() { controlWebview?.destroy() super.onDestroy() - fragmentChannel.close() } override fun collapseAppBar() { @@ -864,10 +866,9 @@ abstract class BaseMainActivity : lastPosition = 0 viewpager.setCurrentItem(0, false) viewpager.offscreenPageLimit = pages.size + // todo check if post is necessary viewpager.post { - if (!fragmentChannel.isClosedForSend) { - fragmentChannel.offer(0) - } + fragmentEmit(0) } // trigger hook so title is set } } 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 4b18088c..97067b21 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -27,6 +27,8 @@ import com.pitchedapps.frost.web.FrostEmitter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -39,8 +41,10 @@ import kotlinx.coroutines.flow.onEach @UseExperimental(ExperimentalCoroutinesApi::class) class MainActivity : BaseMainActivity() { - - override val fragmentChannel = BroadcastChannel(10) + + private val fragmentMutableFlow = MutableSharedFlow(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + override val fragmentFlow: SharedFlow = fragmentMutableFlow.asSharedFlow() + override val fragmentEmit: FrostEmitter = FrostEmitter { fragmentMutableFlow.tryEmit(it) } private val headerMutableFlow = MutableStateFlow("") override val headerFlow: SharedFlow = headerMutableFlow.asSharedFlow() @@ -61,9 +65,9 @@ class MainActivity : BaseMainActivity() { return } if (lastPosition != -1) { - fragmentChannel.offer(-(lastPosition + 1)) + fragmentEmit(-(lastPosition + 1)) } - fragmentChannel.offer(position) + fragmentEmit(position) lastPosition = position } 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 83110417..3c5d7412 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt @@ -25,7 +25,8 @@ import kotlinx.coroutines.flow.SharedFlow @UseExperimental(ExperimentalCoroutinesApi::class) interface MainActivityContract : MainFabContract { - val fragmentChannel: BroadcastChannel + val fragmentFlow: SharedFlow + val fragmentEmit: FrostEmitter val headerFlow: SharedFlow val headerEmit: FrostEmitter diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt index 79495b2a..5c97de65 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.flowWithLifecycle import ca.allanwang.kau.utils.ContextHelper import ca.allanwang.kau.utils.fadeScaleTransition import ca.allanwang.kau.utils.setIcon @@ -47,8 +48,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -121,7 +123,6 @@ abstract class BaseFragment : } override var firstLoad: Boolean = true - private var activityReceiver: ReceiveChannel? = null private var onCreateRunnable: ((FragmentContract) -> Unit)? = null override var content: FrostContentParent? = null @@ -152,8 +153,7 @@ abstract class BaseFragment : onCreateRunnable?.invoke(this) onCreateRunnable = null firstLoadRequest() - detachMainObservable() - activityReceiver = attachMainObservable(mainContract) + attach(mainContract) } override fun setUserVisibleHint(isVisibleToUser: Boolean) { @@ -177,10 +177,10 @@ abstract class BaseFragment : mainContract.setTitle(title) } - override fun attachMainObservable(contract: MainActivityContract): ReceiveChannel { - val receiver = contract.fragmentChannel.openSubscription() - launch { - for (flag in receiver) { + override fun attach(contract: MainActivityContract) { + contract.fragmentFlow + .flowWithLifecycle(viewLifecycleOwner.lifecycle) + .onEach { flag -> when (flag) { REQUEST_REFRESH -> { core?.apply { @@ -201,9 +201,7 @@ abstract class BaseFragment : reloadTextSize() } } - } - } - return receiver + }.launchIn(this) } override fun updateFab(contract: MainFabContract) { @@ -222,16 +220,11 @@ abstract class BaseFragment : setOnClickListener { click() } } - override fun detachMainObservable() { - activityReceiver?.cancel() - } - override fun onDestroyView() { super.onDestroyView() L.i { "Fragment on destroy $position ${hashCode()}" } content?.destroy() content = null - detachMainObservable() } override fun onDestroy() { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt index 10c612c5..beac7494 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt @@ -22,7 +22,6 @@ import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.contracts.MainFabContract import com.pitchedapps.frost.views.FrostRecyclerView -import kotlinx.coroutines.channels.ReceiveChannel /** * Created by Allan Wang on 2017-11-07. @@ -77,15 +76,8 @@ interface FragmentContract : FrostContentContainer { /** * Call whenever a fragment is attached so that it may listen * to activity emissions. - * Returns a means of closing the listener, which can be called from [detachMainObservable] */ - fun attachMainObservable(contract: MainActivityContract): ReceiveChannel - - /** - * Call when fragment is detached so that any existing - * observable is disposed - */ - fun detachMainObservable() + fun attach(contract: MainActivityContract) /* * ----------------------------------------- -- cgit v1.2.3 From 0c97abd838ee834ef17d1b2e746fa26ea9663bd5 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 23 Nov 2021 11:50:06 -0800 Subject: Create extension for converting emitters --- .../kotlin/com/pitchedapps/frost/activities/MainActivity.kt | 8 ++++---- .../kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt | 1 - .../kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt | 1 - .../kotlin/com/pitchedapps/frost/views/FrostContentView.kt | 10 ++++------ app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt | 3 +++ 5 files changed, 11 insertions(+), 12 deletions(-) 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 97067b21..2e44e5f9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -24,9 +24,9 @@ import com.pitchedapps.frost.facebook.parsers.BadgeParser import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.views.BadgedIcon import com.pitchedapps.frost.web.FrostEmitter +import com.pitchedapps.frost.web.asFrostEmitter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -41,14 +41,14 @@ import kotlinx.coroutines.flow.onEach @UseExperimental(ExperimentalCoroutinesApi::class) class MainActivity : BaseMainActivity() { - + private val fragmentMutableFlow = MutableSharedFlow(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) override val fragmentFlow: SharedFlow = fragmentMutableFlow.asSharedFlow() - override val fragmentEmit: FrostEmitter = FrostEmitter { fragmentMutableFlow.tryEmit(it) } + override val fragmentEmit: FrostEmitter = fragmentMutableFlow.asFrostEmitter() private val headerMutableFlow = MutableStateFlow("") override val headerFlow: SharedFlow = headerMutableFlow.asSharedFlow() - override val headerEmit: FrostEmitter = FrostEmitter { headerMutableFlow.tryEmit(it) } + override val headerEmit: FrostEmitter = headerMutableFlow.asFrostEmitter() override fun onNestedCreate(savedInstanceState: Bundle?) { with(contentBinding) { 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 3c5d7412..721282aa 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt @@ -20,7 +20,6 @@ import com.mikepenz.iconics.typeface.IIcon import com.pitchedapps.frost.fragments.BaseFragment import com.pitchedapps.frost.web.FrostEmitter import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.flow.SharedFlow @UseExperimental(ExperimentalCoroutinesApi::class) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt index 5c97de65..e10fd141 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt @@ -47,7 +47,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive 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 75d1ffe4..f9d04ad1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -41,6 +41,7 @@ import com.pitchedapps.frost.injectors.ThemeProvider import com.pitchedapps.frost.prefs.Prefs import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.web.FrostEmitter +import com.pitchedapps.frost.web.asFrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -139,22 +140,19 @@ abstract class FrostContentViewBase( override val refreshFlow: SharedFlow = refreshMutableFlow.asSharedFlow() - override val refreshEmit: FrostEmitter = - FrostEmitter { refreshMutableFlow.tryEmit(it) } + override val refreshEmit: FrostEmitter = refreshMutableFlow.asFrostEmitter() private val progressMutableFlow = MutableStateFlow(0) override val progressFlow: SharedFlow = progressMutableFlow.asSharedFlow() - override val progressEmit: FrostEmitter = - FrostEmitter { progressMutableFlow.tryEmit(it) } + override val progressEmit: FrostEmitter = progressMutableFlow.asFrostEmitter() private val titleMutableFlow = MutableStateFlow("") override val titleFlow: SharedFlow = titleMutableFlow.asSharedFlow() - override val titleEmit: FrostEmitter = - FrostEmitter { titleMutableFlow.tryEmit(it) } + override val titleEmit: FrostEmitter = titleMutableFlow.asFrostEmitter() override lateinit var scope: CoroutineScope diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt index 30845a79..ba05a2c4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWeb.kt @@ -25,6 +25,7 @@ import dagger.hilt.DefineComponent import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewComponent +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import javax.inject.Qualifier import javax.inject.Scope @@ -62,6 +63,8 @@ interface FrostWebEntryPoint { fun interface FrostEmitter : (T) -> Unit +fun MutableSharedFlow.asFrostEmitter(): FrostEmitter = FrostEmitter { tryEmit(it) } + @Module @InstallIn(FrostWebComponent::class) object FrostWebFlowModule { -- cgit v1.2.3 From e8d9dca1ede1295f67e27faf731a5caa1bd2810a Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 23 Nov 2021 11:58:30 -0800 Subject: Convert login refresh channel to flow and remove some outdated tests --- .../pitchedapps/frost/activities/LoginActivity.kt | 34 ++++++-- .../com/pitchedapps/frost/utils/KotlinUtils.kt | 36 -------- .../pitchedapps/frost/views/FrostContentView.kt | 3 + .../com/pitchedapps/frost/utils/CoroutineTest.kt | 99 ---------------------- 4 files changed, 28 insertions(+), 144 deletions(-) delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/utils/KotlinUtils.kt 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 949f1ddd..a95e931b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -45,13 +45,20 @@ 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.uniqueOnly +import com.pitchedapps.frost.web.FrostEmitter import com.pitchedapps.frost.web.LoginWebView +import com.pitchedapps.frost.web.asFrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -76,7 +83,15 @@ class LoginActivity : BaseActivity() { private val profile: ImageView by bindView(R.id.profile) private lateinit var profileLoader: RequestManager - private val refreshChannel = Channel(10) + + private val refreshMutableFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + private val refreshFlow: SharedFlow = refreshMutableFlow.asSharedFlow() + + private val refreshEmit: FrostEmitter = refreshMutableFlow.asFrostEmitter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -87,11 +102,12 @@ class LoginActivity : BaseActivity() { toolbar(toolbar) } profileLoader = GlideApp.with(profile) - launch { - for (refreshing in refreshChannel.uniqueOnly(this)) { - swipeRefresh.isRefreshing = refreshing - } - } + + refreshFlow + .distinctUntilChanged() + .onEach { swipeRefresh.isRefreshing = it } + .launchIn(this) + launch { val cookie = web.loadLogin { refresh(it != 100) }.await() L.d { "Login found" } @@ -107,7 +123,7 @@ class LoginActivity : BaseActivity() { } private fun refresh(refreshing: Boolean) { - refreshChannel.offer(refreshing) + refreshEmit(refreshing) } private suspend fun loadInfo(cookie: CookieEntity): Unit = withMainContext { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/KotlinUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/KotlinUtils.kt deleted file mode 100644 index f4357c9b..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/KotlinUtils.kt +++ /dev/null @@ -1,36 +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 . - */ -package com.pitchedapps.frost.utils - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.produce -import kotlinx.coroutines.isActive - -@UseExperimental(ExperimentalCoroutinesApi::class) -fun ReceiveChannel.uniqueOnly(scope: CoroutineScope): ReceiveChannel = scope.produce { - var previous: T? = null - for (current in this@uniqueOnly) { - if (!scope.isActive) { - cancel() - } else if (previous != current) { - previous = current - send(current) - } - } -} 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 f9d04ad1..d2083816 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -132,6 +132,9 @@ abstract class FrostContentViewBase( /** * While this can be conflated, there exist situations where we wish to watch refresh cycles. * Here, we'd need to make sure we don't skip events + * + * TODO ensure there is only one flow provider is this is still separated in login + * Use case for shared flow is to avoid emitting before subscribing; buffer can probably be size 1 */ private val refreshMutableFlow = MutableSharedFlow( extraBufferCapacity = 10, diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt index 2744d0d8..43bd1563 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -41,7 +40,6 @@ 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 @@ -162,103 +160,6 @@ class CoroutineTest { } } - /** - * Not a true throttle, but for things like fetching header badges, we want to avoid simultaneous fetches. - * As a result, I want to test that the usage of offer along with a conflated channel will work as I expect. - * Events should be consumed when there is no pending consumer on previous elements. - */ - @Test - @Ignore("Move to flow") - fun throttledChannel() { - val channel = Channel(Channel.CONFLATED) - runBlocking { - val deferred = async { - listen(channel) { - // Throttle consumer - delay(10) - return@listen false - } - } - (0..100).forEach { - channel.offer(it) - delay(1) - } - channel.close() - val received = deferred.await() - assertTrue( - received.size < 20, - "Received data should be throttled; expected that around 1/10th of all events are consumed, but received ${received.size}" - ) - println(received) - } - } - - @Test - fun uniqueOnly() { - val channel = BroadcastChannel(100) - runBlocking { - val fullReceiver = channel.openSubscription() - val uniqueReceiver = channel.openSubscription().uniqueOnly(this) - - val fullDeferred = async { listen(fullReceiver) } - val uniqueDeferred = async { listen(uniqueReceiver) } - - listOf(0, 1, 2, 3, 3, 3, 4, 3, 5, 5, 1).forEach { - channel.offer(it) - } - channel.close() - - val fullData = fullDeferred.await() - val uniqueData = uniqueDeferred.await() - - assertEquals( - listOf(0, 1, 2, 3, 3, 3, 4, 3, 5, 5, 1), - fullData, - "Full receiver should get all channel events" - ) - assertEquals( - listOf(0, 1, 2, 3, 4, 3, 5, 1), - uniqueData, - "Unique receiver should not have two consecutive events that are equal" - ) - } - } - - /** - * 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(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" - ) - } - } - class TestException(msg: String) : RuntimeException(msg) @Test -- cgit v1.2.3 From 602290c33ac25f9308f8adfc459033cd50e13497 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 23 Nov 2021 12:02:11 -0800 Subject: Remove broadcast channel and flyweight --- .../com/pitchedapps/frost/kotlin/CoroutineUtils.kt | 39 ----- .../com/pitchedapps/frost/kotlin/Flyweight.kt | 174 --------------------- .../com/pitchedapps/frost/kotlin/FlyweightTest.kt | 120 -------------- .../com/pitchedapps/frost/utils/CoroutineTest.kt | 89 ----------- 4 files changed, 422 deletions(-) delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/kotlin/CoroutineUtils.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt delete mode 100644 app/src/test/kotlin/com/pitchedapps/frost/kotlin/FlyweightTest.kt diff --git a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/CoroutineUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/CoroutineUtils.kt deleted file mode 100644 index 6f8a60a9..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/CoroutineUtils.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2019 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 . - */ -package com.pitchedapps.frost.kotlin - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.launch -import kotlin.coroutines.CoroutineContext - -@UseExperimental(ExperimentalCoroutinesApi::class) -fun BroadcastChannel.subscribeDuringJob( - scope: CoroutineScope, - context: CoroutineContext, - onReceive: suspend (T) -> Unit -) { - val receiver = openSubscription() - scope.launch(context) { - for (r in receiver) { - onReceive(r) - } - } - scope.coroutineContext[Job]!!.invokeOnCompletion { receiver.cancel() } -} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt deleted file mode 100644 index 74765b58..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt +++ /dev/null @@ -1,174 +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 . - */ -package com.pitchedapps.frost.kotlin - -import com.pitchedapps.frost.utils.L -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.selects.select -import java.util.concurrent.ConcurrentHashMap - -/** - * Flyweight to keep track of values so long as they are valid. - * Values that have been fetched within [maxAge] from the time of use will be reused. - * If multiple requests are sent with the same key, then the value should only be fetched once. - * Otherwise, they will be fetched using [fetcher]. - * All requests will stem from the supplied [scope]. - */ -class Flyweight( - val scope: CoroutineScope, - val maxAge: Long, - private val fetcher: suspend (K) -> V -) { - - // Receives a key and a pending request - private val actionChannel = Channel>>(Channel.UNLIMITED) - - // Receives a key to invalidate the associated value - private val invalidatorChannel = Channel(Channel.UNLIMITED) - - // Receives a key and the resulting value - private val receiverChannel = Channel>>(Channel.UNLIMITED) - - // Keeps track of keys and associated update times - private val conditionMap: MutableMap = mutableMapOf() - - // Keeps track of keys and associated values - private val resultMap: MutableMap> = 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>> = ConcurrentHashMap() - - private val job: Job - - private fun CompletableDeferred.completeWith(result: Result) { - if (result.isSuccess) - complete(result.getOrNull()!!) - else - completeExceptionally(result.exceptionOrNull()!!) - } - - private val errHandler = - CoroutineExceptionHandler { _, throwable -> L.d { "FbAuth failed ${throwable.message}" } } - - init { - job = - scope.launch(Dispatchers.IO + SupervisorJob() + errHandler) { - launch { - while (isActive) { - select { - /* - * New request received. Continuation should be fulfilled eventually - */ - 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) { - completable.completeWith(lastResult) - } else { - val valueRequestPending = key in pendingMap - pendingMap.getOrPut(key) { mutableListOf() }.add(completable) - if (!valueRequestPending) - fulfill(key) - } - } - /* - * Invalidator received. Existing result associated with key should not be used. - * Note that any unfulfilled request and future requests should still operate, but with a new value. - */ - invalidatorChannel.onReceive { key -> - if (key !in resultMap) { - // Nothing to invalidate. - // If pending requests exist, they are already in the process of being updated. - return@onReceive - } - conditionMap.remove(key) - resultMap.remove(key) - if (pendingMap[key]?.isNotEmpty() == true) - // Refetch value for pending requests - fulfill(key) - } - /* - * Value request fulfilled. Should now fulfill pending requests - */ - receiverChannel.onReceive { (key, result) -> - conditionMap[key] = System.currentTimeMillis() - resultMap[key] = result - pendingMap.remove(key)?.forEach { - it.completeWith(result) - } - } - } - } - } - } - } - - /* - * 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 { - val completable = CompletableDeferred(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) - } - - fun cancel() { - job.cancel() - if (pendingMap.isNotEmpty()) { - val error = CancellationException("Flyweight cancelled") - pendingMap.values.flatten().forEach { it.completeExceptionally(error) } - pendingMap.clear() - } - actionChannel.close() - invalidatorChannel.close() - receiverChannel.close() - conditionMap.clear() - resultMap.clear() - } -} diff --git a/app/src/test/kotlin/com/pitchedapps/frost/kotlin/FlyweightTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/kotlin/FlyweightTest.kt deleted file mode 100644 index 89289322..00000000 --- a/app/src/test/kotlin/com/pitchedapps/frost/kotlin/FlyweightTest.kt +++ /dev/null @@ -1,120 +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 . - */ -package com.pitchedapps.frost.kotlin - -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.runBlocking -import org.junit.Rule -import org.junit.rules.Timeout -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 - -class FlyweightTest { - - @get:Rule - val globalTimeout: Timeout = Timeout.seconds(5) - - lateinit var flyweight: Flyweight - - lateinit var callCount: AtomicInteger - - private val LONG_RUNNING_KEY = -78 - - @BeforeTest - fun before() { - callCount = AtomicInteger(0) - flyweight = Flyweight(GlobalScope, 200L) { - callCount.incrementAndGet() - when (it) { - LONG_RUNNING_KEY -> Thread.sleep(100000) - else -> Thread.sleep(100) - } - it * 2 - } - } - - @Test - fun basic() { - assertEquals(2, runBlocking { flyweight.fetch(1).await() }, "Invalid result") - assertEquals(1, callCount.get(), "1 call expected") - } - - @Test - fun multipleWithOneKey() { - val results: List = runBlocking { - (0..1000).map { - flyweight.fetch(1) - }.map { it.await() } - } - assertEquals(1, callCount.get(), "1 call expected") - assertEquals(1001, results.size, "Incorrect number of results returned") - assertTrue(results.all { it == 2 }, "Result should all be 2") - } - - @Test - fun consecutiveReuse() { - runBlocking { - flyweight.fetch(1).await() - assertEquals(1, callCount.get(), "1 call expected") - flyweight.fetch(1).await() - assertEquals(1, callCount.get(), "Reuse expected") - Thread.sleep(300) - flyweight.fetch(1).await() - assertEquals(2, callCount.get(), "Refetch expected") - } - } - - @Test - fun invalidate() { - runBlocking { - flyweight.fetch(1).await() - assertEquals(1, callCount.get(), "1 call expected") - flyweight.invalidate(1) - flyweight.fetch(1).await() - assertEquals(2, callCount.get(), "New call expected") - } - } - - @Test - fun destroy() { - runBlocking { - val longRunningResult = flyweight.fetch(LONG_RUNNING_KEY) - flyweight.fetch(1).await() - flyweight.cancel() - try { - flyweight.fetch(1).await() - fail("Flyweight should not be fulfilled after it is destroyed") - } 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 (ignore: CancellationException) { - } - } - } -} diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt index 43bd1563..551d0b7b 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt @@ -16,15 +16,10 @@ */ package com.pitchedapps.frost.utils -import com.pitchedapps.frost.kotlin.Flyweight import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -81,49 +76,6 @@ class CoroutineTest { return@withContext data } - /** - * When refreshing, we have a temporary subscriber that hooks onto a single cycle. - * The refresh channel only contains booleans, but for the sake of identification, - * each boolean will have a unique integer attached. - * - * Things to note: - * Subscription should be opened outside of async, since we don't want to miss any events. - */ - @Test - fun refreshSubscriptions() { - val refreshChannel = BroadcastChannel>(100) - runBlocking { - // Listen to all events - val fullReceiver = refreshChannel.openSubscription() - val fullDeferred = async { listen(fullReceiver) } - - refreshChannel.send(true to 1) - refreshChannel.send(false to 2) - refreshChannel.send(true to 3) - - val partialReceiver = refreshChannel.openSubscription() - val partialDeferred = async { transition(partialReceiver) } - refreshChannel.send(false to 4) - refreshChannel.send(true to 5) - refreshChannel.send(false to 6) - refreshChannel.send(true to 7) - refreshChannel.close() - val fullStream = fullDeferred.await() - val partialStream = partialDeferred.await() - - assertEquals( - 7, - fullStream.size, - "Full stream should contain all events" - ) - assertEquals( - listOf(false to 4, true to 5, false to 6), - partialStream, - "Partial stream should include up until first true false pair" - ) - } - } - private fun SharedFlow.takeUntilNull(): Flow = takeWhile { it != null }.filterNotNull() @@ -159,45 +111,4 @@ class CoroutineTest { assertEquals(4, count, "Not all events received") } } - - class TestException(msg: String) : RuntimeException(msg) - - @Test - fun exceptionChecks() { - val mainTag = "main-test" - val mainDispatcher = Executors.newSingleThreadExecutor { r -> - Thread(r, mainTag) - }.asCoroutineDispatcher() - val channel = Channel() - - val job = SupervisorJob() - - val flyweight = Flyweight(GlobalScope, 200L) { - throw TestException("Flyweight exception") - } - - suspend fun crash(): Boolean = withContext(Dispatchers.IO) { - try { - withContext(Dispatchers.Default) { - flyweight.fetch(0).await() - } - true - } catch (e: TestException) { - false - } - } - - runBlocking(mainDispatcher + job) { - launch { - val i = channel.receive() - println("Received $i") - } - launch { - println("A") - println(crash()) - println("B") - channel.offer(1) - } - } - } } -- cgit v1.2.3 From 98b46d9e5341ac827ec776c6e5dd48ac301d4522 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 23 Nov 2021 12:06:40 -0800 Subject: Begin removing experimental coroutines annotation --- .../frost/activities/BaseMainActivity.kt | 2 -- .../pitchedapps/frost/activities/MainActivity.kt | 7 +++-- .../com/pitchedapps/frost/utils/CoroutineTest.kt | 35 ---------------------- 3 files changed, 4 insertions(+), 40 deletions(-) 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 8585f68b..9d16c63a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -130,7 +130,6 @@ import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.math.abs @@ -140,7 +139,6 @@ import kotlin.math.abs * * Most of the logic that is unrelated to handling fragments */ -@UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint abstract class BaseMainActivity : BaseActivity(), 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 2e44e5f9..16606691 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -26,7 +26,6 @@ import com.pitchedapps.frost.views.BadgedIcon import com.pitchedapps.frost.web.FrostEmitter import com.pitchedapps.frost.web.asFrostEmitter import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -39,10 +38,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach -@UseExperimental(ExperimentalCoroutinesApi::class) class MainActivity : BaseMainActivity() { - private val fragmentMutableFlow = MutableSharedFlow(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val fragmentMutableFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) override val fragmentFlow: SharedFlow = fragmentMutableFlow.asSharedFlow() override val fragmentEmit: FrostEmitter = fragmentMutableFlow.asFrostEmitter() diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt index 551d0b7b..7acb4761 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt @@ -17,22 +17,18 @@ package com.pitchedapps.frost.utils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async -import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.count import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import java.util.concurrent.Executors import kotlin.coroutines.EmptyCoroutineContext import kotlin.test.Test @@ -43,39 +39,8 @@ import kotlin.test.assertTrue /** * Collection of tests around coroutines */ -@UseExperimental(ExperimentalCoroutinesApi::class) class CoroutineTest { - /** - * Hooks onto the refresh channel for one true -> false cycle. - * Returns the list of event ids that were emitted - */ - private suspend fun transition(channel: ReceiveChannel>): List> { - var refreshed = false - return listen(channel) { (refreshing, _) -> - if (refreshed && !refreshing) - return@listen true - if (refreshing) - refreshed = true - return@listen false - } - } - - private suspend fun listen( - channel: ReceiveChannel, - shouldEnd: suspend (T) -> Boolean = { false } - ): List = - withContext(Dispatchers.IO) { - val data = mutableListOf() - channel.receiveAsFlow() - for (c in channel) { - data.add(c) - if (shouldEnd(c)) break - } - channel.cancel() - return@withContext data - } - private fun SharedFlow.takeUntilNull(): Flow = takeWhile { it != null }.filterNotNull() -- cgit v1.2.3 From 3599803b05188b8f9f4758b43fe120578fa8cf06 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 23 Nov 2021 12:10:23 -0800 Subject: Remove all experimental annotations --- app/build.gradle | 1 - .../main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt | 3 --- .../main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt | 2 -- .../kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt | 2 -- app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt | 2 -- app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt | 2 -- app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt | 2 -- 7 files changed, 14 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7e064152..f13c71e1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -135,7 +135,6 @@ android { } def compilerArgs = [ - "-Xuse-experimental=kotlin.Experimental", // "-XXLanguage:+InlineClasses", "-Xopt-in=kotlin.RequiresOptIn", ] 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 ae8d442f..8dbf9d5c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -65,7 +65,6 @@ import com.pitchedapps.frost.views.FrostVideoViewer import com.pitchedapps.frost.views.FrostWebView import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -87,7 +86,6 @@ import javax.inject.Inject * Used by notifications. Unlike the other overlays, this runs as a singleInstance * Going back will bring you back to the previous app */ -@UseExperimental(ExperimentalCoroutinesApi::class) class FrostWebActivity : WebOverlayActivityBase() { override fun onCreate(savedInstanceState: Bundle?) { @@ -151,7 +149,6 @@ class WebOverlayDesktopActivity : WebOverlayActivityBase(USER_AGENT_DESKTOP_CONS */ class WebOverlayActivity : WebOverlayActivityBase() -@UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT) : BaseActivity(), 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 721282aa..2b7f7b2c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt @@ -19,10 +19,8 @@ package com.pitchedapps.frost.contracts import com.mikepenz.iconics.typeface.IIcon import com.pitchedapps.frost.fragments.BaseFragment import com.pitchedapps.frost.web.FrostEmitter -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharedFlow -@UseExperimental(ExperimentalCoroutinesApi::class) interface MainActivityContract : MainFabContract { val fragmentFlow: SharedFlow val fragmentEmit: FrostEmitter diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt index d32cb873..7f91f901 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt @@ -20,7 +20,6 @@ import android.view.View import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.web.FrostEmitter import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharedFlow /** @@ -47,7 +46,6 @@ interface FrostContentContainer : CoroutineScope { * Contract for components shared among * all content providers */ -@UseExperimental(ExperimentalCoroutinesApi::class) interface FrostContentParent : DynamicUiContract { val scope: CoroutineScope diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt index e10fd141..a3303638 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/BaseFragment.kt @@ -44,7 +44,6 @@ import com.pitchedapps.frost.utils.REQUEST_TEXT_ZOOM import com.pitchedapps.frost.utils.frostEvent import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.launchIn @@ -59,7 +58,6 @@ import kotlin.coroutines.CoroutineContext * All fragments pertaining to the main view * Must be attached to activities implementing [MainActivityContract] */ -@UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint abstract class BaseFragment : Fragment(), 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 d2083816..c77ae590 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -44,7 +44,6 @@ import com.pitchedapps.frost.web.FrostEmitter import com.pitchedapps.frost.web.asFrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.MutableSharedFlow @@ -96,7 +95,6 @@ abstract class FrostContentView @JvmOverloads constructor( /** * Subsection of [FrostContentView] that is [AndroidEntryPoint] friendly (no generics) */ -@UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint abstract class FrostContentViewBase( context: Context, 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 9e21ede8..04ee7f3c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -29,7 +29,6 @@ import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.fragments.RecyclerContentContract import com.pitchedapps.frost.prefs.Prefs import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import javax.inject.Inject @@ -37,7 +36,6 @@ import javax.inject.Inject * Created by Allan Wang on 2017-05-29. * */ -@UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint class FrostRecyclerView @JvmOverloads constructor( context: Context, -- cgit v1.2.3 From db646e23ce0047461e68be1af7021ce6171ac850 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 23 Nov 2021 18:19:24 -0800 Subject: Remove channels from frost debugger --- .../main/kotlin/com/pitchedapps/frost/settings/Debug.kt | 14 +++++--------- .../kotlin/com/pitchedapps/frost/views/FrostContentView.kt | 7 +++++-- app/src/main/play/en-US/whatsnew | 6 ++---- app/src/main/res/xml/frost_changelog.xml | 5 +++++ docs/Changelog.md | 3 +++ 5 files changed, 20 insertions(+), 15 deletions(-) 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 b2bb1d11..6ba2c64d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt @@ -42,7 +42,7 @@ import com.pitchedapps.frost.utils.frostUriFromFile import com.pitchedapps.frost.utils.sendFrostEmail import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import java.io.File @@ -71,6 +71,7 @@ fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = { val parsers = arrayOf(NotifParser, MessageParser, SearchParser) materialDialog { + // noinspection CheckResult listItems(items = parsers.map { string(it.nameRes) }) { dialog, position, _ -> dialog.dismiss() val parser = parsers[position] @@ -133,20 +134,15 @@ fun SettingsActivity.sendDebug(url: String, html: String?) { onDismiss { job.cancel() } } - val progressChannel = Channel(10) + val progressFlow = MutableStateFlow(0) - launchMain { - for (p in progressChannel) { -// md.setProgress(p) - } - } +// progressFlow.onEach { md.setProgress(it) }.launchIn(this) launchMain { val success = downloader.loadAndZip(ZIP_NAME) { - progressChannel.offer(it) + progressFlow.tryEmit(it) } md.dismiss() - progressChannel.close() if (success) { val zipUri = frostUriFromFile( File(downloader.baseDir, "$ZIP_NAME.zip") 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 c77ae590..16c28c02 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -44,8 +44,8 @@ import com.pitchedapps.frost.web.FrostEmitter import com.pitchedapps.frost.web.asFrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -58,6 +58,7 @@ import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.flow.transformWhile import javax.inject.Inject +@ExperimentalCoroutinesApi class FrostContentWeb @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -68,6 +69,7 @@ class FrostContentWeb @JvmOverloads constructor( override val layoutRes: Int = R.layout.view_content_base_web } +@ExperimentalCoroutinesApi class FrostContentRecycler @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -78,6 +80,7 @@ class FrostContentRecycler @JvmOverloads constructor( override val layoutRes: Int = R.layout.view_content_base_recycler } +@ExperimentalCoroutinesApi abstract class FrostContentView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -96,6 +99,7 @@ abstract class FrostContentView @JvmOverloads constructor( * Subsection of [FrostContentView] that is [AndroidEntryPoint] friendly (no generics) */ @AndroidEntryPoint +@ExperimentalCoroutinesApi abstract class FrostContentViewBase( context: Context, attrs: AttributeSet?, @@ -239,7 +243,6 @@ abstract class FrostContentViewBase( } private var transitionStart: Long = -1 - private var refreshReceiver: ReceiveChannel? = null /** * Hook onto the refresh observable for one cycle diff --git a/app/src/main/play/en-US/whatsnew b/app/src/main/play/en-US/whatsnew index 7ca9e6af..eb46a757 100644 --- a/app/src/main/play/en-US/whatsnew +++ b/app/src/main/play/en-US/whatsnew @@ -1,5 +1,3 @@ -v3.1.2 +v3.2.0 -* Fix loading full size images -* Fix menu tab -* Always load messenger internally \ No newline at end of file +* Improve loading process \ No newline at end of file diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index b3227a6e..2bdfef82 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -6,6 +6,11 @@ --> + + + + + diff --git a/docs/Changelog.md b/docs/Changelog.md index b8dace0a..0e9c638e 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,5 +1,8 @@ # Changelog +## v3.2.0 +* Improve loading process + ## v3.1.2 * Fix loading full size images * Fix menu tab -- cgit v1.2.3