diff options
author | Allan Wang <me@allanwang.ca> | 2021-11-22 22:24:17 -0800 |
---|---|---|
committer | Allan Wang <me@allanwang.ca> | 2021-11-22 22:24:17 -0800 |
commit | 1dd7be9174f1740aa1cae29f6d62d6f83f5917ba (patch) | |
tree | 5734afee77ddd8ea498df7065a1ec3de28a387f7 | |
parent | eb2e0d07f278eb2079666ffabcbee007173c17af (diff) | |
download | frost-1dd7be9174f1740aa1cae29f6d62d6f83f5917ba.tar.gz frost-1dd7be9174f1740aa1cae29f6d62d6f83f5917ba.tar.bz2 frost-1dd7be9174f1740aa1cae29f6d62d6f83f5917ba.zip |
Migrate refresh channel to flow
11 files changed, 380 insertions, 89 deletions
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<Boolean> + val refreshFlow: SharedFlow<Boolean> + + val refreshEmit: FrostEmitter<Boolean> /** * 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<Boolean> = BroadcastChannel(10) + private val refreshMutableFlow = MutableSharedFlow<Boolean>( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + override val refreshFlow: SharedFlow<Boolean> = refreshMutableFlow.asSharedFlow() + + override val refreshEmit: FrostEmitter<Boolean> = + FrostEmitter { refreshMutableFlow.tryEmit(it) } + override val progressChannel: BroadcastChannel<Int> = ConflatedBroadcastChannel() override val titleChannel: BroadcastChannel<String> = 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<Boolean> = web.parent.refreshChannel +// private val refresh: SendChannel<Boolean> = web.parent.refreshChannel + private val refreshEmit = web.parent.refreshEmit private val progress: SendChannel<Int> = web.parent.progressChannel private val title: SendChannel<String> = 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<Boolean> ) { private val mainActivity: MainActivity? = activity as? MainActivity private val webActivity: WebOverlayActivityBase? = activity as? WebOverlayActivityBase private val header: SendChannel<String>? = mainActivity?.headerBadgeChannel - private val refresh: SendChannel<Boolean> = web.parent.refreshChannel private val cookies: List<CookieEntity> = 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 <http://www.gnu.org/licenses/>. + */ +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> : (T) -> Unit + +@Module +@InstallIn(FrostWebComponent::class) +object FrostWebFlowModule { + @Provides + @FrostWebScoped + @FrostRefresh + fun refreshFlow(parent: FrostContentParent): SharedFlow<Boolean> = parent.refreshFlow + + @Provides + @FrostWebScoped + @FrostRefresh + fun refreshEmit(parent: FrostContentParent): FrostEmitter<Boolean> = 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<Boolean> = web.parent.refreshChannel +// protected val refresh: SendChannel<Boolean> = 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 <http://www.gnu.org/licenses/>. + */ +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<Boolean> +) : 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 |