1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-09-18 21:12:24 +02:00

Convert FrostWebStore into singleton

This commit is contained in:
Allan Wang 2023-06-20 01:06:09 -07:00
parent 2794a104e8
commit 3bd266feb4
No known key found for this signature in database
GPG Key ID: C93E3F9C679D7A56
21 changed files with 765 additions and 277 deletions

View File

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

View File

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

View File

@ -21,15 +21,25 @@ 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.UpdateProgressAction
import com.pitchedapps.frost.web.state.UpdateTitleAction
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
import com.pitchedapps.frost.webview.FrostWeb
import javax.inject.Inject
/** The default chrome client */
class FrostChromeClient @Inject internal constructor(private val store: FrostWebStore) :
class FrostChromeClient
@Inject
internal constructor(@FrostWeb 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)
@ -48,7 +58,8 @@ class FrostChromeClient @Inject internal constructor(private val store: FrostWeb
override fun onProgressChanged(view: WebView, newProgress: Int) {
super.onProgressChanged(view, newProgress)
if (store.state.progress == 100) return
// TODO remove?
if (store.state[tabId]?.progress == 100) return
store.dispatch(UpdateProgressAction(newProgress))
}

View File

@ -30,11 +30,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.children
import com.pitchedapps.frost.web.state.FrostWebState
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.ResponseAction
import com.pitchedapps.frost.webview.FrostWebScoped
import javax.inject.Inject
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.TabWebState
import com.pitchedapps.frost.web.state.get
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@ -43,15 +46,17 @@ import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.flow
import mozilla.components.lib.state.ext.observeAsState
@FrostWebScoped
class FrostWebCompose
@Inject
internal constructor(
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
*
@ -96,20 +101,20 @@ internal constructor(
webView?.let { wv ->
val lifecycleOwner = LocalLifecycleOwner.current
val canGoBack by store.observeAsState(initialValue = false) { it.canGoBack }
val canGoBack by store.observeAsState(initialValue = false) { it[tabId]?.canGoBack == true }
BackHandler(captureBackPresses && canGoBack) { wv.goBack() }
LaunchedEffect(wv, store) {
fun storeFlow(action: suspend Flow<FrostWebState>.() -> Unit) = launch {
store.flow(lifecycleOwner).action()
fun storeFlow(action: suspend Flow<TabWebState>.() -> Unit) = launch {
store.flow(lifecycleOwner).mapNotNull { it[tabId] }.action()
}
storeFlow {
mapNotNull { it.transientState.targetUrl }
.distinctUntilChanged()
.collect { url ->
store.dispatch(ResponseAction.LoadUrlResponseAction(url))
store.dispatch(LoadUrlResponseAction(url))
wv.loadUrl(url)
}
}
@ -118,9 +123,11 @@ internal constructor(
.distinctUntilChanged()
.filter { it != 0 }
.collect { steps ->
store.dispatch(ResponseAction.WebStepResponseAction(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)
}
}
}
@ -169,6 +176,10 @@ internal constructor(
},
)
}
companion object {
private val logger = FluentLogger.forEnclosingClass()
}
}

View File

@ -22,14 +22,17 @@ 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.UpdateNavigationAction
import com.pitchedapps.frost.web.state.UpdateProgressAction
import com.pitchedapps.frost.web.state.UpdateTitleAction
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 com.pitchedapps.frost.webview.FrostWeb
import java.io.ByteArrayInputStream
import javax.inject.Inject
@ -61,8 +64,15 @@ abstract class BaseWebViewClient : WebViewClient() {
/** The default webview client */
class FrostWebViewClient
@Inject
internal constructor(private val store: FrostWebStore, override val webHelper: FrostWebHelper) :
BaseWebViewClient() {
internal constructor(
@FrostWeb 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

View File

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

View File

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

View File

@ -16,47 +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
@ -68,7 +36,7 @@ interface FrostBindModule {
@BindsOptionalOf fun adBlock(): FrostAdBlock
}
/** Module containing core Mozilla injections. */
/** Module containing core Frost injections. */
@Module
@InstallIn(SingletonComponent::class)
object FrostModule {
@ -89,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))
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.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(): FrostWebStore {
val middleware = buildList { if (BuildConfig.DEBUG) add(FrostLoggerMiddleware()) }
val store = FrostWebStore(middleware = middleware)
return store
}
}

View File

@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.pitchedapps.frost.compose.webview.FrostWebCompose
import com.pitchedapps.frost.ext.GeckoContextId
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.ext.idData
import com.pitchedapps.frost.ext.toContextId
import com.pitchedapps.frost.extension.FrostCoreExtension
@ -49,5 +50,5 @@ internal constructor(
var tabIndex: Int by mutableStateOf(0)
val frostWebCompose: FrostWebCompose = frostWebComposer.create("test")
val frostWebCompose: FrostWebCompose = frostWebComposer.create(WebTargetId("test"))
}

View File

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

View File

@ -20,13 +20,15 @@ import com.google.common.flogger.FluentLogger
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
class FrostLoggerMiddleware(private val tag: String) : Middleware<FrostWebState, FrostWebAction> {
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 - %s", tag, action::class.simpleName, action)
logger.atInfo().log("FrostWebAction: %s - %s", action::class.simpleName, action)
next(action)
}
@ -34,3 +36,15 @@ class FrostLoggerMiddleware(private val tag: String) : Middleware<FrostWebState,
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)
}
}
}

