diff --git a/app/aars/appcompat.aar b/app/aars/appcompat.aar deleted file mode 100644 index e0a6ebf3d..000000000 Binary files a/app/aars/appcompat.aar and /dev/null differ diff --git a/app/build.gradle b/app/build.gradle index e21a3597c..159d542d1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -397,7 +397,6 @@ android { configurations { armImplementation x86Implementation - all*.exclude group: 'androidx.appcompat', module: 'appcompat' } repositories { @@ -414,8 +413,7 @@ dependencies { implementation deps.lifecycle.runtime implementation deps.lifecycle.viewmodel implementation deps.support.cardview - //implementation deps.support.app_compat - implementation(name:'appcompat', ext:'aar') + implementation deps.support.app_compat implementation deps.support.vector_drawable implementation deps.support.annotations implementation deps.constraint_layout @@ -429,24 +427,25 @@ dependencies { implementation deps.android_components.telemetry implementation deps.android_components.browser_errorpages implementation deps.android_components.browser_search + implementation deps.android_components.browser_state implementation deps.android_components.browser_storage implementation deps.android_components.browser_domains implementation deps.android_components.service_accounts implementation deps.android_components.mozilla_service_location implementation deps.android_components.ui_autocomplete + implementation deps.android_components.concept_engine implementation deps.android_components.concept_fetch implementation deps.android_components.lib_fetch implementation deps.android_components.support_rustlog implementation deps.android_components.support_rusthttp + implementation deps.android_components.feature_accounts + implementation deps.android_components.feature_webcompat implementation deps.android_components.glean implementation deps.app_services.rustlog // TODO this should not be necessary at all, see Services.kt implementation deps.work.runtime - // TODO this should not be necessary at all, see Services.kt - implementation deps.work.runtime - // Kotlin dependency implementation deps.kotlin.stdlib implementation deps.kotlin.coroutines diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java index 3b04eb0ad..085a7693d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java @@ -252,9 +252,7 @@ protected void onCreate(Bundle savedInstanceState) { BitmapCache.getInstance(this).onCreate(); Bundle extras = getIntent() != null ? getIntent().getExtras() : null; - SessionStore.get().setContext(this, extras); - SessionStore.get().initializeServices(); - SessionStore.get().initializeStores(this); + SessionStore.get().initialize(this, extras); SessionStore.get().setLocales(LocaleUtils.getPreferredLanguageTags(this)); EngineProvider.INSTANCE.getOrCreateRuntime(this).appendAppNotesToCrashReport("Firefox Reality " + BuildConfig.VERSION_NAME + "-" + BuildConfig.VERSION_CODE + "-" + BuildConfig.FLAVOR + "-" + BuildConfig.BUILD_TYPE + " (" + BuildConfig.GIT_HASH + ")"); @@ -393,19 +391,7 @@ public void onWindowVideoAvailabilityChanged(@NonNull WindowWidget aWindow) { mWhatsNewWidget.show(UIWidget.REQUEST_FOCUS); } - EngineProvider.INSTANCE.loadExtensions() - .thenAcceptAsync(aVoid -> { - Log.d(LOGTAG, "WebExtensions loaded"); - mWindows.restoreSessions(); - }, getServicesProvider().getExecutors().mainThread()) - .exceptionally(throwable -> { - String msg = throwable.getLocalizedMessage(); - if (msg != null) { - Log.e(LOGTAG, "Extensions load error: " + msg); - } - mWindows.restoreSessions(); - return null; - }); + mWindows.restoreSessions(); } private void attachToWindow(@NonNull WindowWidget aWindow, @Nullable WindowWidget aPrevWindow) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java index e354f6afd..c05b0a173 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java @@ -14,6 +14,7 @@ import org.mozilla.vrbrowser.browser.Accounts; import org.mozilla.vrbrowser.browser.Places; import org.mozilla.vrbrowser.browser.Services; +import org.mozilla.vrbrowser.browser.engine.EngineProvider; import org.mozilla.vrbrowser.db.AppDatabase; import org.mozilla.vrbrowser.db.DataRepository; import org.mozilla.vrbrowser.downloads.DownloadsManager; @@ -37,11 +38,6 @@ public class VRBrowserApplication extends Application implements AppServicesProv @Override public void onCreate() { super.onCreate(); - mAppExecutors = new AppExecutors(); - mBitmapCache = new BitmapCache(this, mAppExecutors.diskIO(), mAppExecutors.mainThread()); - - TelemetryWrapper.init(this); - GleanMetricsService.init(this); } protected void onActivityCreate() { @@ -50,6 +46,11 @@ protected void onActivityCreate() { mAccounts = new Accounts(this); mDownloadsManager = new DownloadsManager(this); mSpeechService = new SpeechService(this); + mAppExecutors = new AppExecutors(); + mBitmapCache = new BitmapCache(this, mAppExecutors.diskIO(), mAppExecutors.mainThread()); + + TelemetryWrapper.init(this, EngineProvider.INSTANCE.getDefaultClient(this)); + GleanMetricsService.init(this, EngineProvider.INSTANCE.getDefaultClient(this)); } @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt index 48013ee63..906a97f38 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt @@ -40,7 +40,7 @@ class Services(val context: Context, places: Places): GeckoSession.NavigationDel companion object { const val CLIENT_ID = "7ad9917f6c55fb77" - const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel" } interface TabReceivedDelegate { fun onTabsReceived(uri: List) @@ -98,9 +98,11 @@ class Services(val context: Context, places: Places): GeckoSession.NavigationDel } } } + public val serverConfig = ServerConfig(Server.RELEASE, CLIENT_ID, REDIRECT_URL) + val accountManager = FxaAccountManager( context = context, - serverConfig = ServerConfig(Server.RELEASE, CLIENT_ID, REDIRECT_URL), + serverConfig = serverConfig, deviceConfig = DeviceConfig( // This is a default name, and can be changed once user is logged in. // E.g. accountManager.authenticatedAccount()?.deviceConstellation()?.setDeviceNameAsync("new name") @@ -150,4 +152,5 @@ class Services(val context: Context, places: Places): GeckoSession.NavigationDel return GeckoResult.ALLOW } + } \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionChangeListener.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionChangeListener.java index c9361379f..7cca32adc 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionChangeListener.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionChangeListener.java @@ -4,9 +4,12 @@ import org.mozilla.vrbrowser.browser.engine.Session; public interface SessionChangeListener { - default void onRemoveSession(Session aSession) {} + default void onSessionAdded(Session aSession) {} + default void onSessionOpened(Session aSession) {} + default void onSessionClosed(String aId) {} + default void onSessionRemoved(String aId) {} + default void onSessionStateChanged(Session aSession, boolean aActive) {} default void onCurrentSessionChange(GeckoSession aOldSession, GeckoSession aSession) {} default void onStackSession(Session aSession) {} default void onUnstackSession(Session aSession, Session aParent) {} - default void onActiveStateChange(Session aSession, boolean aActive) {} } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java index 6c838b516..a38b327b8 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java @@ -205,7 +205,7 @@ public void setTelemetryEnabled(boolean isEnabled) { // If the state of Telemetry is not the same, we reinitialize it. final boolean hasEnabled = isTelemetryEnabled(); if (hasEnabled != isEnabled) { - TelemetryWrapper.init(mContext); + TelemetryWrapper.init(mContext, EngineProvider.INSTANCE.getDefaultClient(mContext)); } TelemetryHolder.get().getConfiguration().setUploadEnabled(isEnabled); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/adapter/ComponentsAdapter.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/adapter/ComponentsAdapter.kt new file mode 100644 index 000000000..e0bf6f53e --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/adapter/ComponentsAdapter.kt @@ -0,0 +1,77 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.browser.adapter + +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.EngineState +import mozilla.components.browser.state.state.ReaderState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.store.BrowserStore +import org.mozilla.geckoview.GeckoSession +import org.mozilla.vrbrowser.browser.components.GeckoEngineSession +import org.mozilla.vrbrowser.browser.engine.Session + +class ComponentsAdapter private constructor( + val store: BrowserStore = BrowserStore() +) { + companion object { + private val instance: ComponentsAdapter = ComponentsAdapter() + + @JvmStatic + fun get(): ComponentsAdapter = instance + } + + fun addSession(session: Session) { + store.dispatch(TabListAction.AddTabAction( + tab = session.toTabSessionState() + )) + } + + fun removeSession(id: String) { + store.dispatch(TabListAction.RemoveTabAction( + tabId = id + )) + } + + fun selectSession(session: Session) { + store.dispatch(TabListAction.SelectTabAction( + tabId = session.id + )) + } + + fun link(tabId: String, geckoSession: GeckoSession) { + store.dispatch(EngineAction.LinkEngineSessionAction( + tabId, + GeckoEngineSession(geckoSession) + )) + } + + fun unlink(tabId: String) { + store.dispatch(EngineAction.UnlinkEngineSessionAction( + tabId + )) + } +} + +private fun Session.toTabSessionState(): TabSessionState { + return TabSessionState( + id = id, + content = ContentState( + url = currentUri, + private = isPrivateMode, + title = currentTitle + ), + parentId = null, + extensionState = emptyMap(), + readerState = ReaderState(), + engineState = EngineState( + GeckoEngineSession(geckoSession), + null + ) + ) +} \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoEngineSession.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoEngineSession.kt new file mode 100644 index 000000000..d8cc39ee1 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoEngineSession.kt @@ -0,0 +1,45 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.browser.components + +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.EngineSessionState +import mozilla.components.concept.engine.Settings +import org.json.JSONObject +import org.mozilla.geckoview.GeckoSession +import org.mozilla.vrbrowser.browser.engine.SessionStore + +class GeckoEngineSession( + val geckoSession: GeckoSession +): EngineSession() { + constructor() : + this(SessionStore.get().createSession(false, false).geckoSession) + + override fun loadUrl(url: String, parent: EngineSession?, flags: LoadUrlFlags, additionalHeaders: Map?) { + geckoSession.loadUri(url) + } + + override val settings: Settings = object : Settings() {} + override fun clearFindMatches() = Unit + override fun disableTrackingProtection() = Unit + override fun enableTrackingProtection(policy: TrackingProtectionPolicy) = Unit + override fun exitFullScreenMode() = Unit + override fun findAll(text: String) = Unit + override fun findNext(forward: Boolean) = Unit + override fun goBack() = Unit + override fun goForward() = Unit + override fun loadData(data: String, mimeType: String, encoding: String) = Unit + override fun recoverFromCrash(): Boolean = true + override fun reload() = Unit + override fun restoreState(state: EngineSessionState) = true + override fun saveState(): EngineSessionState = DummyEngineSessionState() + override fun stopLoading() = Unit + override fun toggleDesktopMode(enable: Boolean, reload: Boolean) = Unit +} + +private class DummyEngineSessionState : EngineSessionState { + override fun toJSON(): JSONObject = JSONObject() +} \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoResult.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoResult.kt new file mode 100644 index 000000000..d6c67a703 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoResult.kt @@ -0,0 +1,71 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.browser.components + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.launch +import mozilla.components.concept.engine.CancellableOperation +import org.mozilla.geckoview.GeckoResult +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +/** + * Wait for a GeckoResult to be complete in a co-routine. + */ +suspend fun GeckoResult.await() = suspendCoroutine { continuation -> + then({ + continuation.resume(it) + GeckoResult() + }, { + continuation.resumeWithException(it) + GeckoResult() + }) +} + +/** + * Converts a [GeckoResult] to a [CancellableOperation]. + */ +fun GeckoResult.asCancellableOperation(): CancellableOperation { + val geckoResult = this + return object : CancellableOperation { + override fun cancel(): Deferred { + val result = CompletableDeferred() + geckoResult.cancel().then({ + result.complete(it ?: false) + GeckoResult() + }, { throwable -> + result.completeExceptionally(throwable) + GeckoResult() + }) + return result + } + } +} + +/** + * Create a GeckoResult from a co-routine. + */ +@Suppress("TooGenericExceptionCaught") +fun CoroutineScope.launchGeckoResult( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T +) = GeckoResult().apply { + launch(context, start) { + try { + val value = block() + complete(value) + } catch (exception: Throwable) { + completeExceptionally(exception) + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoWebExtension.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoWebExtension.kt new file mode 100644 index 000000000..a934acea1 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoWebExtension.kt @@ -0,0 +1,406 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.browser.components + +import android.graphics.Bitmap +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.webextension.* +import mozilla.components.support.base.log.logger.Logger +import org.json.JSONObject +import org.mozilla.geckoview.AllowOrDeny +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession +import org.mozilla.vrbrowser.browser.engine.SessionStore +import org.mozilla.geckoview.WebExtension as GeckoNativeWebExtension +import org.mozilla.geckoview.WebExtension.Action as GeckoNativeWebExtensionAction + +/** + * Gecko-based implementation of [WebExtension], wrapping the native web + * extension object provided by GeckoView. + */ +@Suppress("TooManyFunctions") +class GeckoWebExtension( + id: String, + url: String, + val runtime: GeckoRuntime, + allowContentMessaging: Boolean = true, + supportActions: Boolean = false, + @Suppress("Deprecation") // https://github.com/mozilla-mobile/android-components/issues/6356 + val nativeExtension: GeckoNativeWebExtension = GeckoNativeWebExtension( + url, + id, + createWebExtensionFlags(allowContentMessaging), + runtime.webExtensionController + ), + private val connectedPorts: MutableMap = mutableMapOf() +) : WebExtension(id, url, supportActions) { + + private val logger = Logger("GeckoWebExtension") + + constructor(native: GeckoNativeWebExtension, runtime: GeckoRuntime) : + this(native.id, native.location, runtime, true, true, native) + + /** + * Uniquely identifies a port using its name and the session it + * was opened for. Ports connected from background scripts will + * have a null session. + */ + data class PortId(val name: String, val session: EngineSession? = null) + + /** + * See [WebExtension.registerBackgroundMessageHandler]. + */ + override fun registerBackgroundMessageHandler(name: String, messageHandler: MessageHandler) { + val portDelegate = object : GeckoNativeWebExtension.PortDelegate { + + override fun onPortMessage(message: Any, port: GeckoNativeWebExtension.Port) { + messageHandler.onPortMessage(message, GeckoPort(port)) + } + + override fun onDisconnect(port: GeckoNativeWebExtension.Port) { + connectedPorts.remove(PortId(name)) + messageHandler.onPortDisconnected(GeckoPort(port)) + } + } + + val messageDelegate = object : GeckoNativeWebExtension.MessageDelegate { + + override fun onConnect(port: GeckoNativeWebExtension.Port) { + port.setDelegate(portDelegate) + val geckoPort = GeckoPort(port) + connectedPorts[PortId(name)] = geckoPort + messageHandler.onPortConnected(geckoPort) + } + + override fun onMessage( + // We don't use the same delegate instance for multiple apps so we don't need to verify the name. + name: String, + message: Any, + sender: GeckoNativeWebExtension.MessageSender + ): GeckoResult? { + val response = messageHandler.onMessage(message, null) + return response?.let { GeckoResult.fromValue(it) } + } + } + + nativeExtension.setMessageDelegate(messageDelegate, name) + } + + /** + * See [WebExtension.registerContentMessageHandler]. + */ + override fun registerContentMessageHandler(session: EngineSession, name: String, messageHandler: MessageHandler) { + val portDelegate = object : GeckoNativeWebExtension.PortDelegate { + + override fun onPortMessage(message: Any, port: GeckoNativeWebExtension.Port) { + messageHandler.onPortMessage(message, GeckoPort(port, session)) + } + + override fun onDisconnect(port: GeckoNativeWebExtension.Port) { + val geckoPort = GeckoPort(port, session) + connectedPorts.remove(PortId(name, session)) + messageHandler.onPortDisconnected(geckoPort) + } + } + + val messageDelegate = object : GeckoNativeWebExtension.MessageDelegate { + + override fun onConnect(port: GeckoNativeWebExtension.Port) { + port.setDelegate(portDelegate) + val geckoPort = GeckoPort(port, session) + connectedPorts[PortId(name, session)] = geckoPort + messageHandler.onPortConnected(geckoPort) + } + + override fun onMessage( + // We don't use the same delegate instance for multiple apps so we don't need to verify the name. + name: String, + message: Any, + sender: GeckoNativeWebExtension.MessageSender + ): GeckoResult? { + val response = messageHandler.onMessage(message, session) + return response?.let { GeckoResult.fromValue(it) } + } + } + + val geckoSession = (session as GeckoEngineSession).geckoSession + geckoSession.webExtensionController.setMessageDelegate(nativeExtension, messageDelegate, name) + } + + /** + * See [WebExtension.hasContentMessageHandler]. + */ + override fun hasContentMessageHandler(session: EngineSession, name: String): Boolean { + val geckoSession = (session as GeckoEngineSession).geckoSession + return geckoSession.webExtensionController.getMessageDelegate(nativeExtension, name) != null + } + + /** + * See [WebExtension.getConnectedPort]. + */ + override fun getConnectedPort(name: String, session: EngineSession?): Port? { + return connectedPorts[PortId(name, session)] + } + + /** + * See [WebExtension.disconnectPort]. + */ + override fun disconnectPort(name: String, session: EngineSession?) { + val portId = PortId(name, session) + val port = connectedPorts[portId] + port?.let { + it.disconnect() + connectedPorts.remove(portId) + } + } + + /** + * See [WebExtension.registerActionHandler]. + */ + override fun registerActionHandler(actionHandler: ActionHandler) { + if (!supportActions) { + logger.error("Attempt to register default action handler but browser and page " + + "action support is turned off for this extension: $id") + return + } + + val actionDelegate = object : GeckoNativeWebExtension.ActionDelegate { + + override fun onBrowserAction( + ext: GeckoNativeWebExtension, + // Session will always be null here for the global default delegate + session: GeckoSession?, + action: GeckoNativeWebExtensionAction + ) { + actionHandler.onBrowserAction(this@GeckoWebExtension, null, action.convert()) + } + + override fun onPageAction( + ext: GeckoNativeWebExtension, + // Session will always be null here for the global default delegate + session: GeckoSession?, + action: GeckoNativeWebExtensionAction + ) { + // Page action API doesn't support enable/disable so we enable by default. + actionHandler.onPageAction( + this@GeckoWebExtension, + null, + action.convert().copy(enabled = true) + ) + } + + override fun onTogglePopup( + ext: GeckoNativeWebExtension, + action: GeckoNativeWebExtensionAction + ): GeckoResult? { + val session = actionHandler.onToggleActionPopup(this@GeckoWebExtension, action.convert()) + return session?.let { GeckoResult.fromValue((session as GeckoEngineSession).geckoSession) } + } + } + + nativeExtension.setActionDelegate(actionDelegate) + } + + /** + * See [WebExtension.registerActionHandler]. + */ + override fun registerActionHandler(session: EngineSession, actionHandler: ActionHandler) { + if (!supportActions) { + logger.error("Attempt to register action handler on session but browser and page " + + "action support is turned off for this extension: $id") + return + } + + val actionDelegate = object : GeckoNativeWebExtension.ActionDelegate { + + override fun onBrowserAction( + ext: GeckoNativeWebExtension, + geckoSession: GeckoSession?, + action: GeckoNativeWebExtensionAction + ) { + actionHandler.onBrowserAction(this@GeckoWebExtension, session, action.convert()) + } + + override fun onPageAction( + ext: GeckoNativeWebExtension, + geckoSession: GeckoSession?, + action: GeckoNativeWebExtensionAction + ) { + // Page action API doesn't support enable/disable so we enable by default. + actionHandler.onPageAction( + this@GeckoWebExtension, + session, + action.convert().copy(enabled = true) + ) + } + } + + val geckoSession = (session as GeckoEngineSession).geckoSession + geckoSession.webExtensionController.setActionDelegate(nativeExtension, actionDelegate) + } + + /** + * See [WebExtension.hasActionHandler]. + */ + override fun hasActionHandler(session: EngineSession): Boolean { + val geckoSession = (session as GeckoEngineSession).geckoSession + return geckoSession.webExtensionController.getActionDelegate(nativeExtension) != null + } + + /** + * See [WebExtension.registerTabHandler]. + */ + override fun registerTabHandler(tabHandler: TabHandler) { + + val tabDelegate = object : GeckoNativeWebExtension.TabDelegate { + + override fun onNewTab( + ext: GeckoNativeWebExtension, + tabDetails: GeckoNativeWebExtension.CreateTabDetails + ): GeckoResult? { + val geckoSession: GeckoSession = SessionStore.get().createSession(false, false).geckoSession + val geckoEngineSession: GeckoEngineSession = GeckoEngineSession(geckoSession) + tabHandler.onNewTab( + this@GeckoWebExtension, + geckoEngineSession, + tabDetails.active == true, + tabDetails.url ?: "" + ) + return GeckoResult.fromValue(geckoEngineSession.geckoSession) + } + } + + nativeExtension.tabDelegate = tabDelegate + } + + /** + * See [WebExtension.registerTabHandler]. + */ + override fun registerTabHandler(session: EngineSession, tabHandler: TabHandler) { + + val tabDelegate = object : GeckoNativeWebExtension.SessionTabDelegate { + + override fun onUpdateTab( + ext: GeckoNativeWebExtension, + geckoSession: GeckoSession, + tabDetails: GeckoNativeWebExtension.UpdateTabDetails + ): GeckoResult { + + return if (tabHandler.onUpdateTab( + this@GeckoWebExtension, + session, + tabDetails.active == true, + tabDetails.url) + ) { + GeckoResult.ALLOW + } else { + GeckoResult.DENY + } + } + + override fun onCloseTab( + ext: GeckoNativeWebExtension?, + geckoSession: GeckoSession + ): GeckoResult { + + return if (ext != null) { + if (tabHandler.onCloseTab(this@GeckoWebExtension, session)) { + GeckoResult.ALLOW + } else { + GeckoResult.DENY + } + } else { + GeckoResult.DENY + } + } + } + + val geckoSession = (session as GeckoEngineSession).geckoSession + geckoSession.webExtensionController.setTabDelegate(nativeExtension, tabDelegate) + } + + /** + * See [WebExtension.getMetadata]. + */ + override fun getMetadata(): Metadata? { + return nativeExtension.metaData?.let { + Metadata( + name = it.name, + description = it.description, + developerName = it.creatorName, + developerUrl = it.creatorUrl, + homePageUrl = it.homepageUrl, + version = it.version, + permissions = it.permissions.toList(), + hostPermissions = it.origins.toList(), + disabledFlags = DisabledFlags.select(it.disabledFlags), + optionsPageUrl = it.optionsPageUrl, + openOptionsPageInTab = it.openOptionsPageInTab, + baseUrl = it.baseUrl + ) + } + } + + override fun isEnabled(): Boolean { + return nativeExtension.metaData?.enabled ?: true + } + + override fun isAllowedInPrivateBrowsing(): Boolean { + return isBuiltIn() || nativeExtension.metaData?.allowedInPrivateBrowsing ?: false + } +} + +/** + * Gecko-based implementation of [Port], wrapping the native port provided by GeckoView. + */ +class GeckoPort( + internal val nativePort: GeckoNativeWebExtension.Port, + engineSession: EngineSession? = null +) : Port(engineSession) { + + override fun postMessage(message: JSONObject) { + nativePort.postMessage(message) + } + + override fun name(): String { + return nativePort.name + } + + override fun senderUrl(): String { + return nativePort.sender.url + } + + override fun disconnect() { + nativePort.disconnect() + } +} + +private fun createWebExtensionFlags(allowContentMessaging: Boolean): Long { + return if (allowContentMessaging) { + GeckoNativeWebExtension.Flags.ALLOW_CONTENT_MESSAGING + } else { + GeckoNativeWebExtension.Flags.NONE + } +} + +private fun GeckoNativeWebExtensionAction.convert(): Action { + val loadIcon: (suspend (Int) -> Bitmap?)? = icon?.let { + { size -> icon?.get(size)?.await() } + } + + val onClick = { click() } + + return Action( + title, + enabled, + loadIcon, + badgeText, + badgeTextColor, + badgeBackgroundColor, + onClick + ) +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoWebExtensionRuntime.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoWebExtensionRuntime.kt new file mode 100644 index 000000000..78ec66f72 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/components/GeckoWebExtensionRuntime.kt @@ -0,0 +1,267 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.browser.components + +import android.content.Context +import mozilla.components.concept.engine.CancellableOperation +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.webextension.* +import org.mozilla.geckoview.AllowOrDeny +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.WebExtensionController + +class GeckoWebExtensionRuntime( + private val context: Context, + private val runtime: GeckoRuntime +): WebExtensionRuntime { + + private var webExtensionDelegate: WebExtensionDelegate? = null + private val webExtensionActionHandler = object : ActionHandler { + override fun onBrowserAction(extension: WebExtension, session: EngineSession?, action: Action) { + webExtensionDelegate?.onBrowserActionDefined(extension, action) + } + + override fun onPageAction(extension: WebExtension, session: EngineSession?, action: Action) { + webExtensionDelegate?.onPageActionDefined(extension, action) + } + + override fun onToggleActionPopup(extension: WebExtension, action: Action): EngineSession? { + return webExtensionDelegate?.onToggleActionPopup(extension, GeckoEngineSession(), action) + } + } + private val webExtensionTabHandler = object : TabHandler { + override fun onNewTab(webExtension: WebExtension, engineSession: EngineSession, active: Boolean, url: String) { + webExtensionDelegate?.onNewTab(webExtension, engineSession, active, url) + } + } + + /** + * See [Engine.installWebExtension]. + */ + override fun installWebExtension( + id: String, + url: String, + allowContentMessaging: Boolean, + supportActions: Boolean, + onSuccess: ((WebExtension) -> Unit), + onError: ((String, Throwable) -> Unit) + ): CancellableOperation { + val ext = GeckoWebExtension(id, url, runtime, allowContentMessaging, supportActions) + return installWebExtension(ext, onSuccess, onError) + } + + internal fun installWebExtension( + ext: GeckoWebExtension, + onSuccess: ((WebExtension) -> Unit) = { }, + onError: ((String, Throwable) -> Unit) = { _, _ -> } + ): CancellableOperation { + val geckoResult = if (ext.isBuiltIn()) { + if (ext.supportActions) { + // We currently have to install the global action handler before we + // install the extension which is why this is done here directly. + // This code can be removed from the engine once the new GV addon + // management API (specifically installBuiltIn) lands. Then the + // global handlers will be invoked with the latest state whenever + // they are registered: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1599897 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1582185 + ext.registerActionHandler(webExtensionActionHandler) + ext.registerTabHandler(webExtensionTabHandler) + } + + // For now we have to use registerWebExtension for builtin extensions until we get the + // new installBuiltIn call on the controller: https://bugzilla.mozilla.org/show_bug.cgi?id=1601067 + runtime.registerWebExtension(ext.nativeExtension).apply { + then({ + webExtensionDelegate?.onInstalled(ext) + onSuccess(ext) + GeckoResult() + }, { throwable -> + onError(ext.id, throwable) + GeckoResult() + }) + } + } else { + runtime.webExtensionController.install(ext.url).apply { + then({ + val installedExtension = GeckoWebExtension(it!!, runtime) + webExtensionDelegate?.onInstalled(installedExtension) + installedExtension.registerActionHandler(webExtensionActionHandler) + installedExtension.registerTabHandler(webExtensionTabHandler) + onSuccess(installedExtension) + GeckoResult() + }, { throwable -> + onError(ext.id, throwable) + GeckoResult() + }) + } + } + return geckoResult.asCancellableOperation() + } + + /** + * See [Engine.uninstallWebExtension]. + */ + override fun uninstallWebExtension( + ext: WebExtension, + onSuccess: () -> Unit, + onError: (String, Throwable) -> Unit + ) { + runtime.webExtensionController.uninstall((ext as GeckoWebExtension).nativeExtension).then({ + webExtensionDelegate?.onUninstalled(ext) + onSuccess() + GeckoResult() + }, { throwable -> + onError(ext.id, throwable) + GeckoResult() + }) + } + + /** + * See [Engine.updateWebExtension]. + */ + override fun updateWebExtension( + extension: WebExtension, + onSuccess: (WebExtension?) -> Unit, + onError: (String, Throwable) -> Unit + ) { + runtime.webExtensionController.update((extension as GeckoWebExtension).nativeExtension).then({ geckoExtension -> + val updatedExtension = if (geckoExtension != null) { + GeckoWebExtension(geckoExtension, runtime).also { + it.registerActionHandler(webExtensionActionHandler) + it.registerTabHandler(webExtensionTabHandler) + } + } else { + null + } + onSuccess(updatedExtension) + GeckoResult() + }, { throwable -> + onError(extension.id, throwable) + GeckoResult() + }) + } + + /** + * See [Engine.registerWebExtensionDelegate]. + */ + override fun registerWebExtensionDelegate( + webExtensionDelegate: WebExtensionDelegate + ) { + this.webExtensionDelegate = webExtensionDelegate + + val promptDelegate = object : WebExtensionController.PromptDelegate { + override fun onInstallPrompt(ext: org.mozilla.geckoview.WebExtension): GeckoResult? { + val extension = GeckoWebExtension(ext, runtime) + return if (webExtensionDelegate.onInstallPermissionRequest(extension)) { + GeckoResult.ALLOW + } else { + GeckoResult.DENY + } + } + + override fun onUpdatePrompt( + current: org.mozilla.geckoview.WebExtension, + updated: org.mozilla.geckoview.WebExtension, + newPermissions: Array, + newOrigins: Array + ): GeckoResult? { + // NB: We don't have a user flow for handling updated origins so we ignore them for now. + val result = GeckoResult() + webExtensionDelegate.onUpdatePermissionRequest( + GeckoWebExtension(current, runtime), + GeckoWebExtension(updated, runtime), + newPermissions.toList() + ) { + allow -> if (allow) result.complete(AllowOrDeny.ALLOW) else result.complete(AllowOrDeny.DENY) + } + return result + } + } + + val debuggerDelegate = object : WebExtensionController.DebuggerDelegate { + override fun onExtensionListUpdated() { + webExtensionDelegate.onExtensionListUpdated() + } + } + + runtime.webExtensionController.promptDelegate = promptDelegate + runtime.webExtensionController.setDebuggerDelegate(debuggerDelegate) + } + + /** + * See [Engine.listInstalledWebExtensions]. + */ + override fun listInstalledWebExtensions(onSuccess: (List) -> Unit, onError: (Throwable) -> Unit) { + runtime.webExtensionController.list().then({ + val extensions = it?.map { + extension -> + GeckoWebExtension(extension, runtime) + } ?: emptyList() + + extensions.forEach { extension -> + // As a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1621385, + // we set all installed extensions to be allowed in private browsing mode. + // We need to revert back to false which is now the default. + if (!extension.isBuiltIn() && extension.isAllowedInPrivateBrowsing()) { + setAllowedInPrivateBrowsing(extension, false) + } + + extension.registerActionHandler(webExtensionActionHandler) + extension.registerTabHandler(webExtensionTabHandler) + } + + onSuccess(extensions) + GeckoResult() + }, { throwable -> + onError(throwable) + GeckoResult() + }) + } + + /** + * See [Engine.enableWebExtension]. + */ + override fun enableWebExtension( + extension: WebExtension, + source: EnableSource, + onSuccess: (WebExtension) -> Unit, + onError: (Throwable) -> Unit + ) { + runtime.webExtensionController.enable((extension as GeckoWebExtension).nativeExtension, source.id).then({ + val enabledExtension = GeckoWebExtension(it!!, runtime) + webExtensionDelegate?.onEnabled(enabledExtension) + onSuccess(enabledExtension) + GeckoResult() + }, { throwable -> + onError(throwable) + GeckoResult() + }) + } + + /** + * See [Engine.disableWebExtension]. + */ + override fun disableWebExtension( + extension: WebExtension, + source: EnableSource, + onSuccess: (WebExtension) -> Unit, + onError: (Throwable) -> Unit + ) { + runtime.webExtensionController.disable((extension as GeckoWebExtension).nativeExtension, source.id).then({ + val disabledExtension = GeckoWebExtension(it!!, runtime) + webExtensionDelegate?.onDisabled(disabledExtension) + onSuccess(disabledExtension) + GeckoResult() + }, { throwable -> + onError(throwable) + GeckoResult() + }) + } + +} \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/EngineProvider.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/EngineProvider.kt index a1e840cf6..14304c528 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/EngineProvider.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/EngineProvider.kt @@ -5,18 +5,18 @@ package org.mozilla.vrbrowser.browser.engine import android.content.Context -import org.mozilla.geckoview.* +import org.mozilla.geckoview.ContentBlocking +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoRuntimeSettings +import org.mozilla.geckoview.GeckoWebExecutor import org.mozilla.vrbrowser.BuildConfig import org.mozilla.vrbrowser.browser.SettingsStore import org.mozilla.vrbrowser.browser.content.TrackingProtectionPolicy import org.mozilla.vrbrowser.browser.content.TrackingProtectionStore import org.mozilla.vrbrowser.crashreporting.CrashReporterService -import java.util.concurrent.CompletableFuture object EngineProvider { - private val WEB_EXTENSIONS = arrayOf("webcompat_vimeo", "webcompat_youtube") - private var runtime: GeckoRuntime? = null private var executor: GeckoWebExecutor? = null private var client: GeckoViewFetchClient? = null @@ -58,18 +58,6 @@ object EngineProvider { return runtime!! } - fun loadExtensions() : CompletableFuture { - val futures : List> = WEB_EXTENSIONS.map { - val future = CompletableFuture() - val url = "resource://android/assets/web_extensions/$it/" - runtime!!.webExtensionController.installBuiltIn(url).accept { - future.complete(null) - } - future - } - return CompletableFuture.allOf(*futures.toTypedArray()) - } - fun createGeckoWebExecutor(context: Context): GeckoWebExecutor { return GeckoWebExecutor(getOrCreateRuntime(context)) } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java index 2fa941d7c..d579c5a49 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java @@ -49,12 +49,10 @@ import java.net.URI; import java.net.URISyntaxException; -import java.util.Comparator; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Predicate; import static java.util.Objects.requireNonNull; import static org.mozilla.vrbrowser.utils.ServoUtils.createServoSession; @@ -116,12 +114,11 @@ public interface DrmStateChangedListener { public static final int SESSION_DO_NOT_OPEN = 1; protected Session(Context aContext, GeckoRuntime aRuntime, - @NonNull SessionSettings aSettings, @SessionOpenModeFlags int aOpenMode) { + @NonNull SessionSettings aSettings) { mContext = aContext; mRuntime = aRuntime; initialize(); - mState = createSession(aSettings, aOpenMode); - mState.setActive(aOpenMode == SESSION_OPEN); + mState = createSession(aSettings); } protected Session(Context aContext, GeckoRuntime aRuntime, @NonNull SessionState aRestoreState) { @@ -161,16 +158,13 @@ private void initialize() { protected void shutdown() { if (mState.mSession != null) { - if (mState.mSession.isOpen()) { - mState.mSession.close(); - } - mState.mDisplay = null; + closeSession(mState); + mSessionChangeListeners.forEach(listener -> { + listener.onSessionRemoved(mState.mId); + }); mState.mSession = null; } - for (SessionChangeListener listener : mSessionChangeListeners) { - listener.onRemoveSession(this); - } if (mState.mParentId != null) { Session parent = SessionStore.get().getSession(mState.mParentId); if (parent != null) { @@ -465,10 +459,15 @@ private void restore() { } mState.mSession = createGeckoSession(settings); - if (!mState.mSession.isOpen()) { - mState.mSession.open(mRuntime); + + // We call restore when a session is first activated and when it's recreated. + // We only need to notify of the session creation if it's not a recreation. + if (mState.mSessionState == null) { + mSessionChangeListeners.forEach(listener -> listener.onSessionAdded(this)); } + openSession(); + // data:text URLs can not be restored. if (mState.mSessionState != null && ((mState.mUri == null) || mState.mUri.startsWith("data:text"))) { mState.mSessionState = null; @@ -489,19 +488,16 @@ private void restore() { } dumpAllState(); + mState.setActive(true); } - private SessionState createSession(@NonNull SessionSettings aSettings, @SessionOpenModeFlags int aOpenMode) { + private SessionState createSession(@NonNull SessionSettings aSettings) { SessionState state = new SessionState(); state.mSettings = aSettings; state.mSession = createGeckoSession(aSettings); - if (aOpenMode == SESSION_OPEN && !state.mSession.isOpen()) { - state.mSession.open(mRuntime); - } - return state; } @@ -521,24 +517,29 @@ private GeckoSession createGeckoSession(@NonNull SessionSettings aSettings) { session = new GeckoSession(geckoSettings); } - session.getSettings().setUserAgentOverride(aSettings.getUserAgentOverride()); + if (session != null) { + session.getSettings().setUserAgentOverride(aSettings.getUserAgentOverride()); + } setupSessionListeners(session); return session; } - public void recreateSession() { + void recreateSession() { boolean wasFullScreen = mState.mFullScreen; - SessionState previous = mState; + GeckoSession previousGeckoSession = null; + if (mState.mSession != null) { + previousGeckoSession = mState.mSession; + closeSession(mState); + } + mState = mState.recreate(); restore(); - GeckoSession previousGeckoSession = null; - if (previous.mSession != null) { - previousGeckoSession = previous.mSession; - closeSession(previous); + for (SessionChangeListener listener: mSessionChangeListeners) { + listener.onSessionStateChanged(this, true); } for (SessionChangeListener listener : mSessionChangeListeners) { @@ -552,8 +553,18 @@ public void recreateSession() { } } + void openSession() { + if (!mState.mSession.isOpen()) { + mState.mSession.open(mRuntime); + } + + mSessionChangeListeners.forEach(listener -> { + listener.onSessionOpened(this); + }); + } + private void closeSession(@NonNull SessionState aState) { - if (aState.mSession == null) { + if (aState.mSession == null || !aState.mSession.isOpen()) { return; } cleanSessionListeners(aState.mSession); @@ -567,6 +578,8 @@ private void closeSession(@NonNull SessionState aState) { aState.mSession.close(); aState.setActive(false); mFirstContentfulPaint = false; + + mSessionChangeListeners.forEach(listener -> listener.onSessionClosed(aState.mId)); } public void captureBitmap() { @@ -783,6 +796,7 @@ public void setActive(boolean aActive) { if (mState.mSession != null) { mState.mSession.setActive(aActive); mState.setActive(aActive); + } else if (aActive) { restore(); } else { @@ -790,7 +804,7 @@ public void setActive(boolean aActive) { } for (SessionChangeListener listener: mSessionChangeListeners) { - listener.onActiveStateChange(this, aActive); + listener.onSessionStateChanged(this, aActive); } } @@ -848,9 +862,15 @@ public void toggleServo() { .withServo(!isInstanceOfServoSession(mState.mSession)) .build(); - mState = createSession(settings, SESSION_OPEN); + mState = createSession(settings); + openSession(); closeSession(previous); + mState.setActive(true); + for (SessionChangeListener listener: mSessionChangeListeners) { + listener.onSessionStateChanged(this, true); + } + loadUri(uri); } @@ -1658,7 +1678,7 @@ public void onHideAction(@NonNull GeckoSession aSession, int aHideReason) { // SessionChangeListener @Override - public void onRemoveSession(Session aParent) { + public void onSessionRemoved(String aId) { if (mState.mParentId != null) { mState.mParentId = null; // Parent stack session closed. Notify canGoBack state changed @@ -1669,7 +1689,7 @@ public void onRemoveSession(Session aParent) { } @Override - public void onActiveStateChange(Session aSession, boolean aActive) { + public void onSessionStateChanged(Session aSession, boolean aActive) { if (mState.mParentId != null) { // Parent stack session has been attached/detached. Notify canGoBack state changed for (GeckoSession.NavigationDelegate listener : mNavigationListeners) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java index 25ef0d9a6..e67882dde 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java @@ -56,7 +56,7 @@ public class SessionState { public transient ArrayList mMediaElements = new ArrayList<>(); public transient @WebXRState int mWebXRState = WEBXR_UNUSED; public transient @PopupState int mPopUpState = POPUP_UNUSED; - public transient @PopupState int mDrmState = DRM_UNUSED; + public transient @DrmState int mDrmState = DRM_UNUSED; @JsonAdapter(SessionState.GeckoSessionStateAdapter.class) public GeckoSession.SessionState mSessionState; public long mLastUse; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java index b67eecdab..dd5557691 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java @@ -4,32 +4,61 @@ import android.content.res.Configuration; import android.os.Bundle; import android.util.Log; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.mozilla.geckoview.GeckoRuntime; import org.mozilla.geckoview.GeckoSession; +import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.VRBrowserApplication; import org.mozilla.vrbrowser.browser.BookmarksStore; import org.mozilla.vrbrowser.browser.HistoryStore; import org.mozilla.vrbrowser.browser.PermissionDelegate; import org.mozilla.vrbrowser.browser.Services; +import org.mozilla.vrbrowser.browser.SessionChangeListener; +import org.mozilla.vrbrowser.browser.adapter.ComponentsAdapter; +import org.mozilla.vrbrowser.browser.components.GeckoWebExtensionRuntime; import org.mozilla.vrbrowser.browser.content.TrackingProtectionStore; +import org.mozilla.vrbrowser.browser.extensions.BuiltinExtension; import org.mozilla.vrbrowser.db.SitePermission; import org.mozilla.vrbrowser.utils.SystemUtils; import org.mozilla.vrbrowser.utils.UrlUtils; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executor; +import java.util.regex.Pattern; import java.util.stream.Collectors; -public class SessionStore implements GeckoSession.PermissionDelegate{ +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import mozilla.components.concept.engine.EngineSession; +import mozilla.components.concept.engine.webextension.Action; +import mozilla.components.concept.engine.webextension.WebExtension; +import mozilla.components.concept.engine.webextension.WebExtensionDelegate; +import mozilla.components.feature.accounts.FxaCapability; +import mozilla.components.feature.accounts.FxaWebChannelFeature; +import mozilla.components.feature.webcompat.WebCompatFeature; +import mozilla.components.lib.state.Store; + +public class SessionStore implements + GeckoSession.PermissionDelegate, + WebExtensionDelegate, + SessionChangeListener { + private static final String LOGTAG = SystemUtils.createLogtag(SessionStore.class); private static final int MAX_GECKO_SESSIONS = 5; + private static final List> BUILTIN_WEB_EXTENSIONS = Arrays.asList( + new Pair<>("fxr-webcompat_youtube@mozilla.org", "resource://android/assets/extensions/fxr_youtube/"), + new Pair<>("fxr-webcompat_vimeo@mozilla.org", "resource://android/assets/extensions/fxr_vimeo/") + ); + private static SessionStore mInstance; public static SessionStore get() { @@ -50,12 +79,15 @@ public static SessionStore get() { private Services mServices; private boolean mSuspendPending; private TrackingProtectionStore mTrackingProtectionStore; + private GeckoWebExtensionRuntime mWebExtensionRuntime; + private FxaWebChannelFeature mWebChannelsFeature; + private Store.Subscription mStoreSubscription; private SessionStore() { mSessions = new ArrayList<>(); } - public void setContext(Context context, Bundle aExtras) { + public void initialize(Context context, Bundle aExtras) { mContext = context; mMainExecutor = ((VRBrowserApplication)context.getApplicationContext()).getExecutors().mainThread(); @@ -89,15 +121,39 @@ public void onTrackingProtectionLevelUpdated(int level) { }); } }); - } - public void initializeServices() { - mServices = ((VRBrowserApplication)mContext.getApplicationContext()).getServices(); - } + mWebExtensionRuntime = new GeckoWebExtensionRuntime(mContext, mRuntime); + mWebExtensionRuntime.registerWebExtensionDelegate(this); + + mServices = ((VRBrowserApplication)context.getApplicationContext()).getServices(); - public void initializeStores(Context context) { mBookmarksStore = new BookmarksStore(context); mHistoryStore = new HistoryStore(context); + + // Web Extensions initialization + BUILTIN_WEB_EXTENSIONS.forEach(extension -> BuiltinExtension.install(mWebExtensionRuntime, extension.first, extension.second)); + WebCompatFeature.INSTANCE.install(mWebExtensionRuntime); + mWebChannelsFeature = new FxaWebChannelFeature( + mContext, + null, + mWebExtensionRuntime, + ComponentsAdapter.get().getStore(), + mServices.getAccountManager(), + mServices.getServerConfig(), + Collections.singleton(FxaCapability.CHOOSE_WHAT_TO_SYNC)); + + mWebChannelsFeature.start(); + + if (BuildConfig.DEBUG) { + mStoreSubscription = ComponentsAdapter.get().getStore().observeManually(browserState -> { + Log.d(LOGTAG, "Session status BEGIN"); + browserState.getTabs().forEach(tabSessionState -> Log.d(LOGTAG, "BrowserStore Session: " + tabSessionState.getId())); + mSessions.forEach(session -> Log.d(LOGTAG, "SessionStore Session: " + session.getId())); + Log.d(LOGTAG, "Session status END"); + return null; + }); + mStoreSubscription.resume(); + } } @NonNull @@ -106,6 +162,7 @@ private Session addSession(@NonNull Session aSession) { aSession.addNavigationListener(mServices); mSessions.add(aSession); sessionActiveStateChanged(); + return aSession; } @@ -116,13 +173,28 @@ public Session createSession(boolean aPrivateMode) { } @NonNull - /* package */ Session createSession(@NonNull SessionSettings aSettings, @Session.SessionOpenModeFlags int aOpenMode) { - return addSession(new Session(mContext, mRuntime, aSettings, aOpenMode)); + public Session createSession(boolean openSession, boolean aPrivateMode) { + SessionSettings settings = new SessionSettings(new SessionSettings.Builder().withDefaultSettings(mContext).withPrivateBrowsing(aPrivateMode)); + return createSession(settings, openSession ? Session.SESSION_OPEN : Session.SESSION_DO_NOT_OPEN); + } + + @NonNull + Session createSession(@NonNull SessionSettings aSettings, @Session.SessionOpenModeFlags int aOpenMode) { + Session session = new Session(mContext, mRuntime, aSettings); + session.addSessionChangeListener(this); + if (aOpenMode == Session.SESSION_OPEN) { + onSessionAdded(session); + session.openSession(); + session.setActive(true); + } + return addSession(session); } @NonNull public Session createSuspendedSession(SessionState aRestoreState) { - return addSession(new Session(mContext, mRuntime, aRestoreState)); + Session session = new Session(mContext, mRuntime, aRestoreState); + session.addSessionChangeListener(this); + return addSession(session); } @NonNull @@ -131,12 +203,12 @@ public Session createSuspendedSession(final String aUri, final boolean aPrivateM state.mUri = aUri; state.mSettings = new SessionSettings(new SessionSettings.Builder().withDefaultSettings(mContext).withPrivateBrowsing(aPrivateMode)); Session session = new Session(mContext, mRuntime, state); + session.addSessionChangeListener(this); return addSession(session); } private void shutdownSession(@NonNull Session aSession) { aSession.setPermissionDelegate(null); - aSession.removeNavigationListener(mServices); aSession.shutdown(); } @@ -253,6 +325,10 @@ public TrackingProtectionStore getTrackingProtectionStore() { return mTrackingProtectionStore; } + public GeckoWebExtensionRuntime getWebExtensionRuntime() { + return mWebExtensionRuntime; + } + public void purgeSessionHistory() { for (Session session: mSessions) { session.purgeHistory(); @@ -271,6 +347,14 @@ public void onDestroy() { if (mHistoryStore != null) { mHistoryStore.removeAllListeners(); } + + if (mWebChannelsFeature != null) { + mWebChannelsFeature.stop(); + } + + if (BuildConfig.DEBUG && mStoreSubscription != null) { + mStoreSubscription.unsubscribe(); + } } public void onConfigurationChanged(Configuration newConfig) { @@ -359,4 +443,139 @@ public void removePermissionException(@NonNull String uri, @SitePermission.Categ mPermissionDelegate.removePermissionException(uri, category); } } + + // WebExtensionDelegate + + @Override + public void onInstalled(@NonNull WebExtension webExtension) { + Log.d(LOGTAG, "onInstalled: " + webExtension.getId()); + if (webExtension.getMetadata() != null) { + webExtension.getMetadata().getHostPermissions().forEach(permission -> { + mSessions.forEach(session -> { + Pattern domainPattern = Pattern.compile(Pattern.quote(permission)); + if (domainPattern.matcher(session.getCurrentUri()).find()) { + session.reload(); + } + }); + }); + } + } + + @Override + public void onUninstalled(@NonNull WebExtension webExtension) { + Log.d(LOGTAG, "onUninstalled: " + webExtension.getId()); + if (webExtension.getMetadata() != null) { + webExtension.getMetadata().getHostPermissions().forEach(permission -> { + mSessions.forEach(session -> { + Pattern domainPattern = Pattern.compile(Pattern.quote(permission)); + if (domainPattern.matcher(session.getCurrentUri()).find()) { + session.reload(); + } + }); + }); + } + } + + @Override + public void onEnabled(@NonNull WebExtension webExtension) { + + } + + @Override + public void onDisabled(@NonNull WebExtension webExtension) { + + } + + @Override + public void onAllowedInPrivateBrowsingChanged(@NonNull WebExtension webExtension) { + + } + + @Override + public void onNewTab(@NonNull WebExtension webExtension, @NonNull EngineSession engineSession, boolean b, @NonNull String s) { + + } + + @Override + public void onBrowserActionDefined(@NonNull WebExtension webExtension, @NonNull Action action) { + + } + + @Override + public void onPageActionDefined(@NonNull WebExtension webExtension, @NonNull Action action) { + + } + + @Nullable + @Override + public EngineSession onToggleActionPopup(@NonNull WebExtension webExtension, @NonNull EngineSession engineSession, @NonNull Action action) { + return null; + } + + @Override + public boolean onInstallPermissionRequest(@NonNull WebExtension webExtension) { + return false; + } + + @Override + public void onUpdatePermissionRequest(@NonNull WebExtension webExtension, @NonNull WebExtension webExtension1, @NonNull List list, @NonNull Function1 function1) { + + } + + @Override + public void onExtensionListUpdated() { + + } + + // SessionChangeListener + + @Override + public void onSessionAdded(Session aSession) { + ComponentsAdapter.get().addSession(aSession); + } + + @Override + public void onSessionOpened(Session aSession) { + ComponentsAdapter.get().link(aSession.getId(), aSession.getGeckoSession()); + } + + @Override + public void onSessionClosed(String aId) { + ComponentsAdapter.get().unlink(aId); + } + + @Override + public void onSessionRemoved(String aId) { + ComponentsAdapter.get().removeSession(aId); + } + + @Override + public void onSessionStateChanged(Session aSession, boolean aActive) { + if (aActive) { + ComponentsAdapter.get().selectSession(aSession); + } + } + + @Override + public void onCurrentSessionChange(GeckoSession aOldSession, GeckoSession aSession) { + if (aOldSession != null && getSession(aOldSession) != null) { + ComponentsAdapter.get().unlink(getSession(aOldSession).getId()); + } + if (aSession != null && getSession(aSession) != null) { + ComponentsAdapter.get().link(getSession(aSession).getId(), aSession); + } + + } + + @Override + public void onStackSession(Session aSession) { + ComponentsAdapter.get().addSession(aSession); + ComponentsAdapter.get().link(aSession.getId(), aSession.getGeckoSession()); + } + + @Override + public void onUnstackSession(Session aSession, Session aParent) { + // unlink/remove are called by destroySession + destroySession(aSession); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/extensions/BuiltinExtension.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/extensions/BuiltinExtension.java new file mode 100644 index 000000000..7834fc7e9 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/extensions/BuiltinExtension.java @@ -0,0 +1,35 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.browser.extensions; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.utils.SystemUtils; + +import mozilla.components.concept.engine.webextension.WebExtensionRuntime; + +/** + * Feature to enable a Builtin extension via the Web Compatibility System-Addon. + */ +public class BuiltinExtension { + + private static final String LOGTAG = SystemUtils.createLogtag(BuiltinExtension.class); + + /** + * Installs the web extension in the runtime through the WebExtensionRuntime install method + */ + public static void install(@NonNull WebExtensionRuntime runtime, @NonNull String extensionId, @NonNull String extensionUrl) { + runtime.installWebExtension(extensionId, extensionUrl, false, false, webExtension -> { + Log.i(LOGTAG, extensionId + " Web Extension successfully installed"); + return null; + }, (s, throwable) -> { + Log.e(LOGTAG, "Error installing the " + extensionId + " Web Extension: " + throwable.getLocalizedMessage()); + return null; + }); + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java index 1a72cf8ac..fea1ec5b0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java @@ -9,23 +9,21 @@ import org.mozilla.telemetry.TelemetryHolder; import org.mozilla.vrbrowser.BuildConfig; +import org.mozilla.vrbrowser.GleanMetrics.Control; import org.mozilla.vrbrowser.GleanMetrics.Distribution; import org.mozilla.vrbrowser.GleanMetrics.FirefoxAccount; +import org.mozilla.vrbrowser.GleanMetrics.Immersive; import org.mozilla.vrbrowser.GleanMetrics.LegacyTelemetry; +import org.mozilla.vrbrowser.GleanMetrics.Pages; import org.mozilla.vrbrowser.GleanMetrics.Pings; import org.mozilla.vrbrowser.GleanMetrics.Searches; import org.mozilla.vrbrowser.GleanMetrics.Url; -import org.mozilla.vrbrowser.GleanMetrics.Control; -import org.mozilla.vrbrowser.GleanMetrics.Pages; -import org.mozilla.vrbrowser.GleanMetrics.Immersive; import org.mozilla.vrbrowser.GleanMetrics.Windows; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.utils.SystemUtils; import org.mozilla.vrbrowser.utils.UrlUtils; -import static org.mozilla.vrbrowser.ui.widgets.Windows.WindowPlacement; -import static org.mozilla.vrbrowser.ui.widgets.Windows.MAX_WINDOWS; import java.net.URI; import java.util.HashMap; @@ -34,10 +32,15 @@ import java.util.Map; import java.util.UUID; +import mozilla.components.concept.fetch.Client; import mozilla.components.service.glean.Glean; import mozilla.components.service.glean.config.Configuration; +import mozilla.components.service.glean.net.ConceptFetchHttpUploader; import mozilla.telemetry.glean.GleanTimerId; +import static org.mozilla.vrbrowser.ui.widgets.Windows.MAX_WINDOWS; +import static org.mozilla.vrbrowser.ui.widgets.Windows.WindowPlacement; + public class GleanMetricsService { @@ -54,7 +57,7 @@ public class GleanMetricsService { private static GleanTimerId openPrivateWindowTimerId[] = new GleanTimerId[MAX_WINDOWS]; // We should call this at the application initial stage. - public static void init(Context aContext) { + public static void init(@NonNull Context aContext, @NonNull Client client) { if (initialized) return; @@ -69,7 +72,11 @@ public static void init(Context aContext) { } LegacyTelemetry.INSTANCE.clientId().set(UUID.fromString(TelemetryHolder.get().getClientId())); - Configuration config = new Configuration(Configuration.DEFAULT_TELEMETRY_ENDPOINT, BuildConfig.BUILD_TYPE); + + Configuration config = new Configuration( + ConceptFetchHttpUploader.fromClient(client), + Configuration.DEFAULT_TELEMETRY_ENDPOINT, + BuildConfig.BUILD_TYPE); Glean.INSTANCE.initialize(aContext, true, config); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java index 06d36935a..bfe7d5868 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java @@ -24,6 +24,7 @@ import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.EngineProvider; import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.utils.SystemUtils; @@ -34,6 +35,7 @@ import java.util.HashSet; import java.util.Map; +import mozilla.components.concept.fetch.Client; import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient; import static java.lang.Math.toIntExact; @@ -136,7 +138,7 @@ private class Extra { // We should call this at the application initial stage. Instead, // it would be called when users turn on/off the setting of telemetry. // e.g., SettingsStore.getInstance(context).setTelemetryEnabled(); - public static void init(Context aContext) { + public static void init(@NonNull Context aContext, @NonNull Client client) { // When initializing the telemetry library it will make sure that all directories exist and // are readable/writable. final StrictMode.ThreadPolicy threadPolicy = StrictMode.allowThreadDiskWrites(); @@ -162,9 +164,8 @@ public static void init(Context aContext) { } else { scheduler = new JobSchedulerTelemetryScheduler(); } - final TelemetryClient client = new TelemetryClient(new HttpURLConnectionClient()); - TelemetryHolder.set(new Telemetry(configuration, storage, client, scheduler) + TelemetryHolder.set(new Telemetry(configuration, storage, new TelemetryClient(client), scheduler) .addPingBuilder(new TelemetryCorePingBuilder(configuration)) .addPingBuilder(new TelemetryMobileEventPingBuilder(configuration))); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java index 6e6f5744d..561e19baa 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java @@ -1217,7 +1217,6 @@ public void onStackSession(Session aSession) { public void onUnstackSession(Session aSession, Session aParent) { if (mSession == aSession) { setSession(aParent, WindowWidget.DEACTIVATE_CURRENT_SESSION); - SessionStore.get().destroySession(aSession); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java index 5e9edadb5..28b35a623 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java @@ -52,6 +52,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import mozilla.components.Build; import mozilla.components.concept.sync.AccountObserver; import mozilla.components.concept.sync.AuthType; import mozilla.components.concept.sync.OAuthAccount; @@ -76,7 +77,9 @@ class VersionGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown (MotionEvent e) { - mBinding.buildText.setText(mIsHash ? StringUtils.versionCodeToDate(getContext(), BuildConfig.VERSION_CODE) : BuildConfig.GIT_HASH); + mBinding.buildText.setText(mIsHash ? + StringUtils.versionCodeToDate(getContext(), BuildConfig.VERSION_CODE) : + BuildConfig.GIT_HASH + " (AC " + Build.version + ")"); mIsHash = !mIsHash; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/SystemUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/SystemUtils.java index 9ef84fec1..959cc8a04 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/SystemUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/SystemUtils.java @@ -112,4 +112,5 @@ public static void clearCrashFiles(@NonNull Context context, final ArrayList() + // We use the HttpURLConnectionClient for tests as the GeckoWebExecutor based client needs + // full GeckoRuntime initialization and it crashes in the test environment. + val client = HttpURLConnectionClient() + TelemetryWrapper.init(app, client) + GleanMetricsService.init(app, client) + } + @Test fun testURLTelemetry() { assertFalse(Url.domains.testHasValue()) @@ -138,8 +151,6 @@ class GleanMetricsServiceTest { @Test fun testLegacyTelemetry() { - assertFalse(LegacyTelemetry.clientId.testHasValue()) - LegacyTelemetry.clientId.set(java.util.UUID.fromString(TelemetryHolder.get().getClientId())) assertTrue(LegacyTelemetry.clientId.testHasValue()) assertEquals(LegacyTelemetry.clientId.testGetValue().toString(), TelemetryHolder.get().getClientId()) } diff --git a/versions.gradle b/versions.gradle index e72e52e54..bd4615b22 100644 --- a/versions.gradle +++ b/versions.gradle @@ -25,7 +25,7 @@ def versions = [:] // GeckoView versions can be found here: // https://maven.mozilla.org/?prefix=maven2/org/mozilla/geckoview/ versions.gecko_view = "79.0.20200623034439" -versions.android_components = "41.0.0" +versions.android_components = "44.0.0" // Note that android-components also depends on application-services, // and in fact is our main source of appservices-related functionality. // The version number below tracks the application-services version @@ -66,15 +66,19 @@ android_components.telemetry = "org.mozilla.components:service-telemetry:$versio android_components.glean = "org.mozilla.components:service-glean:$versions.android_components" android_components.browser_errorpages = "org.mozilla.components:browser-errorpages:$versions.android_components" android_components.browser_search = "org.mozilla.components:browser-search:$versions.android_components" +android_components.browser_state = "org.mozilla.components:browser-state:$versions.android_components" android_components.browser_storage = "org.mozilla.components:browser-storage-sync:$versions.android_components" android_components.browser_domains = "org.mozilla.components:browser-domains:$versions.android_components" android_components.service_accounts = "org.mozilla.components:service-firefox-accounts:$versions.android_components" android_components.mozilla_service_location = "org.mozilla.components:service-location:${versions.android_components}" android_components.ui_autocomplete = "org.mozilla.components:ui-autocomplete:$versions.android_components" +android_components.concept_engine = "org.mozilla.components:concept-engine:$versions.android_components" android_components.concept_fetch = "org.mozilla.components:concept-fetch:$versions.android_components" android_components.lib_fetch = "org.mozilla.components:lib-fetch-httpurlconnection:$versions.android_components" android_components.support_rustlog = "org.mozilla.components:support-rustlog:$versions.android_components" android_components.support_rusthttp = "org.mozilla.components:support-rusthttp:$versions.android_components" +android_components.feature_accounts = "org.mozilla.components:feature-accounts:$versions.android_components" +android_components.feature_webcompat = "org.mozilla.components:feature-webcompat:$versions.android_components" android_components.support_test = "org.mozilla.components:support-test:$versions.android_components" android_components.support_test_appservices = "org.mozilla.components:support-test-appservices:$versions.android_components" deps.android_components = android_components