mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-11-09 12:32:30 +01:00
Merge pull request #1945 from AllanWang/webview
This commit is contained in:
commit
66ba83ee2a
@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Kotlin2JvmCompilerArguments">
|
||||
<option name="jvmTarget" value="17" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.21" />
|
||||
</component>
|
||||
|
@ -156,7 +156,7 @@ dependencies {
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
|
||||
|
||||
def hilt = "2.43.2"
|
||||
def hilt = "2.46.1"
|
||||
implementation "com.google.dagger:hilt-android:${hilt}"
|
||||
kapt "com.google.dagger:hilt-android-compiler:${hilt}"
|
||||
|
||||
@ -171,6 +171,7 @@ dependencies {
|
||||
implementation "com.squareup.moshi:moshi-kotlin:${moshi}"
|
||||
implementation "com.squareup.moshi:moshi-adapters:${moshi}"
|
||||
ksp "com.squareup.moshi:moshi-kotlin-codegen:${moshi}"
|
||||
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.11"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
||||
|
||||
|
@ -19,6 +19,7 @@ package com.pitchedapps.frost
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.components.FrostDataStore
|
||||
import com.pitchedapps.frost.db.FrostDb
|
||||
@ -26,12 +27,15 @@ import com.pitchedapps.frost.ext.FrostAccountId
|
||||
import com.pitchedapps.frost.ext.idData
|
||||
import com.pitchedapps.frost.ext.launchActivity
|
||||
import com.pitchedapps.frost.extension.FrostCoreExtension
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.main.MainActivity
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabListAction
|
||||
import com.pitchedapps.frost.web.state.state.HomeTabSessionState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -43,22 +47,44 @@ import kotlinx.coroutines.withContext
|
||||
* and will launch another activity without history after doing initialization work.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class StartActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
||||
class StartActivity : AppCompatActivity() {
|
||||
|
||||
@Inject lateinit var frostDb: FrostDb
|
||||
|
||||
@Inject lateinit var dataStore: FrostDataStore
|
||||
|
||||
@Inject lateinit var frostCoreExtension: FrostCoreExtension
|
||||
|
||||
@Inject lateinit var store: FrostWebStore
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
launch {
|
||||
lifecycleScope.launch {
|
||||
val id = withContext(Dispatchers.IO) { getCurrentAccountId() }
|
||||
|
||||
frostCoreExtension.install()
|
||||
// frostCoreExtension.install()
|
||||
|
||||
logger.atInfo().log("Starting Frost with id %s", id)
|
||||
|
||||
// TODO load real tabs
|
||||
store.dispatch(TabListAction.SetHomeTabs(data = listOf(FbItem.Feed, FbItem.Menu)))
|
||||
// Test something scrollable
|
||||
store.dispatch(
|
||||
TabAction(
|
||||
tabId = HomeTabSessionState.homeTabId(0),
|
||||
TabAction.ContentAction.UpdateUrlAction(
|
||||
"https://github.com/AllanWang/Frost-for-Facebook"
|
||||
),
|
||||
),
|
||||
)
|
||||
store.dispatch(
|
||||
TabAction(
|
||||
tabId = HomeTabSessionState.homeTabId(1),
|
||||
TabAction.ContentAction.UpdateUrlAction("https://github.com/AllanWang/KAU"),
|
||||
),
|
||||
)
|
||||
|
||||
launchActivity<MainActivity>(
|
||||
intentBuilder = {
|
||||
flags =
|
||||
|
@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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.compose.webview
|
||||
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.view.NestedWebView
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.WebStepResponseAction
|
||||
import com.pitchedapps.frost.web.state.get
|
||||
import com.pitchedapps.frost.web.state.state.ContentState
|
||||
import com.pitchedapps.frost.webview.FrostChromeClient
|
||||
import com.pitchedapps.frost.webview.FrostWebViewClient
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.ext.flow
|
||||
import mozilla.components.lib.state.ext.observeAsState
|
||||
|
||||
class FrostWebCompose(
|
||||
val tabId: WebTargetId,
|
||||
private val store: FrostWebStore,
|
||||
private val client: FrostWebViewClient,
|
||||
private val chromeClient: FrostChromeClient,
|
||||
) {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
/**
|
||||
* Webview implementation in compose
|
||||
*
|
||||
* Based off of
|
||||
* https://github.com/google/accompanist/blob/main/web/src/main/java/com/google/accompanist/web/WebView.kt
|
||||
*
|
||||
* @param modifier A compose modifier
|
||||
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
|
||||
* the WebView back. navigation from outside the composable.
|
||||
* @param onCreated Called when the WebView is first created, this can be used to set additional
|
||||
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they
|
||||
* will be subsequently overwritten after this lambda is called.
|
||||
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved if
|
||||
* you need to save and restore state in this WebView.
|
||||
*/
|
||||
@Composable
|
||||
fun WebView(
|
||||
modifier: Modifier = Modifier,
|
||||
captureBackPresses: Boolean = true,
|
||||
onCreated: (WebView) -> Unit = {},
|
||||
onDispose: (WebView) -> Unit = {},
|
||||
) {
|
||||
|
||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||
|
||||
logger.atInfo().log("Webview %s %s", tabId, webView?.hashCode())
|
||||
|
||||
webView?.let { wv ->
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val canGoBack by
|
||||
store.observeAsState(initialValue = false) { it[tabId]?.content?.canGoBack == true }
|
||||
|
||||
BackHandler(captureBackPresses && canGoBack) { wv.goBack() }
|
||||
|
||||
LaunchedEffect(wv, store) {
|
||||
fun storeFlow(action: suspend Flow<ContentState>.() -> Unit) = launch {
|
||||
store.flow(lifecycleOwner).mapNotNull { it[tabId]?.content }.action()
|
||||
}
|
||||
|
||||
storeFlow {
|
||||
mapNotNull { it.transientState.targetUrl }
|
||||
.distinctUntilChanged()
|
||||
.collect { url ->
|
||||
store.dispatch(LoadUrlResponseAction(url))
|
||||
wv.loadUrl(url)
|
||||
}
|
||||
}
|
||||
storeFlow {
|
||||
mapNotNull { it.transientState.navStep }
|
||||
.distinctUntilChanged()
|
||||
.filter { it != 0 }
|
||||
.collect { steps ->
|
||||
store.dispatch(WebStepResponseAction(steps))
|
||||
if (wv.canGoBackOrForward(steps)) {
|
||||
wv.goBackOrForward(steps)
|
||||
} else {
|
||||
logger.atWarning().log("web %s cannot go back %d steps", tabId, steps)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
val childView =
|
||||
NestedWebView(context)
|
||||
.apply {
|
||||
onCreated(this)
|
||||
|
||||
logger.atInfo().log("Created webview for %s", tabId)
|
||||
|
||||
this.layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
|
||||
// state.viewState?.let {
|
||||
// this.restoreState(it)
|
||||
// }
|
||||
|
||||
webChromeClient = chromeClient
|
||||
webViewClient = client
|
||||
|
||||
val url = store.state[tabId]?.content?.url
|
||||
if (url != null) loadUrl(url)
|
||||
}
|
||||
.also { webView = it }
|
||||
|
||||
// Workaround a crash on certain devices that expect WebView to be
|
||||
// wrapped in a ViewGroup.
|
||||
// b/243567497
|
||||
val parentLayout = NestedScrollView(context)
|
||||
parentLayout.layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
parentLayout.addView(childView)
|
||||
|
||||
parentLayout
|
||||
},
|
||||
modifier = modifier,
|
||||
onRelease = { parentFrame ->
|
||||
val wv = parentFrame.getChildAt(0) as WebView
|
||||
onDispose(wv)
|
||||
logger.atInfo().log("Released webview for %s", tabId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// override fun onReceivedError(
|
||||
// view: WebView,
|
||||
// request: WebResourceRequest?,
|
||||
// error: WebResourceError?
|
||||
// ) {
|
||||
// super.onReceivedError(view, request, error)
|
||||
//
|
||||
// if (error != null) {
|
||||
// state.errorsForCurrentRequest.add(WebViewError(request, error))
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -28,6 +28,8 @@ import kotlinx.coroutines.flow.map
|
||||
*/
|
||||
@JvmInline value class FrostAccountId(val id: Long)
|
||||
|
||||
@JvmInline value class WebTargetId(val id: String)
|
||||
|
||||
/**
|
||||
* Representation of gecko context id.
|
||||
*
|
||||
|
@ -39,6 +39,7 @@ import androidx.compose.material.icons.filled.Store
|
||||
import androidx.compose.material.icons.filled.Today
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.main.MainTabItem
|
||||
import compose.icons.FontAwesomeIcons
|
||||
import compose.icons.fontawesomeicons.Solid
|
||||
@ -118,8 +119,9 @@ enum class FbItem(
|
||||
}
|
||||
|
||||
/** Converts [FbItem] to [MainTabItem]. */
|
||||
fun FbItem.tab(context: Context): MainTabItem =
|
||||
fun FbItem.tab(context: Context, id: WebTargetId): MainTabItem =
|
||||
MainTabItem(
|
||||
id = id,
|
||||
title = context.getString(titleId),
|
||||
icon = icon,
|
||||
url = url,
|
||||
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2018 Allan Wang
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.pitchedapps.frost.facebook
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 21/12/17.
|
||||
*
|
||||
* Collection of regex matchers Input text must be properly unescaped
|
||||
*
|
||||
* See [StringEscapeUtils]
|
||||
*/
|
||||
|
||||
/** Matches the fb_dtsg component of a page containing it as a hidden value */
|
||||
val FB_DTSG_MATCHER: Regex by lazy { Regex("name=\"fb_dtsg\" value=\"(.*?)\"") }
|
||||
val FB_REV_MATCHER: Regex by lazy { Regex("\"app_version\":\"(.*?)\"") }
|
||||
|
||||
/** Matches user id from cookie */
|
||||
val FB_USER_MATCHER: Regex = Regex("c_user=([0-9]*);")
|
||||
|
||||
val FB_EPOCH_MATCHER: Regex = Regex(":([0-9]+)")
|
||||
val FB_NOTIF_ID_MATCHER: Regex = Regex("notif_([0-9]+)")
|
||||
val FB_MESSAGE_NOTIF_ID_MATCHER: Regex = Regex("(?:thread|user)_fbid_([0-9]+)")
|
||||
val FB_CSS_URL_MATCHER: Regex = Regex("url\\([\"|']?(.*?)[\"|']?\\)")
|
||||
val FB_JSON_URL_MATCHER: Regex = Regex("\"(http.*?)\"")
|
||||
val FB_IMAGE_ID_MATCHER: Regex = Regex("fbcdn.*?/[0-9]+_([0-9]+)_")
|
||||
val FB_REDIRECT_URL_MATCHER: Regex = Regex("url=(.*?fbcdn.*?)\"")
|
||||
|
||||
operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex)
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2023 Allan Wang
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.pitchedapps.frost.facebook
|
||||
|
||||
import com.pitchedapps.frost.facebook.FbUrlFormatter.Companion.VIDEO_REDIRECT
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/** [true] if url contains [FACEBOOK_COM] */
|
||||
inline val String?.isFacebookUrl
|
||||
get() = this != null && (contains(FACEBOOK_COM) || contains(FBCDN_NET))
|
||||
|
||||
inline val String?.isMessengerUrl
|
||||
get() = this != null && contains(MESSENGER_COM)
|
||||
|
||||
inline val String?.isFbCookie
|
||||
get() = this != null && contains("c_user")
|
||||
|
||||
/** [true] if url is a video and can be accepted by VideoViewer */
|
||||
inline val String.isVideoUrl
|
||||
get() = startsWith(VIDEO_REDIRECT) || (startsWith("https://video-") && contains(FBCDN_NET))
|
||||
|
||||
/** [true] if url directly leads to a usable image */
|
||||
inline val String.isImageUrl: Boolean
|
||||
get() {
|
||||
return contains(FBCDN_NET) && (contains(".png") || contains(".jpg"))
|
||||
}
|
||||
|
||||
/** [true] if url can be retrieved to get a direct image url */
|
||||
inline val String.isIndirectImageUrl: Boolean
|
||||
get() {
|
||||
return contains("/photo/view_full_size/") && contains("fbid=")
|
||||
}
|
||||
|
||||
/** [true] if url can be displayed in a different webview */
|
||||
inline val String?.isIndependent: Boolean
|
||||
get() {
|
||||
if (this == null || length < 5) return false // ignore short queries
|
||||
if (this[0] == '#' && !contains('/')) return false // ignore element values
|
||||
if (startsWith("http") && !isFacebookUrl) return true // ignore non facebook urls
|
||||
if (dependentSegments.any { contains(it) }) return false // ignore known dependent segments
|
||||
return true
|
||||
}
|
||||
|
||||
val dependentSegments =
|
||||
arrayOf(
|
||||
"photoset_token",
|
||||
"direct_action_execute",
|
||||
"messages/?pageNum",
|
||||
"sharer.php",
|
||||
"events/permalink",
|
||||
"events/feed/watch",
|
||||
/*
|
||||
* Add new members to groups
|
||||
*
|
||||
* No longer dependent again as of 12/20/2018
|
||||
*/
|
||||
// "madminpanel",
|
||||
/** Editing images */
|
||||
"/confirmation/?",
|
||||
/** Remove entry from "people you may know" */
|
||||
"/pymk/xout/",
|
||||
/*
|
||||
* Facebook messages have the following cases for the tid query
|
||||
* mid* or id* for newer threads, which can be launched in new windows
|
||||
* or a hash for old threads, which must be loaded on old threads
|
||||
*/
|
||||
"messages/read/?tid=id",
|
||||
"messages/read/?tid=mid",
|
||||
// For some reason townhall doesn't load independently
|
||||
// This will allow it to load, but going back unfortunately messes up the menu client
|
||||
// See https://github.com/AllanWang/Frost-for-Facebook/issues/1593
|
||||
"/townhall/"
|
||||
)
|
||||
|
||||
inline val String?.isExplicitIntent
|
||||
get() = this != null && (startsWith("intent://") || startsWith("market://"))
|
||||
|
||||
fun String.urlEncode(): String = URLEncoder.encode(this, StandardCharsets.UTF_8.name())
|
@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2018 Allan Wang
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.pitchedapps.frost.facebook
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-07-07.
|
||||
*
|
||||
* Custom url builder so we can easily test it without the Android framework
|
||||
*/
|
||||
inline val String.formattedFbUrl: String
|
||||
get() = FbUrlFormatter(this).toString()
|
||||
|
||||
inline val Uri.formattedFbUri: Uri
|
||||
get() {
|
||||
val url = toString()
|
||||
return if (url.startsWith("http")) Uri.parse(url.formattedFbUrl) else this
|
||||
}
|
||||
|
||||
class FbUrlFormatter(url: String) {
|
||||
private val queries = mutableMapOf<String, String>()
|
||||
private val cleaned: String
|
||||
|
||||
/**
|
||||
* Formats all facebook urls
|
||||
*
|
||||
* The order is very important:
|
||||
* 1. Wrapper links (discardables) are stripped away, resulting in the actual link
|
||||
* 2. CSS encoding is converted to normal encoding
|
||||
* 3. Url is completely decoded
|
||||
* 4. Url is split into sections
|
||||
*/
|
||||
init {
|
||||
cleaned = clean(url)
|
||||
}
|
||||
|
||||
fun clean(url: String): String {
|
||||
if (url.isBlank()) return ""
|
||||
var cleanedUrl = url
|
||||
if (cleanedUrl.startsWith("#!")) cleanedUrl = cleanedUrl.substring(2)
|
||||
val urlRef = cleanedUrl
|
||||
discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) }
|
||||
val changed = cleanedUrl != urlRef
|
||||
converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) }
|
||||
try {
|
||||
cleanedUrl = URLDecoder.decode(cleanedUrl, StandardCharsets.UTF_8.name())
|
||||
} catch (e: Exception) {
|
||||
logger.atWarning().withCause(e).log("Failed url formatting")
|
||||
return url
|
||||
}
|
||||
cleanedUrl = cleanedUrl.replace("&", "&")
|
||||
if (changed && !cleanedUrl.contains("?")) // ensure we aren't missing '?'
|
||||
cleanedUrl = cleanedUrl.replaceFirst("&", "?")
|
||||
val qm = cleanedUrl.indexOf("?")
|
||||
if (qm > -1) {
|
||||
cleanedUrl.substring(qm + 1).split("&").forEach {
|
||||
val p = it.split("=")
|
||||
queries[p[0]] = p.elementAtOrNull(1) ?: ""
|
||||
}
|
||||
cleanedUrl = cleanedUrl.substring(0, qm)
|
||||
}
|
||||
discardableQueries.forEach { queries.remove(it) }
|
||||
// Convert desktop urls to mobile ones
|
||||
cleanedUrl = cleanedUrl.replace(WWW_FACEBOOK_COM, FACEBOOK_BASE_COM)
|
||||
if (cleanedUrl.startsWith("/")) cleanedUrl = FB_URL_BASE + cleanedUrl.substring(1)
|
||||
cleanedUrl =
|
||||
cleanedUrl.replaceFirst(
|
||||
".facebook.com//",
|
||||
".facebook.com/"
|
||||
) // sometimes we are given a bad url
|
||||
logger.atFinest().log("Formatted url from %s to %s", url, cleanedUrl)
|
||||
return cleanedUrl
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
buildString {
|
||||
append(cleaned)
|
||||
if (queries.isNotEmpty()) {
|
||||
append("?")
|
||||
queries.forEach { (k, v) ->
|
||||
if (v.isEmpty()) {
|
||||
append("${k.urlEncode()}&")
|
||||
} else {
|
||||
append("${k.urlEncode()}=${v.urlEncode()}&")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.removeSuffix("&")
|
||||
|
||||
fun toLogList(): List<String> {
|
||||
val list = mutableListOf(cleaned)
|
||||
queries.forEach { (k, v) -> list.add("\n- $k\t=\t$v") }
|
||||
list.add("\n\n${toString()}")
|
||||
return list
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
const val VIDEO_REDIRECT = "/video_redirect/?src="
|
||||
|
||||
/**
|
||||
* Items here are explicitly removed from the url Taken from FaceSlim
|
||||
* https://github.com/indywidualny/FaceSlim/blob/master/app/src/main/java/org/indywidualni/fblite/util/Miscellany.java
|
||||
*
|
||||
* Note: Typically, in this case, the redirect url should have all the necessary queries I am
|
||||
* unsure how Facebook reacts in all cases, so the ones after the redirect are appended on
|
||||
* afterwards That shouldn't break anything
|
||||
*/
|
||||
val discardable =
|
||||
arrayOf(
|
||||
"http://lm.facebook.com/l.php?u=",
|
||||
"https://lm.facebook.com/l.php?u=",
|
||||
"http://m.facebook.com/l.php?u=",
|
||||
"https://m.facebook.com/l.php?u=",
|
||||
"http://touch.facebook.com/l.php?u=",
|
||||
"https://touch.facebook.com/l.php?u=",
|
||||
VIDEO_REDIRECT
|
||||
)
|
||||
|
||||
/**
|
||||
* Queries that are not necessary for independent links
|
||||
*
|
||||
* acontext is not required for "friends interested in" notifications
|
||||
*/
|
||||
val discardableQueries =
|
||||
arrayOf(
|
||||
"ref",
|
||||
"refid",
|
||||
"SharedWith",
|
||||
"fbclid",
|
||||
"h",
|
||||
"_ft_",
|
||||
"_tn_",
|
||||
"_xt_",
|
||||
"bacr",
|
||||
"frefs",
|
||||
"hc_ref",
|
||||
"loc_ref",
|
||||
"pn_ref"
|
||||
)
|
||||
|
||||
val converter =
|
||||
listOf(
|
||||
"\\3C " to "%3C",
|
||||
"\\3E " to "%3E",
|
||||
"\\23 " to "%23",
|
||||
"\\25 " to "%25",
|
||||
"\\7B " to "%7B",
|
||||
"\\7D " to "%7D",
|
||||
"\\7C " to "%7C",
|
||||
"\\5C " to "%5C",
|
||||
"\\5E " to "%5E",
|
||||
"\\7E " to "%7E",
|
||||
"\\5B " to "%5B",
|
||||
"\\5D " to "%5D",
|
||||
"\\60 " to "%60",
|
||||
"\\3B " to "%3B",
|
||||
"\\2F " to "%2F",
|
||||
"\\3F " to "%3F",
|
||||
"\\3A " to "%3A",
|
||||
"\\40 " to "%40",
|
||||
"\\3D " to "%3D",
|
||||
"\\26 " to "%26",
|
||||
"\\24 " to "%24",
|
||||
"\\2B " to "%2B",
|
||||
"\\22 " to "%22",
|
||||
"\\2C " to "%2C",
|
||||
"\\20 " to "%20"
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2023 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.hilt
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.BuildConfig
|
||||
import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.components.usecases.HomeTabsUseCases
|
||||
import com.pitchedapps.frost.main.MainActivity
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.util.Optional
|
||||
import javax.inject.Singleton
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage
|
||||
import mozilla.components.browser.icons.BrowserIcons
|
||||
import mozilla.components.browser.session.storage.SessionStorage
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.EngineAction
|
||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.DefaultSettings
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.Settings
|
||||
import mozilla.components.concept.engine.permission.SitePermissionsStorage
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.feature.prompts.PromptMiddleware
|
||||
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
||||
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.support.base.android.NotificationsDelegate
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FrostGeckoModule {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun geckoRuntime(@ApplicationContext context: Context): GeckoRuntime {
|
||||
val settings =
|
||||
GeckoRuntimeSettings.Builder()
|
||||
.consoleOutput(BuildConfig.DEBUG)
|
||||
.loginAutofillEnabled(true)
|
||||
// .debugLogging(false)
|
||||
.debugLogging(BuildConfig.DEBUG)
|
||||
.javaScriptEnabled(true)
|
||||
.build()
|
||||
|
||||
return GeckoRuntime.create(context, settings)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun client(@ApplicationContext context: Context, runtime: GeckoRuntime): Client {
|
||||
return GeckoViewFetchClient(context, runtime)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun settings(@Frost userAgent: Optional<String>): Settings {
|
||||
return DefaultSettings(userAgentString = userAgent.getOrNull())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun engine(
|
||||
@ApplicationContext context: Context,
|
||||
settings: Settings,
|
||||
runtime: GeckoRuntime
|
||||
): Engine {
|
||||
return GeckoEngine(context, settings, runtime)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun browserIcons(@ApplicationContext context: Context, client: Client): BrowserIcons {
|
||||
return BrowserIcons(context, client)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun sitePermissionStorage(
|
||||
@ApplicationContext context: Context,
|
||||
runtime: GeckoRuntime
|
||||
): SitePermissionsStorage {
|
||||
return GeckoSitePermissionsStorage(runtime, OnDiskSitePermissionsStorage(context))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun sessionStorage(@ApplicationContext context: Context, engine: Engine): SessionStorage {
|
||||
return SessionStorage(context, engine)
|
||||
}
|
||||
|
||||
private class LoggerMiddleWare : Middleware<BrowserState, BrowserAction> {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<BrowserState, BrowserAction>,
|
||||
next: (BrowserAction) -> Unit,
|
||||
action: BrowserAction
|
||||
) {
|
||||
if (action is EngineAction.LoadUrlAction) {
|
||||
logger.atInfo().log("BrowserAction: LoadUrlAction %s", action.url)
|
||||
} else {
|
||||
logger.atInfo().log("BrowserAction: %s - %s", action::class.simpleName, action)
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun browserStore(
|
||||
@ApplicationContext context: Context,
|
||||
icons: BrowserIcons,
|
||||
sitePermissionsStorage: SitePermissionsStorage,
|
||||
engine: Engine,
|
||||
notificationsDelegate: NotificationsDelegate,
|
||||
): BrowserStore {
|
||||
|
||||
val middleware = buildList {
|
||||
if (BuildConfig.DEBUG) add(LoggerMiddleWare())
|
||||
add(HomeTabsUseCases.HomeMiddleware())
|
||||
add(PromptMiddleware())
|
||||
// add(DownloadMiddleware(context, DownloadService::class.java))
|
||||
addAll(EngineMiddleware.create(engine))
|
||||
}
|
||||
|
||||
val store = BrowserStore(middleware = middleware)
|
||||
icons.install(engine, store)
|
||||
WebNotificationFeature(
|
||||
context = context,
|
||||
engine = engine,
|
||||
browserIcons = icons,
|
||||
smallIcon = R.mipmap.ic_launcher_round,
|
||||
sitePermissionsStorage = sitePermissionsStorage,
|
||||
activityClass = MainActivity::class.java,
|
||||
notificationsDelegate = notificationsDelegate,
|
||||
)
|
||||
return store
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun notificationDelegate(@ApplicationContext context: Context): NotificationsDelegate {
|
||||
return NotificationsDelegate(NotificationManagerCompat.from(context))
|
||||
}
|
||||
}
|
@ -16,46 +16,15 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.hilt
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.BuildConfig
|
||||
import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.components.usecases.HomeTabsUseCases
|
||||
import com.pitchedapps.frost.main.MainActivity
|
||||
import com.pitchedapps.frost.web.FrostAdBlock
|
||||
import dagger.BindsOptionalOf
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.util.Optional
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage
|
||||
import mozilla.components.browser.icons.BrowserIcons
|
||||
import mozilla.components.browser.session.storage.SessionStorage
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.EngineAction
|
||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.DefaultSettings
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.Settings
|
||||
import mozilla.components.concept.engine.permission.SitePermissionsStorage
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.feature.prompts.PromptMiddleware
|
||||
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
||||
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.support.base.android.NotificationsDelegate
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
|
||||
@Qualifier annotation class Frost
|
||||
|
||||
@ -63,9 +32,11 @@ import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface FrostBindModule {
|
||||
@BindsOptionalOf @Frost fun userAgent(): String
|
||||
|
||||
@BindsOptionalOf fun adBlock(): FrostAdBlock
|
||||
}
|
||||
|
||||
/** Module containing core Mozilla injections. */
|
||||
/** Module containing core Frost injections. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FrostModule {
|
||||
@ -86,115 +57,4 @@ object FrostModule {
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Safari/537.36"
|
||||
|
||||
@Provides @Singleton @Frost fun userAgent(): String = USER_AGENT_WINDOWS_FROST
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun geckoRuntime(@ApplicationContext context: Context): GeckoRuntime {
|
||||
val settings =
|
||||
GeckoRuntimeSettings.Builder()
|
||||
.consoleOutput(BuildConfig.DEBUG)
|
||||
.loginAutofillEnabled(true)
|
||||
// .debugLogging(false)
|
||||
.debugLogging(BuildConfig.DEBUG)
|
||||
.javaScriptEnabled(true)
|
||||
.build()
|
||||
|
||||
return GeckoRuntime.create(context, settings)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun client(@ApplicationContext context: Context, runtime: GeckoRuntime): Client {
|
||||
return GeckoViewFetchClient(context, runtime)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun settings(@Frost userAgent: Optional<String>): Settings {
|
||||
return DefaultSettings(userAgentString = userAgent.getOrNull())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun engine(
|
||||
@ApplicationContext context: Context,
|
||||
settings: Settings,
|
||||
runtime: GeckoRuntime
|
||||
): Engine {
|
||||
return GeckoEngine(context, settings, runtime)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun browserIcons(@ApplicationContext context: Context, client: Client): BrowserIcons {
|
||||
return BrowserIcons(context, client)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun sitePermissionStorage(
|
||||
@ApplicationContext context: Context,
|
||||
runtime: GeckoRuntime
|
||||
): SitePermissionsStorage {
|
||||
return GeckoSitePermissionsStorage(runtime, OnDiskSitePermissionsStorage(context))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun sessionStorage(@ApplicationContext context: Context, engine: Engine): SessionStorage {
|
||||
return SessionStorage(context, engine)
|
||||
}
|
||||
|
||||
private class LoggerMiddleWare : Middleware<BrowserState, BrowserAction> {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<BrowserState, BrowserAction>,
|
||||
next: (BrowserAction) -> Unit,
|
||||
action: BrowserAction
|
||||
) {
|
||||
if (action is EngineAction.LoadUrlAction) {
|
||||
logger.atInfo().log("BrowserAction: LoadUrlAction %s", action.url)
|
||||
} else {
|
||||
logger.atInfo().log("BrowserAction: %s - %s", action::class.simpleName, action)
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun browserStore(
|
||||
@ApplicationContext context: Context,
|
||||
icons: BrowserIcons,
|
||||
sitePermissionsStorage: SitePermissionsStorage,
|
||||
engine: Engine,
|
||||
notificationsDelegate: NotificationsDelegate,
|
||||
): BrowserStore {
|
||||
|
||||
val middleware = buildList {
|
||||
if (BuildConfig.DEBUG) add(LoggerMiddleWare())
|
||||
add(HomeTabsUseCases.HomeMiddleware())
|
||||
add(PromptMiddleware())
|
||||
// add(DownloadMiddleware(context, DownloadService::class.java))
|
||||
addAll(EngineMiddleware.create(engine))
|
||||
}
|
||||
|
||||
val store = BrowserStore(middleware = middleware)
|
||||
icons.install(engine, store)
|
||||
WebNotificationFeature(
|
||||
context = context,
|
||||
engine = engine,
|
||||
browserIcons = icons,
|
||||
smallIcon = R.mipmap.ic_launcher_round,
|
||||
sitePermissionsStorage = sitePermissionsStorage,
|
||||
activityClass = MainActivity::class.java,
|
||||
notificationsDelegate = notificationsDelegate,
|
||||
)
|
||||
return store
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun notificationDelegate(@ApplicationContext context: Context): NotificationsDelegate {
|
||||
return NotificationsDelegate(NotificationManagerCompat.from(context))
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2023 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.hilt
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.BuildConfig
|
||||
import com.pitchedapps.frost.web.state.FrostLoggerMiddleware
|
||||
import com.pitchedapps.frost.web.state.FrostWebReducer
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Module containing core WebView injections. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FrostWebViewModule {
|
||||
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
@Provides @Singleton fun cookieManager(): CookieManager = CookieManager.getInstance()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun frostWebStore(frostWebReducer: FrostWebReducer): FrostWebStore {
|
||||
val middleware = buildList { if (BuildConfig.DEBUG) add(FrostLoggerMiddleware()) }
|
||||
|
||||
return FrostWebStore(frostWebReducer = frostWebReducer, middleware = middleware)
|
||||
}
|
||||
}
|
@ -22,9 +22,10 @@ import androidx.activity.compose.setContent
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.compose.FrostTheme
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.facebook.tab
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import mozilla.components.lib.state.ext.observeAsState
|
||||
|
||||
/**
|
||||
* Main activity.
|
||||
@ -34,19 +35,25 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var store: FrostWebStore
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
logger.atInfo().log("onCreate main activity")
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
val tabs = FbItem.defaults().map { it.tab(this) } // TODO allow custom tabs
|
||||
|
||||
setContent {
|
||||
FrostTheme {
|
||||
MainScreen(
|
||||
tabs = tabs,
|
||||
)
|
||||
// MainScreen(
|
||||
// tabs = tabs,
|
||||
// )
|
||||
|
||||
val tabs =
|
||||
store.observeAsState(initialValue = null) { it.homeTabs.map { it.tab } }.value
|
||||
?: return@FrostTheme
|
||||
|
||||
MainScreenWebView(homeTabs = tabs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,12 @@
|
||||
package com.pitchedapps.frost.main
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
|
||||
/** Data representation of a single main tab entry. */
|
||||
data class MainTabItem(val title: String, val icon: ImageVector, val url: String)
|
||||
data class MainTabItem(
|
||||
val id: WebTargetId,
|
||||
val title: String,
|
||||
val icon: ImageVector,
|
||||
val url: String
|
||||
)
|
||||
|
@ -43,21 +43,6 @@ import com.pitchedapps.frost.ext.GeckoContextId
|
||||
import com.pitchedapps.frost.ext.components
|
||||
import mozilla.components.browser.state.helper.Target
|
||||
|
||||
@Composable
|
||||
fun MainScreen2(modifier: Modifier) {
|
||||
// Scaffold(
|
||||
// modifier = modifier,
|
||||
// topBar = {
|
||||
// MainTopBar(modifier = modifier)
|
||||
// },
|
||||
// )
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainTopBar(modifier: Modifier) {
|
||||
// TopAppBar(title = { /*TODO*/ })
|
||||
}
|
||||
|
||||
/**
|
||||
* Screen for MainActivity.
|
||||
*
|
||||
|
@ -26,6 +26,8 @@ import com.pitchedapps.frost.ext.idData
|
||||
import com.pitchedapps.frost.ext.toContextId
|
||||
import com.pitchedapps.frost.extension.FrostCoreExtension
|
||||
import com.pitchedapps.frost.hilt.FrostComponents
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.webview.FrostWebComposer
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
@ -39,6 +41,9 @@ internal constructor(
|
||||
@ApplicationContext context: Context,
|
||||
val components: FrostComponents,
|
||||
val frostCoreExtension: FrostCoreExtension,
|
||||
val store: FrostWebStore,
|
||||
val frostWebComposer: FrostWebComposer,
|
||||
// sample: FrostWebEntrySample,
|
||||
) : ViewModel() {
|
||||
|
||||
val contextIdFlow: Flow<GeckoContextId?> =
|
||||
|
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2023 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.main
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.pitchedapps.frost.compose.webview.FrostWebCompose
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabListAction.SelectHomeTab
|
||||
import com.pitchedapps.frost.webview.FrostWebComposer
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.ext.observeAsState
|
||||
|
||||
@Composable
|
||||
fun MainScreenWebView(modifier: Modifier = Modifier, homeTabs: List<MainTabItem>) {
|
||||
val vm: MainScreenViewModel = viewModel()
|
||||
|
||||
val selectedHomeTab by vm.store.observeAsState(initialValue = null) { it.selectedHomeTab }
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = { MainTopBar(modifier = modifier) },
|
||||
bottomBar = {
|
||||
MainBottomBar(
|
||||
selectedTab = selectedHomeTab,
|
||||
items = homeTabs,
|
||||
onSelect = { vm.store.dispatch(SelectHomeTab(it)) },
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
MainScreenWebContainer(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
selectedTab = selectedHomeTab,
|
||||
items = homeTabs,
|
||||
store = vm.store,
|
||||
frostWebComposer = vm.frostWebComposer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainTopBar(modifier: Modifier = Modifier) {
|
||||
TopAppBar(modifier = modifier, title = { Text(text = "Title") })
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedTab: WebTargetId?,
|
||||
items: List<MainTabItem>,
|
||||
onSelect: (WebTargetId) -> Unit
|
||||
) {
|
||||
NavigationBar(modifier = modifier) {
|
||||
items.forEach { item ->
|
||||
NavigationBarItem(
|
||||
icon = { Icon(item.icon, contentDescription = item.title) },
|
||||
selected = selectedTab == item.id,
|
||||
onClick = { onSelect(item.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainScreenWebContainer(
|
||||
modifier: Modifier,
|
||||
selectedTab: WebTargetId?,
|
||||
items: List<MainTabItem>,
|
||||
store: FrostWebStore,
|
||||
frostWebComposer: FrostWebComposer
|
||||
) {
|
||||
val homeTabComposables = remember(items) { items.map { frostWebComposer.create(it.id) } }
|
||||
|
||||
PullRefresh(
|
||||
modifier = modifier,
|
||||
store = store,
|
||||
) {
|
||||
MainPager(selectedTab, items = homeTabComposables)
|
||||
// homeTabComposables.find { it.tabId == selectedTab }?.WebView()
|
||||
|
||||
// MultiViewContainer(store = store)
|
||||
|
||||
// SampleContainer(selectedTab = selectedTab, items = items)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MainPager(selectedTab: WebTargetId?, items: List<FrostWebCompose>) {
|
||||
|
||||
val pagerState = rememberPagerState { items.size }
|
||||
|
||||
LaunchedEffect(selectedTab, items) {
|
||||
val i = items.indexOfFirst { it.tabId == selectedTab }
|
||||
if (i != -1) {
|
||||
pagerState.scrollToPage(i)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
userScrollEnabled = false,
|
||||
beyondBoundsPageCount = 10, // Do not allow view release
|
||||
) { page ->
|
||||
items[page].WebView()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun PullRefresh(modifier: Modifier, store: FrostWebStore, content: @Composable () -> Unit) {
|
||||
val refreshScope = rememberCoroutineScope()
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
|
||||
fun refresh() =
|
||||
refreshScope.launch {
|
||||
refreshing = true
|
||||
delay(1500)
|
||||
refreshing = false
|
||||
}
|
||||
|
||||
val state = rememberPullRefreshState(refreshing, ::refresh)
|
||||
|
||||
Box(modifier.pullRefresh(state)) {
|
||||
content()
|
||||
|
||||
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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.view
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.webkit.WebView
|
||||
import androidx.core.view.NestedScrollingChild3
|
||||
import androidx.core.view.NestedScrollingChildHelper
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 20/12/17.
|
||||
*
|
||||
* Webview extension that handles nested scrolls
|
||||
*/
|
||||
class NestedWebView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
|
||||
WebView(context, attrs, defStyleAttr), NestedScrollingChild3 {
|
||||
|
||||
// No JvmOverloads due to hilt
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
|
||||
private lateinit var childHelper: NestedScrollingChildHelper
|
||||
private var lastY: Int = 0
|
||||
private val scrollOffset = IntArray(2)
|
||||
private val scrollConsumed = IntArray(2)
|
||||
private var nestedOffsetY: Int = 0
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
||||
fun init() {
|
||||
// To avoid leaking constructor
|
||||
childHelper = NestedScrollingChildHelper(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle nested scrolling against SwipeRecyclerView Courtesy of takahirom
|
||||
*
|
||||
* https://github.com/takahirom/webview-in-coordinatorlayout/blob/master/app/src/main/java/com/github/takahirom/webview_in_coodinator_layout/NestedWebView.java
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
val event = MotionEvent.obtain(ev)
|
||||
val action = event.action
|
||||
if (action == MotionEvent.ACTION_DOWN) nestedOffsetY = 0
|
||||
val eventY = event.y.toInt()
|
||||
event.offsetLocation(0f, nestedOffsetY.toFloat())
|
||||
val returnValue: Boolean
|
||||
when (action) {
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
var deltaY = lastY - eventY
|
||||
// NestedPreScroll
|
||||
if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
|
||||
deltaY -= scrollConsumed[1]
|
||||
}
|
||||
lastY = eventY - scrollOffset[1]
|
||||
returnValue = super.onTouchEvent(event)
|
||||
// NestedScroll
|
||||
if (dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
|
||||
event.offsetLocation(0f, scrollOffset[1].toFloat())
|
||||
nestedOffsetY += scrollOffset[1]
|
||||
lastY -= scrollOffset[1]
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
returnValue = super.onTouchEvent(event)
|
||||
lastY = eventY
|
||||
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
returnValue = super.onTouchEvent(event)
|
||||
stopNestedScroll()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return returnValue
|
||||
}
|
||||
|
||||
/*
|
||||
* ---------------------------------------------
|
||||
* Nested Scrolling Content
|
||||
* ---------------------------------------------
|
||||
*/
|
||||
|
||||
override fun setNestedScrollingEnabled(enabled: Boolean) {
|
||||
childHelper.isNestedScrollingEnabled = enabled
|
||||
}
|
||||
|
||||
override fun isNestedScrollingEnabled() = childHelper.isNestedScrollingEnabled
|
||||
|
||||
override fun startNestedScroll(axes: Int, type: Int): Boolean {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun startNestedScroll(axes: Int) = childHelper.startNestedScroll(axes)
|
||||
|
||||
override fun stopNestedScroll(type: Int) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun stopNestedScroll() = childHelper.stopNestedScroll()
|
||||
|
||||
override fun hasNestedScrollingParent(type: Int): Boolean {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun hasNestedScrollingParent() = childHelper.hasNestedScrollingParent()
|
||||
|
||||
override fun dispatchNestedScroll(
|
||||
dxConsumed: Int,
|
||||
dyConsumed: Int,
|
||||
dxUnconsumed: Int,
|
||||
dyUnconsumed: Int,
|
||||
offsetInWindow: IntArray?,
|
||||
type: Int,
|
||||
consumed: IntArray
|
||||
) =
|
||||
childHelper.dispatchNestedScroll(
|
||||
dxConsumed,
|
||||
dyConsumed,
|
||||
dxUnconsumed,
|
||||
dyUnconsumed,
|
||||
offsetInWindow,
|
||||
type,
|
||||
consumed,
|
||||
)
|
||||
|
||||
override fun dispatchNestedScroll(
|
||||
dxConsumed: Int,
|
||||
dyConsumed: Int,
|
||||
dxUnconsumed: Int,
|
||||
dyUnconsumed: Int,
|
||||
offsetInWindow: IntArray?,
|
||||
type: Int
|
||||
) =
|
||||
childHelper.dispatchNestedScroll(
|
||||
dxConsumed,
|
||||
dyConsumed,
|
||||
dxUnconsumed,
|
||||
dyUnconsumed,
|
||||
offsetInWindow,
|
||||
type,
|
||||
)
|
||||
|
||||
override fun dispatchNestedScroll(
|
||||
dxConsumed: Int,
|
||||
dyConsumed: Int,
|
||||
dxUnconsumed: Int,
|
||||
dyUnconsumed: Int,
|
||||
offsetInWindow: IntArray?
|
||||
) =
|
||||
childHelper.dispatchNestedScroll(
|
||||
dxConsumed,
|
||||
dyConsumed,
|
||||
dxUnconsumed,
|
||||
dyUnconsumed,
|
||||
offsetInWindow,
|
||||
)
|
||||
|
||||
override fun dispatchNestedPreScroll(
|
||||
dx: Int,
|
||||
dy: Int,
|
||||
consumed: IntArray?,
|
||||
offsetInWindow: IntArray?,
|
||||
type: Int
|
||||
): Boolean = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
|
||||
|
||||
override fun dispatchNestedPreScroll(
|
||||
dx: Int,
|
||||
dy: Int,
|
||||
consumed: IntArray?,
|
||||
offsetInWindow: IntArray?
|
||||
) = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
|
||||
|
||||
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean) =
|
||||
childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
|
||||
|
||||
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float) =
|
||||
childHelper.dispatchNestedPreFling(velocityX, velocityY)
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2023 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.text.TextUtils
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
interface FrostAdBlock {
|
||||
|
||||
val data: Set<String>
|
||||
|
||||
/**
|
||||
* Initialize ad block data.
|
||||
*
|
||||
* Required to be called once
|
||||
*/
|
||||
fun init()
|
||||
}
|
||||
|
||||
fun FrostAdBlock.isAd(url: String?): Boolean {
|
||||
url ?: return false
|
||||
val httpUrl = url.toHttpUrlOrNull() ?: return false
|
||||
return isAdHost(httpUrl.host)
|
||||
}
|
||||
|
||||
tailrec fun FrostAdBlock.isAdHost(host: String): Boolean {
|
||||
if (TextUtils.isEmpty(host)) return false
|
||||
val index = host.indexOf(".")
|
||||
if (index < 0 || index + 1 < host.length) return false
|
||||
if (data.contains(host)) return true
|
||||
return isAdHost(host.substring(index + 1))
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2023 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.content.Context
|
||||
import android.webkit.CookieManager
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.facebook.FACEBOOK_COM
|
||||
import com.pitchedapps.frost.facebook.HTTPS_FACEBOOK_COM
|
||||
import com.pitchedapps.frost.facebook.HTTPS_MESSENGER_COM
|
||||
import com.pitchedapps.frost.facebook.MESSENGER_COM
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FrostCookie @Inject internal constructor(private val cookieManager: CookieManager) {
|
||||
|
||||
val fbCookie: String?
|
||||
get() = cookieManager.getCookie(HTTPS_FACEBOOK_COM)
|
||||
|
||||
val messengerCookie: String?
|
||||
get() = cookieManager.getCookie(HTTPS_MESSENGER_COM)
|
||||
|
||||
private suspend fun CookieManager.suspendSetWebCookie(domain: String, cookie: String?): Boolean {
|
||||
cookie ?: return true
|
||||
return withContext(NonCancellable) {
|
||||
// Save all cookies regardless of result, then check if all succeeded
|
||||
val result =
|
||||
cookie.split(";").map { async { setSingleWebCookie(domain, it) } }.awaitAll().all { it }
|
||||
logger.atInfo().log("Cookies set for %s, %b", domain, result)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.setSingleWebCookie(domain: String, cookie: String): Boolean =
|
||||
suspendCoroutine { cont ->
|
||||
setCookie(domain, cookie.trim()) { cont.resume(it) }
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
||||
removeAllCookies { cont.resume(it) }
|
||||
}
|
||||
|
||||
suspend fun save(id: Long) {
|
||||
logger.atInfo().log("Saving cookies for %d", id)
|
||||
// prefs.userId = id
|
||||
cookieManager.flush()
|
||||
// val cookie = CookieEntity(prefs.userId, null, webCookie)
|
||||
// cookieDao.save(cookie)
|
||||
}
|
||||
|
||||
suspend fun reset() {
|
||||
// prefs.userId = -1L
|
||||
withContext(Dispatchers.Main + NonCancellable) {
|
||||
with(cookieManager) {
|
||||
removeAllCookies()
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun switchUser(id: Long) {
|
||||
// val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
|
||||
// switchUser(cookie)
|
||||
}
|
||||
|
||||
// suspend fun switchUser(cookie: CookieEntity?) {
|
||||
// if (cookie?.cookie == null) {
|
||||
// L.d { "Switching User; null cookie" }
|
||||
// return
|
||||
// }
|
||||
// withContext(Dispatchers.Main + NonCancellable) {
|
||||
// L.d { "Switching User" }
|
||||
// prefs.userId = cookie.id
|
||||
// CookieManager.getInstance().apply {
|
||||
// removeAllCookies()
|
||||
// suspendSetWebCookie(FB_COOKIE_DOMAIN, cookie.cookie)
|
||||
// suspendSetWebCookie(MESSENGER_COOKIE_DOMAIN, cookie.cookieMessenger)
|
||||
// flush()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/** Helper function to remove the current cookies and launch the proper login page */
|
||||
suspend fun logout(context: Context, deleteCookie: Boolean = true) {
|
||||
// val cookies = arrayListOf<CookieEntity>()
|
||||
// if (context is Activity) cookies.addAll(context.cookies().filter { it.id != prefs.userId
|
||||
// })
|
||||
// logout(prefs.userId, deleteCookie)
|
||||
// context.launchLogin(cookies, true)
|
||||
}
|
||||
|
||||
/** Clear the cookies of the given id */
|
||||
suspend fun logout(id: Long, deleteCookie: Boolean = true) {
|
||||
logger.atInfo().log("Logging out user %d", id)
|
||||
// TODO save cookies?
|
||||
if (deleteCookie) {
|
||||
// cookieDao.deleteById(id)
|
||||
logger.atInfo().log("Deleted cookies")
|
||||
}
|
||||
reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifications may come from different accounts, and we need to switch the cookies to load them
|
||||
* When coming back to the main app, switch back to our original account before continuing
|
||||
*/
|
||||
suspend fun switchBackUser() {
|
||||
// if (prefs.prevId == -1L) return
|
||||
// val prevId = prefs.prevId
|
||||
// prefs.prevId = -1L
|
||||
// if (prevId != prefs.userId) {
|
||||
// switchUser(prevId)
|
||||
// L.d { "Switch back user" }
|
||||
// L._d { "${prefs.userId} to $prevId" }
|
||||
// }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
/** Domain information. Dot prefix still matters for Android browsers. */
|
||||
private const val FB_COOKIE_DOMAIN = ".$FACEBOOK_COM"
|
||||
private const val MESSENGER_COOKIE_DOMAIN = ".$MESSENGER_COM"
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2023 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.facebook.isFacebookUrl
|
||||
import com.pitchedapps.frost.facebook.isMessengerUrl
|
||||
import java.util.Optional
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
class FrostWebHelper @Inject internal constructor(frostAdBlock: Optional<FrostAdBlock>) {
|
||||
private val frostAdBlock = frostAdBlock.getOrNull()
|
||||
|
||||
/** Returns true if url should be intercepted (replaced with blank resource) */
|
||||
fun shouldInterceptUrl(url: String): Boolean {
|
||||
val httpUrl = url.toHttpUrlOrNull() ?: return false
|
||||
val host = httpUrl.host
|
||||
if (host.contains("facebook") || host.contains("fbcdn")) return false
|
||||
if (frostAdBlock?.isAdHost(host) == true) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if url should allow refreshes.
|
||||
*
|
||||
* Some urls are known to be invalid entrypoints, and cannot be refreshed. Others may contain
|
||||
* editable data without save state, so disabling swipe to refresh is preferred.
|
||||
*/
|
||||
fun allowUrlSwipeToRefresh(url: String): Boolean {
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2023 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.state
|
||||
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
|
||||
typealias FrostWebMiddleware = Middleware<FrostWebState, FrostWebAction>
|
||||
|
||||
class FrostLoggerMiddleware : FrostWebMiddleware {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<FrostWebState, FrostWebAction>,
|
||||
next: (FrostWebAction) -> Unit,
|
||||
action: FrostWebAction
|
||||
) {
|
||||
logger.atInfo().log("FrostWebAction: %s - %s", action::class.simpleName, action)
|
||||
next(action)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
||||
|
||||
class FrostCookieMiddleware : FrostWebMiddleware {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<FrostWebState, FrostWebAction>,
|
||||
next: (FrostWebAction) -> Unit,
|
||||
action: FrostWebAction
|
||||
) {
|
||||
when (action) {
|
||||
else -> next(action)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2023 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.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import mozilla.components.lib.state.Action
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt
|
||||
*
|
||||
* For firefox example
|
||||
*/
|
||||
sealed interface FrostWebAction : Action
|
||||
|
||||
/**
|
||||
* [FrostWebAction] dispatched to indicate that the store is initialized and ready to use. This
|
||||
* action is dispatched automatically before any other action is processed. Its main purpose is to
|
||||
* trigger initialization logic in middlewares. The action itself has no effect on the
|
||||
* [FrostWebState].
|
||||
*/
|
||||
object InitAction : FrostWebAction
|
||||
|
||||
/** Actions affecting multiple tabs */
|
||||
sealed interface TabListAction : FrostWebAction {
|
||||
data class SetHomeTabs(val data: List<FbItem>) : TabListAction
|
||||
|
||||
data class SelectHomeTab(val id: WebTargetId) : TabListAction
|
||||
}
|
||||
|
||||
/** Action affecting a single tab */
|
||||
data class TabAction(val tabId: WebTargetId, val action: Action) : FrostWebAction {
|
||||
sealed interface Action
|
||||
|
||||
sealed interface ContentAction : Action {
|
||||
|
||||
/** Action indicating current url state. */
|
||||
data class UpdateUrlAction(val url: String) : ContentAction
|
||||
|
||||
/** Action indicating current title state. */
|
||||
data class UpdateTitleAction(val title: String?) : ContentAction
|
||||
|
||||
data class UpdateNavigationAction(val canGoBack: Boolean, val canGoForward: Boolean) :
|
||||
ContentAction
|
||||
|
||||
data class UpdateProgressAction(val progress: Int) : ContentAction
|
||||
}
|
||||
|
||||
/** Action triggered by user, leading to transient state changes. */
|
||||
sealed interface UserAction : Action {
|
||||
|
||||
/** Action to load new url. */
|
||||
data class LoadUrlAction(val url: String) : UserAction
|
||||
|
||||
object GoBackAction : UserAction
|
||||
|
||||
object GoForwardAction : UserAction
|
||||
}
|
||||
|
||||
/** Response triggered by webview, indicating [UserAction] fulfillment. */
|
||||
sealed interface ResponseAction : Action {
|
||||
|
||||
data class LoadUrlResponseAction(val url: String) : ResponseAction
|
||||
|
||||
data class WebStepResponseAction(val steps: Int) : ResponseAction
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 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.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.reducer.ContentStateReducer
|
||||
import com.pitchedapps.frost.web.state.reducer.TabListReducer
|
||||
import com.pitchedapps.frost.web.state.state.FloatingTabSessionState
|
||||
import com.pitchedapps.frost.web.state.state.HomeTabSessionState
|
||||
import com.pitchedapps.frost.web.state.state.SessionState
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt
|
||||
*
|
||||
* For firefox example
|
||||
*/
|
||||
class FrostWebReducer
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val tabListReducer: TabListReducer,
|
||||
private val contentStateReducer: ContentStateReducer
|
||||
) {
|
||||
fun reduce(state: FrostWebState, action: FrostWebAction): FrostWebState {
|
||||
return when (action) {
|
||||
is InitAction -> state
|
||||
is TabListAction -> tabListReducer.reduce(state, action)
|
||||
is TabAction ->
|
||||
state.updateTabState(action.tabId) { session ->
|
||||
val newContent = contentStateReducer.reduce(session.content, action.action)
|
||||
session.createCopy(content = newContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Unchecked_Cast")
|
||||
internal fun FrostWebState.updateTabState(
|
||||
tabId: WebTargetId,
|
||||
update: (SessionState) -> SessionState,
|
||||
): FrostWebState {
|
||||
val floatingTabMatch = floatingTab?.takeIf { it.id == tabId }
|
||||
if (floatingTabMatch != null)
|
||||
return copy(floatingTab = update(floatingTabMatch) as FloatingTabSessionState)
|
||||
|
||||
val newHomeTabs = homeTabs.updateTabs(tabId, update) as List<HomeTabSessionState>?
|
||||
if (newHomeTabs != null) return copy(homeTabs = newHomeTabs)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the corresponding tab in the list and replaces it using [update].
|
||||
*
|
||||
* @param tabId ID of the tab to change.
|
||||
* @param update Returns a new version of the tab state.
|
||||
*/
|
||||
internal fun <T : SessionState> List<T>.updateTabs(
|
||||
tabId: WebTargetId,
|
||||
update: (T) -> T,
|
||||
): List<SessionState>? {
|
||||
val tabIndex = indexOfFirst { it.id == tabId }
|
||||
if (tabIndex == -1) return null
|
||||
return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size)
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2023 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.state
|
||||
|
||||
import com.pitchedapps.frost.ext.FrostAccountId
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.state.FloatingTabSessionState
|
||||
import com.pitchedapps.frost.web.state.state.HomeTabSessionState
|
||||
import mozilla.components.lib.state.State
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt
|
||||
*
|
||||
* for Firefox example.
|
||||
*/
|
||||
data class FrostWebState(
|
||||
val auth: AuthWebState = AuthWebState(),
|
||||
val selectedHomeTab: WebTargetId? = null,
|
||||
val homeTabs: List<HomeTabSessionState> = emptyList(),
|
||||
var floatingTab: FloatingTabSessionState? = null,
|
||||
) : State
|
||||
|
||||
/**
|
||||
* Auth web state.
|
||||
*
|
||||
* Unlike GeckoView, WebView currently has a singleton cookie manager.
|
||||
*
|
||||
* Cookies are tied to the entire app, rather than per tab.
|
||||
*
|
||||
* @param currentUser User based on loaded cookies
|
||||
* @param homeUser User selected for home screen
|
||||
*/
|
||||
data class AuthWebState(
|
||||
val currentUser: AuthUser = AuthUser.Unknown,
|
||||
val homeUser: AuthUser = AuthUser.Unknown,
|
||||
) {
|
||||
sealed interface AuthUser {
|
||||
data class User(val id: FrostAccountId) : AuthUser
|
||||
|
||||
data class Transitioning(val targetId: FrostAccountId?) : AuthUser
|
||||
|
||||
object LoggedOut : AuthUser
|
||||
|
||||
object Unknown : AuthUser
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2023 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.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.state.SessionState
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/store/BrowserStore.kt
|
||||
*
|
||||
* For firefox example.
|
||||
*/
|
||||
class FrostWebStore(
|
||||
initialState: FrostWebState = FrostWebState(),
|
||||
frostWebReducer: FrostWebReducer,
|
||||
middleware: List<Middleware<FrostWebState, FrostWebAction>> = emptyList(),
|
||||
) :
|
||||
Store<FrostWebState, FrostWebAction>(
|
||||
initialState,
|
||||
frostWebReducer::reduce,
|
||||
middleware,
|
||||
"FrostStore",
|
||||
) {
|
||||
init {
|
||||
dispatch(InitAction)
|
||||
}
|
||||
}
|
||||
|
||||
operator fun FrostWebState.get(tabId: WebTargetId): SessionState? {
|
||||
if (floatingTab?.id == tabId) return floatingTab
|
||||
return homeTabs.find { it.id == tabId }
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2023 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.state.helper
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.FrostWebState
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.state.SessionState
|
||||
import mozilla.components.lib.state.Store
|
||||
import mozilla.components.lib.state.ext.observeAsComposableState
|
||||
|
||||
/**
|
||||
* Helper for allowing a component consumer to specify which tab a component should target.
|
||||
*
|
||||
* Based off of mozilla.components.browser.state.helper.Target:
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/helper/Target.kt
|
||||
*/
|
||||
sealed class Target {
|
||||
/**
|
||||
* Looks up this target in the given [FrostWebStore] and returns the matching [SessionState] if
|
||||
* available. Otherwise returns `null`.
|
||||
*
|
||||
* @param store to lookup this target in.
|
||||
*/
|
||||
fun lookupIn(store: FrostWebStore): SessionState? = lookupIn(store.state)
|
||||
|
||||
/**
|
||||
* Looks up this target in the given [FrostWebState] and returns the matching [SessionState] if
|
||||
* available. Otherwise returns `null`.
|
||||
*
|
||||
* @param state to lookup this target in.
|
||||
*/
|
||||
abstract fun lookupIn(state: FrostWebState): SessionState?
|
||||
|
||||
/**
|
||||
* Observes this target and represents the mapped state (using [map]) via [State].
|
||||
*
|
||||
* Everytime the [Store] state changes and the result of the [observe] function changes for this
|
||||
* state, the returned [State] will be updated causing recomposition of every [State.value] usage.
|
||||
*
|
||||
* The [Store] observer will automatically be removed when this composable disposes or the current
|
||||
* [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
|
||||
*
|
||||
* @param store that should get observed
|
||||
* @param observe function that maps a [SessionState] to the (sub) state that should get observed
|
||||
* for changes.
|
||||
*/
|
||||
@Composable
|
||||
fun <R> observeAsComposableStateFrom(
|
||||
store: FrostWebStore,
|
||||
observe: (SessionState?) -> R,
|
||||
): State<SessionState?> {
|
||||
return store.observeAsComposableState(
|
||||
map = { state -> lookupIn(state) },
|
||||
observe = { state -> observe(lookupIn(state)) },
|
||||
)
|
||||
}
|
||||
|
||||
data class HomeTab(val id: WebTargetId) : Target() {
|
||||
override fun lookupIn(state: FrostWebState): SessionState? {
|
||||
return state.homeTabs.find { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
object FloatingTab : Target() {
|
||||
override fun lookupIn(state: FrostWebState): SessionState? {
|
||||
return state.floatingTab
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2023 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.state.reducer
|
||||
|
||||
import com.pitchedapps.frost.web.state.TabAction.Action
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateNavigationAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateProgressAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateTitleAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateUrlAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.WebStepResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.GoBackAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.GoForwardAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.LoadUrlAction
|
||||
import com.pitchedapps.frost.web.state.state.ContentState
|
||||
import com.pitchedapps.frost.web.state.state.TransientWebState
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ContentStateReducer @Inject internal constructor() {
|
||||
|
||||
fun reduce(state: ContentState, action: Action): ContentState {
|
||||
return when (action) {
|
||||
is UpdateUrlAction -> state.copy(url = action.url)
|
||||
is UpdateProgressAction -> state.copy(progress = action.progress)
|
||||
is UpdateNavigationAction ->
|
||||
state.copy(
|
||||
canGoBack = action.canGoBack,
|
||||
canGoForward = action.canGoForward,
|
||||
)
|
||||
is UpdateTitleAction -> state.copy(title = action.title)
|
||||
is UserAction ->
|
||||
state.copy(
|
||||
transientState =
|
||||
FrostTransientWebReducer.reduce(
|
||||
state.transientState,
|
||||
action,
|
||||
),
|
||||
)
|
||||
is ResponseAction ->
|
||||
state.copy(
|
||||
transientState =
|
||||
FrostTransientFulfillmentWebReducer.reduce(
|
||||
state.transientState,
|
||||
action,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object FrostTransientWebReducer {
|
||||
fun reduce(state: TransientWebState, action: UserAction): TransientWebState {
|
||||
return when (action) {
|
||||
is LoadUrlAction -> state.copy(targetUrl = action.url)
|
||||
is GoBackAction -> state.copy(navStep = state.navStep - 1)
|
||||
is GoForwardAction -> state.copy(navStep = state.navStep + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object FrostTransientFulfillmentWebReducer {
|
||||
fun reduce(state: TransientWebState, action: ResponseAction): TransientWebState {
|
||||
return when (action) {
|
||||
is LoadUrlResponseAction ->
|
||||
if (state.targetUrl == action.url) state.copy(targetUrl = null) else state
|
||||
is WebStepResponseAction -> state.copy(navStep = state.navStep - action.steps)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2023 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.state.reducer
|
||||
|
||||
import android.content.Context
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.facebook.tab
|
||||
import com.pitchedapps.frost.web.state.AuthWebState
|
||||
import com.pitchedapps.frost.web.state.FrostWebState
|
||||
import com.pitchedapps.frost.web.state.TabListAction
|
||||
import com.pitchedapps.frost.web.state.TabListAction.SetHomeTabs
|
||||
import com.pitchedapps.frost.web.state.state.ContentState
|
||||
import com.pitchedapps.frost.web.state.state.HomeTabSessionState
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TabListReducer
|
||||
@Inject
|
||||
internal constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
fun reduce(state: FrostWebState, action: TabListAction): FrostWebState {
|
||||
return when (action) {
|
||||
is SetHomeTabs -> {
|
||||
val tabs =
|
||||
action.data.mapIndexed { i, fbItem -> fbItem.toHomeTabSession(context, i, state.auth) }
|
||||
state.copy(homeTabs = tabs)
|
||||
}
|
||||
is TabListAction.SelectHomeTab -> state.copy(selectedHomeTab = action.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun FbItem.toHomeTabSession(
|
||||
context: Context,
|
||||
i: Int,
|
||||
auth: AuthWebState
|
||||
): HomeTabSessionState =
|
||||
HomeTabSessionState(
|
||||
userId = auth.currentUser,
|
||||
content = ContentState(url = url),
|
||||
tab = tab(context, id = HomeTabSessionState.homeTabId(i)),
|
||||
)
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2023 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.state.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.main.MainTabItem
|
||||
import com.pitchedapps.frost.web.state.AuthWebState.AuthUser
|
||||
|
||||
/** Data representation of single session. */
|
||||
interface SessionState {
|
||||
val id: WebTargetId
|
||||
val userId: AuthUser
|
||||
val content: ContentState
|
||||
|
||||
fun createCopy(
|
||||
id: WebTargetId = this.id,
|
||||
userId: AuthUser = this.userId,
|
||||
content: ContentState = this.content
|
||||
): SessionState
|
||||
}
|
||||
|
||||
/** Session for home screen, which includes nav bar data */
|
||||
data class HomeTabSessionState(
|
||||
override val userId: AuthUser,
|
||||
override val content: ContentState,
|
||||
val tab: MainTabItem,
|
||||
) : SessionState {
|
||||
|
||||
override val id: WebTargetId
|
||||
get() = tab.id
|
||||
|
||||
override fun createCopy(id: WebTargetId, userId: AuthUser, content: ContentState) =
|
||||
copy(userId = userId, content = content, tab = tab.copy(id = id))
|
||||
|
||||
companion object {
|
||||
fun homeTabId(index: Int): WebTargetId = WebTargetId("home-tab--$index")
|
||||
}
|
||||
}
|
||||
|
||||
data class FloatingTabSessionState(
|
||||
override val id: WebTargetId,
|
||||
override val userId: AuthUser,
|
||||
override val content: ContentState,
|
||||
) : SessionState {
|
||||
override fun createCopy(id: WebTargetId, userId: AuthUser, content: ContentState) =
|
||||
copy(id = id, userId = userId, content = content)
|
||||
}
|
||||
|
||||
/** Data relating to webview content */
|
||||
data class ContentState(
|
||||
val url: String,
|
||||
val title: String? = null,
|
||||
val progress: Int = 0,
|
||||
val loading: Boolean = false,
|
||||
val canGoBack: Boolean = false,
|
||||
val canGoForward: Boolean = false,
|
||||
val transientState: TransientWebState = TransientWebState(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Transient web state.
|
||||
*
|
||||
* While we typically don't want to store this, our webview is not a composable, and requires a
|
||||
* bridge to handle events.
|
||||
*
|
||||
* This state is not a list of pending actions, but rather a snapshot of the expected changes so
|
||||
* that conflicting events can be ignored.
|
||||
*
|
||||
* @param targetUrl url destination if nonnull
|
||||
* @param navStep pending steps. Positive = steps forward, negative = steps backward
|
||||
*/
|
||||
data class TransientWebState(
|
||||
val targetUrl: String? = null,
|
||||
val navStep: Int = 0,
|
||||
)
|
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.webview
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateProgressAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateTitleAction
|
||||
import com.pitchedapps.frost.web.state.get
|
||||
|
||||
/** The default chrome client */
|
||||
class FrostChromeClient(private val tabId: WebTargetId, private val store: FrostWebStore) :
|
||||
WebChromeClient() {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
override fun getDefaultVideoPoster(): Bitmap? =
|
||||
super.getDefaultVideoPoster() ?: Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
||||
logger
|
||||
.atInfo()
|
||||
.log("Chrome Console %d: %s", consoleMessage.lineNumber(), consoleMessage.message())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onReceivedTitle(view: WebView, title: String) {
|
||||
super.onReceivedTitle(view, title)
|
||||
if (title.startsWith("http")) return
|
||||
store.dispatch(UpdateTitleAction(title))
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
// TODO remove?
|
||||
if (store.state[tabId]?.content?.progress == 100) return
|
||||
store.dispatch(UpdateProgressAction(newProgress))
|
||||
}
|
||||
|
||||
// override fun onShowFileChooser(
|
||||
// webView: WebView,
|
||||
// filePathCallback: ValueCallback<Array<Uri>?>,
|
||||
// fileChooserParams: FileChooserParams
|
||||
// ): Boolean {
|
||||
// callbacks.openMediaPicker(filePathCallback, fileChooserParams)
|
||||
// return true
|
||||
// }
|
||||
|
||||
// override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean
|
||||
// {
|
||||
// callbacks.onJsAlert(url, message, result)
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult):
|
||||
// Boolean {
|
||||
// callbacks.onJsConfirm(url, message, result)
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onJsBeforeUnload(
|
||||
// view: WebView,
|
||||
// url: String,
|
||||
// message: String,
|
||||
// result: JsResult
|
||||
// ): Boolean {
|
||||
// callbacks.onJsBeforeUnload(url, message, result)
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onJsPrompt(
|
||||
// view: WebView,
|
||||
// url: String,
|
||||
// message: String,
|
||||
// defaultValue: String?,
|
||||
// result: JsPromptResult
|
||||
// ): Boolean {
|
||||
// callbacks.onJsPrompt(url, message, defaultValue, result)
|
||||
// return true
|
||||
// }
|
||||
|
||||
// override fun onGeolocationPermissionsShowPrompt(
|
||||
// origin: String,
|
||||
// callback: GeolocationPermissions.Callback
|
||||
// ) {
|
||||
// L.i { "Requesting geolocation" }
|
||||
// context.kauRequestPermissions(PERMISSION_ACCESS_FINE_LOCATION) { granted, _ ->
|
||||
// L.i { "Geolocation response received; ${if (granted) "granted" else "denied"}" }
|
||||
// callback(origin, granted, true)
|
||||
// }
|
||||
// }
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.webview
|
||||
|
||||
import android.webkit.WebViewClient
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.FrostWebHelper
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import dagger.BindsInstance
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.DefineComponent
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.EntryPoints
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Inject
|
||||
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
|
||||
|
||||
@Qualifier annotation class FrostWeb
|
||||
|
||||
@FrostWebScoped @DefineComponent(parent = SingletonComponent::class) interface FrostWebComponent
|
||||
|
||||
@DefineComponent.Builder
|
||||
interface FrostWebComponentBuilder {
|
||||
|
||||
@BindsInstance fun tabId(@FrostWeb tabId: WebTargetId): FrostWebComponentBuilder
|
||||
|
||||
fun build(): FrostWebComponent
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(FrostWebComponent::class)
|
||||
internal object FrostWebModule {
|
||||
|
||||
@Provides
|
||||
fun client(
|
||||
@FrostWeb tabId: WebTargetId,
|
||||
store: FrostWebStore,
|
||||
webHelper: FrostWebHelper
|
||||
): WebViewClient = FrostWebViewClient(tabId, store, webHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Using this injection seems to be buggy, leading to an invalid param tabId error:
|
||||
*
|
||||
* Cause: not a valid name: tabId-4xHwVBUParam
|
||||
*/
|
||||
class FrostWebEntrySample
|
||||
@Inject
|
||||
internal constructor(private val frostWebComponentBuilder: FrostWebComponentBuilder) {
|
||||
fun test(tabId: WebTargetId): WebViewClient {
|
||||
val component = frostWebComponentBuilder.tabId(tabId).build()
|
||||
return EntryPoints.get(component, FrostWebEntryPoint::class.java).client()
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(FrostWebComponent::class)
|
||||
interface FrostWebEntryPoint {
|
||||
fun client(): WebViewClient
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2023 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.webview
|
||||
|
||||
import com.pitchedapps.frost.compose.webview.FrostWebCompose
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.FrostWebHelper
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import javax.inject.Inject
|
||||
|
||||
class FrostWebComposer
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val store: FrostWebStore,
|
||||
private val webHelper: FrostWebHelper,
|
||||
) {
|
||||
|
||||
fun create(tabId: WebTargetId): FrostWebCompose {
|
||||
val client = FrostWebViewClient(tabId, store, webHelper)
|
||||
val chromeClient = FrostChromeClient(tabId, store)
|
||||
return FrostWebCompose(tabId, store, client, chromeClient)
|
||||
}
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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.webview
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.facebook.FACEBOOK_BASE_COM
|
||||
import com.pitchedapps.frost.facebook.WWW_FACEBOOK_COM
|
||||
import com.pitchedapps.frost.facebook.isExplicitIntent
|
||||
import com.pitchedapps.frost.web.FrostWebHelper
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateNavigationAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateProgressAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateTitleAction
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-31.
|
||||
*
|
||||
* Collection of webview clients
|
||||
*/
|
||||
|
||||
/** The base of all webview clients Used to ensure that resources are properly intercepted */
|
||||
abstract class BaseWebViewClient : WebViewClient() {
|
||||
|
||||
protected abstract val webHelper: FrostWebHelper
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
val requestUrl = request.url?.toString() ?: return null
|
||||
return if (webHelper.shouldInterceptUrl(requestUrl)) BLANK_RESOURCE else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
val BLANK_RESOURCE =
|
||||
WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
/** The default webview client */
|
||||
class FrostWebViewClient(
|
||||
private val tabId: WebTargetId,
|
||||
private val store: FrostWebStore,
|
||||
override val webHelper: FrostWebHelper
|
||||
) : BaseWebViewClient() {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
/** True if current url supports refresh. See [doUpdateVisitedHistory] for updates */
|
||||
internal var urlSupportsRefresh: Boolean = true
|
||||
|
||||
override fun doUpdateVisitedHistory(view: WebView, url: String, isReload: Boolean) {
|
||||
super.doUpdateVisitedHistory(view, url, isReload)
|
||||
urlSupportsRefresh = webHelper.allowUrlSwipeToRefresh(url)
|
||||
store.dispatch(
|
||||
UpdateNavigationAction(
|
||||
canGoBack = view.canGoBack(),
|
||||
canGoForward = view.canGoForward(),
|
||||
),
|
||||
)
|
||||
// web.parent.swipeAllowedByPage = urlSupportsRefresh
|
||||
// view.jsInject(JsAssets.AUTO_RESIZE_TEXTAREA.maybe(prefs.autoExpandTextBox), prefs = prefs)
|
||||
// v { "History $url; refresh $urlSupportsRefresh" }
|
||||
}
|
||||
|
||||
/** Main injections for facebook content */
|
||||
// protected open val facebookJsInjectors: List<InjectorContract> =
|
||||
// listOf(
|
||||
// // CssHider.CORE,
|
||||
// CssHider.HEADER,
|
||||
// CssHider.COMPOSER.maybe(!prefs.showComposer),
|
||||
// CssHider.STORIES.maybe(!prefs.showStories),
|
||||
// CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!prefs.showSuggestedFriends),
|
||||
// CssHider.SUGGESTED_GROUPS.maybe(!prefs.showSuggestedGroups),
|
||||
// CssHider.SUGGESTED_POSTS.maybe(!prefs.showSuggestedPosts),
|
||||
// themeProvider.injector(ThemeCategory.FACEBOOK),
|
||||
// CssHider.NON_RECENT.maybe(
|
||||
// (web.url?.contains("?sk=h_chr") ?: false) && prefs.aggressiveRecents
|
||||
// ),
|
||||
// CssHider.ADS,
|
||||
// CssHider.POST_ACTIONS.maybe(!prefs.showPostActions),
|
||||
// CssHider.POST_REACTIONS.maybe(!prefs.showPostReactions),
|
||||
// CssAsset.FullSizeImage.maybe(prefs.fullSizeImage),
|
||||
// JsAssets.DOCUMENT_WATCHER,
|
||||
// JsAssets.HORIZONTAL_SCROLLING,
|
||||
// JsAssets.AUTO_RESIZE_TEXTAREA.maybe(prefs.autoExpandTextBox),
|
||||
// JsAssets.CLICK_A,
|
||||
// JsAssets.CONTEXT_A,
|
||||
// JsAssets.MEDIA,
|
||||
// JsAssets.SCROLL_STOP,
|
||||
// )
|
||||
//
|
||||
// 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)
|
||||
store.dispatch(UpdateProgressAction(0))
|
||||
store.dispatch(UpdateTitleAction(null))
|
||||
// v { "loading $url ${web.settings.userAgentString}" }
|
||||
// refresh.offer(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 -> {
|
||||
// // refresh.offer(false)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
// if (!url.isFacebookUrl && !url.isMessengerUrl) {
|
||||
// refresh.offer(false)
|
||||
// return
|
||||
// }
|
||||
// onPageFinishedActions(url)
|
||||
}
|
||||
|
||||
// internal open fun onPageFinishedActions(url: String) {
|
||||
// if (url.startsWith("${FbItem.Messages.url}/read/") && prefs.messageScrollToBottom) {
|
||||
// web.pageDown(true)
|
||||
// }
|
||||
// injectAndFinish()
|
||||
// }
|
||||
|
||||
// Temp open
|
||||
// internal open fun injectAndFinish() {
|
||||
// v { "page finished reveal" }
|
||||
// // refresh.offer(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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun handleHtml(html: String?) {
|
||||
logger.atFine().log("Handle html: %s", html)
|
||||
}
|
||||
|
||||
fun emit(flag: Int) {
|
||||
logger.atInfo().log("Emit %d", flag)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 WebView.launchRequest(request: WebResourceRequest): Boolean {
|
||||
// v { "Launching url: ${request.url}" }
|
||||
// return 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
|
||||
// }
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
logger.atFinest().log("Url loading: %s", request.url)
|
||||
val path = request.url?.path ?: return super.shouldOverrideUrlLoading(view, request)
|
||||
logger.atFinest().log("Url path: %s", path)
|
||||
val url = request.url.toString()
|
||||
if (url.isExplicitIntent) {
|
||||
// view.context.startActivityForUri(request.url)
|
||||
return true
|
||||
}
|
||||
// if (path.startsWith("/composer/")) {
|
||||
// return launchRequest(request)
|
||||
// }
|
||||
// if (url.isIndirectImageUrl) {
|
||||
// return launchImage(url.formattedFbUrl, cookie = fbCookie.webCookie)
|
||||
// }
|
||||
// if (url.isImageUrl) {
|
||||
// return launchImage(url.formattedFbUrl)
|
||||
// }
|
||||
// if (prefs.linksInDefaultApp && view.context.startActivityForUri(request.url)) {
|
||||
// return true
|
||||
// }
|
||||
// Convert desktop urls to mobile ones
|
||||
if (url.contains("https://www.facebook.com") && webHelper.allowUrlSwipeToRefresh(url)) {
|
||||
view.loadUrl(url.replace(WWW_FACEBOOK_COM, FACEBOOK_BASE_COM))
|
||||
return true
|
||||
}
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
@ -1,12 +1,14 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath('com.google.dagger:hilt-android-gradle-plugin:2.43.2')
|
||||
// https://mvnrepository.com/artifact/com.google.dagger/hilt-android-gradle-plugin
|
||||
classpath('com.google.dagger:hilt-android-gradle-plugin:2.46.1')
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version '8.0.2' apply false
|
||||
// https://mvnrepository.com/artifact/com.android.application/com.android.application.gradle.plugin?repo=google
|
||||
id 'com.android.application' version '8.1.0-beta04' apply false
|
||||
id 'com.android.library' version '8.0.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
|
||||
// https://mvnrepository.com/artifact/com.google.devtools.ksp/com.google.devtools.ksp.gradle.plugin
|
||||
|
Loading…
Reference in New Issue
Block a user