diff options
Diffstat (limited to 'mobile/android/android-components/samples/browser/src')
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 Binary files differnew file mode 100644 index 0000000000..455b15fc84 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png 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 Binary files differnew file mode 100644 index 0000000000..cdc89f3dee --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..84ef408f6a --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 0000000000..e6df10d76b --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 0000000000..c01ea2a106 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..0a810a25a3 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 0000000000..3909d6df1e --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 0000000000..40a7e0cc99 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..15b39d10eb --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 0000000000..190b2d260a --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 0000000000..0e9b1e4e1b --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..7b7aa5dfdb --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 0000000000..6ad22ca834 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 0000000000..73759f1a06 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..646c51a8ae --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 0000000000..e249dfc1d3 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png 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) |