View File

@ -16,6 +16,7 @@
*/
package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.ext.WebTargetId
import mozilla.components.lib.state.Action
/**
@ -34,32 +35,40 @@ sealed interface FrostWebAction : Action
*/
object InitAction : FrostWebAction
/** Action indicating current url state. */
data class UpdateUrlAction(val url: String) : FrostWebAction
/** Action affecting a single tab */
data class TabAction(val tabId: WebTargetId, val action: Action) : FrostWebAction {
sealed interface Action
/** Action indicating current title state. */
data class UpdateTitleAction(val title: String?) : FrostWebAction
sealed interface ContentAction : Action {
data class UpdateNavigationAction(val canGoBack: Boolean, val canGoForward: Boolean) :
FrostWebAction
/** Action indicating current url state. */
data class UpdateUrlAction(val url: String) : ContentAction
data class UpdateProgressAction(val progress: Int) : FrostWebAction
/** Action indicating current title state. */
data class UpdateTitleAction(val title: String?) : ContentAction
/** Action triggered by user, leading to transient state changes. */
sealed interface UserAction : FrostWebAction {
data class UpdateNavigationAction(val canGoBack: Boolean, val canGoForward: Boolean) :
ContentAction
/** Action to load new url. */
data class LoadUrlAction(val url: String) : UserAction
data class UpdateProgressAction(val progress: Int) : ContentAction
}
object GoBackAction : UserAction
/** Action triggered by user, leading to transient state changes. */
sealed interface UserAction : Action {
object GoForwardAction : UserAction
}
/** Response triggered by webview, indicating [UserAction] fulfillment. */
sealed interface ResponseAction : FrostWebAction {
data class LoadUrlResponseAction(val url: String) : ResponseAction
data class WebStepResponseAction(val steps: Int) : ResponseAction
/** 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
}
}

View File

@ -16,6 +16,9 @@
*/
package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.web.state.reducer.ContentStateReducer
/**
* 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
@ -26,50 +29,36 @@ internal object FrostWebReducer {
fun reduce(state: FrostWebState, action: FrostWebAction): FrostWebState {
return when (action) {
is InitAction -> state
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,
),
)
is TabAction ->
state.updateTabState(action.tabId) { ContentStateReducer.reduce(it, action.action) }
}
}
}
private object FrostTransientWebReducer {
fun reduce(state: TransientWebState, action: UserAction): TransientWebState {
return when (action) {
is UserAction.LoadUrlAction -> state.copy(targetUrl = action.url)
is UserAction.GoBackAction -> state.copy(navStep = state.navStep - 1)
is UserAction.GoForwardAction -> state.copy(navStep = state.navStep + 1)
}
}
internal fun FrostWebState.updateTabState(
tabId: WebTargetId,
update: (TabWebState) -> TabWebState,
): FrostWebState {
val floatingTabMatch = floatingTab?.takeIf { it.id == tabId }
if (floatingTabMatch != null) return copy(floatingTab = update(floatingTabMatch))
val newHomeTabs = homeTabs.updateTabs(tabId, update)
if (newHomeTabs != null) return copy(homeTabs = newHomeTabs)
return this
}
private object FrostTransientFulfillmentWebReducer {
fun reduce(state: TransientWebState, action: ResponseAction): TransientWebState {
return when (action) {
is ResponseAction.LoadUrlResponseAction ->
if (state.targetUrl == action.url) state.copy(targetUrl = null) else state
is ResponseAction.WebStepResponseAction -> state.copy(navStep = state.navStep - action.steps)
}
}
/**
* 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 List<TabWebState>.updateTabs(
tabId: WebTargetId,
update: (TabWebState) -> TabWebState,
): List<TabWebState>? {
val tabIndex = indexOfFirst { it.id == tabId }
if (tabIndex == -1) return null
return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size)
}

View File

@ -16,6 +16,8 @@
*/
package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.ext.FrostAccountId
import com.pitchedapps.frost.ext.WebTargetId
import mozilla.components.lib.state.State
/**
@ -25,6 +27,39 @@ import mozilla.components.lib.state.State
* for Firefox example.
*/
data class FrostWebState(
val auth: AuthWebState = AuthWebState(),
val homeTabs: List<TabWebState> = emptyList(),
var floatingTab: TabWebState? = 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
}
}
data class TabWebState(
val id: WebTargetId,
val userId: AuthWebState.AuthUser,
val baseUrl: String? = null,
val url: String? = null,
val title: String? = null,
@ -32,7 +67,7 @@ data class FrostWebState(
val canGoBack: Boolean = false,
val canGoForward: Boolean = false,
val transientState: TransientWebState = TransientWebState(),
) : State
)
/**
* Transient web state.

View File

@ -16,7 +16,7 @@
*/
package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.ext.WebTargetId
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.Store
@ -27,7 +27,6 @@ import mozilla.components.lib.state.Store
* For firefox example.
*/
class FrostWebStore(
tag: String,
initialState: FrostWebState = FrostWebState(),
middleware: List<Middleware<FrostWebState, FrostWebAction>> = emptyList(),
) :
@ -35,10 +34,14 @@ class FrostWebStore(
initialState,
FrostWebReducer::reduce,
middleware,
"FrostStore-$tag",
"FrostStore",
) {
init {
dispatch(InitAction)
dispatch(UserAction.LoadUrlAction(FB_URL_BASE))
}
}
operator fun FrostWebState.get(tabId: WebTargetId): TabWebState? {
if (floatingTab?.id == tabId) return floatingTab
return homeTabs.find { it.id == tabId }
}

View File

@ -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.TabWebState
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 [TabWebState] if
* available. Otherwise returns `null`.
*
* @param store to lookup this target in.
*/
fun lookupIn(store: FrostWebStore): TabWebState? = lookupIn(store.state)
/**
* Looks up this target in the given [FrostWebState] and returns the matching [TabWebState] if
* available. Otherwise returns `null`.
*
* @param state to lookup this target in.
*/
abstract fun lookupIn(state: FrostWebState): TabWebState?
/**
* 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 [TabWebState] to the (sub) state that should get observed
* for changes.
*/
@Composable
fun <R> observeAsComposableStateFrom(
store: FrostWebStore,
observe: (TabWebState?) -> R,
): State<TabWebState?> {
return store.observeAsComposableState(
map = { state -> lookupIn(state) },
observe = { state -> observe(lookupIn(state)) },
)
}
data class HomeTab(val id: WebTargetId) : Target() {
override fun lookupIn(state: FrostWebState): TabWebState? {
return state.homeTabs.find { it.id == id }
}
}
object FloatingTab : Target() {
override fun lookupIn(state: FrostWebState): TabWebState? {
return state.floatingTab
}
}
}

View File

@ -0,0 +1,83 @@
/*
* 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
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.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.TabWebState
import com.pitchedapps.frost.web.state.TransientWebState
internal object ContentStateReducer {
fun reduce(state: TabWebState, action: Action): TabWebState {
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 TabAction.UserAction ->
state.copy(
transientState =
FrostTransientWebReducer.reduce(
state.transientState,
action,
),
)
is TabAction.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: TabAction.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)
}
}
}

View File

@ -16,18 +16,10 @@
*/
package com.pitchedapps.frost.webview
import com.pitchedapps.frost.compose.webview.FrostWebCompose
import com.pitchedapps.frost.web.state.FrostLoggerMiddleware
import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.ext.WebTargetId
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.android.components.ViewModelComponent
import javax.inject.Inject
import javax.inject.Qualifier
import javax.inject.Scope
@ -45,38 +37,11 @@ annotation class FrostWebScoped
@FrostWebScoped @DefineComponent(parent = ViewModelComponent::class) interface FrostWebComponent
/** Using this component seems to be buggy, leading to an invalid param tabId error. */
@DefineComponent.Builder
interface FrostWebComponentBuilder {
fun id(@BindsInstance @FrostWeb id: String): FrostWebComponentBuilder
@BindsInstance fun tabId(@FrostWeb tabId: WebTargetId): FrostWebComponentBuilder
fun build(): FrostWebComponent
}
@Module
@InstallIn(FrostWebComponent::class)
internal object FrostWebModule {
@Provides
@FrostWebScoped
fun frostStore(@FrostWeb id: String): FrostWebStore {
val logger = FrostLoggerMiddleware(id)
return FrostWebStore(tag = id, middleware = listOf(logger))
}
}
class FrostWebComposer
@Inject
internal constructor(private val frostWebComponentBuilder: FrostWebComponentBuilder) {
fun create(id: String): FrostWebCompose {
val frostWebComponent = frostWebComponentBuilder.id(id).build()
val frostWebEntryPoint = EntryPoints.get(frostWebComponent, FrostWebEntryPoint::class.java)
return frostWebEntryPoint.frostWebCompose()
}
@EntryPoint
@InstallIn(FrostWebComponent::class)
interface FrostWebEntryPoint {
fun frostWebCompose(): FrostWebCompose
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.FrostChromeClient
import com.pitchedapps.frost.compose.webview.FrostWebCompose
import com.pitchedapps.frost.compose.webview.FrostWebViewClient
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)
}
}

View File

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