1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 12:02:33 +01:00

Merge pull request #1946 from AllanWang/gecko-removal

This commit is contained in:
Allan Wang 2023-06-20 17:52:03 -07:00 committed by GitHub
commit 6beb8e05b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 16 additions and 1061 deletions

View File

@ -175,53 +175,14 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
// https://maven.mozilla.org/?prefix=maven2/org/mozilla/geckoview/geckoview-beta/
def geckoviewChannel = "beta"
def geckoviewVersion = "105.0.20220908185813"
// implementation "org.mozilla.geckoview:geckoview-${geckoviewChannel}:${geckoviewVersion}"
// https://maven.mozilla.org/maven2/org/mozilla/components/browser-engine-gecko/maven-metadata.xml
// https://github.com/mozilla-mobile/reference-browser/blob/master/buildSrc/src/main/java/AndroidComponents.kt
// https://nightly.maven.mozilla.org/maven2/org/mozilla/components/browser-engine-gecko/maven-metadata.xml
def mozillaAndroidComponents = "116.0.20230617040331"
implementation "org.mozilla.components:browser-engine-gecko:${mozillaAndroidComponents}"
implementation "org.mozilla.components:browser-domains:${mozillaAndroidComponents}"
implementation "org.mozilla.components:browser-icons:${mozillaAndroidComponents}"
implementation "org.mozilla.components:browser-menu:${mozillaAndroidComponents}"
implementation "org.mozilla.components:browser-session-storage:${mozillaAndroidComponents}"
implementation "org.mozilla.components:browser-state:${mozillaAndroidComponents}"
implementation "org.mozilla.components:browser-toolbar:${mozillaAndroidComponents}"
implementation "org.mozilla.components:concept-engine:${mozillaAndroidComponents}"
implementation "org.mozilla.components:concept-menu:${mozillaAndroidComponents}"
implementation "org.mozilla.components:concept-storage:${mozillaAndroidComponents}"
implementation "org.mozilla.components:concept-toolbar:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-app-links:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-addons:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-autofill:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-awesomebar:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-contextmenu:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-downloads:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-findinpage:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-logins:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-media:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-prompts:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-push:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-session:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-sitepermissions:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-tabs:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-toolbar:${mozillaAndroidComponents}"
implementation "org.mozilla.components:feature-webnotifications:${mozillaAndroidComponents}"
implementation "org.mozilla.components:support-ktx:${mozillaAndroidComponents}"
implementation "org.mozilla.components:support-webextensions:${mozillaAndroidComponents}"
implementation "org.mozilla.components:ui-autocomplete:${mozillaAndroidComponents}"
implementation "org.mozilla.components:compose-engine:${mozillaAndroidComponents}"
implementation "org.mozilla.components:lib-state:${mozillaAndroidComponents}"
// Kept for reference; not needed
implementation "org.mozilla.components:browser-state:${mozillaAndroidComponents}"
// Compose
def composeVersion = "1.4.3"

View File

