summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/samples/browser/src
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/samples/browser/src')
-rw-r--r--mobile/android/android-components/samples/browser/src/androidTest/assets/index.html5
-rw-r--r--mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt139
-rw-r--r--mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt47
-rw-r--r--mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml177
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js5
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json16
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js24
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.pngbin0 -> 1633 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json22
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html13
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt308
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt89
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt190
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt515
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt29
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt132
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt46
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt142
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt125
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt137
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt94
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt26
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt251
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt12
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt200
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt105
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt54
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt114
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt19
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt20
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt19
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt20
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt75
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt17
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt16
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt21
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt14
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt89
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt53
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt84
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt20
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt71
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml10
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml13
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml21
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml156
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml32
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml11
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml96
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml11
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml18
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml28
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml82
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml44
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml29
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml44
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml12
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml8
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml8
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 2076 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 1781 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 4121 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 1533 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 1322 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2535 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 2650 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 2439 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 5575 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 4138 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 3674 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 8832 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 5519 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 4778 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 12481 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/values/colors.xml7
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml7
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/values/strings.xml10
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml10
-rw-r--r--mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt17
-rw-r--r--mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt11
82 files changed, 4257 insertions, 0 deletions
diff --git a/mobile/android/android-components/samples/browser/src/androidTest/assets/index.html b/mobile/android/android-components/samples/browser/src/androidTest/assets/index.html
new file mode 100644
index 0000000000..9f5632c044
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/androidTest/assets/index.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+<h1 id="website_title">Hello World!</h1>
+</body>
+</html>
diff --git a/mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt b/mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt
new file mode 100644
index 0000000000..9c3b3e2247
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt
@@ -0,0 +1,139 @@
+/* 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.samples.browser
+
+import android.os.SystemClock
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.pressImeActionButton
+import androidx.test.espresso.action.ViewActions.replaceText
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiSelector
+import mozilla.components.support.android.test.rules.WebserverRule
+import org.junit.Assert.assertTrue
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+
+private const val INITIAL_WAIT_SECONDS = 5L
+private const val WAIT_FOR_WEB_CONTENT_SECONDS = 15L
+
+/**
+ * A collection of "smoke tests" to verify that the basic browsing functionality is working.
+ */
+
+@LargeTest
+class SmokeTests {
+ @get:Rule
+ val activityRule: ActivityScenarioRule<BrowserActivity> = ActivityScenarioRule(BrowserActivity::class.java)
+
+ @get:Rule
+ val webserverRule: WebserverRule = WebserverRule()
+
+ /**
+ * This test loads a website from a local webserver by typing into the URL bar. After that it verifies that the
+ * web content is visible.
+ */
+
+ @Test
+ fun loadWebsiteTest() {
+ // Disable on API21 - https://github.com/mozilla-mobile/android-components/issues/6482
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.LOLLIPOP) {
+ waitForIdle()
+
+ enterUrl(webserverRule.url())
+
+ verifyWebsiteContent("Hello World!")
+ verifyUrlInToolbar(webserverRule.url())
+ }
+ }
+
+ @Ignore("Intermittent: https://bugzilla.mozilla.org/show_bug.cgi?id=1794873")
+ @Test
+ fun loadWebsitesInMultipleTabsTest() {
+ // Disable on API21 - https://github.com/mozilla-mobile/android-components/issues/6482
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.LOLLIPOP) {
+ waitForIdle()
+
+ enterUrl(webserverRule.url())
+
+ verifyWebsiteContent("Hello World!")
+ verifyUrlInToolbar(webserverRule.url())
+
+ navigateToTabsTray()
+ openNewTabInTabsTray()
+
+ enterUrl(webserverRule.url())
+
+ verifyWebsiteContent("Hello World!")
+ verifyUrlInToolbar(webserverRule.url())
+
+ navigateToTabsTray()
+ openNewTabInTabsTray()
+
+ enterUrl(webserverRule.url())
+
+ verifyWebsiteContent("Hello World!")
+ verifyUrlInToolbar(webserverRule.url())
+
+ navigateToTabsTray()
+ openNewTabInTabsTray()
+ }
+ }
+}
+
+private fun waitForIdle() {
+ // Meh! We need a better idle strategy here. Because of bug 1441059 our load request gets lost if it happens
+ // to fast and then only "about:blank" gets loaded. So a "quick" fix here is to just wait a bit.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1441059
+ SystemClock.sleep(TimeUnit.SECONDS.toMillis(INITIAL_WAIT_SECONDS))
+}
+
+private fun navigateToTabsTray() {
+ onView(withContentDescription(mozilla.components.feature.tabs.R.string.mozac_feature_tabs_toolbar_tabs_button))
+ .perform(click())
+}
+
+private fun openNewTabInTabsTray() {
+ onView(withId(R.id.newTab))
+ .perform(click())
+}
+
+private fun enterUrl(url: String) {
+ onView(withId(mozilla.components.browser.toolbar.R.id.mozac_browser_toolbar_url_view))
+ .perform(click())
+
+ onView(withId(mozilla.components.browser.toolbar.R.id.mozac_browser_toolbar_edit_url_view))
+ .perform(replaceText(url), pressImeActionButton())
+}
+
+private fun verifyUrlInToolbar(url: String) {
+ onView(withId(mozilla.components.browser.toolbar.R.id.mozac_browser_toolbar_url_view))
+ .check(matches(withText(url)))
+}
+
+private fun verifyWebsiteContent(text: String) {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.waitForIdle()
+
+ val waitingTime: Long = TimeUnit.SECONDS.toMillis(WAIT_FOR_WEB_CONTENT_SECONDS)
+
+ assertTrue(
+ device
+ .findObject(
+ UiSelector()
+ .textContains(text),
+ )
+ .waitForExists(waitingTime),
+ )
+}
diff --git a/mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt b/mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt
new file mode 100644
index 0000000000..2741064e4c
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt
@@ -0,0 +1,47 @@
+/* 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.samples.browser
+
+import android.content.Context
+import mozilla.components.browser.engine.gecko.GeckoEngine
+import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
+import mozilla.components.concept.engine.Engine
+import mozilla.components.experiment.NimbusExperimentDelegate
+import mozilla.components.feature.webcompat.WebCompatFeature
+import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
+import mozilla.components.lib.crash.handler.CrashHandlerService
+import mozilla.components.support.base.log.Log
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoRuntimeSettings
+
+/**
+ * Helper class for lazily instantiating components needed by the application.
+ */
+class Components(private val applicationContext: Context) : DefaultComponents(applicationContext) {
+ private val runtime by lazy {
+ // Allow for exfiltrating Gecko metrics through the Glean SDK.
+ val builder = GeckoRuntimeSettings.Builder().aboutConfigEnabled(true)
+ builder.experimentDelegate(NimbusExperimentDelegate())
+ builder.crashHandler(CrashHandlerService::class.java)
+ GeckoRuntime.create(applicationContext, builder.build())
+ }
+
+ override val engine: Engine by lazy {
+ GeckoEngine(applicationContext, engineSettings, runtime).also {
+ it.installBuiltInWebExtension("borderify@mozac.org", "resource://android/assets/extensions/borderify/") {
+ throwable ->
+ Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install borderify")
+ }
+ it.installBuiltInWebExtension("testext@mozac.org", "resource://android/assets/extensions/test/") {
+ throwable ->
+ Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install testext")
+ }
+ WebCompatFeature.install(it)
+ WebCompatReporterFeature.install(it)
+ }
+ }
+
+ override val client by lazy { GeckoViewFetchClient(applicationContext, runtime) }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..36b24ebd5f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <!-- This is needed because the android.permission.CAMERA above automatically
+ adds a requirements for camera hardware and we don't want add those restrictions -->
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.camera.autofocus"
+ android:required="false" />
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage" />
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar"
+ android:name=".SampleApplication"
+ android:usesCleartextTraffic="true"
+ tools:ignore="DataExtractionRules,UnusedAttribute"
+ android:dataExtractionRules="@xml/data_extraction_rules">
+ <activity android:name=".BrowserActivity"
+ android:launchMode="singleTask"
+ android:exported="true"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".ExternalAppBrowserActivity"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
+ android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
+ android:exported="false"
+ android:taskAffinity=""
+ android:persistableMode="persistNever"
+ android:autoRemoveFromRecents="false" />
+
+ <activity
+ android:theme="@style/Theme.AppCompat.Light"
+ android:name=".addons.AddonsActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:parentActivityName=".BrowserActivity" />
+
+ <activity
+ android:theme="@style/Theme.AppCompat.Light"
+ android:name=".addons.AddonDetailsActivity"
+ android:label="@string/mozac_feature_addons_addons" />
+
+ <activity android:name=".addons.InstalledAddonDetailsActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:parentActivityName=".addons.AddonsActivity"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".addons.PermissionsDetailsActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".addons.AddonSettingsActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".addons.NotYetSupportedAddonActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".addons.WebExtensionActionPopupActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".IntentReceiverActivity"
+ android:relinquishTaskIdentity="true"
+ android:taskAffinity=""
+ android:exported="true"
+ android:excludeFromRecents="true" >
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="mozilla.components.pwa.category.SHORTCUT" />
+
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/plain" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="mozilla.components.feature.pwa.VIEW_PWA" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="https" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".autofill.AutofillUnlockActivity"
+ android:exported="false"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+
+ <activity android:name=".autofill.AutofillConfirmActivity"
+ android:exported="false"
+ android:theme="@style/Theme.AppCompat.Translucent" />
+
+ <activity android:name=".autofill.AutofillSearchActivity"
+ android:exported="false" />
+
+ <service
+ android:name=".autofill.AutofillService"
+ android:label="@string/app_name"
+ android:exported="true"
+ android:permission="android.permission.BIND_AUTOFILL_SERVICE">
+ <intent-filter>
+ <action android:name="android.service.autofill.AutofillService"/>
+ </intent-filter>
+ <meta-data
+ android:name="android.autofill"
+ android:resource="@xml/service_configuration" />
+ </service>
+
+ <service
+ android:name=".customtabs.CustomTabsService"
+ android:exported="true"
+ tools:ignore="ExportedService">
+ <intent-filter>
+ <action android:name="android.support.customtabs.action.CustomTabsService" />
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=".downloads.DownloadService"
+ android:foregroundServiceType="dataSync" />
+
+ <service android:name=".media.MediaSessionService"
+ android:foregroundServiceType="mediaPlayback"
+ android:exported="false" />
+
+ </application>
+
+</manifest>
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js
new file mode 100644
index 0000000000..af58957d88
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js
@@ -0,0 +1,5 @@
+/* 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/. */
+
+document.body.style.border = "5px solid red"; \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json
new file mode 100644
index 0000000000..cc2db02cbf
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json
@@ -0,0 +1,16 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "borderify@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - Borderify",
+ "version": "${version}",
+ "content_scripts": [
+ {
+ "matches": ["*://www.mozilla.org/*"],
+ "js": ["borderify.js"]
+ }
+ ]
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js
new file mode 100644
index 0000000000..950936be4c
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+/* Avoid adding ID selector rules in this style sheet, since they could
+ * inadvertently match elements in the article content. */
+
+// Counts to three and sends a greeting via the browser action of a newly created tab.
+browser.tabs.onCreated.addListener((tab) => {
+ let counter = 0;
+ let intervalId = setInterval(() => {
+ var message;
+ if (++counter <= 3) {
+ message = "" + counter;
+ } else {
+ message = "Hi!";
+ clearInterval(intervalId);
+ }
+ browser.browserAction.setBadgeTextColor({tabId: tab.id, color: "#FFFFFF"});
+ browser.browserAction.setBadgeText({tabId: tab.id, text: message});
+ }, 1000);
+});
+
+browser.browserAction.setBadgeBackgroundColor({color: "#AAAAAA"}); \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png
new file mode 100644
index 0000000000..455b15fc84
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json
new file mode 100644
index 0000000000..04dc17aa2f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json
@@ -0,0 +1,22 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "testext@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - Test extension",
+ "description": "This extension is used for testing web extension functionality in Android Components",
+ "version": "${version}",
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "browser_action": {
+ "default_icon": "icon.png",
+ "default_title": "Test",
+ "default_popup": "popup.html"
+ },
+ "permissions": [
+ "tabs"
+ ]
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html
new file mode 100644
index 0000000000..40d1467569
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html>
+<!-- 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/. -->
+
+<html>
+ <head></head>
+ <body style="font-size: 36px">
+ <h1>Hello world!</h1>
+ <p>This is a browser action default popup.</p>
+ </body>
+</html>
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt
new file mode 100644
index 0000000000..7cef15d75f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt
@@ -0,0 +1,308 @@
+/* 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.samples.browser
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.CallSuper
+import androidx.fragment.app.Fragment
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.toolbar.display.DisplayToolbar
+import mozilla.components.feature.app.links.AppLinksFeature
+import mozilla.components.feature.downloads.DownloadsFeature
+import mozilla.components.feature.downloads.manager.FetchDownloadManager
+import mozilla.components.feature.privatemode.feature.SecureWindowFeature
+import mozilla.components.feature.prompts.PromptFeature
+import mozilla.components.feature.session.CoordinateScrollingFeature
+import mozilla.components.feature.session.SessionFeature
+import mozilla.components.feature.session.SwipeRefreshFeature
+import mozilla.components.feature.sitepermissions.SitePermissionsFeature
+import mozilla.components.feature.sitepermissions.SitePermissionsRules
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
+import mozilla.components.feature.toolbar.ToolbarFeature
+import mozilla.components.lib.state.ext.consumeFlow
+import mozilla.components.support.base.feature.ActivityResultHandler
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
+import mozilla.components.support.locale.ActivityContextWrapper
+import mozilla.components.support.utils.ext.requestInPlacePermissions
+import org.mozilla.samples.browser.databinding.FragmentBrowserBinding
+import org.mozilla.samples.browser.downloads.DownloadService
+import org.mozilla.samples.browser.ext.components
+import org.mozilla.samples.browser.integration.ContextMenuIntegration
+import org.mozilla.samples.browser.integration.FindInPageIntegration
+
+/**
+ * Base fragment extended by [BrowserFragment] and [ExternalAppBrowserFragment].
+ * This class only contains shared code focused on the main browsing content.
+ * UI code specific to the app or to custom tabs can be found in the subclasses.
+ */
+@SuppressWarnings("LargeClass")
+abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler {
+ private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
+ private val toolbarFeature = ViewBoundFeatureWrapper<ToolbarFeature>()
+ private val contextMenuIntegration = ViewBoundFeatureWrapper<ContextMenuIntegration>()
+ private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
+ private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
+ private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
+ private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
+ private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
+ private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
+
+ protected val sessionId: String?
+ get() = arguments?.getString(SESSION_ID_KEY)
+
+ private val activityResultHandler: List<ViewBoundFeatureWrapper<*>> = listOf(
+ promptFeature,
+ )
+
+ private var _binding: FragmentBrowserBinding? = null
+ val binding get() = _binding!!
+
+ @CallSuper
+ @Suppress("LongMethod")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ _binding = FragmentBrowserBinding.inflate(inflater, container, false)
+
+ binding.toolbar.display.menuBuilder = components.menuBuilder
+ val originalContext = ActivityContextWrapper.getOriginalContext(requireActivity())
+ binding.engineView.setActivityContext(originalContext)
+
+ sessionFeature.set(
+ feature = SessionFeature(
+ components.store,
+ components.sessionUseCases.goBack,
+ binding.engineView,
+ sessionId,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ toolbarFeature.set(
+ feature = ToolbarFeature(
+ binding.toolbar,
+ components.store,
+ components.sessionUseCases.loadUrl,
+ components.defaultSearchUseCase,
+ sessionId,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ binding.toolbar.display.indicators += listOf(
+ DisplayToolbar.Indicators.TRACKING_PROTECTION,
+ DisplayToolbar.Indicators.HIGHLIGHT,
+ )
+
+ swipeRefreshFeature.set(
+ feature = SwipeRefreshFeature(
+ components.store,
+ components.sessionUseCases.reload,
+ binding.swipeToRefresh,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ downloadsFeature.set(
+ feature = DownloadsFeature(
+ requireContext().applicationContext,
+ store = components.store,
+ useCases = components.downloadsUseCases,
+ fragmentManager = childFragmentManager,
+ onDownloadStopped = { download, id, status ->
+ Logger.debug("Download done. ID#$id $download with status $status")
+ },
+ downloadManager = FetchDownloadManager(
+ requireContext().applicationContext,
+ components.store,
+ DownloadService::class,
+ notificationsDelegate = components.notificationsDelegate,
+ ),
+ tabId = sessionId,
+ onNeedToRequestPermissions = { permissions ->
+ requestInPlacePermissions(REQUEST_KEY_DOWNLOAD_PERMISSIONS, permissions) { result ->
+ downloadsFeature.get()?.onPermissionsResult(
+ result.keys.toTypedArray(),
+ result.values.map {
+ when (it) {
+ true -> PackageManager.PERMISSION_GRANTED
+ false -> PackageManager.PERMISSION_DENIED
+ }
+ }.toIntArray(),
+ )
+ }
+ },
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ val scrollFeature = CoordinateScrollingFeature(components.store, binding.engineView, binding.toolbar)
+
+ contextMenuIntegration.set(
+ feature = ContextMenuIntegration(
+ context = requireContext(),
+ fragmentManager = parentFragmentManager,
+ browserStore = components.store,
+ tabsUseCases = components.tabsUseCases,
+ contextMenuUseCases = components.contextMenuUseCases,
+ parentView = binding.root,
+ sessionId = sessionId,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ appLinksFeature.set(
+ feature = AppLinksFeature(
+ context = requireContext(),
+ store = components.store,
+ sessionId = sessionId,
+ fragmentManager = parentFragmentManager,
+ launchInApp = { components.preferences.getBoolean(DefaultComponents.PREF_LAUNCH_EXTERNAL_APP, false) },
+ loadUrlUseCase = components.sessionUseCases.loadUrl,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ promptFeature.set(
+ feature = PromptFeature(
+ fragment = this,
+ store = components.store,
+ customTabId = sessionId,
+ tabsUseCases = components.tabsUseCases,
+ fragmentManager = parentFragmentManager,
+ fileUploadsDirCleaner = components.fileUploadsDirCleaner,
+ onNeedToRequestPermissions = { permissions ->
+ requestInPlacePermissions(REQUEST_KEY_PROMPT_PERMISSIONS, permissions) { result ->
+ promptFeature.get()?.onPermissionsResult(
+ result.keys.toTypedArray(),
+ result.values.map {
+ when (it) {
+ true -> PackageManager.PERMISSION_GRANTED
+ false -> PackageManager.PERMISSION_DENIED
+ }
+ }.toIntArray(),
+ )
+ }
+ },
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ sitePermissionsFeature.set(
+ feature = SitePermissionsFeature(
+ context = requireContext(),
+ sessionId = sessionId,
+ storage = components.permissionStorage,
+ fragmentManager = parentFragmentManager,
+ sitePermissionsRules = SitePermissionsRules(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ camera = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ location = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ notification = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ microphone = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ persistentStorage = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ mediaKeySystemAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ crossOriginStorageAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ ),
+ onNeedToRequestPermissions = { permissions ->
+ requestInPlacePermissions(REQUEST_KEY_SITE_PERMISSIONS, permissions) { result ->
+ sitePermissionsFeature.get()?.onPermissionsResult(
+ result.keys.toTypedArray(),
+ result.values.map {
+ when (it) {
+ true -> PackageManager.PERMISSION_GRANTED
+ false -> PackageManager.PERMISSION_DENIED
+ }
+ }.toIntArray(),
+ )
+ }
+ },
+ onShouldShowRequestPermissionRationale = { shouldShowRequestPermissionRationale(it) },
+ store = components.store,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ findInPageIntegration.set(
+ feature = FindInPageIntegration(components.store, binding.findInPage, binding.engineView),
+ owner = this,
+ view = binding.root,
+ )
+
+ val secureWindowFeature = SecureWindowFeature(
+ window = requireActivity().window,
+ store = components.store,
+ customTabId = sessionId,
+ )
+
+ // Observe the lifecycle for supported features
+ lifecycle.addObservers(
+ scrollFeature,
+ secureWindowFeature,
+ )
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ consumeFlow(components.store) { flow ->
+ flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(sessionId) }
+ .ifAnyChanged { tab ->
+ arrayOf(
+ tab.content.loading,
+ tab.content.canGoBack,
+ tab.content.canGoForward,
+ )
+ }
+ .collect {
+ binding.toolbar.invalidateActions()
+ }
+ }
+ }
+
+ @CallSuper
+ override fun onBackPressed(): Boolean =
+ listOf(findInPageIntegration, toolbarFeature, sessionFeature).any { it.onBackPressed() }
+
+ @CallSuper
+ override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
+ return activityResultHandler.any { it.onActivityResult(requestCode, data, resultCode) }
+ }
+
+ companion object {
+ private const val SESSION_ID_KEY = "session_id"
+
+ private const val REQUEST_KEY_DOWNLOAD_PERMISSIONS = "downloadFeature"
+ private const val REQUEST_KEY_PROMPT_PERMISSIONS = "promptFeature"
+ private const val REQUEST_KEY_SITE_PERMISSIONS = "sitePermissionsFeature"
+
+ @JvmStatic
+ protected fun Bundle.putSessionId(sessionId: String?) {
+ putString(SESSION_ID_KEY, sessionId)
+ }
+ }
+ override fun onDestroyView() {
+ super.onDestroyView()
+ binding.engineView.setActivityContext(null)
+ _binding = null
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt
new file mode 100644
index 0000000000..99a05bc4f4
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt
@@ -0,0 +1,89 @@
+/* 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.samples.browser
+
+import android.content.ComponentCallbacks2
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.util.AttributeSet
+import android.view.View
+import androidx.fragment.app.Fragment
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.contextmenu.ext.DefaultSelectionActionDelegate
+import mozilla.components.feature.intent.ext.getSessionId
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.locale.LocaleAwareAppCompatActivity
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.webextensions.WebExtensionPopupObserver
+import org.mozilla.samples.browser.addons.WebExtensionActionPopupActivity
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Activity that holds the [BrowserFragment].
+ */
+open class BrowserActivity : LocaleAwareAppCompatActivity(), ComponentCallbacks2 {
+ private val webExtensionPopupObserver by lazy {
+ WebExtensionPopupObserver(components.store, ::openPopup)
+ }
+
+ /**
+ * Returns a new instance of [BrowserFragment] to display.
+ */
+ open fun createBrowserFragment(sessionId: String?): Fragment =
+ BrowserFragment.create(sessionId)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ if (savedInstanceState == null) {
+ val sessionId = SafeIntent(intent).getSessionId()
+ supportFragmentManager.beginTransaction().apply {
+ replace(R.id.container, createBrowserFragment(sessionId))
+ commit()
+ }
+ }
+
+ lifecycle.addObserver(webExtensionPopupObserver)
+ components.historyStorage.registerStorageMaintenanceWorker()
+ components.notificationsDelegate.bindToActivity(this)
+ }
+
+ override fun onBackPressed() {
+ supportFragmentManager.fragments.forEach {
+ if (it is UserInteractionHandler && it.onBackPressed()) {
+ return
+ }
+ }
+
+ onBackPressedDispatcher.onBackPressed()
+ }
+
+ override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
+ when (name) {
+ EngineView::class.java.name -> components.engine.createView(context, attrs).apply {
+ selectionActionDelegate = DefaultSelectionActionDelegate(
+ store = components.store,
+ context = context,
+ )
+ }.asView()
+ else -> super.onCreateView(parent, name, context, attrs)
+ }
+
+ private fun openPopup(webExtensionState: WebExtensionState) {
+ val intent = Intent(this, WebExtensionActionPopupActivity::class.java)
+ intent.putExtra("web_extension_id", webExtensionState.id)
+ intent.putExtra("web_extension_name", webExtensionState.name)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ startActivity(intent)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ components.notificationsDelegate.unBindActivity(this)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt
new file mode 100644
index 0000000000..2ef0a03003
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt
@@ -0,0 +1,190 @@
+/* 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.samples.browser
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import mozilla.components.browser.thumbnails.BrowserThumbnails
+import mozilla.components.feature.awesomebar.AwesomeBarFeature
+import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
+import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
+import mozilla.components.feature.search.SearchFeature
+import mozilla.components.feature.session.FullScreenFeature
+import mozilla.components.feature.tabs.WindowFeature
+import mozilla.components.feature.tabs.toolbar.TabsToolbarFeature
+import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
+import mozilla.components.feature.toolbar.WebExtensionToolbarFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import mozilla.components.support.ktx.android.view.enterImmersiveMode
+import mozilla.components.support.ktx.android.view.exitImmersiveMode
+import org.mozilla.samples.browser.ext.components
+import org.mozilla.samples.browser.integration.ReaderViewIntegration
+
+/**
+ * Fragment used for browsing the web within the main app.
+ */
+class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
+ private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
+ private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
+ private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
+ private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
+ private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
+ private val mediaSessionFullscreenFeature =
+ ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
+
+ @Suppress("LongMethod")
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ super.onCreateView(inflater, container, savedInstanceState)
+ val binding = super.binding
+ ToolbarAutocompleteFeature(binding.toolbar, components.engine).apply {
+ updateAutocompleteProviders(
+ providers = listOf(components.historyStorage, components.shippedDomainsProvider),
+ refreshAutocomplete = false,
+ )
+ }
+
+ TabsToolbarFeature(
+ toolbar = binding.toolbar,
+ store = components.store,
+ sessionId = sessionId,
+ lifecycleOwner = viewLifecycleOwner,
+ showTabs = ::showTabs,
+ countBasedOnSelectedTabType = false,
+ )
+
+ AwesomeBarFeature(binding.awesomeBar, binding.toolbar, binding.engineView, components.icons)
+ .addHistoryProvider(
+ components.historyStorage,
+ components.sessionUseCases.loadUrl,
+ components.engine,
+ )
+ .addSessionProvider(
+ resources,
+ components.store,
+ components.tabsUseCases.selectTab,
+ )
+ .addSearchActionProvider(
+ components.store,
+ searchUseCase = components.searchUseCases.defaultSearch,
+ )
+ .addSearchProvider(
+ requireContext(),
+ components.store,
+ components.searchUseCases.defaultSearch,
+ fetchClient = components.client,
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ engine = components.engine,
+ filterExactMatch = true,
+ )
+ .addClipboardProvider(
+ requireContext(),
+ components.sessionUseCases.loadUrl,
+ components.engine,
+ )
+
+ readerViewFeature.set(
+ feature = ReaderViewIntegration(
+ requireContext(),
+ components.engine,
+ components.store,
+ binding.toolbar,
+ binding.readerViewBar,
+ binding.readerViewAppearanceButton,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ fullScreenFeature.set(
+ feature = FullScreenFeature(
+ components.store,
+ components.sessionUseCases,
+ sessionId,
+ ) { inFullScreen ->
+ if (inFullScreen) {
+ activity?.enterImmersiveMode()
+ } else {
+ activity?.exitImmersiveMode()
+ }
+ },
+ owner = this,
+ view = binding.root,
+ )
+
+ mediaSessionFullscreenFeature.set(
+ feature = MediaSessionFullscreenFeature(
+ requireActivity(),
+ components.store,
+ sessionId,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ thumbnailsFeature.set(
+ feature = BrowserThumbnails(requireContext(), binding.engineView, components.store),
+ owner = this,
+ view = binding.root,
+ )
+
+ webExtToolbarFeature.set(
+ feature = WebExtensionToolbarFeature(
+ binding.toolbar,
+ components.store,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ searchFeature.set(
+ feature = SearchFeature(components.store) { request, _ ->
+ if (request.isPrivate) {
+ components.searchUseCases.newPrivateTabSearch.invoke(request.query)
+ } else {
+ components.searchUseCases.newTabSearch.invoke(request.query)
+ }
+ },
+ owner = this,
+ view = binding.root,
+ )
+
+ val windowFeature = WindowFeature(components.store, components.tabsUseCases)
+ lifecycle.addObserver(windowFeature)
+
+ return binding.root
+ }
+
+ private fun showTabs() {
+ // For now we are performing manual fragment transactions here. Once we can use the new
+ // navigation support library we may want to pass navigation graphs around.
+ activity?.supportFragmentManager?.beginTransaction()?.apply {
+ replace(R.id.container, TabsTrayFragment())
+ commit()
+ }
+ }
+
+ override fun onBackPressed(): Boolean {
+ return when {
+ fullScreenFeature.onBackPressed() -> true
+ readerViewFeature.onBackPressed() -> true
+ else -> super.onBackPressed()
+ }
+ }
+
+ companion object {
+ fun create(sessionId: String? = null) = BrowserFragment().apply {
+ arguments = Bundle().apply {
+ putSessionId(sessionId)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt
new file mode 100644
index 0000000000..cda6c31982
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt
@@ -0,0 +1,515 @@
+/* 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.samples.browser
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.widget.Toast
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
+import mozilla.components.browser.engine.system.SystemEngine
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.WebExtensionBrowserMenuBuilder
+import mozilla.components.browser.menu.item.BrowserMenuCheckbox
+import mozilla.components.browser.menu.item.BrowserMenuDivider
+import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
+import mozilla.components.browser.menu.item.BrowserMenuImageText
+import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.browser.session.storage.SessionStorage
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
+import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import mozilla.components.concept.fetch.Client
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.feature.addons.amo.AMOAddonsProvider
+import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
+import mozilla.components.feature.addons.update.DefaultAddonUpdater
+import mozilla.components.feature.app.links.AppLinksInterceptor
+import mozilla.components.feature.app.links.AppLinksUseCases
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.contextmenu.ContextMenuUseCases
+import mozilla.components.feature.customtabs.CustomTabIntentProcessor
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.downloads.DownloadMiddleware
+import mozilla.components.feature.downloads.DownloadsUseCases
+import mozilla.components.feature.intent.processing.TabIntentProcessor
+import mozilla.components.feature.media.MediaSessionFeature
+import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
+import mozilla.components.feature.prompts.PromptMiddleware
+import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.WebAppInterceptor
+import mozilla.components.feature.pwa.WebAppShortcutManager
+import mozilla.components.feature.pwa.WebAppUseCases
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
+import mozilla.components.feature.readerview.ReaderViewMiddleware
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.search.middleware.SearchMiddleware
+import mozilla.components.feature.search.region.RegionMiddleware
+import mozilla.components.feature.session.HistoryDelegate
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.session.middleware.LastAccessMiddleware
+import mozilla.components.feature.session.middleware.undo.UndoMiddleware
+import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.feature.webnotifications.WebNotificationFeature
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.service.digitalassetlinks.local.StatementApi
+import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
+import mozilla.components.service.location.LocationService
+import mozilla.components.service.sync.logins.SyncableLoginsStorage
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.worker.Frequency
+import org.mozilla.samples.browser.addons.AddonsActivity
+import org.mozilla.samples.browser.autofill.AutofillConfirmActivity
+import org.mozilla.samples.browser.autofill.AutofillSearchActivity
+import org.mozilla.samples.browser.autofill.AutofillUnlockActivity
+import org.mozilla.samples.browser.downloads.DownloadService
+import org.mozilla.samples.browser.ext.components
+import org.mozilla.samples.browser.integration.FindInPageIntegration
+import org.mozilla.samples.browser.media.MediaSessionService
+import org.mozilla.samples.browser.request.SampleUrlEncodedRequestInterceptor
+import java.util.concurrent.TimeUnit
+import mozilla.components.ui.colors.R.color as photonColors
+import mozilla.components.ui.icons.R as iconsR
+
+private const val DAY_IN_MINUTES = 24 * 60L
+
+@SuppressLint("NewApi")
+@Suppress("LargeClass")
+open class DefaultComponents(private val applicationContext: Context) {
+ companion object {
+ const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences"
+ const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app"
+ const val PREF_GLOBAL_PRIVACY_CONTROL = "sample_browser_global_privacy_control"
+ }
+
+ val preferences: SharedPreferences =
+ applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE)
+
+ private val securePreferences by lazy { SecureAbove22Preferences(applicationContext, "key_store") }
+
+ val autofillConfiguration by lazy {
+ AutofillConfiguration(
+ storage = SyncableLoginsStorage(applicationContext, lazy { securePreferences }),
+ publicSuffixList = publicSuffixList,
+ unlockActivity = AutofillUnlockActivity::class.java,
+ confirmActivity = AutofillConfirmActivity::class.java,
+ searchActivity = AutofillSearchActivity::class.java,
+ applicationName = "Sample Browser",
+ httpClient = client,
+ )
+ }
+
+ val publicSuffixList by lazy { PublicSuffixList(applicationContext) }
+
+ // Engine Settings
+ val engineSettings by lazy {
+ DefaultSettings().apply {
+ historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
+ requestInterceptor = SampleUrlEncodedRequestInterceptor(applicationContext)
+ remoteDebuggingEnabled = true
+ supportMultipleWindows = true
+ preferredColorScheme = PreferredColorScheme.Dark
+ httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
+ globalPrivacyControlEnabled = applicationContext.components.preferences.getBoolean(
+ PREF_GLOBAL_PRIVACY_CONTROL,
+ false,
+ )
+ }
+ }
+
+ private val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)
+
+ val notificationsDelegate: NotificationsDelegate by lazy {
+ NotificationsDelegate(
+ notificationManagerCompat,
+ )
+ }
+
+ val addonUpdater =
+ DefaultAddonUpdater(applicationContext, Frequency(1, TimeUnit.DAYS), notificationsDelegate)
+
+ // Engine
+ open val engine: Engine by lazy {
+ SystemEngine(applicationContext, engineSettings)
+ }
+
+ open val client: Client by lazy { HttpURLConnectionClient() }
+
+ val icons by lazy { BrowserIcons(applicationContext, client) }
+
+ // Storage
+ private val lazyHistoryStorage = lazy { PlacesHistoryStorage(applicationContext) }
+ val historyStorage by lazy { lazyHistoryStorage.value }
+
+ val sessionStorage by lazy { SessionStorage(applicationContext, engine) }
+
+ val permissionStorage by lazy { OnDiskSitePermissionsStorage(applicationContext) }
+
+ val thumbnailStorage by lazy { ThumbnailStorage(applicationContext) }
+
+ val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy {
+ FileUploadsDirCleaner { applicationContext.cacheDir }
+ }
+
+ val store by lazy {
+ BrowserStore(
+ middleware = listOf(
+ DownloadMiddleware(applicationContext, DownloadService::class.java),
+ ReaderViewMiddleware(),
+ ThumbnailsMiddleware(thumbnailStorage),
+ UndoMiddleware(),
+ RegionMiddleware(
+ applicationContext,
+ LocationService.default(),
+ ),
+ SearchMiddleware(applicationContext),
+ RecordingDevicesMiddleware(applicationContext, notificationsDelegate),
+ LastAccessMiddleware(),
+ PromptMiddleware(),
+ SessionPrioritizationMiddleware(),
+ ) + EngineMiddleware.create(engine),
+ ).apply {
+ WebNotificationFeature(
+ applicationContext,
+ engine,
+ icons,
+ R.mipmap.ic_launcher_foreground,
+ permissionStorage,
+ IntentReceiverActivity::class.java,
+ notificationsDelegate = notificationsDelegate,
+ )
+
+ MediaSessionFeature(applicationContext, MediaSessionService::class.java, this).start()
+ }
+ }
+
+ val customTabsStore by lazy { CustomTabsServiceStore() }
+
+ val sessionUseCases by lazy { SessionUseCases(store) }
+
+ val customTabsUseCases by lazy { CustomTabsUseCases(store, sessionUseCases.loadUrl) }
+
+ // Addons
+ val addonManager by lazy {
+ AddonManager(store, engine, addonsProvider, addonUpdater)
+ }
+
+ val addonsProvider by lazy {
+ AMOAddonsProvider(
+ applicationContext,
+ client,
+ collectionName = "7dfae8669acc4312a65e8ba5553036",
+ maxCacheAgeInMinutes = DAY_IN_MINUTES,
+ )
+ }
+
+ val supportedAddonsChecker by lazy {
+ DefaultSupportedAddonsChecker(applicationContext, Frequency(1, TimeUnit.DAYS))
+ }
+
+ val searchUseCases by lazy {
+ SearchUseCases(store, tabsUseCases, sessionUseCases)
+ }
+
+ val defaultSearchUseCase by lazy {
+ { searchTerms: String ->
+ searchUseCases.defaultSearch.invoke(
+ searchTerms = searchTerms,
+ searchEngine = null,
+ parentSessionId = null,
+ )
+ }
+ }
+ val appLinksUseCases by lazy { AppLinksUseCases(applicationContext) }
+
+ val appLinksInterceptor by lazy {
+ AppLinksInterceptor(
+ applicationContext,
+ interceptLinkClicks = true,
+ launchInApp = {
+ applicationContext.components.preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
+ },
+ )
+ }
+
+ val webAppInterceptor by lazy {
+ WebAppInterceptor(
+ applicationContext,
+ webAppManifestStorage,
+ )
+ }
+
+ val webAppManifestStorage by lazy { ManifestStorage(applicationContext) }
+ val webAppShortcutManager by lazy { WebAppShortcutManager(applicationContext, client, webAppManifestStorage) }
+ val webAppUseCases by lazy { WebAppUseCases(applicationContext, store, webAppShortcutManager) }
+
+ // Digital Asset Links checking
+ val relationChecker by lazy {
+ StatementRelationChecker(StatementApi(client))
+ }
+
+ // Intent
+ val tabIntentProcessor by lazy {
+ TabIntentProcessor(tabsUseCases, searchUseCases.newTabSearch)
+ }
+ val externalAppIntentProcessors by lazy {
+ listOf(
+ WebAppIntentProcessor(store, customTabsUseCases.addWebApp, sessionUseCases.loadUrl, webAppManifestStorage),
+ CustomTabIntentProcessor(customTabsUseCases.add, applicationContext.resources),
+ )
+ }
+
+ // Menu
+ val menuBuilder by lazy {
+ WebExtensionBrowserMenuBuilder(
+ menuItems,
+ store = store,
+ style = WebExtensionBrowserMenuBuilder.Style(
+ webExtIconTintColorResource = photonColors.photonGrey90,
+ ),
+ onAddonsManagerTapped = {
+ val intent = Intent(applicationContext, AddonsActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ applicationContext.startActivity(intent)
+ },
+ )
+ }
+
+ private val menuItems by lazy {
+ val items = mutableListOf(
+ menuToolbar,
+ BrowserMenuHighlightableItem(
+ "No Highlight",
+ iconsR.drawable.mozac_ic_share_android_24,
+ android.R.color.black,
+ highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = ContextCompat.getColor(applicationContext, android.R.color.holo_green_dark),
+ label = "Highlight",
+ ),
+ ) {
+ Toast.makeText(applicationContext, "Highlight", Toast.LENGTH_SHORT).show()
+ },
+ BrowserMenuImageText("Share", iconsR.drawable.mozac_ic_share_android_24, android.R.color.black) {
+ Toast.makeText(applicationContext, "Share", Toast.LENGTH_SHORT).show()
+ },
+ SimpleBrowserMenuItem("Settings") {
+ Toast.makeText(applicationContext, "Settings", Toast.LENGTH_SHORT).show()
+ },
+ SimpleBrowserMenuItem("Find In Page") {
+ FindInPageIntegration.launch?.invoke()
+ },
+ SimpleBrowserMenuItem("Save to PDF") {
+ sessionUseCases.saveToPdf.invoke()
+ },
+
+ SimpleBrowserMenuItem("Translate (auto)") {
+ var detectedFrom =
+ store.state.selectedTab?.translationsState?.translationEngineState
+ ?.detectedLanguages?.documentLangTag
+ ?: "en"
+ var detectedTo =
+ store.state.selectedTab?.translationsState?.translationEngineState
+ ?.detectedLanguages?.userPreferredLangTag
+ ?: "en"
+ sessionUseCases.translate.invoke(
+ fromLanguage = detectedFrom,
+ toLanguage = detectedTo,
+ options = null,
+ )
+ },
+ SimpleBrowserMenuItem("Print") {
+ sessionUseCases.printContent.invoke()
+ },
+ SimpleBrowserMenuItem("Restore after Translate") {
+ sessionUseCases.translateRestore.invoke()
+ },
+ SimpleBrowserMenuItem("Restore after crash") {
+ sessionUseCases.crashRecovery.invoke()
+ },
+ BrowserMenuDivider(),
+ )
+
+ items.add(
+ SimpleBrowserMenuItem("Add to homescreen") {
+ MainScope().launch {
+ webAppUseCases.addToHomescreen()
+ }
+ }.apply {
+ visible = { webAppUseCases.isPinningSupported() && store.state.selectedTabId != null }
+ },
+ )
+
+ items.add(
+ SimpleBrowserMenuItem("Open in App") {
+ val getRedirect = appLinksUseCases.appLinkRedirect
+ store.state.selectedTab?.let {
+ val redirect = getRedirect.invoke(it.content.url)
+ redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ appLinksUseCases.openAppLink.invoke(redirect.appIntent)
+ }
+ }.apply {
+ visible = {
+ store.state.selectedTab?.let {
+ appLinksUseCases.appLinkRedirect(it.content.url).hasExternalApp()
+ } ?: false
+ }
+ },
+ )
+
+ items.add(
+ BrowserMenuCheckbox(
+ "Request desktop site",
+ {
+ store.state.selectedTab?.content?.desktopMode == true
+ },
+ ) { checked ->
+ sessionUseCases.requestDesktopSite(checked)
+ }.apply {
+ visible = { store.state.selectedTab != null }
+ },
+ )
+ items.add(
+ BrowserMenuCheckbox(
+ "Open links in apps",
+ {
+ preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
+ },
+ ) { checked ->
+ preferences.edit().putBoolean(PREF_LAUNCH_EXTERNAL_APP, checked).apply()
+ },
+ )
+
+ items.add(
+ BrowserMenuCheckbox(
+ "Tell websites not to share and sell data",
+ {
+ preferences.getBoolean(PREF_GLOBAL_PRIVACY_CONTROL, false)
+ },
+ ) { checked ->
+ preferences.edit().putBoolean(PREF_GLOBAL_PRIVACY_CONTROL, checked).apply()
+ engine.settings.globalPrivacyControlEnabled = checked
+ sessionUseCases.reload()
+ },
+ )
+
+ items
+ }
+
+ private val menuToolbar by lazy {
+ val back = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = iconsR.drawable.mozac_ic_back_24,
+ primaryImageTintResource = photonColors.photonBlue90,
+ primaryContentDescription = "Back",
+ isInPrimaryState = {
+ store.state.selectedTab?.content?.canGoBack ?: true
+ },
+ disableInSecondaryState = true,
+ secondaryImageTintResource = photonColors.photonGrey40,
+ ) {
+ sessionUseCases.goBack()
+ }
+
+ val forward = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = iconsR.drawable.mozac_ic_forward_24,
+ primaryContentDescription = "Forward",
+ primaryImageTintResource = photonColors.photonBlue90,
+ isInPrimaryState = {
+ store.state.selectedTab?.content?.canGoForward ?: true
+ },
+ disableInSecondaryState = true,
+ secondaryImageTintResource = photonColors.photonGrey40,
+ ) {
+ sessionUseCases.goForward()
+ }
+
+ val refresh = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = iconsR.drawable.mozac_ic_arrow_clockwise_24,
+ primaryContentDescription = "Refresh",
+ primaryImageTintResource = photonColors.photonBlue90,
+ isInPrimaryState = {
+ store.state.selectedTab?.content?.loading == false
+ },
+ secondaryImageResource = iconsR.drawable.mozac_ic_stop,
+ secondaryContentDescription = "Stop",
+ secondaryImageTintResource = photonColors.photonBlue90,
+ disableInSecondaryState = false,
+ ) {
+ if (store.state.selectedTab?.content?.loading == true) {
+ sessionUseCases.stopLoading()
+ } else {
+ sessionUseCases.reload()
+ }
+ }
+
+ BrowserMenuItemToolbar(listOf(back, forward, refresh))
+ }
+
+ val shippedDomainsProvider by lazy {
+ // Assume this is used together with other autocomplete providers (like history) which have priority 0
+ // and set priority 1 for the domains provider to ensure other providers' results are shown first.
+ ShippedDomainsProvider(1).also { it.initialize(applicationContext) }
+ }
+
+ val tabsUseCases: TabsUseCases by lazy { TabsUseCases(store) }
+ val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) }
+ val contextMenuUseCases: ContextMenuUseCases by lazy { ContextMenuUseCases(store) }
+
+ val crashReporter: CrashReporter by lazy {
+ CrashReporter(
+ applicationContext,
+ services = listOf(
+ object : CrashReporterService {
+ override val id: String
+ get() = "xxx"
+ override val name: String
+ get() = "Test"
+
+ override fun createCrashReportUrl(identifier: String): String? {
+ return null
+ }
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? {
+ return null
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ return null
+ }
+
+ override fun report(
+ throwable: Throwable,
+ breadcrumbs: ArrayList<Breadcrumb>,
+ ): String? {
+ return null
+ }
+ },
+ ),
+ notificationsDelegate = notificationsDelegate,
+ ).install(applicationContext)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt
new file mode 100644
index 0000000000..b7d75cf8ce
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt
@@ -0,0 +1,29 @@
+/* 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.samples.browser
+
+import androidx.fragment.app.Fragment
+import mozilla.components.feature.pwa.ext.getWebAppManifest
+
+/**
+ * Activity that holds the [BrowserFragment] that is launched within an external app,
+ * such as custom tabs and progressive web apps.
+ */
+class ExternalAppBrowserActivity : BrowserActivity() {
+
+ override fun createBrowserFragment(sessionId: String?): Fragment {
+ return if (sessionId != null) {
+ val manifest = intent.getWebAppManifest()
+
+ ExternalAppBrowserFragment.create(
+ sessionId,
+ manifest = manifest,
+ )
+ } else {
+ // Fall back to browser fragment
+ super.createBrowserFragment(sessionId)
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt
new file mode 100644
index 0000000000..f397eb870f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt
@@ -0,0 +1,132 @@
+/* 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.samples.browser
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.customtabs.CustomTabWindowFeature
+import mozilla.components.feature.customtabs.CustomTabsToolbarFeature
+import mozilla.components.feature.pwa.ext.getWebAppManifest
+import mozilla.components.feature.pwa.ext.putWebAppManifest
+import mozilla.components.feature.pwa.feature.ManifestUpdateFeature
+import mozilla.components.feature.pwa.feature.WebAppActivityFeature
+import mozilla.components.feature.pwa.feature.WebAppContentFeature
+import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature
+import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Fragment used for browsing within an external app, such as for custom tabs and PWAs.
+ */
+class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
+ private val customTabsToolbarFeature = ViewBoundFeatureWrapper<CustomTabsToolbarFeature>()
+ private val hideToolbarFeature = ViewBoundFeatureWrapper<WebAppHideToolbarFeature>()
+
+ private val manifest: WebAppManifest?
+ get() = arguments?.getWebAppManifest()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ super.onCreateView(inflater, container, savedInstanceState)
+ val binding = super.binding
+
+ val manifest = this.manifest
+
+ customTabsToolbarFeature.set(
+ feature = CustomTabsToolbarFeature(
+ components.store,
+ binding.toolbar,
+ sessionId,
+ components.customTabsUseCases,
+ components.menuBuilder,
+ window = activity?.window,
+ closeListener = { activity?.finish() },
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ hideToolbarFeature.set(
+ feature = WebAppHideToolbarFeature(
+ components.store,
+ components.customTabsStore,
+ sessionId,
+ manifest,
+ ) { toolbarVisible ->
+ binding.toolbar.isVisible = toolbarVisible
+ },
+ owner = this,
+ view = binding.toolbar,
+ )
+
+ val windowFeature = CustomTabWindowFeature(
+ requireActivity(),
+ components.store,
+ sessionId!!,
+ )
+ lifecycle.addObserver(windowFeature)
+
+ if (manifest != null) {
+ activity?.lifecycle?.addObservers(
+ WebAppActivityFeature(
+ requireActivity(),
+ components.icons,
+ manifest,
+ ),
+ ManifestUpdateFeature(
+ requireContext(),
+ components.store,
+ components.webAppShortcutManager,
+ components.webAppManifestStorage,
+ sessionId!!,
+ manifest,
+ ),
+ WebAppContentFeature(
+ components.store,
+ sessionId,
+ manifest,
+ ),
+ )
+ viewLifecycleOwner.lifecycle.addObserver(
+ WebAppSiteControlsFeature(
+ context?.applicationContext!!,
+ components.store,
+ components.sessionUseCases.reload,
+ sessionId!!,
+ manifest,
+ icons = components.icons,
+ notificationsDelegate = components.notificationsDelegate,
+ ),
+ )
+ }
+
+ return binding.root
+ }
+
+ /**
+ * Calls [onBackPressed] for features in the base class first,
+ * before trying to call the external app [UserInteractionHandler].
+ */
+ override fun onBackPressed(): Boolean =
+ super.onBackPressed() || customTabsToolbarFeature.onBackPressed()
+
+ companion object {
+ fun create(
+ sessionId: String,
+ manifest: WebAppManifest?,
+ ) = ExternalAppBrowserFragment().apply {
+ arguments = Bundle().apply {
+ putSessionId(sessionId)
+ putWebAppManifest(manifest)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt
new file mode 100644
index 0000000000..7a046d6909
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt
@@ -0,0 +1,46 @@
+/* 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.samples.browser
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import org.mozilla.samples.browser.ext.components
+
+class IntentReceiverActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ MainScope().launch {
+ val intent = intent?.let { Intent(it) } ?: Intent()
+ val intentProcessors = components.externalAppIntentProcessors + components.tabIntentProcessor
+
+ // Explicitly remove the new task and clear task flags (Our browser activity is a single
+ // task activity and we never want to start a second task here).
+ intent.flags = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
+ intent.flags = intent.flags and Intent.FLAG_ACTIVITY_CLEAR_TASK.inv()
+
+ // LauncherActivity is started with the "excludeFromRecents" flag (set in manifest). We
+ // do not want to propagate this flag from the launcher activity to the browser.
+ intent.flags = intent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.inv()
+
+ val processor = intentProcessors.firstOrNull { it.process(intent) }
+
+ val activityClass = if (processor in components.externalAppIntentProcessors) {
+ ExternalAppBrowserActivity::class
+ } else {
+ BrowserActivity::class
+ }
+
+ intent.setClassName(applicationContext, activityClass.java.name)
+
+ finish()
+ startActivity(intent)
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt
new file mode 100644
index 0000000000..a6ae1e25b7
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt
@@ -0,0 +1,142 @@
+/* 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.samples.browser
+
+import android.app.Application
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import mozilla.appservices.Megazord
+import mozilla.components.browser.state.action.SystemAction
+import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
+import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.glean.BuildInfo
+import mozilla.components.service.glean.Glean
+import mozilla.components.service.glean.config.Configuration
+import mozilla.components.service.glean.net.ConceptFetchHttpUploader
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.base.facts.processor.LogFactProcessor
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.ktx.android.content.isMainProcess
+import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
+import mozilla.components.support.rustlog.RustLog
+import mozilla.components.support.webextensions.WebExtensionSupport
+import java.util.Calendar
+import java.util.TimeZone
+import java.util.concurrent.TimeUnit
+
+@Suppress("MagicNumber")
+internal object GleanBuildInfo {
+ val buildInfo: BuildInfo by lazy {
+ BuildInfo(
+ versionCode = "0.0.1",
+ versionName = "0.0.1",
+ buildDate = Calendar.getInstance(
+ TimeZone.getTimeZone("GMT+0"),
+ ).also { cal -> cal.set(2019, 9, 23, 12, 52, 8) },
+ )
+ }
+}
+
+class SampleApplication : Application() {
+ private val logger = Logger("SampleApplication")
+
+ val components by lazy { Components(this) }
+
+ @OptIn(DelicateCoroutinesApi::class) // Usage of GlobalScope
+ override fun onCreate() {
+ super.onCreate()
+
+ Megazord.init()
+ RustLog.enable()
+
+ Log.addSink(AndroidLogSink())
+
+ components.crashReporter.install(this)
+
+ if (!isMainProcess()) {
+ return
+ }
+
+ val httpClient = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+ val config = Configuration(httpClient = httpClient)
+ // IMPORTANT: the following lines initialize the Glean SDK but disable upload
+ // of pings. If, for testing purposes, upload is required to be on, change the
+ // next line to `uploadEnabled = true`.
+ Glean.initialize(
+ applicationContext,
+ uploadEnabled = false,
+ configuration = config,
+ buildInfo = GleanBuildInfo.buildInfo,
+ )
+
+ Facts.registerProcessor(LogFactProcessor())
+
+ components.engine.warmUp()
+ restoreBrowserState()
+
+ GlobalScope.launch(Dispatchers.IO) {
+ components.webAppManifestStorage.warmUpScopes(System.currentTimeMillis())
+ }
+ components.downloadsUseCases.restoreDownloads()
+ try {
+ GlobalPlacesDependencyProvider.initialize(components.historyStorage)
+ GlobalAddonDependencyProvider.initialize(
+ components.addonManager,
+ components.addonUpdater,
+ )
+ WebExtensionSupport.initialize(
+ components.engine,
+ components.store,
+ onNewTabOverride = {
+ _, engineSession, url ->
+ components.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession)
+ },
+ onCloseTabOverride = {
+ _, sessionId ->
+ components.tabsUseCases.removeTab(sessionId)
+ },
+ onSelectTabOverride = {
+ _, sessionId ->
+ components.tabsUseCases.selectTab(sessionId)
+ },
+ onUpdatePermissionRequest = components.addonUpdater::onUpdatePermissionRequest,
+ onExtensionsLoaded = { extensions ->
+ components.addonUpdater.registerForFutureUpdates(extensions)
+ components.supportedAddonsChecker.registerForChecks()
+ },
+ )
+ } catch (e: UnsupportedOperationException) {
+ // Web extension support is only available for engine gecko
+ Logger.error("Failed to initialize web extension support", e)
+ }
+ }
+
+ @DelicateCoroutinesApi
+ private fun restoreBrowserState() = GlobalScope.launch(Dispatchers.Main) {
+ components.tabsUseCases.restore(components.sessionStorage)
+
+ components.sessionStorage.autoSave(components.store)
+ .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
+ .whenGoingToBackground()
+ .whenSessionsChange()
+ }
+
+ override fun onTrimMemory(level: Int) {
+ super.onTrimMemory(level)
+
+ logger.debug("onTrimMemory: $level")
+
+ runOnlyInMainProcess {
+ components.store.dispatch(SystemAction.LowMemoryAction(level))
+
+ components.icons.onTrimMemory(level)
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt
new file mode 100644
index 0000000000..0abe64f243
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt
@@ -0,0 +1,125 @@
+/* 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.samples.browser
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.snackbar.Snackbar
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.tabstray.TabsAdapter
+import mozilla.components.browser.tabstray.TabsTray
+import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.feature.tabs.tabstray.TabsFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import org.mozilla.samples.browser.databinding.FragmentTabstrayBinding
+import org.mozilla.samples.browser.ext.components
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * A fragment for displaying the tabs tray.
+ */
+class TabsTrayFragment : Fragment(), UserInteractionHandler {
+ private val tabsFeature: ViewBoundFeatureWrapper<TabsFeature> = ViewBoundFeatureWrapper()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.fragment_tabstray, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val binding = FragmentTabstrayBinding.bind(view)
+ binding.toolbar.setNavigationIcon(iconsR.drawable.mozac_ic_back_24)
+ binding.toolbar.setNavigationOnClickListener {
+ closeTabsTray()
+ }
+
+ binding.toolbar.inflateMenu(R.menu.tabstray_menu)
+ binding.toolbar.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.newTab -> {
+ components.tabsUseCases.addTab.invoke("about:blank", selectTab = true)
+ closeTabsTray()
+ }
+ }
+ true
+ }
+
+ val tabsAdapter = createTabsAdapter(view)
+ binding.tabsTray.adapter = tabsAdapter
+ binding.tabsTray.layoutManager = GridLayoutManager(context, 2)
+
+ tabsFeature.set(
+ feature = TabsFeature(
+ tabsTray = tabsAdapter,
+ store = components.store,
+ onCloseTray = ::closeTabsTray,
+ ),
+ owner = this,
+ view = view,
+ )
+ }
+
+ override fun onBackPressed(): Boolean {
+ closeTabsTray()
+ return true
+ }
+
+ private fun closeTabsTray() {
+ activity?.supportFragmentManager?.beginTransaction()?.apply {
+ replace(R.id.container, BrowserFragment.create())
+ commit()
+ }
+ }
+
+ private fun createTabsAdapter(view: View): TabsAdapter {
+ val removeUseCase = RemoveTabWithUndoUseCase(
+ components.tabsUseCases.removeTab,
+ view,
+ components.tabsUseCases.undo,
+ )
+ return TabsAdapter(
+ thumbnailLoader = ThumbnailLoader(components.thumbnailStorage),
+ delegate = object : TabsTray.Delegate {
+ override fun onTabSelected(tab: TabSessionState, source: String?) {
+ components.tabsUseCases.selectTab(tab.id)
+ closeTabsTray()
+ }
+
+ override fun onTabClosed(tab: TabSessionState, source: String?) {
+ removeUseCase.invoke(tab.id)
+ }
+ },
+ )
+ }
+}
+
+private class RemoveTabWithUndoUseCase(
+ private val actual: TabsUseCases.RemoveTabUseCase,
+ private val view: View,
+ private val undo: TabsUseCases.UndoTabRemovalUseCase,
+) : TabsUseCases.RemoveTabUseCase {
+ override fun invoke(tabId: String) {
+ actual.invoke(tabId)
+ showSnackbar()
+ }
+
+ private fun showSnackbar() {
+ Snackbar.make(
+ view,
+ "Tab removed.",
+ Snackbar.LENGTH_LONG,
+ ).setAction(
+ "Undo",
+ ) {
+ undo.invoke()
+ }.show()
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt
new file mode 100644
index 0000000000..1b8000d5b8
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt
@@ -0,0 +1,137 @@
+/* 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.samples.browser.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.View
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.text.HtmlCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.showInformationDialog
+import mozilla.components.feature.addons.ui.translateDescription
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.feature.addons.update.DefaultAddonUpdater
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import org.mozilla.samples.browser.R
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+import mozilla.components.feature.addons.R as addonsR
+
+/**
+ * An activity to show the details of an add-on.
+ */
+class AddonDetailsActivity : AppCompatActivity() {
+
+ private val updateAttemptStorage: DefaultAddonUpdater.UpdateAttemptStorage by lazy {
+ DefaultAddonUpdater.UpdateAttemptStorage(this)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_add_on_details)
+ val addon = requireNotNull(intent.getParcelableExtraCompat("add_on", Addon::class.java))
+ bind(addon)
+ }
+
+ private fun bind(addon: Addon) {
+ title = addon.translateName(this)
+
+ bindDetails(addon)
+
+ bindAuthor(addon)
+
+ bindVersion(addon)
+
+ bindLastUpdated(addon)
+
+ bindWebsite(addon)
+
+ bindRating(addon)
+ }
+
+ private fun bindRating(addon: Addon) {
+ addon.rating?.let {
+ val ratingView = findViewById<RatingBar>(R.id.rating_view)
+ val reviewCountView = findViewById<TextView>(R.id.users_count)
+
+ val ratingContentDescription = getString(
+ addonsR.string.mozac_feature_addons_rating_content_description,
+ )
+ ratingView.contentDescription = String.format(ratingContentDescription, it.average)
+ ratingView.rating = it.average
+
+ reviewCountView.text = getFormattedAmount(it.reviews)
+ }
+ }
+
+ private fun bindWebsite(addon: Addon) {
+ findViewById<View>(R.id.home_page_text).setOnClickListener {
+ val intent =
+ Intent(Intent.ACTION_VIEW).setData(Uri.parse(addon.homepageUrl))
+ startActivity(intent)
+ }
+ }
+
+ private fun bindLastUpdated(addon: Addon) {
+ val lastUpdatedView = findViewById<TextView>(R.id.last_updated_text)
+ lastUpdatedView.text = formatDate(addon.updatedAt)
+ }
+
+ private fun bindVersion(addon: Addon) {
+ val versionView = findViewById<TextView>(R.id.version_text)
+ versionView.text = addon.installedState?.version?.ifEmpty { addon.version } ?: addon.version
+
+ if (addon.isInstalled()) {
+ versionView.setOnLongClickListener {
+ showUpdaterDialog(addon)
+ true
+ }
+ }
+ }
+
+ private fun showUpdaterDialog(addon: Addon) {
+ val context = this@AddonDetailsActivity
+ val scope = CoroutineScope(Dispatchers.IO)
+ scope.launch {
+ val updateAttempt = updateAttemptStorage.findUpdateAttemptBy(addon.id)
+ updateAttempt?.let {
+ withContext(Dispatchers.Main) {
+ it.showInformationDialog(context)
+ }
+ }
+ }
+ }
+
+ private fun bindAuthor(addon: Addon) {
+ val authorsView = findViewById<TextView>(R.id.author_text)
+ authorsView.text = addon.author?.name.orEmpty()
+ }
+
+ private fun bindDetails(addon: Addon) {
+ val detailsView = findViewById<TextView>(R.id.details)
+ val detailsText = addon.translateDescription(this)
+
+ val parsedText = detailsText.replace("\n", "<br/>")
+ val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)
+
+ detailsView.text = text
+ detailsView.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ private fun formatDate(text: String): String {
+ val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
+ return DateFormat.getDateInstance().format(formatter.parse(text)!!)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt
new file mode 100644
index 0000000000..222a33c33e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt
@@ -0,0 +1,94 @@
+/* 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.samples.browser.addons
+
+import android.content.Context
+import android.os.Bundle
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.support.utils.ext.getParcelableCompat
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.databinding.ActivityAddOnSettingsBinding
+import org.mozilla.samples.browser.databinding.FragmentAddOnSettingsBinding
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * An activity to show the settings of an add-on.
+ */
+class AddonSettingsActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityAddOnSettingsBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityAddOnSettingsBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ val addon = requireNotNull(intent.getParcelableExtraCompat("add_on", Addon::class.java))
+ title = addon.translateName(this)
+
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.addonSettingsContainer, AddonSettingsFragment.create(addon))
+ .commit()
+ }
+
+ override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
+ when (name) {
+ EngineView::class.java.name -> components.engine.createView(context, attrs).asView()
+ else -> super.onCreateView(parent, name, context, attrs)
+ }
+
+ /**
+ * A fragment to show the settings of an add-on with [EngineView].
+ */
+ class AddonSettingsFragment : Fragment() {
+ private lateinit var addon: Addon
+ private lateinit var engineSession: EngineSession
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ addon = requireNotNull(arguments?.getParcelableCompat("add_on", Addon::class.java))
+ engineSession = components.engine.createSession()
+
+ return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val binding = FragmentAddOnSettingsBinding.bind(view)
+ binding.addonSettingsEngineView.render(engineSession)
+ addon.installedState?.optionsPageUrl?.let {
+ engineSession.loadUrl(it)
+ }
+ }
+
+ override fun onDestroyView() {
+ engineSession.close()
+ super.onDestroyView()
+ }
+
+ companion object {
+ /**
+ * Create an [AddonSettingsFragment] with add_on as a required parameter.
+ */
+ fun create(addon: Addon) = AddonSettingsFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable("add_on", addon)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt
new file mode 100644
index 0000000000..1f30df8331
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt
@@ -0,0 +1,26 @@
+/* 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.samples.browser.addons
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import org.mozilla.samples.browser.R
+
+/**
+ * An activity to manage add-ons.
+ */
+class AddonsActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ if (savedInstanceState == null) {
+ supportFragmentManager.beginTransaction().apply {
+ replace(R.id.container, AddonsFragment())
+ commit()
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt
new file mode 100644
index 0000000000..1e3115a8db
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt
@@ -0,0 +1,251 @@
+/* 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.samples.browser.addons
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManagerException
+import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
+import mozilla.components.feature.addons.ui.AddonsManagerAdapter
+import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
+import mozilla.components.feature.addons.ui.PermissionsDialogFragment
+import mozilla.components.feature.addons.ui.translateName
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.databinding.FragmentAddOnsBinding
+import org.mozilla.samples.browser.databinding.OverlayAddOnProgressBinding
+import org.mozilla.samples.browser.ext.components
+import java.util.concurrent.CancellationException
+import androidx.browser.R as androidxBrowserR
+import mozilla.components.browser.menu.R as menuR
+import mozilla.components.feature.addons.R as addonsR
+
+/**
+ * Fragment use for managing add-ons.
+ */
+class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
+ private lateinit var recyclerView: RecyclerView
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private var adapter: AddonsManagerAdapter? = null
+
+ private var _binding: FragmentAddOnsBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ _binding = FragmentAddOnsBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(rootView, savedInstanceState)
+ bindRecyclerView(rootView)
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ this@AddonsFragment.view?.let { view ->
+ bindRecyclerView(view)
+ }
+
+ findPreviousPermissionDialogFragment()?.let { dialog ->
+ dialog.onPositiveButtonClicked = onConfirmPermissionButtonClicked
+ }
+
+ findPreviousInstallationDialogFragment()?.let { dialog ->
+ dialog.onConfirmButtonClicked = onConfirmInstallationButtonClicked
+ }
+ }
+
+ private fun bindRecyclerView(rootView: View) {
+ recyclerView = rootView.findViewById(R.id.add_ons_list)
+ recyclerView.layoutManager = LinearLayoutManager(requireContext())
+ scope.launch {
+ try {
+ val context = requireContext()
+ val addons = context.components.addonManager.getAddons()
+
+ val style = AddonsManagerAdapter.Style(
+ dividerColor = androidxBrowserR.color.browser_actions_divider_color,
+ dividerHeight = menuR.dimen.mozac_browser_menu_item_divider_height,
+ )
+
+ scope.launch(Dispatchers.Main) {
+ if (adapter == null) {
+ adapter = AddonsManagerAdapter(
+ addonsManagerDelegate = this@AddonsFragment,
+ addons = addons,
+ style = style,
+ store = context.components.store,
+ )
+ recyclerView.adapter = adapter
+ } else {
+ adapter?.updateAddons(addons)
+ }
+ }
+ } catch (e: AddonManagerException) {
+ scope.launch(Dispatchers.Main) {
+ Toast.makeText(
+ activity,
+ addonsR.string.mozac_feature_addons_failed_to_query_extensions,
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+ }
+
+ override fun onAddonItemClicked(addon: Addon) {
+ val context = requireContext()
+
+ if (addon.isInstalled()) {
+ val intent = Intent(context, InstalledAddonDetailsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ context.startActivity(intent)
+ } else {
+ val intent = Intent(context, AddonDetailsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ this.startActivity(intent)
+ }
+ }
+
+ override fun onInstallAddonButtonClicked(addon: Addon) {
+ showPermissionDialog(addon)
+ }
+
+ override fun onNotYetSupportedSectionClicked(unsupportedAddons: List<Addon>) {
+ val intent = Intent(context, NotYetSupportedAddonActivity::class.java)
+ intent.putExtra("add_ons", ArrayList(unsupportedAddons))
+ requireContext().startActivity(intent)
+ }
+
+ private fun isAlreadyADialogCreated(): Boolean {
+ return findPreviousPermissionDialogFragment() != null && findPreviousInstallationDialogFragment() != null
+ }
+
+ private fun findPreviousPermissionDialogFragment(): PermissionsDialogFragment? {
+ return parentFragmentManager.findFragmentByTag(
+ PERMISSIONS_DIALOG_FRAGMENT_TAG,
+ ) as? PermissionsDialogFragment
+ }
+
+ private fun findPreviousInstallationDialogFragment(): AddonInstallationDialogFragment? {
+ return parentFragmentManager.findFragmentByTag(
+ INSTALLATION_DIALOG_FRAGMENT_TAG,
+ ) as? AddonInstallationDialogFragment
+ }
+
+ private fun showPermissionDialog(addon: Addon) {
+ if (isInstallationInProgress) {
+ return
+ }
+
+ val dialog = PermissionsDialogFragment.newInstance(
+ addon = addon,
+ onPositiveButtonClicked = onConfirmPermissionButtonClicked,
+ )
+
+ if (!isAlreadyADialogCreated() && isAdded) {
+ dialog.show(parentFragmentManager, PERMISSIONS_DIALOG_FRAGMENT_TAG)
+ }
+ }
+
+ private fun showInstallationDialog(addon: Addon) {
+ if (isInstallationInProgress) {
+ return
+ }
+ val dialog = AddonInstallationDialogFragment.newInstance(
+ addon = addon,
+ onConfirmButtonClicked = onConfirmInstallationButtonClicked,
+ )
+
+ if (!isAlreadyADialogCreated() && isAdded) {
+ dialog.show(parentFragmentManager, INSTALLATION_DIALOG_FRAGMENT_TAG)
+ }
+ }
+
+ private val onConfirmInstallationButtonClicked: ((Addon, Boolean) -> Unit) = { addon, allowInPrivateBrowsing ->
+ if (allowInPrivateBrowsing) {
+ requireContext().components.addonManager.setAddonAllowedInPrivateBrowsing(
+ addon,
+ allowInPrivateBrowsing,
+ )
+ }
+ }
+
+ private val onConfirmPermissionButtonClicked: ((Addon) -> Unit) = { addon ->
+ val includedBinding = OverlayAddOnProgressBinding.bind(binding.addonProgressOverlay.addonProgressOverlay)
+
+ includedBinding.root.visibility = View.VISIBLE
+ isInstallationInProgress = true
+
+ val installOperation = requireContext().components.addonManager.installAddon(
+ url = addon.downloadUrl,
+ onSuccess = { installedAddon ->
+ context?.let {
+ adapter?.updateAddon(installedAddon)
+ includedBinding.root.visibility = View.GONE
+ isInstallationInProgress = false
+ showInstallationDialog(installedAddon)
+ }
+ },
+ onError = { e ->
+ // No need to display an error message if installation was cancelled by the user.
+ if (e !is CancellationException) {
+ Toast.makeText(
+ requireContext(),
+ getString(
+ addonsR.string.mozac_feature_addons_failed_to_install,
+ addon.translateName(requireContext()),
+ ),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+
+ includedBinding.root.visibility = View.GONE
+ isInstallationInProgress = false
+ },
+ )
+
+ includedBinding.cancelButton.setOnClickListener {
+ MainScope().launch {
+ // Hide the installation progress overlay once cancellation is successful.
+ if (installOperation.cancel().await()) {
+ includedBinding.root.visibility = View.GONE
+ }
+ }
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ /**
+ * Whether or not an add-on installation is in progress.
+ */
+ private var isInstallationInProgress = false
+
+ companion object {
+ private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
+ private const val INSTALLATION_DIALOG_FRAGMENT_TAG = "ADDONS_INSTALLATION_DIALOG_FRAGMENT"
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt
new file mode 100644
index 0000000000..b459840911
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt
@@ -0,0 +1,12 @@
+/* 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.samples.browser.addons
+
+import java.text.NumberFormat
+import java.util.Locale
+
+internal fun getFormattedAmount(amount: Int): String {
+ return NumberFormat.getNumberInstance(Locale.getDefault()).format(amount)
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt
new file mode 100644
index 0000000000..1d84830d29
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt
@@ -0,0 +1,200 @@
+/* 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.samples.browser.addons
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SwitchCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManagerException
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import org.mozilla.samples.browser.BrowserActivity
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.ext.components
+import mozilla.components.feature.addons.R as addonsR
+
+/**
+ * An activity to show the details of a installed add-on.
+ */
+@Suppress("LargeClass")
+class InstalledAddonDetailsActivity : AppCompatActivity() {
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_installed_add_on_details)
+ val addon = requireNotNull(intent.getParcelableExtraCompat("add_on", Addon::class.java))
+ bindAddon(addon)
+ }
+
+ private fun bindAddon(addon: Addon) {
+ scope.launch {
+ try {
+ val context = baseContext
+ val addons = context.components.addonManager.getAddons()
+ scope.launch(Dispatchers.Main) {
+ addons.find { addon.id == it.id }.let {
+ if (it == null) {
+ throw AddonManagerException(Exception("Addon ${addon.id} not found"))
+ } else {
+ bindUI(it)
+ }
+ }
+ }
+ } catch (e: AddonManagerException) {
+ scope.launch(Dispatchers.Main) {
+ Toast.makeText(
+ baseContext,
+ addonsR.string.mozac_feature_addons_failed_to_query_extensions,
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+ }
+
+ private fun bindUI(addon: Addon) {
+ title = addon.translateName(this)
+
+ bindEnableSwitch(addon)
+
+ bindSettings(addon)
+
+ bindDetails(addon)
+
+ bindPermissions(addon)
+
+ bindAllowInPrivateBrowsingSwitch(addon)
+
+ bindRemoveButton(addon)
+ }
+
+ private fun bindEnableSwitch(addon: Addon) {
+ val switch = findViewById<SwitchCompat>(R.id.enable_switch)
+ switch.isChecked = addon.isEnabled()
+ switch.setOnCheckedChangeListener { _, isChecked ->
+ if (isChecked) {
+ this.components.addonManager.enableAddon(
+ addon,
+ onSuccess = {
+ switch.isChecked = true
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_successfully_enabled,
+ addon,
+ )
+ },
+ onError = {
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_failed_to_enable,
+ addon,
+ )
+ },
+ )
+ } else {
+ this.components.addonManager.disableAddon(
+ addon,
+ onSuccess = {
+ switch.isChecked = false
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_successfully_disabled,
+ addon,
+ )
+ },
+ onError = {
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_failed_to_disable,
+ addon,
+ )
+ },
+ )
+ }
+ }
+ }
+
+ private fun bindSettings(addon: Addon) {
+ val view = findViewById<View>(R.id.settings)
+ val optionsPageUrl = addon.installedState?.optionsPageUrl
+ view.isEnabled = optionsPageUrl != null
+ view.setOnClickListener {
+ if (addon.installedState?.openOptionsPageInTab == true) {
+ components.tabsUseCases.addTab(optionsPageUrl as String)
+ val intent = Intent(this, BrowserActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ this.startActivity(intent)
+ } else {
+ val intent = Intent(this, AddonSettingsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ this.startActivity(intent)
+ }
+ }
+ }
+
+ private fun bindDetails(addon: Addon) {
+ findViewById<View>(R.id.details).setOnClickListener {
+ val intent = Intent(this, AddonDetailsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ this.startActivity(intent)
+ }
+ }
+
+ private fun bindPermissions(addon: Addon) {
+ findViewById<View>(R.id.permissions).setOnClickListener {
+ val intent = Intent(this, PermissionsDetailsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ this.startActivity(intent)
+ }
+ }
+
+ private fun bindAllowInPrivateBrowsingSwitch(addon: Addon) {
+ val switch = findViewById<SwitchCompat>(R.id.allow_in_private_browsing_switch)
+ switch.isChecked = addon.isAllowedInPrivateBrowsing()
+ switch.setOnCheckedChangeListener { _, isChecked ->
+ this.components.addonManager.setAddonAllowedInPrivateBrowsing(
+ addon,
+ isChecked,
+ onSuccess = {
+ switch.isChecked = isChecked
+ },
+ )
+ }
+ }
+
+ private fun bindRemoveButton(addon: Addon) {
+ findViewById<View>(R.id.remove_add_on).setOnClickListener {
+ this.components.addonManager.uninstallAddon(
+ addon,
+ onSuccess = {
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_successfully_uninstalled,
+ addon,
+ )
+ finish()
+ },
+ onError = { _, _ ->
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_failed_to_uninstall,
+ addon,
+ )
+ },
+ )
+ }
+ }
+
+ private fun showAddonToast(@StringRes textId: Int, addon: Addon) {
+ Toast.makeText(
+ this,
+ getString(textId, addon.translateName(context = this)),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt
new file mode 100644
index 0000000000..dac2e3dca1
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt
@@ -0,0 +1,105 @@
+/* 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.samples.browser.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.UnsupportedAddonsAdapter
+import mozilla.components.feature.addons.ui.UnsupportedAddonsAdapterDelegate
+import mozilla.components.support.utils.ext.getParcelableArrayListCompat
+import mozilla.components.support.utils.ext.getParcelableArrayListExtraCompat
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.ext.components
+
+private const val LEARN_MORE_URL =
+ "https://support.mozilla.org/kb/add-compatibility-firefox-preview"
+
+/**
+ * Activity for managing unsupported add-ons.
+ */
+class NotYetSupportedAddonActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val addons = requireNotNull(intent.getParcelableArrayListExtraCompat("add_ons", Addon::class.java))
+
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.container, NotYetSupportedAddonFragment.create(addons))
+ .commit()
+ }
+
+ /**
+ * Fragment for managing add-ons that are not yet supported by the browser.
+ */
+ class NotYetSupportedAddonFragment : Fragment(), UnsupportedAddonsAdapterDelegate {
+ private lateinit var addons: List<Addon>
+ private var adapter: UnsupportedAddonsAdapter? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ addons =
+ requireNotNull(arguments?.getParcelableArrayListCompat("add_ons", Addon::class.java))
+ return inflater.inflate(R.layout.fragment_not_yet_supported_addons, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val context = requireContext()
+ val recyclerView: RecyclerView = view.findViewById(R.id.unsupported_add_ons_list)
+ adapter = UnsupportedAddonsAdapter(
+ addonManager = context.components.addonManager,
+ unsupportedAddonsAdapterDelegate = this@NotYetSupportedAddonFragment,
+ addons = addons,
+ )
+
+ recyclerView.layoutManager = LinearLayoutManager(context)
+ recyclerView.adapter = adapter
+
+ view.findViewById<View>(R.id.learn_more_label).setOnClickListener {
+ val intent = Intent(Intent.ACTION_VIEW).setData(Uri.parse(LEARN_MORE_URL))
+ startActivity(intent)
+ }
+ }
+
+ override fun onUninstallError(addonId: String, throwable: Throwable) {
+ Toast.makeText(context, "Failed to remove add-on", Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onUninstallSuccess() {
+ Toast.makeText(context, "Successfully removed add-on", Toast.LENGTH_SHORT)
+ .show()
+ if (adapter?.itemCount == 0) {
+ activity?.onBackPressedDispatcher?.onBackPressed()
+ }
+ }
+
+ companion object {
+ /**
+ * Create an [NotYetSupportedAddonFragment] with add_ons as a required parameter.
+ */
+ fun create(addons: ArrayList<Addon>) = NotYetSupportedAddonFragment().apply {
+ arguments = Bundle().apply {
+ putParcelableArrayList("add_ons", addons)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt
new file mode 100644
index 0000000000..20424be237
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt
@@ -0,0 +1,54 @@
+/* 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.samples.browser.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.AddonPermissionsAdapter
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import org.mozilla.samples.browser.R
+
+private const val LEARN_MORE_URL =
+ "https://support.mozilla.org/kb/permission-request-messages-firefox-extensions"
+
+/**
+ * An activity to show the permissions of an add-on.
+ */
+class PermissionsDetailsActivity : AppCompatActivity(), View.OnClickListener {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_add_on_permissions)
+ val addon = requireNotNull(intent.getParcelableExtraCompat("add_on", Addon::class.java))
+ title = addon.translateName(this)
+
+ bindPermissions(addon)
+
+ bindLearnMore()
+ }
+
+ private fun bindPermissions(addon: Addon) {
+ val recyclerView = findViewById<RecyclerView>(R.id.add_ons_permissions)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ val sortedPermissions = addon.translatePermissions(this).sorted()
+ recyclerView.adapter = AddonPermissionsAdapter(sortedPermissions)
+ }
+
+ private fun bindLearnMore() {
+ findViewById<View>(R.id.learn_more_label).setOnClickListener(this)
+ }
+
+ override fun onClick(v: View?) {
+ val intent =
+ Intent(Intent.ACTION_VIEW).setData(Uri.parse(LEARN_MORE_URL))
+ startActivity(intent)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt
new file mode 100644
index 0000000000..0dc1e300c3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt
@@ -0,0 +1,114 @@
+/* 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.samples.browser.addons
+
+import android.content.Context
+import android.os.Bundle
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.lib.state.ext.consumeFrom
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.databinding.FragmentAddOnSettingsBinding
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * An activity to show the pop up action of a web extension.
+ */
+class WebExtensionActionPopupActivity : AppCompatActivity() {
+ private lateinit var webExtensionId: String
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_add_on_settings)
+
+ webExtensionId = requireNotNull(intent.getStringExtra("web_extension_id"))
+ intent.getStringExtra("web_extension_name")?.let {
+ title = it
+ }
+
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.addonSettingsContainer, WebExtensionActionPopupFragment.create(webExtensionId))
+ .commit()
+ }
+
+ override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
+ when (name) {
+ EngineView::class.java.name -> components.engine.createView(context, attrs).asView()
+ else -> super.onCreateView(parent, name, context, attrs)
+ }
+
+ /**
+ * A fragment to show the web extension action popup with [EngineView].
+ */
+ class WebExtensionActionPopupFragment : Fragment(), EngineSession.Observer {
+ private var engineSession: EngineSession? = null
+ private lateinit var webExtensionId: String
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ webExtensionId = requireNotNull(arguments?.getString("web_extension_id"))
+ engineSession = components.store.state.extensions[webExtensionId]?.popupSession
+
+ return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val binding = FragmentAddOnSettingsBinding.bind(view)
+ val session = engineSession
+ if (session != null) {
+ binding.addonSettingsEngineView.render(session)
+ session.register(this, view)
+ consumePopupSession()
+ } else {
+ consumeFrom(requireContext().components.store) { state ->
+ state.extensions[webExtensionId]?.let { extState ->
+ extState.popupSession?.let {
+ if (engineSession == null) {
+ binding.addonSettingsEngineView.render(it)
+ it.register(this, view)
+ consumePopupSession()
+ engineSession = it
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onWindowRequest(windowRequest: WindowRequest) {
+ if (windowRequest.type == WindowRequest.Type.CLOSE) {
+ activity?.finish()
+ } else {
+ engineSession?.loadUrl(windowRequest.url)
+ }
+ }
+ private fun consumePopupSession() {
+ components.store.dispatch(
+ WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null),
+ )
+ }
+
+ companion object {
+ /**
+ * Create an [WebExtensionActionPopupFragment] with webExtensionId as a required parameter.
+ */
+ fun create(webExtensionId: String) = WebExtensionActionPopupFragment().apply {
+ arguments = Bundle().apply {
+ putString("web_extension_id", webExtensionId)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt
new file mode 100644
index 0000000000..c5fa99c09f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt
@@ -0,0 +1,19 @@
+/* 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.samples.browser.autofill
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.ui.AbstractAutofillConfirmActivity
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Activity responsible for asking the user to confirm before autofilling a third-party app.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillConfirmActivity : AbstractAutofillConfirmActivity() {
+ override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt
new file mode 100644
index 0000000000..5bf14fc138
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt
@@ -0,0 +1,20 @@
+/* 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.samples.browser.autofill
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.ui.AbstractAutofillSearchActivity
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Activity responsible for letting the user manually search and pick credentials for auto-filling a
+ * third-party app.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillSearchActivity : AbstractAutofillSearchActivity() {
+ override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt
new file mode 100644
index 0000000000..a5c6811a6e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt
@@ -0,0 +1,19 @@
+/* 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.samples.browser.autofill
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AbstractAutofillService
+import mozilla.components.feature.autofill.AutofillConfiguration
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Service responsible for implementing Android's Autofill framework.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillService : AbstractAutofillService() {
+ override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt
new file mode 100644
index 0000000000..50bba51c1f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt
@@ -0,0 +1,20 @@
+/* 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.samples.browser.autofill
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.ui.AbstractAutofillUnlockActivity
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Activity responsible for unlocking the autofill service by asking the user to verify with a
+ * fingerprint or alternative device unlocking mechanism.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillUnlockActivity : AbstractAutofillUnlockActivity() {
+ override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt
new file mode 100644
index 0000000000..c7918e18d1
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt
@@ -0,0 +1,75 @@
+/* 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.samples.browser.awesomebar
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.platform.AbstractComposeView
+import mozilla.components.compose.browser.awesomebar.AwesomeBar
+import mozilla.components.concept.awesomebar.AwesomeBar
+
+/**
+ * This wrapper wraps the `AwesomeBar()` composable and exposes it as a `View` and `concept-awesomebar`
+ * implementation.
+ */
+class AwesomeBarWrapper @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : AbstractComposeView(context, attrs, defStyleAttr), AwesomeBar {
+ private val providers = mutableStateOf(emptyList<AwesomeBar.SuggestionProvider>())
+ private val text = mutableStateOf("")
+ private var onEditSuggestionListener: ((String) -> Unit)? = null
+ private var onStopListener: (() -> Unit)? = null
+
+ @Composable
+ override fun Content() {
+ AwesomeBar(
+ text = text.value,
+ providers = providers.value,
+ onSuggestionClicked = { suggestion ->
+ suggestion.onSuggestionClicked?.invoke()
+ onStopListener?.invoke()
+ },
+ onAutoComplete = { suggestion ->
+ onEditSuggestionListener?.invoke(suggestion.editSuggestion!!)
+ },
+ )
+ }
+
+ override fun addProviders(vararg providers: AwesomeBar.SuggestionProvider) {
+ val newProviders = this.providers.value.toMutableList()
+ newProviders.addAll(providers)
+ this.providers.value = newProviders
+ }
+
+ override fun containsProvider(provider: AwesomeBar.SuggestionProvider): Boolean {
+ return providers.value.any { current -> current.id == provider.id }
+ }
+
+ override fun onInputChanged(text: String) {
+ this.text.value = text
+ }
+
+ override fun removeAllProviders() {
+ providers.value = emptyList()
+ }
+
+ override fun removeProviders(vararg providers: AwesomeBar.SuggestionProvider) {
+ val newProviders = this.providers.value.toMutableList()
+ newProviders.removeAll(providers)
+ this.providers.value = newProviders
+ }
+
+ override fun setOnEditSuggestionListener(listener: (String) -> Unit) {
+ onEditSuggestionListener = listener
+ }
+
+ override fun setOnStopListener(listener: () -> Unit) {
+ onStopListener = listener
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt
new file mode 100644
index 0000000000..225cfee4d4
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt
@@ -0,0 +1,17 @@
+/* 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.samples.browser.customtabs
+
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.customtabs.AbstractCustomTabsService
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import org.mozilla.samples.browser.ext.components
+
+class CustomTabsService : AbstractCustomTabsService() {
+ override val engine: Engine by lazy { components.engine }
+ override val customTabsServiceStore: CustomTabsServiceStore by lazy { components.customTabsStore }
+ override val relationChecker: RelationChecker by lazy { components.relationChecker }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt
new file mode 100644
index 0000000000..cc9897c90f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt
@@ -0,0 +1,16 @@
+/* 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.samples.browser.downloads
+
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.downloads.AbstractFetchDownloadService
+import mozilla.components.support.base.android.NotificationsDelegate
+import org.mozilla.samples.browser.ext.components
+
+class DownloadService : AbstractFetchDownloadService() {
+ override val httpClient by lazy { components.client }
+ override val store: BrowserStore by lazy { components.store }
+ override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt
new file mode 100644
index 0000000000..2dc924356a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt
@@ -0,0 +1,21 @@
+/* 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.samples.browser.ext
+
+import android.content.Context
+import org.mozilla.samples.browser.Components
+import org.mozilla.samples.browser.SampleApplication
+
+/**
+ * Get the SampleApplication object from a context.
+ */
+val Context.application: SampleApplication
+ get() = applicationContext as SampleApplication
+
+/**
+ * Get the components of this application.
+ */
+val Context.components: Components
+ get() = application.components
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt
new file mode 100644
index 0000000000..0a07e89d57
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt
@@ -0,0 +1,14 @@
+/* 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.samples.browser.ext
+
+import androidx.fragment.app.Fragment
+import org.mozilla.samples.browser.Components
+
+/**
+ * Get the components of this application.
+ */
+val Fragment.components: Components
+ get() = context!!.components
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt
new file mode 100644
index 0000000000..d2b6c07b83
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt
@@ -0,0 +1,89 @@
+/* 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.samples.browser.integration
+
+import android.content.Context
+import android.view.View
+import androidx.fragment.app.FragmentManager
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.app.links.AppLinksUseCases
+import mozilla.components.feature.contextmenu.ContextMenuCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createAddContactCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createCopyEmailAddressCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createCopyImageLocationCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createCopyLinkCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createOpenImageInNewTabCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createSaveImageCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createShareEmailAddressCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createShareLinkCandidate
+import mozilla.components.feature.contextmenu.ContextMenuFeature
+import mozilla.components.feature.contextmenu.ContextMenuUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.ui.widgets.DefaultSnackbarDelegate
+import org.mozilla.samples.browser.databinding.FragmentBrowserBinding
+
+@Suppress("LongParameterList", "UndocumentedPublicClass")
+class ContextMenuIntegration(
+ context: Context,
+ fragmentManager: FragmentManager,
+ browserStore: BrowserStore,
+ tabsUseCases: TabsUseCases,
+ contextMenuUseCases: ContextMenuUseCases,
+ parentView: View,
+ sessionId: String? = null,
+) : LifecycleAwareFeature {
+
+ private val candidates = run {
+ if (sessionId != null) {
+ val snackbarDelegate = DefaultSnackbarDelegate()
+ listOf(
+ createCopyLinkCandidate(context, parentView, snackbarDelegate),
+ createShareLinkCandidate(context),
+ createOpenImageInNewTabCandidate(
+ context,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ ),
+ createSaveImageCandidate(context, contextMenuUseCases),
+ createCopyImageLocationCandidate(context, parentView, snackbarDelegate),
+ createAddContactCandidate(context),
+ createShareEmailAddressCandidate(context),
+ createCopyEmailAddressCandidate(context, parentView, snackbarDelegate),
+ )
+ } else {
+ val appLinksCandidate = ContextMenuCandidate.createOpenInExternalAppCandidate(
+ context = context,
+ appLinksUseCases = AppLinksUseCases(
+ context = context,
+ launchInApp = { true },
+ ),
+ )
+ ContextMenuCandidate.defaultCandidates(
+ context,
+ tabsUseCases,
+ contextMenuUseCases,
+ parentView,
+ ) + appLinksCandidate
+ }
+ }
+
+ private val feature = ContextMenuFeature(
+ fragmentManager,
+ browserStore,
+ candidates,
+ FragmentBrowserBinding.bind(parentView).engineView,
+ contextMenuUseCases,
+ )
+
+ override fun start() {
+ feature.start()
+ }
+
+ override fun stop() {
+ feature.stop()
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt
new file mode 100644
index 0000000000..3e56201ce0
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt
@@ -0,0 +1,53 @@
+/* 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.samples.browser.integration
+
+import android.view.View
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.findinpage.FindInPageFeature
+import mozilla.components.feature.findinpage.view.FindInPageView
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+
+@Suppress("UndocumentedPublicClass")
+class FindInPageIntegration(
+ private val store: BrowserStore,
+ private val view: FindInPageView,
+ engineView: EngineView,
+) : LifecycleAwareFeature, UserInteractionHandler {
+ private val feature = FindInPageFeature(store, view, engineView, ::onClose)
+
+ override fun start() {
+ feature.start()
+ launch = this::launch
+ }
+
+ override fun stop() {
+ feature.stop()
+ launch = null
+ }
+
+ override fun onBackPressed(): Boolean {
+ return feature.onBackPressed()
+ }
+
+ private fun onClose() {
+ view.asView().visibility = View.GONE
+ }
+
+ private fun launch() {
+ val session = store.state.selectedTab ?: return
+
+ view.asView().visibility = View.VISIBLE
+ feature.bind(session)
+ }
+
+ companion object {
+ var launch: (() -> Unit)? = null
+ private set
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt
new file mode 100644
index 0000000000..76bcf69d5f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt
@@ -0,0 +1,84 @@
+/* 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.samples.browser.integration
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.core.content.ContextCompat
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.readerview.ReaderViewFeature
+import mozilla.components.feature.readerview.view.ReaderViewControlsView
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import org.mozilla.samples.browser.R
+import mozilla.components.ui.colors.R as colorsR
+import mozilla.components.ui.icons.R as iconsR
+
+@Suppress("UndocumentedPublicClass")
+class ReaderViewIntegration(
+ context: Context,
+ engine: Engine,
+ store: BrowserStore,
+ toolbar: BrowserToolbar,
+ view: ReaderViewControlsView,
+ readerViewAppearanceButton: FloatingActionButton,
+) : LifecycleAwareFeature, UserInteractionHandler {
+
+ private var readerViewButtonVisible = false
+
+ private val readerViewButton: BrowserToolbar.ToggleButton = BrowserToolbar.ToggleButton(
+ image = getReaderDrawable(context),
+ imageSelected = getReaderDrawable(context).mutate().apply {
+ setTint(ContextCompat.getColor(context, colorsR.color.photonBlue40))
+ },
+ contentDescription = context.getString(R.string.mozac_reader_view_description),
+ contentDescriptionSelected = context.getString(R.string.mozac_reader_view_description_selected),
+ selected = store.state.selectedTab?.readerState?.active ?: false,
+ visible = { readerViewButtonVisible },
+ ) { enabled ->
+ if (enabled) {
+ feature.showReaderView()
+ readerViewAppearanceButton.show()
+ } else {
+ feature.hideReaderView()
+ feature.hideControls()
+ readerViewAppearanceButton.hide()
+ }
+ }
+
+ init {
+ toolbar.addPageAction(readerViewButton)
+ readerViewAppearanceButton.setOnClickListener { feature.showControls() }
+ }
+
+ private val feature = ReaderViewFeature(context, engine, store, view) { available, active ->
+ readerViewButtonVisible = available
+ readerViewButton.setSelected(active)
+
+ if (active) readerViewAppearanceButton.show() else readerViewAppearanceButton.hide()
+ toolbar.invalidateActions()
+ }
+
+ override fun start() {
+ feature.start()
+ }
+
+ override fun stop() {
+ feature.stop()
+ }
+
+ override fun onBackPressed(): Boolean {
+ return feature.onBackPressed()
+ }
+}
+
+private fun getReaderDrawable(context: Context): Drawable {
+ val drawable = iconsR.drawable.mozac_ic_reader_view_24
+ return ContextCompat.getDrawable(context, drawable)!!
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt
new file mode 100644
index 0000000000..352330e925
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt
@@ -0,0 +1,20 @@
+/* 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.samples.browser.media
+
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.feature.media.service.AbstractMediaSessionService
+import mozilla.components.support.base.android.NotificationsDelegate
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * See [AbstractMediaSessionService].
+ */
+class MediaSessionService : AbstractMediaSessionService() {
+ override val crashReporter: CrashReporting? by lazy { components.crashReporter }
+ override val store: BrowserStore by lazy { components.store }
+ override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt
new file mode 100644
index 0000000000..509efe9e7e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt
@@ -0,0 +1,71 @@
+/* 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.samples.browser.request
+
+import android.content.Context
+import mozilla.components.browser.errorpages.ErrorPages
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.concept.engine.request.RequestInterceptor.ErrorResponse
+import mozilla.components.concept.engine.request.RequestInterceptor.InterceptionResponse
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Example of a request interceptor that loads error pages with URL encoding (images)
+ */
+class SampleUrlEncodedRequestInterceptor(val context: Context) : RequestInterceptor {
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): InterceptionResponse? {
+ return when (uri) {
+ "sample:about" -> InterceptionResponse.Content("<h1>I am the sample browser</h1>")
+ else -> {
+ var response = context.components.appLinksInterceptor.onLoadRequest(
+ engineSession,
+ uri,
+ lastUri,
+ hasUserGesture,
+ isSameDomain,
+ isRedirect,
+ isDirectNavigation,
+ isSubframeRequest,
+ )
+
+ if (response == null && !isDirectNavigation) {
+ response = context.components.webAppInterceptor.onLoadRequest(
+ engineSession,
+ uri,
+ lastUri,
+ hasUserGesture,
+ isSameDomain,
+ isRedirect,
+ isDirectNavigation,
+ isSubframeRequest,
+ )
+ }
+
+ response
+ }
+ }
+ }
+
+ override fun onErrorRequest(
+ session: EngineSession,
+ errorType: ErrorType,
+ uri: String?,
+ ): ErrorResponse {
+ val errorPage = ErrorPages.createUrlEncodedErrorPage(context, errorType, uri)
+ return ErrorResponse(errorPage)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml b/mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml
new file mode 100644
index 0000000000..8e89011a37
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="true" android:color="@android:color/black" />
+ <item android:state_checked="false" android:color="@color/photonGrey40" />
+</selector> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml b/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml
new file mode 100644
index 0000000000..673ffa98a7
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:pathData="M14.5,8c-0.971,0 -1,1 -1.75,1a0.765,0.765 0,0 1,-0.75 -0.75V5a1,1 0,0 0,-1 -1H7.75A0.765,0.765 0,0 1,7 3.25c0,-0.75 1,-0.779 1,-1.75C8,0.635 7.1,0 6,0S4,0.635 4,1.5c0,0.971 1,1 1,1.75a0.765,0.765 0,0 1,-0.75 0.75H1a1,1 0,0 0,-1 1v2.25A0.765,0.765 0,0 0,0.75 8c0.75,0 0.779,-1 1.75,-1C3.365,7 4,7.9 4,9s-0.635,2 -1.5,2c-0.971,0 -1,-1 -1.75,-1a0.765,0.765 0,0 0,-0.75 0.75V15a1,1 0,0 0,1 1h3.25a0.765,0.765 0,0 0,0.75 -0.75c0,-0.75 -1,-0.779 -1,-1.75 0,-0.865 0.9,-1.5 2,-1.5s2,0.635 2,1.5c0,0.971 -1,1 -1,1.75a0.765,0.765 0,0 0,0.75 0.75H11a1,1 0,0 0,1 -1v-3.25a0.765,0.765 0,0 1,0.75 -0.75c0.75,0 0.779,1 1.75,1 0.865,0 1.5,-0.9 1.5,-2s-0.635,-2 -1.5,-2z"
+ android:fillColor="@android:color/black"/>
+</vector>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml b/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml
new file mode 100644
index 0000000000..cf50e31dbf
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:autoMirrored="true"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M2,1h20c1.1,0 2,0.9 2,2v18c0,1.1 -0.9,2 -2,2H2c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2z" />
+ <path
+ android:fillColor="#FFF"
+ android:pathData="M12,3h9c0.6,0 1,0.4 1,1v16c0,0.6 -0.4,1 -1,1h-9L12,3zM5.5,12.5l2.7,-3.7c0.2,-0.3 0.6,-0.3 0.8,-0.1l0.7,0.5c0.2,0.2 0.2,0.5 0,0.7L5.8,15c-0.2,0.2 -0.5,0.3 -0.8,0.1l-2.2,-2.2c-0.2,-0.2 -0.2,-0.5 0,-0.7l0.8,-0.8c0.2,-0.2 0.5,-0.2 0.7,0l1.2,1.1z" />
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M15,9l-1,1 2,2 -2,2 1,1 2,-2 2,2 1,-1 -2,-2 2,-2 -1,-1 -2,2.01L15,9z" />
+</vector>
+
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml
new file mode 100644
index 0000000000..5a3896533a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="6dp">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <TextView
+ android:id="@+id/details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ tools:text="@tools:sample/lorem/random" />
+
+ <TextView
+ android:id="@+id/author_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/details"
+ android:text="@string/mozac_feature_addons_author" />
+
+ <TextView
+ android:id="@+id/author_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/details"
+ android:layout_alignParentEnd="true"
+ tools:text="@tools:sample/full_names" />
+
+ <View
+ android:id="@+id/author_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/author_label"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/photonGrey40"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/version_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/author_divider"
+ android:text="@string/mozac_feature_addons_version" />
+
+ <TextView
+ android:id="@+id/version_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/author_divider"
+ android:layout_alignParentEnd="true"
+ tools:text="1.2.3" />
+
+ <View
+ android:id="@+id/version_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/version_label"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/photonGrey40"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/last_updated_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/version_divider"
+ android:text="@string/mozac_feature_addons_last_updated" />
+
+ <TextView
+ android:id="@+id/last_updated_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/version_divider"
+ android:layout_alignParentEnd="true"
+ tools:text="Oct 16, 2019" />
+
+ <View
+ android:id="@+id/last_updated_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/last_updated_label"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/photonGrey40"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/home_page_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/last_updated_divider"
+ android:text="@string/mozac_feature_addons_home_page" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/home_page_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/last_updated_divider"
+ android:layout_alignParentEnd="true"
+ android:contentDescription="@string/mozac_feature_addons_home_page"
+ app:srcCompat="@drawable/mozac_ic_link_24"
+ app:tint="@android:color/black" />
+
+ <View
+ android:id="@+id/home_page_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/home_page_label"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/photonGrey40"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/rating_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/home_page_divider"
+ android:text="@string/mozac_feature_addons_rating" />
+
+ <RatingBar
+ android:id="@+id/rating_view"
+ style="@style/Widget.AppCompat.RatingBar.Small"
+ android:layout_width="wrap_content"
+ android:layout_height="20dp"
+ android:layout_below="@+id/home_page_divider"
+ android:layout_toStartOf="@+id/users_count"
+ android:isIndicator="true"
+ android:numStars="5" />
+
+ <TextView
+ android:id="@+id/users_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/home_page_divider"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="6dp"
+ tools:text="591,642" />
+
+ </RelativeLayout>
+</ScrollView>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml
new file mode 100644
index 0000000000..003949ec45
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:context=".addons.PermissionsDetailsActivity">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/add_ons_permissions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:id="@+id/learn_more_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/add_ons_permissions"
+ android:background="?attr/selectableItemBackground"
+ android:padding="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:text="@string/mozac_feature_addons_learn_more"
+ android:textColor="@android:color/black"
+ app:drawableEndCompat="@drawable/mozac_ic_link_24"
+ app:drawableTint="@android:color/black" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml
new file mode 100644
index 0000000000..f2a64edaf8
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/addonSettingsContainer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:ignore="MergeRootFrame" />
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml
new file mode 100644
index 0000000000..1c713c13b3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?><!-- 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/. -->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="6dp">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/enable_switch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ android:background="?android:attr/selectableItemBackground"
+ android:checked="true"
+ android:clickable="true"
+ android:focusable="true"
+ android:text="@string/mozac_feature_addons_enabled"
+ android:padding="16dp"
+ android:textSize="18sp"/>
+
+ <TextView
+ android:id="@+id/settings"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/enable_switch"
+ android:background="?android:attr/selectableItemBackground"
+ android:drawablePadding="10dp"
+ android:padding="16dp"
+ android:text="@string/mozac_feature_addons_settings"
+ android:textColor="@drawable/addon_textview_selector"
+ android:textSize="18sp"
+ app:drawableStartCompat="@drawable/mozac_ic_preferences"
+ app:drawableTint="@android:color/black" />
+
+ <TextView
+ android:id="@+id/details"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/settings"
+ android:background="?android:attr/selectableItemBackground"
+ android:drawablePadding="6dp"
+ android:padding="16dp"
+ android:text="@string/mozac_feature_addons_details"
+ android:textColor="@drawable/addon_textview_selector"
+ android:textSize="18sp"
+ app:drawableStartCompat="@drawable/mozac_ic_information_24"
+ app:drawableTint="@android:color/black" />
+
+ <TextView
+ android:id="@+id/permissions"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/details"
+ android:background="?android:attr/selectableItemBackground"
+ android:drawablePadding="6dp"
+ android:padding="16dp"
+ android:text="@string/mozac_feature_addons_permissions"
+ android:textColor="@drawable/addon_textview_selector"
+ android:textSize="18sp"
+ app:drawableStartCompat="@drawable/mozac_ic_permissions" />
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/allow_in_private_browsing_switch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ android:layout_below="@+id/permissions"
+ android:background="?android:attr/selectableItemBackground"
+ android:checked="true"
+ android:clickable="true"
+ android:focusable="true"
+ android:text="@string/mozac_feature_addons_settings_allow_in_private_browsing"
+ android:padding="16dp"
+ android:textSize="18sp"/>
+
+ <Button
+ android:id="@+id/remove_add_on"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/allow_in_private_browsing_switch"
+ android:layout_marginTop="16dp"
+ android:text="@string/mozac_feature_addons_remove"
+ android:textAlignment="center"
+ android:textColor="@color/photonRed50" />
+ </RelativeLayout>
+</ScrollView>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..05ab3e0238
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:ignore="MergeRootFrame" />
+
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml
new file mode 100644
index 0000000000..d4bf988d33
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <mozilla.components.concept.engine.EngineView
+ tools:ignore="Instantiatable"
+ android:id="@+id/addonSettingsEngineView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml
new file mode 100644
index 0000000000..f4d63f6fc3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/add_ons_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".BrowserActivity"/>
+
+
+ <include
+ android:id="@+id/addonProgressOverlay"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:visibility="gone"
+ layout="@layout/overlay_add_on_progress" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml
new file mode 100644
index 0000000000..0403dbea20
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".BrowserActivity">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <mozilla.components.browser.toolbar.BrowserToolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:background="#aaaaaa" />
+
+ <mozilla.components.feature.findinpage.view.FindInPageBar
+ android:id="@+id/findInPage"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:background="#FFFFFFFF"
+ android:elevation="10dp"
+ android:padding="4dp"
+ android:visibility="gone"
+ app:findInPageNoMatchesTextColor="@color/photonRed50" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <mozilla.components.ui.widgets.VerticalSwipeRefreshLayout
+ android:id="@+id/swipeToRefresh"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <mozilla.components.concept.engine.EngineView
+ tools:ignore="Instantiatable"
+ android:id="@+id/engineView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ </mozilla.components.ui.widgets.VerticalSwipeRefreshLayout>
+
+ <org.mozilla.samples.browser.awesomebar.AwesomeBarWrapper
+ android:id="@+id/awesomeBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="4dp"
+ android:visibility="gone" />
+
+ <mozilla.components.feature.readerview.view.ReaderViewControlsBar
+ android:id="@+id/readerViewBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:background="#FFFFFFFF"
+ android:elevation="10dp"
+ android:paddingBottom="55dp"
+ android:visibility="gone" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/readerViewAppearanceButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|bottom"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginBottom="72dp"
+ android:src="@drawable/mozac_ic_font"
+ android:visibility="gone"
+ tools:ignore="ContentDescription" />
+
+ </FrameLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml
new file mode 100644
index 0000000000..3dddf561db
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:text="@string/mozac_feature_addons_not_yet_supported_caption2" />
+
+ <TextView
+ android:id="@+id/learn_more_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:background="?attr/selectableItemBackground"
+ android:text="@string/mozac_feature_addons_unsupported_learn_more" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/photonGrey30" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/unsupported_add_ons_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".BrowserActivity"/>
+
+</LinearLayout>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml
new file mode 100644
index 0000000000..c16d4e3ea3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:mozac="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:background="#aaaaaa"/>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/tabsTray"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/toolbar" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml
new file mode 100644
index 0000000000..77cc30fa5e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/addonProgressOverlay"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:elevation="1dp">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/install_hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:drawablePadding="8dp"
+ android:gravity="start|center_vertical"
+ android:padding="16dp"
+ android:text="@string/mozac_extension_install_progress_caption"
+ app:drawableStartCompat="@drawable/mozac_ic_extensions_black" />
+
+ <Button
+ android:id="@+id/cancel_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/install_hint"
+ android:layout_alignParentEnd="true"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/mozac_feature_addons_install_addon_dialog_cancel"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+ </RelativeLayout>
+
+</androidx.cardview.widget.CardView>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml b/mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml
new file mode 100644
index 0000000000..b918b60307
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<menu xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/newTab"
+ android:icon="@drawable/mozac_ic_tab_new"
+ android:title="@string/menu_action_add_tab"
+ app:showAsAction="ifRoom" />
+</menu> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..ff5b811c28
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..ff5b811c28
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..cdc89f3dee
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..84ef408f6a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..e6df10d76b
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..c01ea2a106
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..0a810a25a3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..3909d6df1e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..40a7e0cc99
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..15b39d10eb
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..190b2d260a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..0e9b1e4e1b
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..7b7aa5dfdb
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..6ad22ca834
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..73759f1a06
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..646c51a8ae
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..e249dfc1d3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/values/colors.xml b/mobile/android/android-components/samples/browser/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..b1560d4ab9
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <color name="mozac_ui_tabcounter_default_tint" tools:ignore="UnusedResources">#FFFFFFFF</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml b/mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000000..92e7745041
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<resources>
+ <color name="ic_launcher_background">#45A1FF</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/values/strings.xml b/mobile/android/android-components/samples/browser/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..8789005cc6
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?><!-- 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/. -->
+<resources>
+ <string name="app_name">Sample Browser</string>
+
+ <string name="menu_action_add_tab">Add New Tab</string>
+ <string name="mozac_reader_view_description">Enable Reader View</string>
+ <string name="mozac_reader_view_description_selected">Disable Reader View</string>
+</resources>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml b/mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml
new file mode 100644
index 0000000000..0a338b877a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<autofill-service
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:supportsInlineSuggestions="true"
+ tools:targetApi="r" /> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt b/mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt
new file mode 100644
index 0000000000..41afeb2c30
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt
@@ -0,0 +1,17 @@
+/* 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.samples.browser
+
+import android.content.Context
+import mozilla.components.browser.engine.servo.ServoEngine
+import mozilla.components.concept.engine.Engine
+
+/**
+ * Helper class for lazily instantiating components needed by the application.
+ */
+class Components(applicationContext: Context) : DefaultComponents(applicationContext) {
+ override val engine: Engine by lazy {
+ ServoEngine()
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt b/mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt
new file mode 100644
index 0000000000..220e859f6b
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt
@@ -0,0 +1,11 @@
+/* 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.samples.browser
+
+import android.content.Context
+
+/**
+ * Helper class for lazily instantiating components needed by the application.
+ */
+class Components(applicationContext: Context) : DefaultComponents(applicationContext)