@ -26,7 +26,6 @@ import com.pitchedapps.frost.db.FrostDb
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
@ -53,8 +52,6 @@ class StartActivity : AppCompatActivity() {
@Inject lateinit var dataStore: FrostDataStore
@Inject lateinit var frostCoreExtension: FrostCoreExtension
@Inject lateinit var store: FrostWebStore
override fun onCreate(savedInstanceState: Bundle?) {
@ -63,8 +60,6 @@ class StartActivity : AppCompatActivity() {
lifecycleScope.launch {
val id = withContext(Dispatchers.IO) { getCurrentAccountId() }
// frostCoreExtension.install()
logger.atInfo().log("Starting Frost with id %s", id)
// TODO load real tabs

View File

@ -16,14 +16,10 @@
*/
package com.pitchedapps.frost.components
import com.pitchedapps.frost.web.state.FrostWebStore
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.permission.SitePermissionsStorage
import org.mozilla.geckoview.GeckoRuntime
/**
* Core injections.
@ -34,24 +30,8 @@ import org.mozilla.geckoview.GeckoRuntime
class Core
@Inject
internal constructor(
private val runtimeProvider: Provider<GeckoRuntime>,
private val engineProvider: Provider<Engine>,
private val storeProvider: Provider<BrowserStore>,
private val sessionStorageProvider: Provider<SessionStorage>,
private val sitePermissionsStorageProvider: Provider<SitePermissionsStorage>,
private val storeProvider: Provider<FrostWebStore>,
) {
val runtime: GeckoRuntime
get() = runtimeProvider.get()
val engine: Engine
get() = engineProvider.get()
val store: BrowserStore
val store: FrostWebStore
get() = storeProvider.get()
val sessionStorage: SessionStorage
get() = sessionStorageProvider.get()
val sitePermissionsStorage: SitePermissionsStorage
get() = sitePermissionsStorageProvider.get()
}

View File

@ -16,12 +16,9 @@
*/
package com.pitchedapps.frost.components
import com.pitchedapps.frost.components.usecases.FloatingTabsUseCases
import com.pitchedapps.frost.components.usecases.HomeTabsUseCases
import javax.inject.Inject
import javax.inject.Singleton
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tabs.TabsUseCases
/**
* Collection of frost use cases.
@ -32,8 +29,5 @@ import mozilla.components.feature.tabs.TabsUseCases
class UseCases
@Inject
internal constructor(
val session: SessionUseCases,
val tabs: TabsUseCases,
val homeTabs: HomeTabsUseCases,
val floatingTabs: FloatingTabsUseCases,
)

View File

@ -1,55 +0,0 @@
/*
* 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.components.usecases
import javax.inject.Inject
import javax.inject.Singleton
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
/** Use cases for the floating tabs, located in an overlay activity. */
@Singleton
class FloatingTabsUseCases @Inject internal constructor(private val store: BrowserStore) {
/**
* Create or update the floating tab url.
*
* There is at most one floating tab at all times.
*
* TODO: Add context id.
*/
fun createFloatingTab(url: String) {
if (store.state.findTab(TAB_ID) == null) {
val tab = createTab(url = url, id = TAB_ID)
store.dispatch(TabListAction.AddTabAction(tab = tab, select = false))
}
store.dispatch(EngineAction.LoadUrlAction(tabId = TAB_ID, url = url))
}
/** Remove floating tab screen. */
fun removeFloatingTab() {
store.dispatch(TabListAction.RemoveTabAction(tabId = TAB_ID))
}
companion object {
/** Unique tab id for floating screen. */
const val TAB_ID = "floating_tab_id"
}
}

View File

@ -16,48 +16,23 @@
*/
package com.pitchedapps.frost.components.usecases
import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.ext.GeckoContextId
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.web.state.TabListAction.SelectHomeTab
import com.pitchedapps.frost.web.state.TabListAction.SetHomeTabs
import javax.inject.Inject
import javax.inject.Singleton
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
/** Use cases for the home screen. */
@Singleton
class HomeTabsUseCases @Inject internal constructor(private val store: BrowserStore) {
class HomeTabsUseCases @Inject internal constructor(private val store: FrostWebStore) {
/**
* Create the provided tabs.
*
* If there are existing tabs, they will be replaced.
*/
fun createHomeTabs(
contextId: GeckoContextId,
selectedIndex: Int,
urls: List<String>
): List<TabSessionState> {
store.dispatch(TabListAction.RemoveAllTabsAction())
if (urls.isEmpty()) return emptyList()
val tabs =
urls.mapIndexed { i, url -> createTab(id = tabId(i), url = url, contextId = contextId.id) }
store.dispatch(TabListAction.AddMultipleTabsAction(tabs))
// Preload all tabs
for (tab in tabs) {
store.dispatch(EngineAction.LoadUrlAction(tab.id, tab.content.url))
// if (tab.content.url == MESSENGER_URL) {
// store.dispatch(EngineAction.ToggleDesktopModeAction(tab.id, enable = true))
// }
}
selectHomeTab(selectedIndex)
return tabs
fun createHomeTabs(items: List<FbItem>) {
store.dispatch(SetHomeTabs(items))
}
/**
@ -65,51 +40,7 @@ class HomeTabsUseCases @Inject internal constructor(private val store: BrowserSt
*
* If the index is OOB, the selected tab will be null.
*/
fun selectHomeTab(index: Int) {
store.dispatch(TabListAction.SelectTabAction(tabId(index)))
}
/** Reload tab contents based on index. */
fun reloadTab(index: Int) {
store.dispatch(EngineAction.ReloadAction(tabId(index)))
}
// Cannot use injection
class HomeMiddleware : Middleware<BrowserState, BrowserAction> {
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction
) {
// if (action is ContentAction.UpdateUrlAction) {
// logger.atInfo().log("url update %s", action)
// if (action.sessionId == context.state.tabs[0].id) {
// val customTab = context.state.tabs[3]
// context.dispatch(EngineAction.LoadUrlAction(tabId = customTab.id, url =
// action.url))
// }
// return
// }
next(action)
// when (action) {
// is ContentAction.UpdateUrlAction -> {
// logger.atInfo().log("url update %s", action)
// action.sessionId
// }
// else -> next(action)
// }
}
companion object {
private val logger = FluentLogger.forEnclosingClass()
}
}
companion object {
private const val PREFIX = "frost-home"
private fun isHomeTab(id: String): Boolean = id.startsWith(PREFIX)
private fun tabId(index: Int) = "$PREFIX--$index"
fun selectHomeTab(tabId: WebTargetId) {
store.dispatch(SelectHomeTab(tabId))
}
}

View File

@ -1,49 +0,0 @@
/*
* 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.components.usecases
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.app.links.AppLinksUseCases
import mozilla.components.feature.contextmenu.ContextMenuUseCases
import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tabs.TabsUseCases
/** Module for Mozilla use cases. */
@Module
@InstallIn(SingletonComponent::class)
class UseCasesModule {
@Provides @Singleton fun sessionUseCases(store: BrowserStore) = SessionUseCases(store)
@Provides @Singleton fun tabsUseCases(store: BrowserStore) = TabsUseCases(store)
@Provides @Singleton fun contextMenuUseCases(store: BrowserStore) = ContextMenuUseCases(store)
@Provides @Singleton fun downloadsUseCases(store: BrowserStore) = DownloadsUseCases(store)
@Provides
@Singleton
fun appLinksUseCases(@ApplicationContext context: Context) = AppLinksUseCases(context)
}

View File

@ -1,134 +0,0 @@
/*
* 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.compose
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.helper.Target
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
/**
* Simple Frost web view, with progress bar.
*
* TODO add swipe?
*/
@Composable
fun FrostWeb(engine: Engine, store: BrowserStore, target: Target, modifier: Modifier = Modifier) {
val selectedTab by
target.observeAsComposableStateFrom(
store = store,
observe = { tab ->
// Render if the tab itself changed or when the state of the linked engine session changes
arrayOf(
tab?.id,
tab?.engineState?.engineSession,
tab?.engineState?.crashed,
tab?.content?.firstContentfulPaint,
// Added on top of Mozilla's WebContent observe values
tab?.content?.progress,
)
},
)
Box(modifier = modifier) {
ProgressBar(progress = selectedTab?.content?.progress, modifier = Modifier.zIndex(1f))
WebContent(engine = engine, store = store, state = WebContentState(selectedTab))
}
}
@Composable
private fun ProgressBar(progress: Int?, modifier: Modifier = Modifier) {
val shouldDisplay = progress != null && progress in 0 until 100
LinearProgressIndicator(
modifier = modifier.alpha(if (shouldDisplay) 1f else 0f).height(2.dp),
progress = if (progress == null) 0f else progress.toFloat() * 0.01f,
)
}
/** Minimum amount of [SessionState] needed to update [WebContent] */
private data class WebContentState(
val id: String,
val engineSession: EngineSession?,
val canGoBack: Boolean,
) {
companion object {
operator fun invoke(tab: SessionState?): WebContentState? {
tab ?: return null
return WebContentState(
id = tab.id,
engineSession = tab.engineState.engineSession,
canGoBack = tab.content.canGoBack,
)
}
}
}
/**
* Based on Mozilla WebContent:
* https://github.com/mozilla-mobile/android-components/blob/main/components/compose/engine/src/main/java/mozilla/components/compose/engine/WebContent.kt
*
* WebView from Accompanist:
* https://github.com/google/accompanist/blob/main/web/src/main/java/com/google/accompanist/web/WebView.kt
*
* Blinking Bug: Switching tabs causes an empty state where nothing is rendered. Not as noticeable
* on light themes, but it's a full black screen on dark mode. May be related to
* https://github.com/mozilla-mobile/fenix/issues/1901
*/
@Composable
private fun WebContent(engine: Engine, store: BrowserStore, state: WebContentState?) {
BackHandler(state?.canGoBack == true) {
val id = state?.id ?: return@BackHandler
store.dispatch(EngineAction.GoBackAction(id, userInteraction = true))
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context -> engine.createView(context).asView() },
update = { view ->
val engineView = view as EngineView
if (state == null) {
engineView.release()
} else {
val session = state.engineSession
if (session == null) {
// This tab does not have an EngineSession that we can render yet. Let's dispatch an
// action to request creating one. Once one was created and linked to this session, this
// method will get invoked again.
store.dispatch(EngineAction.CreateEngineSessionAction(state.id))
} else {
engineView.render(session)
}
}
}
)
}

View File

@ -1,80 +0,0 @@
/*
* 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.extension
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.AddonsProvider
import mozilla.components.feature.addons.amo.AddonCollectionProvider
import mozilla.components.feature.addons.update.AddonUpdater
import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.support.base.android.NotificationsDelegate
/** Injections related to addons. */
@Module
@InstallIn(SingletonComponent::class)
object FrostAddOnsModule {
// https://services.addons.mozilla.org/api/v4/accounts/account/17569911/collections/Frost-for-Facebook/addons/?page_size=10&lang=en-US
@Provides
@Singleton
fun addonsProvider(@ApplicationContext context: Context, client: Client): AddonsProvider {
return AddonCollectionProvider(
context = context,
client = client,
collectionUser = "17569911",
collectionName = "Frost-for-Facebook",
)
}
@Provides
@Singleton
fun addonUpdater(
@ApplicationContext context: Context,
notificationsDelegate: NotificationsDelegate
): AddonUpdater {
return DefaultAddonUpdater(
applicationContext = context,
notificationsDelegate = notificationsDelegate
)
}
@Provides
@Singleton
fun addonManager(
store: BrowserStore,
engine: Engine,
addonsProvider: AddonsProvider,
addonUpdater: AddonUpdater
): AddonManager {
return AddonManager(
store = store,
runtime = engine,
addonsProvider = addonsProvider,
addonUpdater = addonUpdater
)
}
}

View File

@ -1,175 +0,0 @@
/*
* 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.extension
import androidx.lifecycle.LifecycleOwner
import com.google.common.flogger.FluentLogger
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.lib.state.ext.flow
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.support.webextensions.WebExtensionController
import org.json.JSONObject
/**
* Frost's built in extension.
*
* Structure based off of
*
* https://github.com/mozilla-mobile/android-components/blob/main/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FxaWebChannelFeature.kt
*/
@Singleton
class FrostCoreExtension
@Inject
internal constructor(
private val engine: Engine,
private val store: BrowserStore,
private val converter: ExtensionModelConverter,
) {
private val extensionController =
WebExtensionController(
WEB_CHANNEL_EXTENSION_ID,
WEB_CHANNEL_EXTENSION_URL,
WEB_CHANNEL_MESSAGING_ID,
)
fun install() {
logger.atInfo().log("extension background start")
val messageHandler = FrostBackgroundMessageHandler()
extensionController.registerBackgroundMessageHandler(
messageHandler,
WEB_CHANNEL_BACKGROUND_MESSAGING_ID,
)
extensionController.install(
engine,
onSuccess = {
logger.atInfo().log("extension install success")
extensionController.sendBackgroundMessage(
JSONObject().apply { put("test", 0) },
WEB_CHANNEL_BACKGROUND_MESSAGING_ID
)
},
onError = { t -> logger.atWarning().withCause(t).log("extension install failure") },
)
}
suspend fun installContent(owner: LifecycleOwner? = null, customTabSessionId: String? = null) {
logger.atInfo().log("extension content start")
store
.flow(owner)
.mapNotNull { state -> state.findCustomTabOrSelectedTab(customTabSessionId) }
.ifChanged { it.engineState.engineSession }
.collect {
it.engineState.engineSession?.let { engineSession ->
logger.atInfo().log("Register content message handler ${it.id}")
registerContentMessageHandler(engineSession)
}
}
}
private fun registerContentMessageHandler(engineSession: EngineSession) {
val messageHandler = FrostMessageHandler(converter)
extensionController.registerContentMessageHandler(engineSession, messageHandler)
}
private class FrostBackgroundMessageHandler : MessageHandler {
override fun onMessage(message: Any, source: EngineSession?): Any? {
logger.atInfo().log("onMessage: %s", message)
return null
}
override fun onPortConnected(port: Port) {
logger.atInfo().log("background onPortConnected: %s", port.name())
}
}
private class FrostMessageHandler(private val converter: ExtensionModelConverter) :
MessageHandler {
override fun onMessage(message: Any, source: EngineSession?): Any? {
if (message is String) {
logger.atInfo().log("onMessage: %s", message)
return null
}
val model = converter.fromJSONObject(message as? JSONObject)
if (model == null) {
logger.atWarning().log("onMessage - unexpected format: %s", message)
return null
}
logger.atFine().log("onMessage: %s", model)
when (model) {
is UrlClick -> {
logger.atInfo().log("UrlClick ${model.url}")
return true
}
else -> {
logger.atWarning().log("onMessage - unhandled: %s", model)
}
}
return null
}
override fun onPortConnected(port: Port) {
logger.atInfo().log("content onPortConnected")
super.onPortConnected(port)
}
override fun onPortMessage(message: Any, port: Port) {
if (message is String) {
logger.atInfo().log("onPortMessage: %s", message)
return
}
val model = converter.fromJSONObject(message as? JSONObject)
if (model == null) {
logger.atWarning().log("onPortMessage - unexpected format: %s", message)
return
}
logger.atFine().log("onPortMessage: %s", model)
// TODO
}
private fun Port.postMessage(message: ExtensionModel) {
val json = converter.toJSONObject(message)
if (json == null) {
logger.atSevere().log("postMessage - unexpected format: %s", message)
return
}
postMessage(json)
}
}
companion object {
private val logger = FluentLogger.forEnclosingClass()
const val WEB_CHANNEL_EXTENSION_ID = "frost_gecko_core@pitchedapps"
const val WEB_CHANNEL_MESSAGING_ID = "frostChannel"
const val WEB_CHANNEL_BACKGROUND_MESSAGING_ID = "frostBackgroundChannel"
const val WEB_CHANNEL_EXTENSION_URL = "resource://android/assets/frostcore/"
}
}

View File

@ -1,72 +0,0 @@
/*
* 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.extension
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import javax.inject.Inject
import javax.inject.Singleton
import org.json.JSONObject
/** Model for messages between Frost native and Frost web extension. */
object ExtensionType {
const val TEST = "type-test"
const val URL_CLICK = "url-click"
fun moshiFactory(): JsonAdapter.Factory {
return PolymorphicJsonAdapterFactory.of(ExtensionModel::class.java, "type")
.withSubtype(TestModel::class.java, TEST)
.withSubtype(UrlClick::class.java, URL_CLICK)
}
}
/**
* kotlinx.serialization seems to support polymorphism:
* https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md
*
* But it won't work for us since we receive data from JS without metadata. As a result, we will map
* the decoding ourself.
*/
sealed interface ExtensionModel {
companion object
}
@JsonClass(generateAdapter = true)
data class TestModel(val message: String = "Test Model Message") : ExtensionModel
@JsonClass(generateAdapter = true) data class UrlClick(val url: String) : ExtensionModel
@OptIn(ExperimentalStdlibApi::class)
@Singleton
class ExtensionModelConverter @Inject internal constructor(moshi: Moshi) {
private val jsonObjectAdapter = moshi.adapter<JSONObject>()
private val modelAdapter = moshi.adapter<ExtensionModel>()
fun toJSONObject(value: ExtensionModel): JSONObject? {
val jsonValue = modelAdapter.toJsonValue(value) ?: return null
return jsonObjectAdapter.fromJsonValue(jsonValue)
}
fun fromJSONObject(value: JSONObject?): ExtensionModel? {
value ?: return null
val jsonValue = jsonObjectAdapter.toJsonValue(value) ?: return null
return modelAdapter.fromJsonValue(jsonValue)
}
}

View File

@ -19,7 +19,6 @@ package com.pitchedapps.frost.hilt
import com.pitchedapps.frost.components.Core
import com.pitchedapps.frost.components.FrostDataStore
import com.pitchedapps.frost.components.UseCases
import com.pitchedapps.frost.extension.ExtensionModelConverter
import javax.inject.Inject
import javax.inject.Singleton
@ -36,6 +35,5 @@ class FrostComponents
internal constructor(
val core: Core,
val useCases: UseCases,
val extensionModelConverter: ExtensionModelConverter,
val dataStore: FrostDataStore,
)

View File

@ -1,173 +0,0 @@
/*
* 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,7 +16,6 @@
*/
package com.pitchedapps.frost.hilt
import com.pitchedapps.frost.extension.ExtensionType
import com.squareup.moshi.*
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
@ -37,7 +36,7 @@ object MoshiModule {
@Singleton
fun moshi(): Moshi {
return Moshi.Builder()
.add(ExtensionType.moshiFactory())
// .add(ExtensionType.moshiFactory())
.add(JSONObjectAdapter())
.addLast(KotlinJsonAdapterFactory())
.build()

View File

@ -1,163 +0,0 @@
/*
* 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.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.ContentAlpha
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pitchedapps.frost.compose.FrostWeb
import com.pitchedapps.frost.ext.GeckoContextId
import com.pitchedapps.frost.ext.components
import mozilla.components.browser.state.helper.Target
/**
* Screen for MainActivity.
*
* Note that [tabs] are passed in without listening to a datastore as they should be effectively
* fixed. We don't want to update tabs as soon as users change the tabs in settingss.
*/
@Composable
fun MainScreen(modifier: Modifier = Modifier, tabs: List<MainTabItem>) {
val vm: MainScreenViewModel = viewModel()
if (tabs.isEmpty()) return // not ready
// val contextId = GeckoContextId("test-context")
val contextId = vm.contextIdFlow.collectAsState(initial = null).value ?: return // not ready
LaunchedEffect(vm) { vm.frostCoreExtension.installContent() }
val onTabSelect =
remember(vm) {
{ selectedIndex: Int ->
if (selectedIndex == vm.tabIndex) {
vm.components.useCases.homeTabs.reloadTab(selectedIndex)
// context.launchFloatingUrl(FACEBOOK_M_URL)
} else {
// Change? What if previous selected tab is not home tab
vm.components.useCases.homeTabs.selectHomeTab(selectedIndex)
vm.tabIndex = selectedIndex
}
}
}
MainContainer(
modifier = modifier,
contextId = contextId,
tabIndex = vm.tabIndex,
tabs = tabs,
onTabSelect = onTabSelect,
)
}
@Composable
private fun MainContainer(
contextId: GeckoContextId,
tabIndex: Int,
tabs: List<MainTabItem>,
onTabSelect: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val components = LocalContext.current.components
LaunchedEffect(contextId, tabs) {
components.useCases.homeTabs.createHomeTabs(contextId, tabIndex, tabs.map { it.url })
}
Column(modifier = modifier) {
MainHeader(
modifier = Modifier.statusBarsPadding(),
title = tabs.getOrNull(tabIndex)?.title ?: "",
)
if (tabs.size > 1) {
MainTabRow(
selectedIndex = tabIndex,
items = tabs,
onTabSelect = onTabSelect,
)
}
// For tab switching, must use SelectedTab
// https://github.com/mozilla-mobile/android-components/issues/12798
FrostWeb(
engine = components.core.engine,
store = components.core.store,
target = Target.SelectedTab,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainHeader(
title: String,
modifier: Modifier = Modifier,
) {
TopAppBar(
modifier = modifier,
title = { Text(text = title) },
)
}
@Composable
fun MainTabRow(
selectedIndex: Int,
items: List<MainTabItem>,
onTabSelect: (Int) -> Unit,
modifier: Modifier = Modifier
) {
TabRow(modifier = modifier, selectedTabIndex = selectedIndex, indicator = {}) {
items.forEachIndexed { i, item ->
val selected = selectedIndex == i
Tab(selected = selected, onClick = { onTabSelect(i) }) {
MainTabItem(modifier = Modifier.padding(16.dp), item = item, selected = selected)
}
}
}
}
@Composable
private fun MainTabItem(item: MainTabItem, selected: Boolean, modifier: Modifier = Modifier) {
val alpha by
animateFloatAsState(
targetValue = if (selected) 1f else ContentAlpha.medium,
label = "Tab Alpha",
)
Icon(
modifier = modifier.alpha(alpha).size(24.dp),
contentDescription = item.title,
imageVector = item.icon,
)
}

View File

@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModel
import com.pitchedapps.frost.ext.GeckoContextId
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
@ -40,7 +39,6 @@ class MainScreenViewModel
internal constructor(
@ApplicationContext context: Context,
val components: FrostComponents,
val frostCoreExtension: FrostCoreExtension,
val store: FrostWebStore,
val frostWebComposer: FrostWebComposer,
// sample: FrostWebEntrySample,