diff options
Diffstat (limited to 'mobile/android/android-components/samples')
315 files changed, 11859 insertions, 0 deletions
diff --git a/mobile/android/android-components/samples/browser/.gitignore b/mobile/android/android-components/samples/browser/.gitignore new file mode 100644 index 0000000000..af6eaebcd7 --- /dev/null +++ b/mobile/android/android-components/samples/browser/.gitignore @@ -0,0 +1,2 @@ +/build +manifest.json diff --git a/mobile/android/android-components/samples/browser/README.md b/mobile/android/android-components/samples/browser/README.md new file mode 100644 index 0000000000..d64d19d181 --- /dev/null +++ b/mobile/android/android-components/samples/browser/README.md @@ -0,0 +1,35 @@ +# [Android Components](../../README.md) > Samples > Browser + +![](src/main/res/mipmap-xhdpi/ic_launcher.png) + +A simple browser app that is composed from the browser components in this repository. + +⚠️ **Note**: This sample application is only a very basic browser. For a full-featured reference browser implementation see the **[reference-browser repository](https://github.com/mozilla-mobile/reference-browser)**. + +## Build variants + +The browser app uses a product flavor: + +* **channel**: Using different release channels of GeckoView: _nightly_, _beta_, _production_. In most cases you want to use the _nightly_ flavor as this will support all of the latest functionality. + +## Glean SDK support + +This sample application comes with Glean SDK telemetry initialized by default, but with upload disabled (no data is being sent). +This is for creating a simpler metric testing workflow for Gecko engineers that need to add their metrics to Gecko and expose them to Mozilla mobile products. +See [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1592935) for more context. + +In order to enable data upload for testing purposes, change the `Glean.setUploadEnabled(false)` to `Glean.setUploadEnabled(true)` in [`SampleApplication.kt`](src/main/java/org/mozilla/samples/browser/SampleApplication.kt). + +Glean will send metrics from any Glean-enabled component used in this sample application: + +- [engine-gecko-nightly](https://github.com/mozilla-mobile/android-components/blob/main/components/browser/engine-gecko-nightly/docs/metrics.md); +- [engine-gecko-beta](https://github.com/mozilla-mobile/android-components/blob/main/components/browser/engine-gecko-beta/docs/metrics.md); +- [engine-gecko](https://github.com/mozilla-mobile/android-components/blob/main/components/browser/engine-gecko/docs/metrics.md); + +Data review for enabling the Glean SDK for this application can be found [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1592935#c6). + +## License + + 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/ diff --git a/mobile/android/android-components/samples/browser/build.gradle b/mobile/android/android-components/samples/browser/build.gradle new file mode 100644 index 0000000000..351becf85b --- /dev/null +++ b/mobile/android/android-components/samples/browser/build.gradle @@ -0,0 +1,186 @@ +/* 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/. */ + +if (findProject(":geckoview") != null) { + buildDir "${topobjdir}/gradle/build/mobile/android/samples-browser" +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +if (findProject(":geckoview") != null) { + apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle" +} + +android { + defaultConfig { + applicationId "org.mozilla.samples.browser" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArgument "clearPackageData", "true" + testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + if (findProject(":geckoview") != null) { + project.configureProductFlavors.delegate = it + project.configureProductFlavors() + } + + flavorDimensions += "engine" + + productFlavors { + gecko { + dimension "engine" + } + + // WebView + system { + dimension "engine" + } + } + + variantFilter { variant -> + if (variant.buildType.name == "release") { + // This is a sample app that we are not releasing. Save some time and do not build + // release versions. + setIgnore(true) + } + } + + buildFeatures { + viewBinding true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose_compiler + } + + namespace 'org.mozilla.samples.browser' +} + +tasks.register("updateBorderifyExtensionVersion", Copy) { task -> + updateExtensionVersion(task, 'src/main/assets/extensions/borderify') +} + +tasks.register("updateTestExtensionVersion", Copy) { task -> + updateExtensionVersion(task, 'src/main/assets/extensions/test') +} + +dependencies { + implementation platform(ComponentsDependencies.androidx_compose_bom) + implementation project(':concept-awesomebar') + implementation project(':concept-fetch') + implementation project(':concept-engine') + implementation project(':concept-tabstray') + implementation project(':concept-toolbar') + implementation project(':concept-storage') + implementation project(':concept-base') + + implementation project(':compose-awesomebar') + + implementation project(':browser-engine-system') + implementation project(':browser-domains') + implementation project(':browser-icons') + implementation project(':browser-session-storage') + implementation project(':browser-state') + implementation project(':browser-tabstray') + implementation project(':browser-thumbnails') + implementation project(':browser-toolbar') + implementation project(':browser-menu') + implementation project(':browser-storage-sync') + + implementation project(':lib-fetch-httpurlconnection') + implementation project(":lib-crash") + implementation project(':lib-dataprotect') + implementation project(":lib-publicsuffixlist") + + implementation project(':feature-awesomebar') + implementation project(":feature-autofill") + implementation project(':feature-app-links') + implementation project(':feature-contextmenu') + implementation project(':feature-customtabs') + implementation project(':feature-downloads') + implementation project(':feature-intent') + implementation project(':feature-media') + implementation project(':feature-readerview') + implementation project(':feature-search') + implementation project(':feature-session') + implementation project(':feature-toolbar') + implementation project(':feature-tabs') + implementation project(':feature-prompts') + implementation project(':feature-privatemode') + implementation project(':feature-pwa') + implementation project(':feature-findinpage') + implementation project(':feature-sitepermissions') + implementation project(':feature-webcompat') + implementation project(':feature-webcompat-reporter') + implementation project(':feature-webnotifications') + implementation project(':feature-addons') + + implementation project(':ui-autocomplete') + implementation project(':ui-tabcounter') + implementation project(':ui-widgets') + + // Add a dependency on service-glean to simplify the testing workflow + // for engineers that want to test Gecko metrics exfiltrated via the Glean + // SDK. See bug 1592935 for more context. + implementation project(':service-glean') + implementation project(':service-location') + implementation project(':service-digitalassetlinks') + implementation project(':service-sync-logins') + + implementation project(':support-base') + implementation project(':support-locale') + implementation project(':support-utils') + implementation project(':support-ktx') + implementation project(':support-webextensions') + implementation project(':support-rustlog') + + geckoImplementation project(':browser-engine-gecko') + + implementation ComponentsDependencies.google_material + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_compose_ui_tooling + implementation ComponentsDependencies.androidx_compose_foundation + implementation ComponentsDependencies.androidx_compose_material + implementation ComponentsDependencies.androidx_core_ktx + implementation ComponentsDependencies.androidx_constraintlayout + implementation ComponentsDependencies.androidx_swiperefreshlayout + implementation ComponentsDependencies.androidx_localbroadcastmanager + + debugImplementation ComponentsDependencies.leakcanary + + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_robolectric + testImplementation ComponentsDependencies.testing_mockito + testImplementation ComponentsDependencies.testing_coroutines + + androidTestImplementation project(':support-android-test') + androidTestImplementation ComponentsDependencies.androidx_test_core + androidTestImplementation ComponentsDependencies.androidx_test_runner + androidTestImplementation ComponentsDependencies.androidx_test_rules + androidTestImplementation ComponentsDependencies.androidx_test_junit + androidTestImplementation ComponentsDependencies.androidx_test_uiautomator + androidTestImplementation ComponentsDependencies.androidx_espresso_core + androidTestImplementation ComponentsDependencies.testing_leakcanary + androidTestImplementation ComponentsDependencies.testing_mockwebserver +} + +preBuild.dependsOn updateBorderifyExtensionVersion +preBuild.dependsOn updateTestExtensionVersion diff --git a/mobile/android/android-components/samples/browser/proguard-rules.pro b/mobile/android/android-components/samples/browser/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/browser/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile 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) diff --git a/mobile/android/android-components/samples/compose-browser/.gitignore b/mobile/android/android-components/samples/compose-browser/.gitignore new file mode 100644 index 0000000000..af6eaebcd7 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/.gitignore @@ -0,0 +1,2 @@ +/build +manifest.json diff --git a/mobile/android/android-components/samples/compose-browser/README.md b/mobile/android/android-components/samples/compose-browser/README.md new file mode 100644 index 0000000000..a8cd90148e --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/README.md @@ -0,0 +1,35 @@ +# [Android Components](../../README.md) > Samples > Browser + +![](src/main/res/mipmap-xhdpi/ic_launcher.png) + +A simple browser app that is composed from the browser components in this repository. + +⚠️ **Note**: This sample application is only a very basic browser. For a full-featured reference browser implementation see the **[reference-browser repository](https://github.com/mozilla-mobile/reference-browser)**. + +## Build variants + +The browser app uses a product flavor: + +* **channel**: Using different release channels of GeckoView: _nightly_, _beta_, _production_. In most cases you want to use the _nightly_ flavor as this will support all of the latest functionality. + +## Glean SDK support + +This sample application comes with Glean SDK telemetry initialized by default, but with upload disabled (no data is being sent). +This is for creating a simpler metric testing workflow for Gecko engineers that need to add their metrics to Gecko and expose them to Mozilla mobile products. +See [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1592935) for more context. + +In order to enable data upload for testing purposes, change the `Glean.setUploadEnabled(false)` to `Glean.setUploadEnabled(true)` in [`SampleApplication.kt`](src/main/java/org/mozilla/samples/browser/SampleApplication.kt). + +Glean will send metrics from any Glean-enabled component used in this sample application: + +- [engine-gecko-nightly](https://github.com/mozilla-mobile/android-components/blob/master/components/browser/engine-gecko-nightly/docs/metrics.md); +- [engine-gecko-beta](https://github.com/mozilla-mobile/android-components/blob/master/components/browser/engine-gecko-beta/docs/metrics.md); +- [engine-gecko](https://github.com/mozilla-mobile/android-components/blob/master/components/browser/engine-gecko/docs/metrics.md); + +Data review for enabling the Glean SDK for this application can be found [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1592935#c6). + +## License + + 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/ diff --git a/mobile/android/android-components/samples/compose-browser/build.gradle b/mobile/android/android-components/samples/compose-browser/build.gradle new file mode 100644 index 0000000000..1dd2422ec2 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/build.gradle @@ -0,0 +1,86 @@ +/* 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/. */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' + +android { + defaultConfig { + applicationId "org.mozilla.samples.compose.browser" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArgument "clearPackageData", "true" + testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose_compiler + } + + namespace 'org.mozilla.samples.compose.browser' +} + +tasks.register("updateBorderifyExtensionVersion", Copy) { task -> + updateExtensionVersion(task, 'src/main/assets/extensions/borderify') +} + +tasks.register("updateTestExtensionVersion", Copy) { task -> + updateExtensionVersion(task, 'src/main/assets/extensions/test') +} + +dependencies { + implementation platform(ComponentsDependencies.androidx_compose_bom) + implementation project(':concept-engine') + implementation project(':concept-awesomebar') + implementation project(':concept-tabstray') + + implementation project(':browser-engine-gecko') + implementation project(':browser-state') + implementation project(':browser-icons') + + implementation project(':compose-awesomebar') + implementation project(':compose-browser-toolbar') + implementation project(':compose-engine') + implementation project(':compose-tabstray') + + implementation project(':feature-awesomebar') + implementation project(':feature-fxsuggest') + implementation project(':feature-search') + implementation project(':feature-session') + implementation project(':feature-tabs') + + implementation project(':service-location') + implementation project(':support-rusthttp') + + implementation project(':ui-icons') + + implementation ComponentsDependencies.androidx_activity_compose + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_core_ktx + implementation ComponentsDependencies.androidx_compose_ui + implementation ComponentsDependencies.androidx_compose_ui_tooling + implementation ComponentsDependencies.androidx_compose_foundation + implementation ComponentsDependencies.androidx_compose_material + implementation ComponentsDependencies.androidx_compose_navigation +} + +preBuild.dependsOn updateBorderifyExtensionVersion +preBuild.dependsOn updateTestExtensionVersion diff --git a/mobile/android/android-components/samples/compose-browser/proguard-rules.pro b/mobile/android/android-components/samples/compose-browser/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/compose-browser/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/compose-browser/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..cb77ad9280 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ +<?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"/> + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + + <application + android:allowBackup="false" + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/Theme.AppCompat.DayNight.NoActionBar" + android:name=".BrowserApplication" + android:usesCleartextTraffic="true" + tools:ignore="DataExtractionRules,UnusedAttribute" + android:dataExtractionRules="@xml/data_extraction_rules"> + <activity android:name=".BrowserComposeActivity" + android:launchMode="singleTask" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> + +</manifest> diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserApplication.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserApplication.kt new file mode 100644 index 0000000000..c4b13b1e3e --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserApplication.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.compose.browser + +import android.app.Application +import mozilla.appservices.Megazord +import mozilla.components.feature.fxsuggest.GlobalFxSuggestDependencyProvider +import mozilla.components.support.rusthttp.RustHttpConfig + +/** + * The global [Application] class of this browser application. + */ +class BrowserApplication : Application() { + val components by lazy { Components(this) } + + override fun onCreate() { + super.onCreate() + + Megazord.init() + RustHttpConfig.setClient(lazy { components.client }) + + GlobalFxSuggestDependencyProvider.initialize(components.fxSuggestStorage) + } +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserComposeActivity.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserComposeActivity.kt new file mode 100644 index 0000000000..1b46e4f2af --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserComposeActivity.kt @@ -0,0 +1,43 @@ +/* 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.compose.browser + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.MaterialTheme +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.mozilla.samples.compose.browser.browser.BrowserScreen +import org.mozilla.samples.compose.browser.ext.components +import org.mozilla.samples.compose.browser.settings.SettingsScreen + +/** + * Ladies and gentleman, the browser. ¯\_(ツ)_/¯ + */ +class BrowserComposeActivity : AppCompatActivity() { + companion object { + const val ROUTE_BROWSER = "browser" + const val ROUTE_SETTINGS = "settings" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + val navController = rememberNavController() + + MaterialTheme { + NavHost(navController, startDestination = ROUTE_BROWSER) { + composable(ROUTE_BROWSER) { BrowserScreen(navController) } + composable(ROUTE_SETTINGS) { SettingsScreen() } + } + } + } + + components.fxSuggestIngestionScheduler.startPeriodicIngestion() + } +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/Components.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/Components.kt new file mode 100644 index 0000000000..874b4081de --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/Components.kt @@ -0,0 +1,70 @@ +/* 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.compose.browser + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import mozilla.components.browser.engine.gecko.GeckoEngine +import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient +import mozilla.components.browser.state.engine.EngineMiddleware +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.fetch.Client +import mozilla.components.feature.fxsuggest.FxSuggestIngestionScheduler +import mozilla.components.feature.fxsuggest.FxSuggestStorage +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.SessionUseCases +import mozilla.components.feature.tabs.TabsUseCases +import mozilla.components.service.location.LocationService +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.samples.compose.browser.app.AppStore + +/** + * Global components of the sample browser. + */ +class Components( + context: Context, +) { + private val runtime by lazy { GeckoRuntime.create(context) } + + val engine: Engine by lazy { GeckoEngine(context, runtime = runtime) } + val client: Client by lazy { GeckoViewFetchClient(context, runtime = runtime) } + + val store: BrowserStore by lazy { + BrowserStore( + middleware = listOf( + RegionMiddleware(context, locationService), + SearchMiddleware(context), + ) + EngineMiddleware.create(engine), + ) + } + + val appStore: AppStore by lazy { AppStore() } + + val sessionUseCases by lazy { SessionUseCases(store) } + val tabsUseCases by lazy { TabsUseCases(store) } + val searchUseCases by lazy { SearchUseCases(store, tabsUseCases, sessionUseCases) } + + val locationService by lazy { LocationService.default() } + + val fxSuggestStorage: FxSuggestStorage by lazy { + FxSuggestStorage(context) + } + + val fxSuggestIngestionScheduler: FxSuggestIngestionScheduler by lazy { + FxSuggestIngestionScheduler(context) + } +} + +/** + * Returns the global [Components] object from within a `@Composable` context. + */ +@Composable +fun components(): Components { + return (LocalContext.current.applicationContext as BrowserApplication).components +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppAction.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppAction.kt new file mode 100644 index 0000000000..cf36ac991b --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppAction.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.compose.browser.app + +import mozilla.components.lib.state.Action + +/** + * Actions for updating the global [AppState] via [AppStore]. + */ +sealed class AppAction : Action { + /** + * Toggles the theme of the app (only for testing purposes). + */ + object ToggleTheme : AppAction() +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppState.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppState.kt new file mode 100644 index 0000000000..163dddb8ec --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppState.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.compose.browser.app + +import mozilla.components.lib.state.State + +/** + * Global state the browser is in (regardless of the currently displayed screen). + */ +data class AppState( + val theme: Int = 1, +) : State diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppStore.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppStore.kt new file mode 100644 index 0000000000..d75e5f3787 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppStore.kt @@ -0,0 +1,22 @@ +/* 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.compose.browser.app + +import mozilla.components.lib.state.Store + +/** + * [Store] for the global [AppState]. + */ +class AppStore : Store<AppState, AppAction>( + initialState = AppState(), + reducer = ::reduce, +) + +private fun reduce(appState: AppState, appAction: AppAction): AppState { + if (appAction is AppAction.ToggleTheme) { + return appState.copy(theme = (appState.theme + 1) % 2) + } + return appState +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreen.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreen.kt new file mode 100644 index 0000000000..2e5a66312d --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreen.kt @@ -0,0 +1,237 @@ +/* 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.compose.browser.browser + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Button +import androidx.compose.material.ContentAlpha +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.navigation.NavController +import mozilla.components.browser.state.helper.Target +import mozilla.components.compose.browser.awesomebar.AwesomeBar +import mozilla.components.compose.browser.toolbar.BrowserToolbar +import mozilla.components.compose.engine.WebContent +import mozilla.components.compose.tabstray.TabCounterButton +import mozilla.components.compose.tabstray.TabList +import mozilla.components.concept.awesomebar.AwesomeBar +import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider +import mozilla.components.feature.awesomebar.provider.SearchActionProvider +import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider +import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider +import mozilla.components.feature.fxsuggest.FxSuggestSuggestionProvider +import mozilla.components.lib.state.Store +import mozilla.components.lib.state.ext.composableStore +import mozilla.components.lib.state.ext.observeAsComposableState +import org.mozilla.samples.compose.browser.BrowserComposeActivity.Companion.ROUTE_SETTINGS +import org.mozilla.samples.compose.browser.components + +/** + * The main browser screen. + */ +@Composable +fun BrowserScreen(navController: NavController) { + val target = Target.SelectedTab + + val store = composableStore<BrowserScreenState, BrowserScreenAction> { restoredState -> + BrowserScreenStore(restoredState ?: BrowserScreenState()) + } + + val editState = store.observeAsComposableState { state -> state.editMode } + val editUrl = store.observeAsComposableState { state -> state.editText } + val loadUrl = components().sessionUseCases.loadUrl + val showTabs = store.observeAsComposableState { state -> state.showTabs } + + BackHandler(enabled = editState.value == true) { + store.dispatch(BrowserScreenAction.ToggleEditMode(false)) + } + + Box { + Column { + BrowserToolbar( + components().store, + target, + editMode = editState.value!!, + onDisplayMenuClicked = { + navController.navigate(ROUTE_SETTINGS) + }, + onTextCommit = { text -> + store.dispatch(BrowserScreenAction.ToggleEditMode(false)) + loadUrl(text) + }, + onTextEdit = { text -> store.dispatch(BrowserScreenAction.UpdateEditText(text)) }, + onDisplayToolbarClick = { + store.dispatch(BrowserScreenAction.ToggleEditMode(true)) + }, + editText = editUrl.value, + hint = "Search or enter address", + browserActions = { + TabCounterButton( + components().store, + onClicked = { store.dispatch(BrowserScreenAction.ShowTabs) }, + ) + }, + ) + + Box { + WebContent( + components().engine, + components().store, + Target.SelectedTab, + ) + + val url = editUrl.value + if (editState.value == true && url != null) { + Suggestions( + url, + onSuggestionClicked = { suggestion -> + store.dispatch(BrowserScreenAction.ToggleEditMode(false)) + suggestion.onSuggestionClicked?.invoke() + }, + onAutoComplete = { suggestion -> + store.dispatch(BrowserScreenAction.UpdateEditText(suggestion.editSuggestion!!)) + }, + ) + } + } + } + + if (showTabs.value == true) { + TabsTray(store) + } + } +} + +/** + * Shows the lit of tabs. + */ +@Composable +fun TabsTray( + store: Store<BrowserScreenState, BrowserScreenAction>, +) { + val components = components() + + BackHandler(onBack = { store.dispatch(BrowserScreenAction.HideTabs) }) + + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .background(Color.Black.copy(alpha = ContentAlpha.medium)) + .clickable { + store.dispatch(BrowserScreenAction.HideTabs) + }, + ) { + Column( + modifier = Modifier + .fillMaxHeight(fraction = 0.8f) + .align(Alignment.BottomStart), + ) { + TabList( + store = components().store, + onTabSelected = { tab -> + components.tabsUseCases.selectTab(tab.id) + store.dispatch(BrowserScreenAction.HideTabs) + }, + onTabClosed = { tab -> + components.tabsUseCases.removeTab(tab.id) + }, + modifier = Modifier.weight(1f), + ) + Button( + onClick = { + components.tabsUseCases.addTab( + url = "about:blank", + selectTab = true, + ) + store.dispatch(BrowserScreenAction.HideTabs) + store.dispatch(BrowserScreenAction.ToggleEditMode(true)) + }, + ) { + Text("+") + } + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun Suggestions( + url: String, + onSuggestionClicked: (AwesomeBar.Suggestion) -> Unit, + onAutoComplete: (AwesomeBar.Suggestion) -> Unit, +) { + val context = LocalContext.current + val components = components() + + val sessionSuggestionProvider = remember(context) { + SessionSuggestionProvider( + context.resources, + components.store, + components.tabsUseCases.selectTab, + ) + } + + val searchActionProvider = remember { + SearchActionProvider(components.store, components.searchUseCases.defaultSearch) + } + + val fxSuggestSuggestionProvider = remember(context) { + FxSuggestSuggestionProvider( + context.resources, + loadUrlUseCase = components.sessionUseCases.loadUrl, + includeSponsoredSuggestions = false, + includeNonSponsoredSuggestions = true, + ) + } + + val searchSuggestionProvider = remember(context) { + SearchSuggestionProvider( + context, + components.store, + components.searchUseCases.defaultSearch, + components.client, + mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS, + engine = components.engine, + filterExactMatch = true, + ) + } + + val clipboardSuggestionProvider = remember(context) { + ClipboardSuggestionProvider( + context, + components.sessionUseCases.loadUrl, + ) + } + + val keyboardController = LocalSoftwareKeyboardController.current + + AwesomeBar( + url, + providers = listOf( + sessionSuggestionProvider, + searchActionProvider, + fxSuggestSuggestionProvider, + searchSuggestionProvider, + clipboardSuggestionProvider, + ), + onSuggestionClicked = { suggestion -> onSuggestionClicked(suggestion) }, + onAutoComplete = { suggestion -> onAutoComplete(suggestion) }, + onScroll = { keyboardController?.hide() }, + ) +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenAction.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenAction.kt new file mode 100644 index 0000000000..c7da237d5a --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenAction.kt @@ -0,0 +1,32 @@ +/* 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.compose.browser.browser + +import mozilla.components.lib.state.Action + +/** + * Actions for updating the [BrowserScreenState] via [BrowserScreenStore]. + */ +sealed class BrowserScreenAction : Action { + /** + * Updates whether the toolbar is in "display" or "edit" mode. + */ + data class ToggleEditMode(val editMode: Boolean) : BrowserScreenAction() + + /** + * Updates the text of the toolbar that is currently being edited (in "edit" mode). + */ + data class UpdateEditText(val text: String) : BrowserScreenAction() + + /** + * Shows the list of tabs on top of the web content. + */ + object ShowTabs : BrowserScreenAction() + + /** + * Hides the list of tabs. + */ + object HideTabs : BrowserScreenAction() +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenState.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenState.kt new file mode 100644 index 0000000000..d3bae7578d --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenState.kt @@ -0,0 +1,22 @@ +/* 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.compose.browser.browser + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import mozilla.components.lib.state.State + +/** + * The state the browser screen is in. + * + * @param editMode Whether the toolbar is in "edit" or "display" mode. + * @param editText The text in the toolbar that is being edited by the user. + */ +@Parcelize +data class BrowserScreenState( + val editMode: Boolean = false, + val editText: String? = null, + val showTabs: Boolean = false, +) : State, Parcelable diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenStore.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenStore.kt new file mode 100644 index 0000000000..6d7dcbb3ac --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenStore.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.compose.browser.browser + +import mozilla.components.lib.state.Store + +/** + * [Store] for maintaining the state of the browser screen. + */ +class BrowserScreenStore( + initialState: BrowserScreenState = BrowserScreenState(), +) : Store<BrowserScreenState, BrowserScreenAction>( + initialState = initialState, + reducer = ::reduce, +) + +private fun reduce(state: BrowserScreenState, action: BrowserScreenAction): BrowserScreenState { + return when (action) { + is BrowserScreenAction.ToggleEditMode -> state.copy( + editMode = action.editMode, + editText = if (action.editMode) null else state.editText, + ) + is BrowserScreenAction.UpdateEditText -> state.copy(editText = action.text) + is BrowserScreenAction.ShowTabs -> state.copy(showTabs = true) + is BrowserScreenAction.HideTabs -> state.copy(showTabs = false) + } +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/ext/Context.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/ext/Context.kt new file mode 100644 index 0000000000..a847394795 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/ext/Context.kt @@ -0,0 +1,15 @@ +/* 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.compose.browser.ext + +import android.content.Context +import org.mozilla.samples.compose.browser.BrowserApplication +import org.mozilla.samples.compose.browser.Components + +val Context.application: BrowserApplication + get() = applicationContext as BrowserApplication + +val Context.components: Components + get() = application.components diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/settings/SettingsScreen.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/settings/SettingsScreen.kt new file mode 100644 index 0000000000..0e83658e7a --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/settings/SettingsScreen.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.compose.browser.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Text +import androidx.compose.runtime.Composable + +/** + * Screen displaying the settings of the browser. + */ +@Composable +fun SettingsScreen() { + Column { + Text("Settings") + } +} diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_background.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..61f5b8183f --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,78 @@ +<?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 + android:height="108dp" + android:width="108dp" + android:viewportHeight="108" + android:viewportWidth="108" + xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#3DDC84" + android:pathData="M0,0h108v108h-108z"/> + <path android:fillColor="#00000000" android:pathData="M9,0L9,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,0L19,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M29,0L29,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M39,0L39,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M49,0L49,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M59,0L59,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M69,0L69,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M79,0L79,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M89,0L89,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M99,0L99,108" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,9L108,9" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,19L108,19" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,29L108,29" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,39L108,39" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,49L108,49" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,59L108,59" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,69L108,69" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,79L108,79" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,89L108,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M0,99L108,99" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,29L89,29" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,39L89,39" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,49L89,49" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,59L89,59" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,69L89,69" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M19,79L89,79" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M29,19L29,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M39,19L39,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M49,19L49,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M59,19L59,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M69,19L69,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> + <path android:fillColor="#00000000" android:pathData="M79,19L79,89" + android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> +</vector> diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_foreground.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..8a7cd0aa55 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_foreground.xml @@ -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/. --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108" + android:tint="#FFFFFF"> + <group android:scaleX="2.9232" + android:scaleY="2.9232" + android:translateX="18.9216" + android:translateY="18.9216"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/> + </group> +</vector> diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..c7743a9582 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher.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/. --> + +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background"/> + <foreground android:drawable="@drawable/ic_launcher_foreground"/> +</adaptive-icon>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..c7743a9582 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.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/. --> + +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background"/> + <foreground android:drawable="@drawable/ic_launcher_foreground"/> +</adaptive-icon>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..3782c0799b --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..8b8c8e4041 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..7a42b483fd --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..3ff74c1d9d --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..497337793e --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..f1c35726b8 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..cd006d2f57 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..9db209fcbc --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..3ca4e817f7 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..0d5a5a266a --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/values/strings.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/values/strings.xml new file mode 100644 index 0000000000..2441d14625 --- /dev/null +++ b/mobile/android/android-components/samples/compose-browser/src/main/res/values/strings.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> + <string name="app_name">Compose Browser</string> +</resources>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..55da967560 --- /dev/null +++ b/mobile/android/android-components/samples/compose-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/crash/build.gradle b/mobile/android/android-components/samples/crash/build.gradle new file mode 100644 index 0000000000..9ea07c4d9f --- /dev/null +++ b/mobile/android/android-components/samples/crash/build.gradle @@ -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/. */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + applicationId "org.mozilla.samples.crash" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + viewBinding true + } + + namespace 'org.mozilla.samples.crash' +} + + +dependencies { + implementation project(':lib-crash') + implementation project(':lib-fetch-httpurlconnection') + implementation project(':service-glean') + implementation project(':support-base') + implementation project(':support-utils') + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.google_material + implementation ComponentsDependencies.androidx_recyclerview +} diff --git a/mobile/android/android-components/samples/crash/lint.xml b/mobile/android/android-components/samples/crash/lint.xml new file mode 100644 index 0000000000..33cf423701 --- /dev/null +++ b/mobile/android/android-components/samples/crash/lint.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/. --> +<lint> + <issue id="IconMissingDensityFolder" severity="ignore"> + <!-- Suppress lint warnings on mdpi --> + <ignore path="src/debug/res/drawable-mdpi"/> + </issue> + + <issue id="GoogleAppIndexingWarning" severity="ignore" /> +</lint>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/crash/proguard-rules.pro b/mobile/android/android-components/samples/crash/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/crash/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/crash/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/crash/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..29c01303bd --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ +<?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.INTERNET" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> + + <application + android:name="org.mozilla.samples.crash.CrashApplication" + 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" + android:dataExtractionRules="@xml/data_extraction_rules" + tools:targetApi="s" + tools:ignore="DataExtractionRules"> + <activity android:name="org.mozilla.samples.crash.CrashActivity" + android:launchMode="singleTask" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + <activity android:name="org.mozilla.samples.crash.CrashListActivity" android:exported="false" /> + + <service android:name="org.mozilla.samples.crash.CrashService" + android:foregroundServiceType="specialUse" + android:process=":samples.crash.service" /> + </application> +</manifest> diff --git a/mobile/android/android-components/samples/crash/src/main/ic_launcher-web.png b/mobile/android/android-components/samples/crash/src/main/ic_launcher-web.png Binary files differnew file mode 100644 index 0000000000..b8f772f66a --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/ic_launcher-web.png diff --git a/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashActivity.kt b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashActivity.kt new file mode 100644 index 0000000000..2bcce48acc --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashActivity.kt @@ -0,0 +1,168 @@ +/* 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.crash + +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import com.google.android.material.snackbar.Snackbar +import mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.lib.crash.Crash +import mozilla.components.support.utils.ext.registerReceiverCompat +import org.mozilla.samples.crash.databinding.ActivityCrashBinding + +class CrashActivity : AppCompatActivity(), View.OnClickListener { + private val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (!Crash.isCrashIntent(intent)) { + return + } + + val crash = Crash.fromIntent(intent) + + Snackbar.make( + findViewById(android.R.id.content), + "Sorry. We crashed.", + Snackbar.LENGTH_LONG, + ) + .setAction("Report") { crashReporter.submitReport(crash) } + .show() + } + } + private lateinit var binding: ActivityCrashBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityCrashBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.fatalCrashButton.setOnClickListener(this) + binding.crashButton.setOnClickListener(this) + binding.fatalServiceCrashButton.setOnClickListener(this) + binding.crashList.setOnClickListener(this) + + crashReporter.recordCrashBreadcrumb( + Breadcrumb( + "CrashActivity onCreate", + emptyMap(), + "sample", + Breadcrumb.Level.DEBUG, + Breadcrumb.Type.NAVIGATION, + ), + ) + } + + override fun onResume() { + super.onResume() + + registerReceiverCompat( + receiver, + IntentFilter(CrashApplication.NON_FATAL_CRASH_BROADCAST), + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + + crashReporter.recordCrashBreadcrumb( + Breadcrumb( + "CrashActivity onResume", + emptyMap(), + "sample", + Breadcrumb.Level.DEBUG, + Breadcrumb.Type.NAVIGATION, + ), + ) + } + + override fun onPause() { + super.onPause() + unregisterReceiver(receiver) + + crashReporter.recordCrashBreadcrumb( + Breadcrumb( + "CrashActivity onPause", + emptyMap(), + "sample", + Breadcrumb.Level.DEBUG, + Breadcrumb.Type.NAVIGATION, + ), + ) + } + + @Suppress("TooGenericExceptionThrown") + override fun onClick(view: View) { + when (view) { + binding.fatalCrashButton -> { + crashReporter.recordCrashBreadcrumb( + Breadcrumb( + "fatal crash button clicked", + emptyMap(), + "sample", + Breadcrumb.Level.INFO, + Breadcrumb.Type.USER, + ), + ) + + throw RuntimeException("Boom!") + } + + binding.crashButton -> { + crashReporter.recordCrashBreadcrumb( + Breadcrumb( + "crash button clicked", + emptyMap(), + "sample", + Breadcrumb.Level.INFO, + Breadcrumb.Type.USER, + ), + ) + + // Pretend GeckoView has crashed by re-building a crash Intent and launching the CrashHandlerService. + val intent = Intent("org.mozilla.gecko.ACTION_CRASHED") + intent.component = ComponentName( + packageName, + "mozilla.components.lib.crash.handler.CrashHandlerService", + ) + intent.putExtra( + "minidumpPath", + "${filesDir.path}/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp", + ) + intent.putExtra("fatal", false) + intent.putExtra( + "extrasPath", + "${filesDir.path}/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra", + ) + intent.putExtra("minidumpSuccess", true) + + ContextCompat.startForegroundService(this, intent) + } + + binding.fatalServiceCrashButton -> { + crashReporter.recordCrashBreadcrumb( + Breadcrumb( + "fatal service crash button clicked", + emptyMap(), + "sample", + Breadcrumb.Level.INFO, + Breadcrumb.Type.USER, + ), + ) + + startService(Intent(this, CrashService::class.java)) + finish() + } + + binding.crashList -> { + startActivity(Intent(this, CrashListActivity::class.java)) + } + + else -> throw java.lang.RuntimeException("Unknown ID") + } + } +} diff --git a/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashApplication.kt b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashApplication.kt new file mode 100644 index 0000000000..2d50615dd7 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashApplication.kt @@ -0,0 +1,152 @@ +/* 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.crash + +import android.app.Application +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.widget.Toast +import androidx.core.app.NotificationManagerCompat +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.lib.crash.Crash +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.crash.service.CrashReporterService +import mozilla.components.lib.crash.service.GleanCrashReporterService +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.android.NotificationsDelegate +import mozilla.components.support.base.log.Log +import mozilla.components.support.base.log.sink.AndroidLogSink +import mozilla.components.support.utils.PendingIntentUtils +import java.util.Calendar +import java.util.TimeZone +import java.util.UUID + +@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 CrashApplication : Application() { + internal lateinit var crashReporter: CrashReporter + + override fun onCreate() { + super.onCreate() + + // We want the log messages of all builds to go to Android logcat + Log.addSink(AndroidLogSink()) + + val notificationManagerCompat = NotificationManagerCompat.from(applicationContext) + + val notificationsDelegate: NotificationsDelegate by lazy { + NotificationsDelegate( + notificationManagerCompat, + ) + } + + crashReporter = CrashReporter( + context = this, + services = listOf( + createDummyCrashService(this), + ), + telemetryServices = listOf(GleanCrashReporterService(applicationContext)), + shouldPrompt = CrashReporter.Prompt.ALWAYS, + promptConfiguration = CrashReporter.PromptConfiguration( + appName = "Sample App", + organizationName = "Mozilla", + message = "As a private browser, we never save and cannot restore your last browsing session.", + theme = R.style.CrashDialogTheme, + ), + nonFatalCrashIntent = createNonFatalPendingIntent(this), + enabled = true, + notificationsDelegate = notificationsDelegate, + ).install(this) + + // Initialize Glean for recording by the GleanCrashReporterService + val httpClient = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() }) + val config = Configuration(httpClient = httpClient) + Glean.initialize( + applicationContext, + uploadEnabled = true, + configuration = config, + buildInfo = GleanBuildInfo.buildInfo, + ) + } + + companion object { + const val NON_FATAL_CRASH_BROADCAST = "org.mozilla.samples.crash.CRASH" + } +} + +@OptIn(DelicateCoroutinesApi::class) +private fun createDummyCrashService(context: Context): CrashReporterService { + // For this sample we create a dummy service. In a real application this would be an instance of SentryCrashService + // or SocorroCrashService. + return object : CrashReporterService { + override val id: String = "dummy" + + override val name: String = "Dummy" + + override fun createCrashReportUrl(identifier: String): String? { + return "https://example.org/$identifier" + } + + override fun report(crash: Crash.UncaughtExceptionCrash): String? { + GlobalScope.launch(Dispatchers.Main) { + Toast.makeText(context, "Uploading uncaught exception crash...", Toast.LENGTH_SHORT).show() + } + return createDummyId() + } + + override fun report(crash: Crash.NativeCodeCrash): String? { + GlobalScope.launch(Dispatchers.Main) { + Toast.makeText(context, "Uploading native crash...", Toast.LENGTH_SHORT).show() + } + return createDummyId() + } + + override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? { + GlobalScope.launch(Dispatchers.Main) { + Toast.makeText(context, "Uploading caught exception...", Toast.LENGTH_SHORT).show() + } + return createDummyId() + } + + private fun createDummyId(): String { + return "dummy${UUID.randomUUID().toString().hashCode()}" + } + } +} + +private fun createNonFatalPendingIntent(context: Context): PendingIntent { + // The PendingIntent can launch whatever you want - an activity, a service... Here we pick a broadcast. Our main + // activity will listener for the broadcast and show an in-app snackbar to ask the user whether we should send + // this crash report. + return PendingIntent.getBroadcast( + context, + 0, + Intent(CrashApplication.NON_FATAL_CRASH_BROADCAST), + PendingIntentUtils.defaultFlags, + ) +} + +val Context.crashReporter: CrashReporter + get() = (applicationContext as CrashApplication).crashReporter diff --git a/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashListActivity.kt b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashListActivity.kt new file mode 100644 index 0000000000..9a0d4d1d1d --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashListActivity.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.crash + +import android.widget.Toast +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.crash.ui.AbstractCrashListActivity + +/** + * Activity showing list of past crashes. + */ +class CrashListActivity : AbstractCrashListActivity() { + override val crashReporter: CrashReporter + get() = (application as CrashApplication).crashReporter + + override fun onCrashServiceSelected(url: String) { + Toast.makeText(this, "Go to: $url", Toast.LENGTH_SHORT).show() + } +} diff --git a/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashService.kt b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashService.kt new file mode 100644 index 0000000000..270f9d505d --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashService.kt @@ -0,0 +1,82 @@ +/* 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.crash + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.widget.Toast +import androidx.core.app.NotificationCompat +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import mozilla.components.support.base.ids.SharedIdsHelper + +private const val NOTIFICATION_CHANNEL_ID = "mozac.lib.crash.notification" +private const val NOTIFICATION_TAG = "mozac.lib.crash.foreground-service" +private const val DELAY_CRASH_MS = 10000L + +/** + * This service will wait 10 seconds and then crash. We need to wait some time because Android still allows to launch + * an activity from a background service if the app was in the foreground a couple of seconds ago. + */ +class CrashService : Service() { + override fun onBind(intent: Intent?): IBinder? = null + + @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage + @Suppress("TooGenericExceptionThrown") + override fun onCreate() { + Toast.makeText(this, "Crashing from background soonish...", Toast.LENGTH_SHORT).show() + + // We need to put this service into foreground because otherwise Android may kill it (with no visible app UI) + // before we can crash. + startForeground(SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG), createNotification()) + + GlobalScope.launch(Dispatchers.Main) { + delay(DELAY_CRASH_MS) + + throw RuntimeException("Background crash") + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return START_NOT_STICKY + } + + private fun createNotification(): Notification { + val channel = ensureChannelExists() + + return NotificationCompat.Builder(this, channel) + .setContentTitle("Crash Service") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .build() + } + + private fun ensureChannelExists(): String { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager: NotificationManager = getSystemService( + Context.NOTIFICATION_SERVICE, + ) as NotificationManager + + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + "Crash Service", + NotificationManager.IMPORTANCE_DEFAULT, + ) + + notificationManager.createNotificationChannel(channel) + } + + return NOTIFICATION_CHANNEL_ID + } +} diff --git a/mobile/android/android-components/samples/crash/src/main/res/layout/activity_crash.xml b/mobile/android/android-components/samples/crash/src/main/res/layout/activity_crash.xml new file mode 100644 index 0000000000..4921aa4849 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/layout/activity_crash.xml @@ -0,0 +1,40 @@ +<?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" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <Button + android:id="@+id/crashButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/crash_nonfatal" + android:textAlignment="center" /> + + <Button + android:id="@+id/fatalCrashButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/crash_fatal" + android:textAlignment="center" /> + + <Button + android:id="@+id/fatalServiceCrashButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/crash_fatal_service" + android:textAlignment="center" /> + + <Button + android:id="@+id/crashList" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/list_of_crashes" + android:textAlignment="center" /> + +</LinearLayout>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..fb7d4e724b --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher.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/. --> + +<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/crash/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..fb7d4e724b --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher_round.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/. --> + +<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/crash/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..506f4c3598 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..361ce175fe --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..302220d5c0 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..777f4f5b51 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..c158a5aa08 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..3f578142cd --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..fd7337a647 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..2233cd4525 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..91bc706ebe --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..7863ad9b3f --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..ea69f75cd7 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..7fdbb4bede --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..6e28647885 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..beccb228ea --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..83a223b8a8 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/crash/src/main/res/values/ic_launcher_background.xml b/mobile/android/android-components/samples/crash/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000000..bce8f507b6 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/values/ic_launcher_background.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/. --> + +<resources> + <color name="ic_launcher_background">#FFF31A</color> +</resources>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/crash/src/main/res/values/strings.xml b/mobile/android/android-components/samples/crash/src/main/res/values/strings.xml new file mode 100644 index 0000000000..ac69e68e60 --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/values/strings.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/. --> +<resources> + <string name="app_name">Crash Sample</string> + + <string name="crash_fatal">Crash (Fatal)</string> + <string name="crash_nonfatal">Crash (Non-Fatal)</string> + <string name="crash_fatal_service">Crash (Fatal; background service)</string> + <string name="list_of_crashes">List of crashes</string> +</resources> diff --git a/mobile/android/android-components/samples/crash/src/main/res/values/styles.xml b/mobile/android/android-components/samples/crash/src/main/res/values/styles.xml new file mode 100644 index 0000000000..bea300304b --- /dev/null +++ b/mobile/android/android-components/samples/crash/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ +<?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> + <!-- inherit from the AppCompat theme --> + <style name="CrashDialogTheme" parent="Theme.Mozac.CrashReporter"> + + <!-- your app branding color for the app bar --> + <item name="colorPrimary">#8BC34A</item> + + <!-- darker variant for the status bar and contextual app bars --> + <item name="colorPrimaryDark">#689F38</item> + + <!-- theme UI controls like checkboxes and text fields --> + <item name="colorAccent">#E040FB</item> + + </style> +</resources>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/crash/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/crash/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..820ae61afa --- /dev/null +++ b/mobile/android/android-components/samples/crash/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/crash/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/crash/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..55da967560 --- /dev/null +++ b/mobile/android/android-components/samples/crash/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/dataprotect/build.gradle b/mobile/android/android-components/samples/dataprotect/build.gradle new file mode 100644 index 0000000000..f9b70a7368 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + applicationId "org.mozilla.samples.dataprotect" + + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + namespace 'org.mozilla.samples.dataprotect' +} + +dependencies { + implementation project(':support-ktx') + implementation project(':lib-dataprotect') + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_recyclerview +} diff --git a/mobile/android/android-components/samples/dataprotect/lint.xml b/mobile/android/android-components/samples/dataprotect/lint.xml new file mode 100644 index 0000000000..33cf423701 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/lint.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/. --> +<lint> + <issue id="IconMissingDensityFolder" severity="ignore"> + <!-- Suppress lint warnings on mdpi --> + <ignore path="src/debug/res/drawable-mdpi"/> + </issue> + + <issue id="GoogleAppIndexingWarning" severity="ignore" /> +</lint>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/dataprotect/proguard-rules.pro b/mobile/android/android-components/samples/dataprotect/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/dataprotect/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/dataprotect/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..72969290ec --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/AndroidManifest.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/. --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <application + android:allowBackup="false" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/AppTheme" + android:dataExtractionRules="@xml/data_extraction_rules" + tools:ignore="DataExtractionRules" + tools:targetApi="s"> + <activity android:name="org.mozilla.samples.dataprotect.MainActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> + +</manifest>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/Constants.kt b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/Constants.kt new file mode 100644 index 0000000000..ab648d4fff --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/Constants.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.dataprotect + +import android.util.Base64 + +object Constants { + const val B64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_PADDING + const val KEYSTORE_LABEL = "samples-dataprotect" +} diff --git a/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/MainActivity.kt b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/MainActivity.kt new file mode 100644 index 0000000000..12e30e638d --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/MainActivity.kt @@ -0,0 +1,44 @@ +/* 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.dataprotect + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.lib.dataprotect.SecureAbove22Preferences + +class MainActivity : AppCompatActivity() { + @Suppress("MagicNumber") + private val itemKeys: List<String> = List(5) { "protected item ${it + 1}" } + + private lateinit var listView: RecyclerView + private lateinit var listAdapter: ProtectedDataAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val prefs = SecureAbove22Preferences(this, "secret-data-storage") + + prepareProtectedData(prefs) + + // setup recycler + listAdapter = ProtectedDataAdapter(prefs, itemKeys) + listView = findViewById(R.id.protecteddata_list) + listView.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(this@MainActivity) + adapter = listAdapter + } + } + + private fun prepareProtectedData(prefs: SecureAbove22Preferences) { + for (datakey in itemKeys) { + val plain = "value for $datakey" + prefs.putString(datakey, plain) + } + } +} diff --git a/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/ProtectedDataAdapter.kt b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/ProtectedDataAdapter.kt new file mode 100644 index 0000000000..366f4a9059 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/ProtectedDataAdapter.kt @@ -0,0 +1,43 @@ +/* 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.dataprotect + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.lib.dataprotect.SecureAbove22Preferences + +class ProtectedDataAdapter( + private val prefs: SecureAbove22Preferences, + private val itemKeys: List<String>, +) : RecyclerView.Adapter<ProtectedDataAdapter.Holder>() { + override fun getItemCount(): Int = itemKeys.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.protecteddata_item, parent, false) + + return Holder(view) + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + val key = itemKeys[position] + var value = prefs.getString(key) + holder.keyView.text = key + holder.valView.text = value + } + + class Holder(val view: View) : RecyclerView.ViewHolder(view) { + var keyView: TextView + var valView: TextView + + init { + keyView = view.findViewById(R.id.protecteddata_item_key_view) + valView = view.findViewById(R.id.protecteddata_item_val_view) + } + } +} diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..772d73c1a8 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,38 @@ +<!-- 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" + xmlns:aapt="http://schemas.android.com/aapt" + android:width="108dp" + android:height="108dp" + android:viewportHeight="108" + android:viewportWidth="108"> + <path + android:fillType="evenOdd" + android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" + android:strokeColor="#00000000" + android:strokeWidth="1"> + <aapt:attr name="android:fillColor"> + <gradient + android:endX="78.5885" + android:endY="90.9159" + android:startX="48.7653" + android:startY="61.0927" + android:type="linear"> + <item + android:color="#44000000" + android:offset="0.0" /> + <item + android:color="#00000000" + android:offset="1.0" /> + </gradient> + </aapt:attr> + </path> + <path + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" + android:strokeColor="#00000000" + android:strokeWidth="1" /> +</vector> diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/drawable/ic_launcher_background.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..84af1fd596 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,174 @@ +<?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="108dp" + android:height="108dp" + android:viewportHeight="108" + android:viewportWidth="108"> + <path + android:fillColor="#26A69A" + android:pathData="M0,0h108v108h-108z" /> + <path + android:fillColor="#00000000" + android:pathData="M9,0L9,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,0L19,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M29,0L29,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M39,0L39,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M49,0L49,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M59,0L59,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M69,0L69,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M79,0L79,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M89,0L89,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M99,0L99,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,9L108,9" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,19L108,19" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,29L108,29" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,39L108,39" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,49L108,49" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,59L108,59" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,69L108,69" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,79L108,79" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,89L108,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,99L108,99" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,29L89,29" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,39L89,39" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,49L89,49" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,59L89,59" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,69L89,69" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,79L89,79" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M29,19L29,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M39,19L39,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M49,19L49,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M59,19L59,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M69,19L69,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M79,19L79,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> +</vector> diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..96d6ab81da --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/layout/activity_main.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/. --> +<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" + tools:context="org.mozilla.samples.dataprotect.MainActivity"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/protecteddata_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" /> + +</LinearLayout>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/layout/protecteddata_item.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/layout/protecteddata_item.xml new file mode 100644 index 0000000000..4ad43964ab --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/layout/protecteddata_item.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/. --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="50sp"> + + <TextView + android:id="@+id/protecteddata_item_key_view" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight=".25" /> + + <TextView + android:id="@+id/protecteddata_item_val_view" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight=".75" /> +</LinearLayout>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..f39d507313 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher.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/. --> + +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..f39d507313 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher_round.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/. --> + +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..a2f5908281 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..1b52399808 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..ff10afd6e1 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..115a4c768a --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..dcd3cd8083 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..459ca609d3 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..8ca12fe024 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..8e19b410a1 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..b824ebdd48 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..4c19a13c23 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/values/colors.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/values/colors.xml new file mode 100644 index 0000000000..3a96673022 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/values/colors.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/. --> +<resources> + <color name="colorPrimary">#3F51B5</color> + <color name="colorPrimaryDark">#303F9F</color> + <color name="colorAccent">#FF4081</color> +</resources> diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/values/strings.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/values/strings.xml new file mode 100644 index 0000000000..2883c42227 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ +<!-- 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">DataProtect Demo App</string> +</resources> diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/values/styles.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/values/styles.xml new file mode 100644 index 0000000000..1f33369bc5 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/src/main/res/values/styles.xml @@ -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/. --> +<resources> + + <!-- Base application theme. --> + <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> + <!-- Customize your theme here. --> + <item name="colorPrimary">@color/colorPrimary</item> + <item name="colorPrimaryDark">@color/colorPrimaryDark</item> + <item name="colorAccent">@color/colorAccent</item> + </style> + +</resources> diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..55da967560 --- /dev/null +++ b/mobile/android/android-components/samples/dataprotect/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/firefox-accounts/.gitignore b/mobile/android/android-components/samples/firefox-accounts/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mobile/android/android-components/samples/firefox-accounts/README.md b/mobile/android/android-components/samples/firefox-accounts/README.md new file mode 100644 index 0000000000..1e81a3c358 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/README.md @@ -0,0 +1,65 @@ +# [Android Components](../../README.md) > Samples > Firefox Accounts (FxA) + +![](src/main/res/mipmap-xhdpi/ic_launcher.png) + +A simple app showcasing the service-firefox-account component. + +## Concepts + +The main concepts shown in the sample app are: + +* Usage of the asynchronous result type `Deferred` +* Setting up a`FirefoxAccount` object, from a previous session or from scratch +* Spawning a custom tab or a WebView to handle the user's authentication flow + +A minimal walkthrough is also provided in the [component README](https://github.com/mozilla-mobile/android-components/tree/main/components/service/firefox-accounts). + +## Setting up the account + +### From a previous session + +`FirefoxAccount` is a representation of the authentication state for the current client. It provides two methods for saving and restoring state: `toJSONString` and `fromJSONString`. + +> The state provided by `toJSONString` should be stored securely, as the credentials inside could in theory let a user stay authenticated forever. + +To restore an account from an existing state in shared preferences: + +```kotlin +// Inside a `launch` or `async` block: +getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).getString(FXA_STATE_KEY, "").let { + FirefoxAccount.fromJSONString(it) +} +``` + +To persist an account's state in shared preferences: + +```kotlin +account.toJSONString().let { + getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).edit().putString(FXA_STATE_KEY, it).apply() +} +``` + +### From scratch + +If no previous auth state was found, we have to create a new one using some default OAuth parameters. Find the hostname, or `CONFIG_URL` for your OAuth provider, then create a `CLIENT_ID` and `REDIRECT_URL` for your application. From there, we can create a `Config` object, and finally our `FirefoxAccount` object: + +```kotlin +val config = Config(CONFIG_URL, CLIENT_ID, REDIRECT_URL) +// Some helpers such as Config.release(CLIENT_ID, REDIRECT_URL) +// are also provided for well-known Firefox Accounts servers. +val account = FirefoxAccount(config) +``` + +## Viewing the web pages + +In order to complete the OAuth flow, the app can spawn a view and capture the code/state parameters in one of three ways: + +* Opening a custom tab, then capturing params via intent filters +* Spawning a WebView with a page load hook +* Spawning an EngineView (WebView/GeckoView) [WIP] + +## License + + 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/ diff --git a/mobile/android/android-components/samples/firefox-accounts/build.gradle b/mobile/android/android-components/samples/firefox-accounts/build.gradle new file mode 100644 index 0000000000..c6a7d616e9 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/build.gradle @@ -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/. */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + applicationId "org.mozilla.samples.fxa" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + splits { + abi { + enable true + reset() + include 'x86', 'arm64-v8a', 'armeabi-v7a' + } + } + + namespace 'org.mozilla.samples.fxa' + +} + +dependencies { + implementation project(':service-firefox-accounts') + implementation project(':feature-qr') + implementation project(':support-base') + implementation project(':support-rustlog') + implementation project(':support-rusthttp') + implementation project(':lib-fetch-httpurlconnection') + + implementation ComponentsDependencies.androidx_constraintlayout + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_browser +} diff --git a/mobile/android/android-components/samples/firefox-accounts/gradle.properties b/mobile/android/android-components/samples/firefox-accounts/gradle.properties new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/gradle.properties diff --git a/mobile/android/android-components/samples/firefox-accounts/proguard-rules.pro b/mobile/android/android-components/samples/firefox-accounts/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..16a41799c4 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ +<?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.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <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" /> + + <application + android:allowBackup="true" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/Theme.AppCompat.Light.DarkActionBar" + android:dataExtractionRules="@xml/data_extraction_rules" + tools:targetApi="s"> + <activity + android:name="org.mozilla.samples.fxa.MainActivity" + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT" /> + <data + android:host="*" + android:scheme="fxaclient" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/LoginFragment.kt b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/LoginFragment.kt new file mode 100644 index 0000000000..2db927dfa8 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/LoginFragment.kt @@ -0,0 +1,107 @@ +/* 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.fxa + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.CookieManager +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.fragment.app.Fragment + +class LoginFragment : Fragment() { + + private lateinit var authUrl: String + private lateinit var redirectUrl: String + private var mWebView: WebView? = null + private var listener: OnLoginCompleteListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + authUrl = it.getString(AUTH_URL)!! + redirectUrl = it.getString(REDIRECT_URL)!! + } + } + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view: View = inflater.inflate(R.layout.fragment_view, container, false) + val webView = view.findViewById<WebView>(R.id.webview) + // Need JS, cookies and localStorage. + webView.settings.domStorageEnabled = true + webView.settings.javaScriptEnabled = true + CookieManager.getInstance().setAcceptCookie(true) + + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + if (url != null && url.startsWith(redirectUrl)) { + val uri = Uri.parse(url) + val code = uri.getQueryParameter("code") + val state = uri.getQueryParameter("state") + if (code != null && state != null) { + listener?.onLoginComplete(code, state, this@LoginFragment) + } + } + + super.onPageStarted(view, url, favicon) + } + } + webView.loadUrl(authUrl) + + mWebView?.destroy() + mWebView = webView + + return view + } + + @Suppress("TooGenericExceptionThrown") + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is OnLoginCompleteListener) { + listener = context + } else { + throw IllegalStateException("$context must implement OnLoginCompleteListener") + } + } + + override fun onDetach() { + super.onDetach() + listener = null + } + + override fun onPause() { + super.onPause() + mWebView?.onPause() + } + + override fun onResume() { + super.onResume() + mWebView?.onResume() + } + + interface OnLoginCompleteListener { + fun onLoginComplete(code: String, state: String, fragment: LoginFragment) + } + + companion object { + const val AUTH_URL = "authUrl" + const val REDIRECT_URL = "redirectUrl" + + fun create(authUrl: String, redirectUrl: String): LoginFragment = + LoginFragment().apply { + arguments = Bundle().apply { + putString(AUTH_URL, authUrl) + putString(REDIRECT_URL, redirectUrl) + } + } + } +} diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/MainActivity.kt b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/MainActivity.kt new file mode 100644 index 0000000000..4103577318 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/MainActivity.kt @@ -0,0 +1,220 @@ +/* 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.fxa + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.CheckBox +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.app.ActivityCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import mozilla.appservices.fxaclient.FxaConfig +import mozilla.appservices.fxaclient.FxaServer +import mozilla.components.concept.sync.Profile +import mozilla.components.feature.qr.QrFeature +import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient +import mozilla.components.service.fxa.FirefoxAccount +import mozilla.components.service.fxa.FxaException +import mozilla.components.support.base.log.Log +import mozilla.components.support.base.log.sink.AndroidLogSink +import mozilla.components.support.rusthttp.RustHttpConfig +import mozilla.components.support.rustlog.RustLog +import kotlin.coroutines.CoroutineContext + +open class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener, CoroutineScope { + private lateinit var account: FirefoxAccount + private var scopesWithoutKeys: Set<String> = setOf("profile") + private var scopesWithKeys: Set<String> = setOf("profile", "https://identity.mozilla.com/apps/oldsync") + private var scopes: Set<String> = scopesWithoutKeys + + private lateinit var qrFeature: QrFeature + + private lateinit var job: Job + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + companion object { + const val CLIENT_ID = "3c49430b43dfba77" + const val CONFIG_URL = "https://accounts.firefox.com" + const val REDIRECT_URL = "$CONFIG_URL/oauth/success/3c49430b43dfba77" + const val FXA_STATE_PREFS_KEY = "fxaAppState" + const val FXA_STATE_KEY = "fxaState" + private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + RustLog.disable() + RustLog.enable() + RustHttpConfig.setClient(lazy { HttpURLConnectionClient() }) + + Log.addSink(AndroidLogSink()) + + setContentView(R.layout.activity_main) + job = Job() + account = initAccount() + + qrFeature = QrFeature( + this, + fragmentManager = supportFragmentManager, + onNeedToRequestPermissions = { permissions -> + ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE_CAMERA_PERMISSIONS) + }, + onScanResult = { pairingUrl -> + launch { + val url = account.beginPairingFlow(pairingUrl, scopes, SampleFxAEntryPoint.HomeMenu) + if (url == null) { + Log.log( + Log.Priority.ERROR, + tag = "mozac-samples-fxa", + message = "Pairing flow failed for $pairingUrl", + ) + return@launch + } + openWebView(url.url) + } + }, + scanMessage = R.string.pair_instructions_message, + ) + + lifecycle.addObserver(qrFeature) + + findViewById<View>(R.id.buttonCustomTabs).setOnClickListener { + launch { + account.beginOAuthFlow(scopes, SampleFxAEntryPoint.HomeMenu)?.let { + openTab(it.url) + } + } + } + + findViewById<View>(R.id.buttonWebView).setOnClickListener { + launch { + account.beginOAuthFlow(scopes, SampleFxAEntryPoint.HomeMenu)?.let { + openWebView(it.url) + } + } + } + + findViewById<View>(R.id.buttonPair).setOnClickListener { + qrFeature.scan() + } + + findViewById<View>(R.id.buttonLogout).setOnClickListener { + getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).edit().putString(FXA_STATE_KEY, "").apply() + val txtView: TextView = findViewById(R.id.txtView) + txtView.text = getString(R.string.logged_out) + } + + findViewById<CheckBox>(R.id.checkboxKeys).setOnCheckedChangeListener { _, isChecked -> + scopes = if (isChecked) scopesWithKeys else scopesWithoutKeys + } + } + + private fun initAccount(): FirefoxAccount { + getAuthenticatedAccount()?.let { + launch { + it.getProfile(true)?.let { profile -> + displayProfile(profile) + } + } + return it + } + + val config = FxaConfig(FxaServer.Custom(CONFIG_URL), CLIENT_ID, REDIRECT_URL) + return FirefoxAccount(config) + } + + override fun onDestroy() { + super.onDestroy() + account.close() + job.cancel() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + val action = intent.action + val data = intent.dataString + + if (Intent.ACTION_VIEW == action && data != null) { + val url = Uri.parse(data) + val code = url.getQueryParameter("code")!! + val state = url.getQueryParameter("state")!! + displayAndPersistProfile(code, state) + } + } + + override fun onLoginComplete(code: String, state: String, fragment: LoginFragment) { + displayAndPersistProfile(code, state) + supportFragmentManager.popBackStack() + } + + private fun getAuthenticatedAccount(): FirefoxAccount? { + val savedJSON = getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).getString(FXA_STATE_KEY, "") + return savedJSON?.let { + try { + FirefoxAccount.fromJSONString(it, null) + } catch (e: FxaException) { + null + } + } + } + + private fun openTab(url: String) { + val customTabsIntent = CustomTabsIntent.Builder() + .setShareState(CustomTabsIntent.SHARE_STATE_ON) + .setShowTitle(true) + .build() + + customTabsIntent.intent.data = Uri.parse(url) + customTabsIntent.launchUrl(this@MainActivity, Uri.parse(url)) + } + + private fun openWebView(url: String) { + supportFragmentManager.beginTransaction().apply { + replace(R.id.container, LoginFragment.create(url, REDIRECT_URL)) + addToBackStack(null) + commit() + } + } + + private fun displayAndPersistProfile(code: String, state: String) { + launch { + account.completeOAuthFlow(code, state) + account.getProfile()?.let { + displayProfile(it) + } + account.toJSONString().let { + getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE) + .edit().putString(FXA_STATE_KEY, it).apply() + } + } + } + + private fun displayProfile(profile: Profile) { + val txtView: TextView = findViewById(R.id.txtView) + txtView.text = getString(R.string.signed_in, "${profile.displayName ?: ""} ${profile.email}") + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { + when (requestCode) { + REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.onPermissionsResult(permissions, grantResults) + else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + override fun onBackPressed() { + if (!qrFeature.onBackPressed()) { + onBackPressedDispatcher.onBackPressed() + } + } +} diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/SampleFxAEntryPoint.kt b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/SampleFxAEntryPoint.kt new file mode 100644 index 0000000000..2e587ee04e --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/SampleFxAEntryPoint.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.fxa + +import mozilla.components.concept.sync.FxAEntryPoint + +/** + * An implementation of [FxAEntryPoint] for the sample application. + */ +enum class SampleFxAEntryPoint(override val entryName: String) : FxAEntryPoint { + HomeMenu("home-menu"), +} diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..d3bdad5ca7 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/activity_main.xml @@ -0,0 +1,76 @@ +<?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:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:id="@+id/container" + tools:context="org.mozilla.samples.fxa.MainActivity"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:orientation="vertical" + android:id="@+id/buttonList" + tools:context="org.mozilla.samples.fxa.MainActivity"> + + <Button + android:id="@+id/buttonCustomTabs" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/sign_in_customtabs" + android:textAlignment="center" /> + + <Button + android:id="@+id/buttonWebView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/sign_in_webview" + android:textAlignment="center" /> + + <Button + android:id="@+id/buttonPair" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/sign_in_pair" + android:textAlignment="center" /> + + <Button + android:id="@+id/buttonLogout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/log_out" + android:textAlignment="center" /> + </LinearLayout> + + <CheckBox + android:id="@+id/checkboxKeys" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/buttonList" + android:text="@string/wants_keys" /> + + <TextView + android:id="@+id/txtView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/checkboxKeys" + android:layout_gravity="center" + android:text="" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/scanresult" + android:textColor="#000" + android:layout_marginTop="10dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:text=""/> + +</RelativeLayout> diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/fragment_view.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/fragment_view.xml new file mode 100644 index 0000000000..44536f4f68 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/fragment_view.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/. --> + +<WebView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/webview" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".LoginFragment" /> diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..e2edeb6cbe --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..0c12478a8e --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..abdaf95771 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..0c8508a62b --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..ba2b17573d --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/values/strings.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/values/strings.xml new file mode 100644 index 0000000000..987e1042d1 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/values/strings.xml @@ -0,0 +1,15 @@ +<?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">FxA Android Demo</string> + <string name="sign_in_webview">FxA sign in: webview</string> + <string name="sign_in_customtabs">FxA sign in: custom tab</string> + <string name="sign_in_pair">FxA sign in: pair</string> + <string name="signed_in">Signed in: %1$s</string> + <string name="log_out">FxA Log Out</string> + <string name="logged_out">Logged out!</string> + <string name="wants_keys">Request sync scope?</string> + <string name="pair_instructions_message"><![CDATA[Scan the QR code shown at <b>firefox.com/pair</b>]]></string> +</resources> diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..820ae61afa --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/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/firefox-accounts/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..55da967560 --- /dev/null +++ b/mobile/android/android-components/samples/firefox-accounts/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/glean/README.md b/mobile/android/android-components/samples/glean/README.md new file mode 100644 index 0000000000..eb2f5abbc1 --- /dev/null +++ b/mobile/android/android-components/samples/glean/README.md @@ -0,0 +1,20 @@ +# [Android Components](../../README.md) > Samples > Glean + +![](src/main/res/mipmap-xhdpi/ic_launcher.png) + +A simple app showcasing Glean, the mobile telemetry SDK & the Experiments library. + +## Concepts + +The main concepts shown in the sample app are: + +* Usage of the `metrics.yaml` file. +* Integration between Glean and the application's build process to generate the specific metrics API. +* Usage of the generated specific metrics API. +* Integration of the Nimbus experimentation library. + +## License + + 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/ diff --git a/mobile/android/android-components/samples/glean/build.gradle b/mobile/android/android-components/samples/glean/build.gradle new file mode 100644 index 0000000000..b4922d6b53 --- /dev/null +++ b/mobile/android/android-components/samples/glean/build.gradle @@ -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/. */ + +buildscript { + repositories { + gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository -> + maven { + url repository + if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) { + allowInsecureProtocol = true + } + } + } + + dependencies { + classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}" + } + } +} + +plugins { + id "com.jetbrains.python.envs" version "$python_envs_plugin" +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + applicationId "org.mozilla.samples.glean" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + debug { + applicationIdSuffix ".debug" + } + } + + buildFeatures { + viewBinding true + buildConfig true + } + + namespace 'org.mozilla.samples.glean' +} + +dependencies { + implementation project(':service-glean') + implementation project(':service-nimbus') + implementation project(':support-base') + implementation project(':support-rusthttp') + implementation project(':support-rustlog') + implementation project(':lib-fetch-httpurlconnection') + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_browser + + implementation project(':samples-glean-library') + + androidTestImplementation ComponentsDependencies.androidx_test_core + androidTestImplementation ComponentsDependencies.androidx_test_runner + androidTestImplementation ComponentsDependencies.androidx_test_rules + androidTestImplementation ComponentsDependencies.androidx_test_junit + androidTestImplementation ComponentsDependencies.androidx_test_uiautomator + androidTestImplementation ComponentsDependencies.androidx_espresso_core + androidTestImplementation ComponentsDependencies.androidx_work_testing + androidTestImplementation ComponentsDependencies.testing_mockwebserver +} + +apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" diff --git a/mobile/android/android-components/samples/glean/metrics.yaml b/mobile/android/android-components/samples/glean/metrics.yaml new file mode 100644 index 0000000000..5759c626b2 --- /dev/null +++ b/mobile/android/android-components/samples/glean/metrics.yaml @@ -0,0 +1,116 @@ +# 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/. + +# This file defines the metrics that are recorded by glean telemetry. They are +# automatically converted to Kotlin code at build time using the `glean_parser` +# PyPI package. +--- + +$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 + +browser.engagement: + click: + type: event + description: > + Just testing events + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + extra_keys: + key1: + type: string + description: "This is key one" + key2: + type: string + description: "This is key two" + expires: 2100-01-01 + + event_no_keys: + type: event + description: > + Just testing events without keys + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + +basic: + os: + type: string + description: > + The name of the os + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + +test: + string_list: + type: string_list + description: > + Testing StringList ping + send_in_pings: + - test-string-list + lifetime: user + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + + counter: + type: counter + description: > + Testing counter + send_in_pings: + - test-string-list + lifetime: user + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + + timespan: + type: timespan + description: > + Testing a timespan + time_unit: microsecond + lifetime: application + bugs: + - https://bugzilla.mozilla.org/1508948 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 + +custom: + counter: + type: counter + description: > + A custom counter that goes on a custom ping + lifetime: ping + send_in_pings: + - sample + bugs: + - https://bugzilla.mozilla.org/1547330 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@test-only.com + expires: 2100-01-01 diff --git a/mobile/android/android-components/samples/glean/pings.yaml b/mobile/android/android-components/samples/glean/pings.yaml new file mode 100644 index 0000000000..7f998f7748 --- /dev/null +++ b/mobile/android/android-components/samples/glean/pings.yaml @@ -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/. + +# This file defines the built-in pings that are recorded by glean telemetry. +# They are # automatically converted to Kotlin code at build time +# using the `glean_parser` PyPI package. +--- + +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +sample: + description: | + A sample custom ping. + include_client_id: true + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com diff --git a/mobile/android/android-components/samples/glean/proguard-rules.pro b/mobile/android/android-components/samples/glean/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/glean/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/README.md b/mobile/android/android-components/samples/glean/samples-glean-library/README.md new file mode 100644 index 0000000000..97a4de14b6 --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/README.md @@ -0,0 +1,5 @@ +samples_glean_library is a toy library that uses glean to record metrics. It +exists simply to show how libraries that are neither the application or glean +itself can record metrics, and those metrics will be stored and sent as part of +the [main pings](../../../components/service/glean/docs/pings.md) that glean +provides. diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle b/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle new file mode 100644 index 0000000000..68df279fab --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle @@ -0,0 +1,55 @@ +/* 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/. */ + +buildscript { + repositories { + gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository -> + maven { + url repository + if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) { + allowInsecureProtocol = true + } + } + } + + dependencies { + classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}" + } + } +} + +plugins { + id "com.jetbrains.python.envs" version "$python_envs_plugin" +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + buildConfig true + } + + namespace 'mozilla.samples.glean.library' +} + +dependencies { + implementation project(':service-glean') +} + +apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" + diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml b/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml new file mode 100644 index 0000000000..c999c586cf --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml @@ -0,0 +1,27 @@ +# 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/. + +# This file defines the built-in pings that are recorded by glean telemetry. +# They are # automatically converted to Kotlin code at build time +# using the `glean_parser` PyPI package. +--- + +$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 + +sample_metrics: + test: + type: counter + description: > + A simple counter defined in a third-party library + # To see the result immediately when pressing the "send ping" button in the + # interface, uncomment the following: + # send_in_pings: + # - baseline + bugs: + - https://bugzilla.mozilla.org/123456789 + data_reviews: + - N/A + notification_emails: + - CHANGE-ME@example.com + expires: 2100-01-01 diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e16cda1d34 --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<!-- 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 /> diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt new file mode 100644 index 0000000000..d9d635ddd4 --- /dev/null +++ b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt @@ -0,0 +1,31 @@ +/* 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.glean.library + +import mozilla.components.service.glean.Glean +import mozilla.samples.glean.library.GleanMetrics.SampleMetrics + +/** + * These are just simple functions to test calling the Glean API + * from a third-party library. + */ +object SamplesGleanLibrary { + /** + * Record to a metric defined in *this* library's metrics.yaml file. + */ + fun recordMetric() { + SampleMetrics.test.add() + } + + /** + * Notate an active experiment. + */ + fun recordExperiment() { + Glean.setExperimentActive( + "third_party_library", + "enabled", + ) + } +} diff --git a/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt new file mode 100644 index 0000000000..1a145e61c6 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt @@ -0,0 +1,39 @@ +/* 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.glean + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Rule +import org.junit.Test +import org.mozilla.samples.glean.GleanMetrics.Test as GleanTestMetrics + +class MainActivityTest { + @get:Rule + val activityRule: ActivityScenarioRule<MainActivity> = ActivityScenarioRule(MainActivity::class.java) + + @Test + fun checkGleanClickData() { + // We don't reset the storage in this test as the GleanTestRule does not + // work nicely in instrumented test. Just check the current value, increment + // by one and make it the expected value. + val expectedValue = if (GleanTestMetrics.counter.testGetValue() != null) { + GleanTestMetrics.counter.testGetValue()!! + 1 + } else { + 1 + } + + // Simulate a click on the button. + onView(withId(R.id.buttonGenerateData)).perform(click()) + + // Use the Glean testing API to check if the expected data was recorded. + assertNotNull(GleanTestMetrics.counter.testGetValue()) + assertEquals(expectedValue, GleanTestMetrics.counter.testGetValue()) + } +} diff --git a/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt new file mode 100644 index 0000000000..61057039d0 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt @@ -0,0 +1,138 @@ +/* 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.glean.pings + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import mozilla.components.service.glean.testing.GleanTestLocalServer +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.json.JSONObject +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.mozilla.samples.glean.MainActivity +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPInputStream + +/** + * Decompress the GZIP returned by the glean-core layer. + * + * @param data the gzipped [ByteArray] to decompress + * @return a [String] containing the uncompressed data. + */ +fun decompressGZIP(data: ByteArray): String { + return GZIPInputStream(ByteArrayInputStream(data)).bufferedReader().use(BufferedReader::readText) +} + +/** + * Convenience method to get the body of a request as a String. + * The UTF8 representation of the request body will be returned. + * If the request body is gzipped, it will be decompressed first. + * + * @return a [String] containing the body of the request. + */ +fun RecordedRequest.getPlainBody(): String { + return if (this.getHeader("Content-Encoding") == "gzip") { + val bodyInBytes = this.body.readByteArray() + decompressGZIP(bodyInBytes) + } else { + this.body.readUtf8() + } +} + +class BaselinePingTest { + private val server = createMockWebServer() + + @get:Rule + val activityRule: ActivityScenarioRule<MainActivity> = ActivityScenarioRule(MainActivity::class.java) + + @get:Rule + val gleanRule = GleanTestLocalServer(context, server.port) + + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + /** + * Create a mock webserver that accepts all requests and replies with "OK". + * @return a [MockWebServer] instance + */ + private fun createMockWebServer(): MockWebServer { + val server = MockWebServer() + server.dispatcher = + object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return MockResponse().setBody("OK") + } + } + + return server + } + + private fun waitForPingContent( + pingName: String, + pingReason: String?, + maxAttempts: Int = 3, + ): JSONObject? { + var attempts = 0 + do { + attempts += 1 + val request = server.takeRequest(20L, TimeUnit.SECONDS) + val docType = request?.path?.split("/")?.get(3) + if (pingName == docType) { + val parsedPayload = JSONObject(request.getPlainBody()) + if (pingReason == null) { + return parsedPayload + } + + // If we requested a specific ping reason, look for it. + val reason = parsedPayload.getJSONObject("ping_info").getString("reason") + if (reason == pingReason) { + return parsedPayload + } + } + } while (attempts < maxAttempts) + + return null + } + + @Test + fun validateBaselinePing() { + // Wait for the app to be idle/ready. + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + device.waitForIdle() + + // Wait for 1 second: this should guarantee we have some valid duration in the + // ping. + Thread.sleep(1000) + + // Move it to background. + device.pressHome() + + // Validate the received data. + val baselinePing = waitForPingContent("baseline", "inactive")!! + val metrics = baselinePing.getJSONObject("metrics") + + // Make sure we have a 'duration' field with a reasonable value: it should be >= 1, since + // we slept for 1000ms. + val timespans = metrics.getJSONObject("timespan") + assertTrue(timespans.getJSONObject("glean.baseline.duration").getLong("value") >= 1L) + + // Make sure there's no errors. + val errors = metrics.optJSONObject("labeled_counter")?.keys() + errors?.forEach { + assertFalse(it.startsWith("glean.error.")) + } + } +} diff --git a/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0fb5a873cb --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ +<?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.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" + tools:ignore="ScopedStorage" /> + + <!-- Note: the usesCleartextTraffic is only required for making instrumentation + tests work on API 23+. Also note that this requires tools:ignore="UnusedAttribute" + for stopping the linter from complaining on API 21 <= x < 23. --> + <application + android:allowBackup="true" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/Theme.AppCompat.Light.DarkActionBar" + android:usesCleartextTraffic="true" + tools:ignore="DataExtractionRules,UnusedAttribute" + android:name=".GleanApplication" + android:dataExtractionRules="@xml/data_extraction_rules"> + <activity android:name="org.mozilla.samples.glean.MainActivity" + android:windowSoftInputMode="adjustResize" + android:launchMode="singleTask" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt new file mode 100644 index 0000000000..bf0ff5f641 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt @@ -0,0 +1,115 @@ +/* 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.glean + +import android.app.Application +import android.content.Context +import android.net.Uri +import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient +import mozilla.components.service.glean.Glean +import mozilla.components.service.glean.config.Configuration +import mozilla.components.service.glean.net.ConceptFetchHttpUploader +import mozilla.components.service.nimbus.Nimbus +import mozilla.components.service.nimbus.NimbusApi +import mozilla.components.service.nimbus.NimbusAppInfo +import mozilla.components.service.nimbus.NimbusServerSettings +import mozilla.components.support.base.log.Log +import mozilla.components.support.base.log.sink.AndroidLogSink +import mozilla.components.support.rusthttp.RustHttpConfig +import mozilla.components.support.rustlog.RustLog +import org.mozilla.samples.glean.GleanMetrics.Basic +import org.mozilla.samples.glean.GleanMetrics.Custom +import org.mozilla.samples.glean.GleanMetrics.GleanBuildInfo +import org.mozilla.samples.glean.GleanMetrics.Pings +import org.mozilla.samples.glean.GleanMetrics.Test + +class GleanApplication : Application() { + + companion object { + lateinit var nimbus: NimbusApi + + const val SAMPLE_GLEAN_PREFERENCES = "sample_glean_preferences" + const val PREF_IS_FIRST_RUN = "isFirstRun" + } + + override fun onCreate() { + super.onCreate() + val settings = applicationContext.getSharedPreferences(SAMPLE_GLEAN_PREFERENCES, Context.MODE_PRIVATE) + val isFirstRun = settings.getBoolean(PREF_IS_FIRST_RUN, true) + + // We want the log messages of all builds to go to Android logcat + + Log.addSink(AndroidLogSink()) + + // Register the sample application's custom pings. + Glean.registerPings(Pings) + + // Initialize the Glean library. Ideally, this is the first thing that + // must be done right after enabling logging. + val client by lazy { HttpURLConnectionClient() } + val httpClient = ConceptFetchHttpUploader.fromClient(client) + val config = Configuration(httpClient = httpClient) + Glean.initialize( + applicationContext, + uploadEnabled = true, + configuration = config, + buildInfo = GleanBuildInfo.buildInfo, + ) + + /** Begin Nimbus component specific code. Note: this is not relevant to Glean */ + initNimbus(isFirstRun) + /** End Nimbus specific code. */ + + Test.timespan.start() + + Custom.counter.add() + + // Set a sample value for a metric. + Basic.os.set("Android") + + settings + .edit() + .putBoolean(PREF_IS_FIRST_RUN, false) + .apply() + } + + /** + * Initialize the Nimbus experiments library. This is only relevant to the Nimbus library, aside + * from recording the experiment in Glean. + */ + private fun initNimbus(isFirstRun: Boolean) { + RustLog.enable() + RustHttpConfig.setClient(lazy { HttpURLConnectionClient() }) + val url = Uri.parse(getString(R.string.nimbus_default_endpoint)) + val appInfo = NimbusAppInfo( + appName = "samples-glean", + channel = "samples", + ) + nimbus = Nimbus( + context = this, + appInfo = appInfo, + server = NimbusServerSettings(url), + ).also { nimbus -> + if (isFirstRun) { + // This file is bundled with the app, but derived from the server at build time. + // We'll use it now, on first run. + nimbus.setExperimentsLocally(R.raw.initial_experiments) + } + // Apply the experiments downloaded on last run, but on first run, it will + // use the contents of `R.raw.initial_experiments`. + nimbus.applyPendingExperiments() + + // In a real application, we might want to fetchExperiments() here. + // + // We won't do that in this app because: + // * the server's experiments will overwrite the current ones + // * it's not clear that the server will have a `test-color` experiment + // by the time you run this + // * an update experiments button is in `MainActivity` + // + // nimbus.fetchExperiments() + } + } +} diff --git a/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt new file mode 100644 index 0000000000..8020022d87 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.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.glean + +import android.graphics.Color +import android.os.Bundle +import androidx.annotation.MainThread +import androidx.appcompat.app.AppCompatActivity +import org.mozilla.experiments.nimbus.EnrolledExperiment +import org.mozilla.experiments.nimbus.NimbusInterface +import org.mozilla.samples.glean.GleanMetrics.BrowserEngagement +import org.mozilla.samples.glean.GleanMetrics.Test +import org.mozilla.samples.glean.databinding.ActivityMainBinding +import org.mozilla.samples.glean.library.SamplesGleanLibrary + +/** + * Main Activity of the glean-sample-app + */ +open class MainActivity : AppCompatActivity(), NimbusInterface.Observer { + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + + setContentView(binding.root) + + // Generate an event when user clicks on the button. + binding.buttonGenerateData.setOnClickListener { + // These first two actions, adding to the string list and incrementing the counter are + // tied to a user lifetime metric which is persistent from launch to launch. + + // Adds the EditText's text content as a new string in the string list metric from the + // metrics.yaml file. + Test.stringList.add(binding.etStringListInput.text.toString()) + // Clear current text to help indicate something happened + binding.etStringListInput.setText("") + + // Increments the test_counter metric from the metrics.yaml file. + Test.counter.add() + + // This is referencing the event ping named 'click' from the metrics.yaml file. In + // order to illustrate adding extra information to the event, it is also adding to the + // 'extras' field a dictionary of values. Note that the dictionary keys must be + // declared in the metrics.yaml file under the 'extra_keys' section of an event metric. + BrowserEngagement.click.record( + BrowserEngagement.ClickExtra( + key1 = "extra_value_1", + key2 = "extra_value_2", + ), + ) + } + + Test.timespan.stop() + + // Update some metrics from a third-party library + SamplesGleanLibrary.recordMetric() + SamplesGleanLibrary.recordExperiment() + + // The following is not relevant to the Glean SDK, but to the Nimbus experiments library + setupNimbusExperiments() + } + + /** Begin Nimbus component specific functions */ + + /** + * This sets up the update receiver and sets the onClickListener for the "Update Experiments" + * button. This is not relevant to the Glean SDK, but to the Nimbus experiments library. + */ + private fun setupNimbusExperiments() { + // Register the main activity as a Nimbus observer + GleanApplication.nimbus.register(this) + + // Attach the click listener for the experiments button to the updateExperiments function + binding.buttonCheckExperiments.setOnClickListener { + // Once the experiments are fetched, then the activity's (a Nimbus observer) + // `onExperimentFetched()` method is called. + GleanApplication.nimbus.fetchExperiments() + } + + configureButton() + } + + /** + * Event to indicate that the experiments have been fetched from the endpoint + */ + override fun onExperimentsFetched() { + println("Experiments fetched") + GleanApplication.nimbus.applyPendingExperiments() + } + + /** + * Event to indicate that the experiment enrollments have been applied. Developers normally + * shouldn't care to observe this and rather rely on `onExperimentsFetched` and `withExperiment` + */ + override fun onUpdatesApplied(updated: List<EnrolledExperiment>) { + runOnUiThread { + configureButton() + } + } + + @MainThread + private fun configureButton() { + val nimbus = GleanApplication.nimbus + val branch = nimbus.getExperimentBranch("sample-experiment-feature") + + val color = when (branch) { + "blue" -> Color.BLUE + "red" -> Color.RED + "control" -> Color.DKGRAY + else -> Color.WHITE + } + val text = when (branch) { + null -> getString(R.string.experiment_not_active) + else -> getString(R.string.experiment_active_branch, branch) + } + + binding.textViewExperimentStatus.setBackgroundColor(color) + binding.textViewExperimentStatus.text = text + } + + /** End Nimbus component functions */ +} diff --git a/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..87e3025d0f --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml @@ -0,0 +1,75 @@ +<?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:id="@+id/buttonList" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:gravity="center" + android:orientation="vertical" + android:padding="10dp" + tools:context="org.mozilla.samples.glean.MainActivity"> + + <!-- This is a dummy linear layout to capture focus and prevent the keyboard from popping + when the app first launches. This is a known issue of linear layouts and a common + workaround. --> + <LinearLayout android:focusable="true" + android:focusableInTouchMode="true" + android:layout_width="0px" + android:layout_height="0px" > + <requestFocus /> + </LinearLayout> + + <EditText + android:id="@+id/etStringListInput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:hint="@string/string_list_input_hint" + android:inputType="textPersonName" + android:autofillHints="" + tools:ignore="UnusedAttribute" /> + + <Button + android:id="@+id/buttonGenerateData" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="5dp" + android:text="@string/generate_data" + android:textAlignment="center" /> + + <TextView + android:id="@+id/textView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="5dp" + android:text="@string/counter_metric_info" /> + + <Button + android:id="@+id/buttonCheckExperiments" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/check_experiments" + android:textAlignment="center" /> + + <TextView + android:id="@+id/textViewExperimentStatus" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@android:color/white" + android:text="@string/experiment_not_active" + android:textStyle="italic" /> + + <TextView + android:id="@+id/textView3" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/check_experiments_btn_description" /> + +</LinearLayout> diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..a2f5908281 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..ff10afd6e1 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..dcd3cd8083 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..8ca12fe024 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..b824ebdd48 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json b/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json new file mode 100644 index 0000000000..e4eadb052b --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json @@ -0,0 +1,60 @@ +{ + "data": [{ + "slug": "test-color", + "endDate": null, + "branches": [ + { + "slug": "control", + "ratio": 1, + "feature": { + "featureId": "sample-experiment-feature", + "enabled": true, + "value": null + } + }, + { + "slug": "red", + "ratio": 1, + "feature": { + "featureId": "sample-experiment-feature", + "enabled": true, + "value": null + } + }, + { + "slug": "blue", + "ratio": 1, + "feature": { + "featureId": "sample-experiment-feature", + "enabled": true, + "value": null + } + } + ], + "featureIds": [ + "sample-experiment-feature" + ], + "probeSets": [], + "startDate": null, + "targeting": "true", + "appName": "samples-glean", + "appId": "org.mozilla.samples.glean.debug", + "channel": "samples", + "bucketConfig": { + "count": 10000, + "start": 0, + "total": 10000, + "namespace": "test-color-1", + "randomizationUnit": "nimbus_id" + }, + "schemaVersion": "1.1.0", + "userFacingName": "Button color sample experiment", + "referenceBranch": "control", + "proposedDuration": null, + "isEnrollmentPaused": false, + "proposedEnrollment": 7, + "userFacingDescription": "A sample experiment to determine the color of a button", + "id": "test-color", + "last_modified": 1607613885917 + }] +} diff --git a/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml b/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml new file mode 100644 index 0000000000..1a6c4363ca --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.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/. --> + +<resources> + <string name="nimbus_default_endpoint" translatable="false">https://settings.stage.mozaws.net</string> +</resources> diff --git a/mobile/android/android-components/samples/glean/src/main/res/values/strings.xml b/mobile/android/android-components/samples/glean/src/main/res/values/strings.xml new file mode 100644 index 0000000000..ed86142323 --- /dev/null +++ b/mobile/android/android-components/samples/glean/src/main/res/values/strings.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/. --> +<resources> + <string name="app_name">Glean - Metrics Demo</string> + <string name="generate_data">Record Text</string> + <string name="string_list_input_hint">Enter some text here</string> + <string name="counter_metric_info"> + Every time you click on the button above, the text is added to the test_string_list metric + and a counter metric type called testCounter is incremented. Both of these will be included + when the ping is sent. The counter and the string list are utilizing the `user` lifetime + and should persist from launch to launch of the app.\n\nAn event metric is also being used + to attach a dictionary of values to the extras of the event ping. See MainActivity for + where all of this is happening. + </string> + <string name="experiment_not_active">Experiment not active</string> + <string name="experiment_active_branch">Experiment active, branch: %1$s</string> + <string name="check_experiments">Check experiments</string> + <string name="check_experiments_btn_description">The CHECK EXPERIMENTS button checks the experiments status of the test-colorexperiment and sets the color accordingly.</string> +</resources> diff --git a/mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..820ae61afa --- /dev/null +++ b/mobile/android/android-components/samples/glean/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/glean/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..55da967560 --- /dev/null +++ b/mobile/android/android-components/samples/glean/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/ios-sample/ios-sample.xcodeproj/project.pbxproj b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..dd311b051a --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.pbxproj @@ -0,0 +1,589 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 227BF22525C2EAD500DB0AB9 /* ios_sampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BF22425C2EAD500DB0AB9 /* ios_sampleApp.swift */; }; + 227BF22725C2EAD500DB0AB9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BF22625C2EAD500DB0AB9 /* ContentView.swift */; }; + 227BF22925C2EAD600DB0AB9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 227BF22825C2EAD600DB0AB9 /* Assets.xcassets */; }; + 227BF22C25C2EAD600DB0AB9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 227BF22B25C2EAD600DB0AB9 /* Preview Assets.xcassets */; }; + 227BF23725C2EAD600DB0AB9 /* ios_sampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BF23625C2EAD600DB0AB9 /* ios_sampleTests.swift */; }; + 227BF24225C2EAD600DB0AB9 /* ios_sampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BF24125C2EAD600DB0AB9 /* ios_sampleUITests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 227BF23325C2EAD600DB0AB9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 227BF21925C2EAD500DB0AB9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 227BF22025C2EAD500DB0AB9; + remoteInfo = "ios-sample"; + }; + 227BF23E25C2EAD600DB0AB9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 227BF21925C2EAD500DB0AB9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 227BF22025C2EAD500DB0AB9; + remoteInfo = "ios-sample"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 227BF22125C2EAD500DB0AB9 /* ios-sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ios-sample.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 227BF22425C2EAD500DB0AB9 /* ios_sampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ios_sampleApp.swift; sourceTree = "<group>"; }; + 227BF22625C2EAD500DB0AB9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; + 227BF22825C2EAD600DB0AB9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; + 227BF22B25C2EAD600DB0AB9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; + 227BF22D25C2EAD600DB0AB9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 227BF23225C2EAD600DB0AB9 /* ios-sampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ios-sampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 227BF23625C2EAD600DB0AB9 /* ios_sampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ios_sampleTests.swift; sourceTree = "<group>"; }; + 227BF23825C2EAD600DB0AB9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 227BF23D25C2EAD600DB0AB9 /* ios-sampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ios-sampleUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 227BF24125C2EAD600DB0AB9 /* ios_sampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ios_sampleUITests.swift; sourceTree = "<group>"; }; + 227BF24325C2EAD600DB0AB9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 227BF25325C2EBFE00DB0AB9 /* dummy.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = dummy.framework; path = "../../components/multiplatform/lib-dummy/build/xcode-frameworks/dummy.framework"; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 227BF21E25C2EAD500DB0AB9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 227BF22F25C2EAD600DB0AB9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 227BF23A25C2EAD600DB0AB9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 227BF21825C2EAD500DB0AB9 = { + isa = PBXGroup; + children = ( + 227BF22325C2EAD500DB0AB9 /* ios-sample */, + 227BF23525C2EAD600DB0AB9 /* ios-sampleTests */, + 227BF24025C2EAD600DB0AB9 /* ios-sampleUITests */, + 227BF22225C2EAD500DB0AB9 /* Products */, + 227BF25225C2EBFE00DB0AB9 /* Frameworks */, + ); + sourceTree = "<group>"; + }; + 227BF22225C2EAD500DB0AB9 /* Products */ = { + isa = PBXGroup; + children = ( + 227BF22125C2EAD500DB0AB9 /* ios-sample.app */, + 227BF23225C2EAD600DB0AB9 /* ios-sampleTests.xctest */, + 227BF23D25C2EAD600DB0AB9 /* ios-sampleUITests.xctest */, + ); + name = Products; + sourceTree = "<group>"; + }; + 227BF22325C2EAD500DB0AB9 /* ios-sample */ = { + isa = PBXGroup; + children = ( + 227BF22425C2EAD500DB0AB9 /* ios_sampleApp.swift */, + 227BF22625C2EAD500DB0AB9 /* ContentView.swift */, + 227BF22825C2EAD600DB0AB9 /* Assets.xcassets */, + 227BF22D25C2EAD600DB0AB9 /* Info.plist */, + 227BF22A25C2EAD600DB0AB9 /* Preview Content */, + ); + path = "ios-sample"; + sourceTree = "<group>"; + }; + 227BF22A25C2EAD600DB0AB9 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 227BF22B25C2EAD600DB0AB9 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = "<group>"; + }; + 227BF23525C2EAD600DB0AB9 /* ios-sampleTests */ = { + isa = PBXGroup; + children = ( + 227BF23625C2EAD600DB0AB9 /* ios_sampleTests.swift */, + 227BF23825C2EAD600DB0AB9 /* Info.plist */, + ); + path = "ios-sampleTests"; + sourceTree = "<group>"; + }; + 227BF24025C2EAD600DB0AB9 /* ios-sampleUITests */ = { + isa = PBXGroup; + children = ( + 227BF24125C2EAD600DB0AB9 /* ios_sampleUITests.swift */, + 227BF24325C2EAD600DB0AB9 /* Info.plist */, + ); + path = "ios-sampleUITests"; + sourceTree = "<group>"; + }; + 227BF25225C2EBFE00DB0AB9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 227BF25325C2EBFE00DB0AB9 /* dummy.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 227BF22025C2EAD500DB0AB9 /* ios-sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 227BF24625C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sample" */; + buildPhases = ( + 227BF21D25C2EAD500DB0AB9 /* Sources */, + 227BF21E25C2EAD500DB0AB9 /* Frameworks */, + 227BF21F25C2EAD500DB0AB9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ios-sample"; + productName = "ios-sample"; + productReference = 227BF22125C2EAD500DB0AB9 /* ios-sample.app */; + productType = "com.apple.product-type.application"; + }; + 227BF23125C2EAD600DB0AB9 /* ios-sampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 227BF24925C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sampleTests" */; + buildPhases = ( + 227BF22E25C2EAD600DB0AB9 /* Sources */, + 227BF22F25C2EAD600DB0AB9 /* Frameworks */, + 227BF23025C2EAD600DB0AB9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 227BF23425C2EAD600DB0AB9 /* PBXTargetDependency */, + ); + name = "ios-sampleTests"; + productName = "ios-sampleTests"; + productReference = 227BF23225C2EAD600DB0AB9 /* ios-sampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 227BF23C25C2EAD600DB0AB9 /* ios-sampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 227BF24C25C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sampleUITests" */; + buildPhases = ( + 227BF23925C2EAD600DB0AB9 /* Sources */, + 227BF23A25C2EAD600DB0AB9 /* Frameworks */, + 227BF23B25C2EAD600DB0AB9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 227BF23F25C2EAD600DB0AB9 /* PBXTargetDependency */, + ); + name = "ios-sampleUITests"; + productName = "ios-sampleUITests"; + productReference = 227BF23D25C2EAD600DB0AB9 /* ios-sampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 227BF21925C2EAD500DB0AB9 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1210; + LastUpgradeCheck = 1210; + TargetAttributes = { + 227BF22025C2EAD500DB0AB9 = { + CreatedOnToolsVersion = 12.1; + }; + 227BF23125C2EAD600DB0AB9 = { + CreatedOnToolsVersion = 12.1; + TestTargetID = 227BF22025C2EAD500DB0AB9; + }; + 227BF23C25C2EAD600DB0AB9 = { + CreatedOnToolsVersion = 12.1; + TestTargetID = 227BF22025C2EAD500DB0AB9; + }; + }; + }; + buildConfigurationList = 227BF21C25C2EAD500DB0AB9 /* Build configuration list for PBXProject "ios-sample" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 227BF21825C2EAD500DB0AB9; + productRefGroup = 227BF22225C2EAD500DB0AB9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 227BF22025C2EAD500DB0AB9 /* ios-sample */, + 227BF23125C2EAD600DB0AB9 /* ios-sampleTests */, + 227BF23C25C2EAD600DB0AB9 /* ios-sampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 227BF21F25C2EAD500DB0AB9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 227BF22C25C2EAD600DB0AB9 /* Preview Assets.xcassets in Resources */, + 227BF22925C2EAD600DB0AB9 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 227BF23025C2EAD600DB0AB9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 227BF23B25C2EAD600DB0AB9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 227BF21D25C2EAD500DB0AB9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 227BF22725C2EAD500DB0AB9 /* ContentView.swift in Sources */, + 227BF22525C2EAD500DB0AB9 /* ios_sampleApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 227BF22E25C2EAD600DB0AB9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 227BF23725C2EAD600DB0AB9 /* ios_sampleTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 227BF23925C2EAD600DB0AB9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 227BF24225C2EAD600DB0AB9 /* ios_sampleUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 227BF23425C2EAD600DB0AB9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 227BF22025C2EAD500DB0AB9 /* ios-sample */; + targetProxy = 227BF23325C2EAD600DB0AB9 /* PBXContainerItemProxy */; + }; + 227BF23F25C2EAD600DB0AB9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 227BF22025C2EAD500DB0AB9 /* ios-sample */; + targetProxy = 227BF23E25C2EAD600DB0AB9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 227BF24425C2EAD600DB0AB9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 227BF24525C2EAD600DB0AB9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 227BF24725C2EAD600DB0AB9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"ios-sample/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = "ios-sample/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sample"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 227BF24825C2EAD600DB0AB9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"ios-sample/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = "ios-sample/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sample"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 227BF24A25C2EAD600DB0AB9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "ios-sampleTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sampleTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios-sample.app/ios-sample"; + }; + name = Debug; + }; + 227BF24B25C2EAD600DB0AB9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "ios-sampleTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sampleTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios-sample.app/ios-sample"; + }; + name = Release; + }; + 227BF24D25C2EAD600DB0AB9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "ios-sampleUITests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sampleUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "ios-sample"; + }; + name = Debug; + }; + 227BF24E25C2EAD600DB0AB9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "ios-sampleUITests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sampleUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "ios-sample"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 227BF21C25C2EAD500DB0AB9 /* Build configuration list for PBXProject "ios-sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 227BF24425C2EAD600DB0AB9 /* Debug */, + 227BF24525C2EAD600DB0AB9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 227BF24625C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 227BF24725C2EAD600DB0AB9 /* Debug */, + 227BF24825C2EAD600DB0AB9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 227BF24925C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 227BF24A25C2EAD600DB0AB9 /* Debug */, + 227BF24B25C2EAD600DB0AB9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 227BF24C25C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 227BF24D25C2EAD600DB0AB9 /* Debug */, + 227BF24E25C2EAD600DB0AB9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 227BF21925C2EAD500DB0AB9 /* Project object */; +} diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "self:"> + </FileRef> +</Workspace> diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AccentColor.colorset/Contents.json b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/Contents.json b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/ContentView.swift b/mobile/android/android-components/samples/ios-sample/ios-sample/ContentView.swift new file mode 100644 index 0000000000..91f9fb1266 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample/ContentView.swift @@ -0,0 +1,18 @@ +/* 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/. */ + +import SwiftUI + +struct ContentView: View { + var body: some View { + Text("Sample app") + .padding() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Info.plist b/mobile/android/android-components/samples/ios-sample/ios-sample/Info.plist new file mode 100644 index 0000000000..efc211a0c1 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Info.plist @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>UIApplicationSceneManifest</key> + <dict> + <key>UIApplicationSupportsMultipleScenes</key> + <true/> + </dict> + <key>UIApplicationSupportsIndirectInputEvents</key> + <true/> + <key>UILaunchScreen</key> + <dict/> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>armv7</string> + </array> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>UISupportedInterfaceOrientations~ipad</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> +</dict> +</plist> diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Preview Content/Preview Assets.xcassets/Contents.json b/mobile/android/android-components/samples/ios-sample/ios-sample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/ios_sampleApp.swift b/mobile/android/android-components/samples/ios-sample/ios-sample/ios_sampleApp.swift new file mode 100644 index 0000000000..6143209faa --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sample/ios_sampleApp.swift @@ -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/. */ + +import SwiftUI + +@main +struct ios_sampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/mobile/android/android-components/samples/ios-sample/ios-sampleTests/Info.plist b/mobile/android/android-components/samples/ios-sample/ios-sampleTests/Info.plist new file mode 100644 index 0000000000..64d65ca495 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sampleTests/Info.plist @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/mobile/android/android-components/samples/ios-sample/ios-sampleTests/ios_sampleTests.swift b/mobile/android/android-components/samples/ios-sample/ios-sampleTests/ios_sampleTests.swift new file mode 100644 index 0000000000..a5bc5eebe0 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sampleTests/ios_sampleTests.swift @@ -0,0 +1,30 @@ +/* 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/. */ + +import XCTest +@testable import ios_sample + +class ios_sampleTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/Info.plist b/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/Info.plist new file mode 100644 index 0000000000..64d65ca495 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/Info.plist @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/ios_sampleUITests.swift b/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/ios_sampleUITests.swift new file mode 100644 index 0000000000..06147c6596 --- /dev/null +++ b/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/ios_sampleUITests.swift @@ -0,0 +1,39 @@ +/* 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/. */ + +import XCTest + +class ios_sampleUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/mobile/android/android-components/samples/sync-logins/README.md b/mobile/android/android-components/samples/sync-logins/README.md new file mode 100644 index 0000000000..c3784c15fe --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/README.md @@ -0,0 +1,73 @@ +# [Android Components](../../README.md) > Samples > Firefox Sync - Logins + +![](src/main/res/mipmap-xhdpi/ic_launcher.png) + +A simple app showcasing the service-sync-logins component. + +## Concepts + +The main concepts shown in the sample app are: + +* Usage of the asynchronous result type `SyncResult`. +* Login to Firefox Accounts that provides the necessary information to fetch the Logins from Firefox Sync. +* Getting the list of Logins from Firefox Sync. + +## `SyncResult` usage + +`SyncResult` represents a chainable asynchronous result, and is used as a convenient method of running potentially long-running tasks (eg. network requests, crypto operations) on threads outside of the UI thread. + +A value or exception can be wrapped in an SyncResult: + +```kotlin +val syncValue = SyncResult.fromValue(42) +val syncException = SyncResult.fromException(Exception("Something went wrong")) +``` + +One can attach `OnValueListener`s or `OnExceptionListener`s to an `SyncResult`. There are a few ways of chaining results in Kotlin: + +* Passing the listeners directly via `then`, with object expressions or otherwise: + + ```kotlin + SyncResult.fromValue(42).then(object : OnValueListener<Integer, Void> { + override fun onValue(value: Integer): SyncResult<Void>? { + // handle the value + return SyncResult<Void>() + } + }, object : OnExceptionListener<Void> { + override fun onException(exception: Exception): SyncResult<Void>? { + // handle the exception + return SyncResult<Void>() + } + }) + ``` + + Since Java 6 does not support simple lambda syntax, this is one of the main ways to chain `SyncResult`s in Java. + +* Passing lambdas via `then`: + + ```kotlin + SyncResult.fromValue(42).then({ value: Int -> // valueListener + // handle the value + return SyncResult<Void>() + }, { exception: Exception -> + // handle the exception + return SyncResult<Void>() + }) + ``` + +* Completing a chain via `whenComplete`: + + ```kotlin + SyncResult.fromValue(42).whenComplete { value: Integer -> + // handle the value + } + ``` + + Since `whenComplete` implies that the chain of promises has come to an end, there is no need to return another SyncResult at the end. + + +## License + + 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/ diff --git a/mobile/android/android-components/samples/sync-logins/build.gradle b/mobile/android/android-components/samples/sync-logins/build.gradle new file mode 100644 index 0000000000..fb654517f0 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/build.gradle @@ -0,0 +1,50 @@ +/* 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/. */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + applicationId "org.mozilla.samples.sync.logins" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + splits { + abi { + enable true + reset() + include 'x86', 'arm64-v8a', 'armeabi-v7a' + } + } + + namespace 'org.mozilla.samples.sync.logins' +} + +dependencies { + implementation project(':concept-storage') + implementation project(':service-firefox-accounts') + implementation project(':service-sync-logins') + implementation project(':support-rustlog') + implementation project(':support-rusthttp') + implementation project(':lib-dataprotect') + implementation project(':lib-fetch-httpurlconnection') + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_browser + implementation ComponentsDependencies.androidx_recyclerview +} diff --git a/mobile/android/android-components/samples/sync-logins/gradle.properties b/mobile/android/android-components/samples/sync-logins/gradle.properties new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/gradle.properties diff --git a/mobile/android/android-components/samples/sync-logins/proguard-rules.pro b/mobile/android/android-components/samples/sync-logins/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/sync-logins/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/sync-logins/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..d6fd659746 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ +<?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.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" + tools:ignore="ScopedStorage" /> + + <application + android:allowBackup="true" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/Theme.AppCompat.Light.DarkActionBar" + android:dataExtractionRules="@xml/data_extraction_rules" + tools:targetApi="s"> + <activity android:name="org.mozilla.samples.sync.logins.MainActivity" + android:windowSoftInputMode="adjustResize" + android:launchMode="singleTask" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT" /> + <data + android:host="*" + android:scheme="fxaclient" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/LoginFragment.kt b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/LoginFragment.kt new file mode 100644 index 0000000000..b6eecc5f9d --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/LoginFragment.kt @@ -0,0 +1,108 @@ +/* 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.sync.logins + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.CookieManager +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.fragment.app.Fragment + +class LoginFragment : Fragment() { + + private lateinit var authUrl: String + private lateinit var redirectUrl: String + private var mWebView: WebView? = null + private var listener: OnLoginCompleteListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + authUrl = it.getString(AUTH_URL)!! + redirectUrl = it.getString(REDIRECT_URL)!! + } + } + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view: View = inflater.inflate(R.layout.fragment_view, container, false) + val webView = view.findViewById<WebView>(R.id.webview) + // Need JS, cookies and localStorage. + webView.settings.domStorageEnabled = true + webView.settings.javaScriptEnabled = true + CookieManager.getInstance().setAcceptCookie(true) + + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + if (url != null && url.startsWith(redirectUrl)) { + val uri = Uri.parse(url) + val code = uri.getQueryParameter("code") + val state = uri.getQueryParameter("state") + val action = uri.getQueryParameter("action") + if (code != null && state != null && action != null) { + listener?.onLoginComplete(code, state, action, this@LoginFragment) + } + } + + super.onPageStarted(view, url, favicon) + } + } + webView.loadUrl(authUrl) + + mWebView?.destroy() + mWebView = webView + + return view + } + + @Suppress("TooGenericExceptionThrown") + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is OnLoginCompleteListener) { + listener = context + } else { + throw IllegalStateException("$context must implement OnLoginCompleteListener") + } + } + + override fun onDetach() { + super.onDetach() + listener = null + } + + override fun onPause() { + super.onPause() + mWebView?.onPause() + } + + override fun onResume() { + super.onResume() + mWebView?.onResume() + } + + interface OnLoginCompleteListener { + fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) + } + + companion object { + const val AUTH_URL = "authUrl" + const val REDIRECT_URL = "redirectUrl" + + fun create(authUrl: String, redirectUrl: String): LoginFragment = + LoginFragment().apply { + arguments = Bundle().apply { + putString(AUTH_URL, authUrl) + putString(REDIRECT_URL, redirectUrl) + } + } + } +} diff --git a/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/MainActivity.kt b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/MainActivity.kt new file mode 100644 index 0000000000..c2f5244f07 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/MainActivity.kt @@ -0,0 +1,181 @@ +/* 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.sync.logins + +import android.os.Bundle +import android.view.View +import android.widget.ArrayAdapter +import android.widget.ListView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import mozilla.appservices.fxaclient.FxaConfig +import mozilla.appservices.fxaclient.FxaServer +import mozilla.components.concept.sync.AccountObserver +import mozilla.components.concept.sync.AuthType +import mozilla.components.concept.sync.DeviceConfig +import mozilla.components.concept.sync.DeviceType +import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.concept.sync.Profile +import mozilla.components.lib.dataprotect.SecureAbove22Preferences +import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient +import mozilla.components.service.fxa.FirefoxAccount +import mozilla.components.service.fxa.FxaAuthData +import mozilla.components.service.fxa.PeriodicSyncConfig +import mozilla.components.service.fxa.SyncConfig +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider +import mozilla.components.service.fxa.sync.SyncReason +import mozilla.components.service.fxa.sync.SyncStatusObserver +import mozilla.components.service.fxa.toAuthType +import mozilla.components.service.sync.logins.SyncableLoginsStorage +import mozilla.components.support.base.log.Log +import mozilla.components.support.base.log.sink.AndroidLogSink +import mozilla.components.support.rusthttp.RustHttpConfig +import mozilla.components.support.rustlog.RustLog +import kotlin.coroutines.CoroutineContext + +const val CLIENT_ID = "3c49430b43dfba77" +const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + +class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener, CoroutineScope, SyncStatusObserver { + private lateinit var keyStorage: SecureAbove22Preferences + + private val loginsStorage = lazy { + SyncableLoginsStorage(this, lazy { keyStorage }) + } + + private lateinit var listView: ListView + private lateinit var adapter: ArrayAdapter<String> + private lateinit var activityContext: MainActivity + private lateinit var account: FirefoxAccount + private val accountManager by lazy { + FxaAccountManager( + applicationContext, + FxaConfig(FxaServer.Release, CLIENT_ID, REDIRECT_URL), + DeviceConfig("A-C Logins Sync Sample", DeviceType.MOBILE, setOf()), + SyncConfig(setOf(SyncEngine.Passwords), PeriodicSyncConfig()), + ) + } + + private lateinit var job: Job + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + RustLog.enable() + RustHttpConfig.setClient(lazy { HttpURLConnectionClient() }) + + Log.addSink(AndroidLogSink()) + + setContentView(R.layout.activity_main) + job = Job() + + // Observe sync state changes. + accountManager.registerForSyncEvents(observer = this, owner = this, autoPause = true) + + listView = findViewById(R.id.logins_list_view) + adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1) + listView.adapter = adapter + activityContext = this + + keyStorage = SecureAbove22Preferences(this, "secret-data-storage") + keyStorage.putString(SyncEngine.Passwords.nativeName, "my-not-so-secret-password") + + accountManager.register(accountObserver, owner = this, autoPause = true) + + launch { + // Initializing loginsStorage is an expensive operation, and is thus a deferred function. + // In order to avoid race conditions with the sync workers trying to access loginsStorage + // that's not fully initialized, we 'await' on loginsStorage initialization before + // kicking off the accountManager. + GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to loginsStorage) + + accountManager.start() + } + + findViewById<View>(R.id.buttonWebView).setOnClickListener { + launch { + val authUrl = accountManager.beginAuthentication(entrypoint = SampleFxAEntryPoint.HomeMenu) + if (authUrl == null) { + Toast.makeText(this@MainActivity, "Account auth error", Toast.LENGTH_LONG).show() + return@launch + } + openWebView(authUrl) + } + } + } + + private val accountObserver = object : AccountObserver { + + @Suppress("EmptyFunctionBlock") + override fun onLoggedOut() {} + + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { + launch { accountManager.syncNow(SyncReason.User) } + } + + @Suppress("EmptyFunctionBlock") + override fun onProfileUpdated(profile: Profile) {} + + override fun onAuthenticationProblems() { + launch { + Toast.makeText(this@MainActivity, "Account auth problem", Toast.LENGTH_LONG).show() + } + } + } + + override fun onDestroy() { + super.onDestroy() + account.close() + job.cancel() + } + + private fun openWebView(url: String) { + supportFragmentManager.beginTransaction().apply { + replace(R.id.container, LoginFragment.create(url, REDIRECT_URL)) + addToBackStack(null) + commit() + } + } + + override fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) { + launch { + accountManager.finishAuthentication( + FxaAuthData(action.toAuthType(), code = code, state = state), + ) + supportFragmentManager.popBackStack() + } + } + + // SyncManager observable interface: + override fun onStarted() { + Toast.makeText(this@MainActivity, "Syncing...", Toast.LENGTH_SHORT).show() + } + + override fun onIdle() { + Toast.makeText(this@MainActivity, "Logins sync success", Toast.LENGTH_SHORT).show() + + launch { + val syncedLogins = loginsStorage.value.list() + adapter.addAll(syncedLogins.map { "Login: " + it.origin }) + adapter.notifyDataSetChanged() + } + } + + override fun onError(error: java.lang.Exception?) { + Toast.makeText( + this@MainActivity, + "Logins sync error ${error?.localizedMessage}", + Toast.LENGTH_SHORT, + ).show() + } +} diff --git a/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/SampleFxAEntryPoint.kt b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/SampleFxAEntryPoint.kt new file mode 100644 index 0000000000..773b5ee844 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/SampleFxAEntryPoint.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.sync.logins + +import mozilla.components.concept.sync.FxAEntryPoint + +/** + * An implementation of [FxAEntryPoint] for the sample application. + */ +enum class SampleFxAEntryPoint(override val entryName: String) : FxAEntryPoint { + HomeMenu("home-menu"), +} diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..c4add9e840 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/res/layout/activity_main.xml @@ -0,0 +1,38 @@ +<?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:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:id="@+id/container" + tools:context="org.mozilla.samples.sync.logins.MainActivity"> + + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:orientation="vertical" + android:id="@+id/buttonList" + tools:context="org.mozilla.samples.sync.logins.MainActivity" + tools:ignore="UselessParent"> + + <ListView + android:id="@+id/logins_list_view" + android:layout_width="fill_parent" + android:layout_height="150dp" + /> + <Button + android:id="@+id/buttonWebView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/sign_in_webview" + android:textAlignment="center" /> + + </LinearLayout> +</RelativeLayout> diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/layout/fragment_view.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/layout/fragment_view.xml new file mode 100644 index 0000000000..44536f4f68 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/res/layout/fragment_view.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/. --> + +<WebView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/webview" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".LoginFragment" /> diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..a2f5908281 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..ff10afd6e1 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..dcd3cd8083 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..8ca12fe024 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..b824ebdd48 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/values/strings.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/values/strings.xml new file mode 100644 index 0000000000..82cbb84eb1 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/src/main/res/values/strings.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/. --> +<resources> + <string name="app_name">Sync - Logins Demo</string> + <string name="sign_in_webview">FxA sign in</string> +</resources> diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..820ae61afa --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/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/sync-logins/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..55da967560 --- /dev/null +++ b/mobile/android/android-components/samples/sync-logins/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/sync/README.md b/mobile/android/android-components/samples/sync/README.md new file mode 100644 index 0000000000..1790aaca5b --- /dev/null +++ b/mobile/android/android-components/samples/sync/README.md @@ -0,0 +1,25 @@ +# [Android Components](../../README.md) > Samples > Firefox Sync + +![](src/main/res/mipmap-xhdpi/ic_launcher.png) + +A simple app showcasing how to use `FxaAccountManager` together with `BackgroundSyncManager` and `browser-storage-sync` components +to (periodically) synchronize FxA data in a background worker. + +## Concepts + +This app demonstrates how to synchronize Firefox Account data (bookmarks, history, ...). + +This app could be "easily" generalized to synchronize other types of data stores, if another implementation of `concept-storage` +is used. + +Following basic bits of functionality are present: + +* Configuring `FxaAccountManager` and `BackgroundSyncManager` +* Making `Syncable` stores (such as `PlacesHistoryStorage`) available to background workers via `GlobalSyncableStoreProvider` +* Configuring listeners to monitor account status (logged in, logged out) and sync status (in-progress, finished, querying local data) + +## License + + 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/ diff --git a/mobile/android/android-components/samples/sync/build.gradle b/mobile/android/android-components/samples/sync/build.gradle new file mode 100644 index 0000000000..bb99c4220b --- /dev/null +++ b/mobile/android/android-components/samples/sync/build.gradle @@ -0,0 +1,58 @@ +/* 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/. */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + applicationId "org.mozilla.samples.sync" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + splits { + abi { + enable true + reset() + include 'x86', 'arm64-v8a', 'armeabi-v7a' + } + } + + buildFeatures { + viewBinding true + buildConfig true + } + + namespace 'org.mozilla.samples.sync' +} + +dependencies { + implementation project(':concept-storage') + implementation project(':concept-toolbar') + implementation project(':browser-storage-sync') + implementation project(':service-firefox-accounts') + implementation project(':service-sync-logins') + implementation project(':service-sync-autofill') + implementation project(':support-rustlog') + implementation project(':support-rusthttp') + implementation project(':lib-fetch-httpurlconnection') + implementation project(':lib-dataprotect') + + implementation ComponentsDependencies.kotlin_reflect + implementation ComponentsDependencies.androidx_fragment + implementation ComponentsDependencies.androidx_recyclerview +} diff --git a/mobile/android/android-components/samples/sync/gradle.properties b/mobile/android/android-components/samples/sync/gradle.properties new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/mobile/android/android-components/samples/sync/gradle.properties diff --git a/mobile/android/android-components/samples/sync/proguard-rules.pro b/mobile/android/android-components/samples/sync/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/sync/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..eb03b1d407 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ +<?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.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + + <application + android:allowBackup="true" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/Theme.AppCompat.Light" + android:dataExtractionRules="@xml/data_extraction_rules" + tools:targetApi="s" + tools:ignore="DataExtractionRules"> + <activity + android:name=".MainActivity" + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT" /> + <data + android:host="*" + android:scheme="fxaclient" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt new file mode 100644 index 0000000000..a2fa4c15b6 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt @@ -0,0 +1,85 @@ +/* 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.sync + +import android.annotation.SuppressLint +import android.content.Context +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.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.concept.sync.Device + +/** + * A fragment representing a list of Items. + * Activities containing this fragment MUST implement the + * [DeviceFragment.OnDeviceListInteractionListener] interface. + */ +class DeviceFragment : Fragment() { + + private var listenerDevice: OnDeviceListInteractionListener? = null + + private val adapter = DeviceRecyclerViewAdapter(listenerDevice) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + val view = inflater.inflate(R.layout.fragment_device_list, container, false) + + // Set the adapter + if (view is RecyclerView) { + with(view) { + layoutManager = LinearLayoutManager(context) + adapter = this@DeviceFragment.adapter + } + } + return view + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is OnDeviceListInteractionListener) { + listenerDevice = context + adapter.mListenerDevice = context + } else { + throw IllegalArgumentException("$context must implement OnDeviceListInteractionListener") + } + } + + override fun onDetach() { + super.onDetach() + listenerDevice = null + } + + /** + * Updates the list of devices. + */ + @SuppressLint("NotifyDataSetChanged") + fun updateDevices(devices: List<Device>) { + adapter.devices.clear() + adapter.devices.addAll(devices) + adapter.notifyDataSetChanged() + } + + /** + * This interface must be implemented by activities that contain this + * fragment to allow an interaction in this fragment to be communicated + * to the activity and potentially other fragments contained in that + * activity. + * + * + * See the Android Training lesson + * [Communicating with Other Fragments](http://developer.android.com/training/basics/fragments/communicating.html) + * for more information. + */ + interface OnDeviceListInteractionListener { + fun onDeviceInteraction(item: Device) + } +} diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt new file mode 100644 index 0000000000..f240b793b9 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt @@ -0,0 +1,68 @@ +/* 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.sync + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.concept.sync.Device +import mozilla.components.concept.sync.DeviceType +import org.mozilla.samples.sync.DeviceFragment.OnDeviceListInteractionListener +import org.mozilla.samples.sync.databinding.FragmentDeviceBinding + +/** + * [RecyclerView.Adapter] that can display a [DummyItem] and makes a call to the + * specified [OnDeviceListInteractionListener]. + */ +class DeviceRecyclerViewAdapter( + var mListenerDevice: OnDeviceListInteractionListener?, +) : RecyclerView.Adapter<DeviceRecyclerViewAdapter.ViewHolder>() { + + val devices = mutableListOf<Device>() + + private val mOnClickListener: View.OnClickListener + + init { + mOnClickListener = View.OnClickListener { v -> + val item = v.tag as Device + // Notify the active callbacks interface (the activity, if the fragment is attached to + // one) that an item has been selected. + mListenerDevice?.onDeviceInteraction(item) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = FragmentDeviceBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = devices[position] + holder.nameView.text = item.displayName + holder.typeView.text = when (item.deviceType) { + DeviceType.DESKTOP -> "Desktop" + DeviceType.MOBILE -> "Mobile" + DeviceType.TABLET -> "Tablet" + DeviceType.TV -> "TV" + DeviceType.VR -> "VR" + DeviceType.UNKNOWN -> "Unknown" + } + + with(holder.itemView) { + tag = item + setOnClickListener(mOnClickListener) + } + } + + override fun getItemCount(): Int = devices.size + + inner class ViewHolder(binding: FragmentDeviceBinding) : RecyclerView.ViewHolder(binding.root) { + val nameView: TextView = binding.deviceName + val typeView: TextView = binding.deviceType + } +} diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt new file mode 100644 index 0000000000..ac3802c0c6 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt @@ -0,0 +1,108 @@ +/* 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.sync + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.CookieManager +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.fragment.app.Fragment + +class LoginFragment : Fragment() { + + private lateinit var authUrl: String + private lateinit var redirectUrl: String + private var mWebView: WebView? = null + private var listener: OnLoginCompleteListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + authUrl = it.getString(AUTH_URL)!! + redirectUrl = it.getString(REDIRECT_URL)!! + } + } + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view: View = inflater.inflate(R.layout.fragment_view, container, false) + val webView = view.findViewById<WebView>(R.id.webview) + // Need JS, cookies and localStorage. + webView.settings.domStorageEnabled = true + webView.settings.javaScriptEnabled = true + CookieManager.getInstance().setAcceptCookie(true) + + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + if (url != null && url.startsWith(redirectUrl)) { + val uri = Uri.parse(url) + val code = uri.getQueryParameter("code") + val state = uri.getQueryParameter("state") + val action = uri.getQueryParameter("action") + if (code != null && state != null && action != null) { + listener?.onLoginComplete(code, state, action, this@LoginFragment) + } + } + + super.onPageStarted(view, url, favicon) + } + } + webView.loadUrl(authUrl) + + mWebView?.destroy() + mWebView = webView + + return view + } + + @Suppress("TooGenericExceptionThrown") + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is OnLoginCompleteListener) { + listener = context + } else { + throw IllegalStateException("$context must implement OnLoginCompleteListener") + } + } + + override fun onDetach() { + super.onDetach() + listener = null + } + + override fun onPause() { + super.onPause() + mWebView?.onPause() + } + + override fun onResume() { + super.onResume() + mWebView?.onResume() + } + + interface OnLoginCompleteListener { + fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) + } + + companion object { + const val AUTH_URL = "authUrl" + const val REDIRECT_URL = "redirectUrl" + + fun create(authUrl: String, redirectUrl: String): LoginFragment = + LoginFragment().apply { + arguments = Bundle().apply { + putString(AUTH_URL, authUrl) + putString(REDIRECT_URL, redirectUrl) + } + } + } +} diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt new file mode 100644 index 0000000000..06f889d441 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt @@ -0,0 +1,464 @@ +/* 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.sync + +import android.os.Bundle +import android.text.method.ScrollingMovementMethod +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.appservices.fxaclient.FxaServer +import mozilla.components.browser.storage.sync.PlacesBookmarksStorage +import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.concept.storage.BookmarkNode +import mozilla.components.concept.sync.AccountEvent +import mozilla.components.concept.sync.AccountEventsObserver +import mozilla.components.concept.sync.AccountObserver +import mozilla.components.concept.sync.AuthFlowError +import mozilla.components.concept.sync.AuthType +import mozilla.components.concept.sync.ConstellationState +import mozilla.components.concept.sync.Device +import mozilla.components.concept.sync.DeviceCapability +import mozilla.components.concept.sync.DeviceCommandIncoming +import mozilla.components.concept.sync.DeviceCommandOutgoing +import mozilla.components.concept.sync.DeviceConfig +import mozilla.components.concept.sync.DeviceConstellationObserver +import mozilla.components.concept.sync.DeviceType +import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.concept.sync.Profile +import mozilla.components.lib.dataprotect.SecureAbove22Preferences +import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient +import mozilla.components.service.fxa.FxaAuthData +import mozilla.components.service.fxa.PeriodicSyncConfig +import mozilla.components.service.fxa.ServerConfig +import mozilla.components.service.fxa.SyncConfig +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider +import mozilla.components.service.fxa.sync.SyncReason +import mozilla.components.service.fxa.sync.SyncStatusObserver +import mozilla.components.service.fxa.toAuthType +import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage +import mozilla.components.service.sync.logins.SyncableLoginsStorage +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.rusthttp.RustHttpConfig +import mozilla.components.support.rustlog.RustLog +import org.mozilla.samples.sync.databinding.ActivityMainBinding +import java.lang.Exception +import kotlin.coroutines.CoroutineContext + +class MainActivity : + AppCompatActivity(), + LoginFragment.OnLoginCompleteListener, + DeviceFragment.OnDeviceListInteractionListener, + CoroutineScope { + private val historyStorage = lazy { + PlacesHistoryStorage(this) + } + + private val bookmarksStorage = lazy { + PlacesBookmarksStorage(this) + } + + private val securePreferences by lazy { SecureAbove22Preferences(this, "key_store") } + + private val passwordsStorage = lazy { + SyncableLoginsStorage(this, lazy { securePreferences }) + } + + private val creditCardsAddressesStorage = lazy { + AutofillCreditCardsAddressesStorage(this, lazy { securePreferences }) + } + + private val creditCardKeyProvider by lazy { creditCardsAddressesStorage.value.crypto } + private val passwordsKeyProvider by lazy { passwordsStorage.value.crypto } + + private val accountManager by lazy { + FxaAccountManager( + this, + ServerConfig(FxaServer.Release, CLIENT_ID, REDIRECT_URL), + DeviceConfig( + name = "A-C Sync Sample - ${System.currentTimeMillis()}", + type = DeviceType.MOBILE, + capabilities = setOf(DeviceCapability.SEND_TAB), + secureStateAtRest = true, + ), + SyncConfig( + setOf( + SyncEngine.History, + SyncEngine.Bookmarks, + SyncEngine.Passwords, + SyncEngine.Addresses, + SyncEngine.CreditCards, + ), + periodicSyncConfig = PeriodicSyncConfig(periodMinutes = 15, initialDelayMinutes = 5), + ), + ) + } + + private var job = Job() + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + companion object { + const val CLIENT_ID = "3c49430b43dfba77" + const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + } + + private val logger = Logger("SampleSync") + + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + + RustLog.enable() + RustHttpConfig.setClient(lazy { HttpURLConnectionClient() }) + + Log.addSink(AndroidLogSink()) + + setContentView(binding.root) + + findViewById<View>(R.id.buttonSignIn).setOnClickListener { + launch { + accountManager.beginAuthentication(entrypoint = SampleFxAEntryPoint.HomeMenu)?.let { openWebView(it) } + } + } + + findViewById<View>(R.id.buttonLogout).setOnClickListener { + launch { accountManager.logout() } + } + + findViewById<View>(R.id.refreshDevice).setOnClickListener { + launch { accountManager.authenticatedAccount()?.deviceConstellation()?.refreshDevices() } + } + + findViewById<View>(R.id.sendTab).setOnClickListener { + launch { + accountManager.authenticatedAccount()?.deviceConstellation()?.let { constellation -> + // Ignore devices that can't receive tabs. + val targets = constellation.state()?.otherDevices?.filter { + it.capabilities.contains(DeviceCapability.SEND_TAB) + } + + targets?.forEach { + constellation.sendCommandToDevice( + it.id, + DeviceCommandOutgoing.SendTab("Sample tab", "https://www.mozilla.org"), + ) + } + + Toast.makeText( + this@MainActivity, + "Sent sample tab to ${targets?.size ?: 0} device(s)", + Toast.LENGTH_SHORT, + ).show() + } + } + } + + // NB: ObserverRegistry takes care of unregistering this observer when appropriate, and + // cleaning up any internal references to 'observer' and 'owner'. + // Observe changes to the account and profile. + accountManager.register(accountObserver, owner = this, autoPause = true) + // Observe sync state changes. + accountManager.registerForSyncEvents(syncObserver, owner = this, autoPause = true) + // Observe incoming device commands. + accountManager.registerForAccountEvents(accountEventsObserver, owner = this, autoPause = true) + + GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage) + GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage) + GlobalSyncableStoreProvider.configureStore( + storePair = SyncEngine.Passwords to passwordsStorage, + keyProvider = lazy { passwordsKeyProvider }, + ) + GlobalSyncableStoreProvider.configureStore( + storePair = SyncEngine.CreditCards to creditCardsAddressesStorage, + keyProvider = lazy { creditCardKeyProvider }, + ) + GlobalSyncableStoreProvider.configureStore(SyncEngine.Addresses to creditCardsAddressesStorage) + + launch { + // Now that our account state observer is registered, we can kick off the account manager. + accountManager.start() + } + + findViewById<View>(R.id.buttonSync).setOnClickListener { + launch { + accountManager.syncNow(SyncReason.User) + accountManager.authenticatedAccount()?.deviceConstellation()?.pollForCommands() + } + } + } + + override fun onDestroy() { + super.onDestroy() + accountManager.close() + job.cancel() + } + + override fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) { + launch { + supportFragmentManager.popBackStack() + accountManager.finishAuthentication( + FxaAuthData(action.toAuthType(), code = code, state = state), + ) + } + } + + override fun onDeviceInteraction(item: Device) { + Toast.makeText( + this@MainActivity, + getString( + R.string.full_device_details, + item.id, + item.displayName, + item.deviceType, + item.subscriptionExpired, + item.subscription, + item.capabilities, + item.lastAccessTime, + ), + Toast.LENGTH_LONG, + ).show() + } + + private fun openWebView(url: String) { + supportFragmentManager.beginTransaction().apply { + replace(R.id.container, LoginFragment.create(url, REDIRECT_URL)) + addToBackStack(null) + commit() + } + } + + private val deviceConstellationObserver = object : DeviceConstellationObserver { + override fun onDevicesUpdate(constellation: ConstellationState) { + launch { + val currentDevice = constellation.currentDevice + + val currentDeviceView: TextView = findViewById(R.id.currentDevice) + if (currentDevice != null) { + currentDeviceView.text = getString( + R.string.full_device_details, + currentDevice.id, + currentDevice.displayName, + currentDevice.deviceType, + currentDevice.subscriptionExpired, + currentDevice.subscription, + currentDevice.capabilities, + currentDevice.lastAccessTime, + ) + } else { + currentDeviceView.text = getString(R.string.current_device_unknown) + } + + val devicesFragment = supportFragmentManager.findFragmentById(R.id.devices_fragment) as DeviceFragment + devicesFragment.updateDevices(constellation.otherDevices) + + Toast.makeText(this@MainActivity, "Devices updated", Toast.LENGTH_SHORT).show() + } + } + } + + @Suppress("SetTextI18n", "NestedBlockDepth") + private val accountEventsObserver = object : AccountEventsObserver { + override fun onEvents(events: List<AccountEvent>) { + val txtView: TextView = findViewById(R.id.latestTabs) + events.forEach { + when (it) { + is AccountEvent.DeviceCommandIncoming -> { + when (it.command) { + is DeviceCommandIncoming.TabReceived -> { + val cmd = it.command as DeviceCommandIncoming.TabReceived + var tabsStringified = "Tab(s) from: ${cmd.from?.displayName}\n" + cmd.entries.forEach { tab -> + tabsStringified += "${tab.title}: ${tab.url}\n" + } + txtView.text = tabsStringified + } + } + } + is AccountEvent.ProfileUpdated -> { + txtView.text = "The user's profile was updated" + } + is AccountEvent.AccountAuthStateChanged -> { + txtView.text = "The account auth state changed" + } + is AccountEvent.AccountDestroyed -> { + txtView.text = "The account was destroyed" + } + is AccountEvent.DeviceConnected -> { + txtView.text = "Another device connected to the account" + } + is AccountEvent.DeviceDisconnected -> { + if (it.isLocalDevice) { + txtView.text = "This device disconnected" + } else { + txtView.text = "The device ${it.deviceId} disconnected" + } + } + is AccountEvent.Unknown -> { + // Unknown events are ignored to allow supporting new + // account events + } + } + } + } + } + + private val accountObserver = object : AccountObserver { + lateinit var lastAuthType: AuthType + + override fun onLoggedOut() { + logger.info("onLoggedOut") + + launch { + val txtView: TextView = findViewById(R.id.fxaStatusView) + txtView.text = getString(R.string.logged_out) + + val historyResultTextView: TextView = findViewById(R.id.historySyncResult) + historyResultTextView.text = "" + val bookmarksResultTextView: TextView = findViewById(R.id.bookmarksSyncResult) + bookmarksResultTextView.text = "" + val currentDeviceTextView: TextView = findViewById(R.id.currentDevice) + currentDeviceTextView.text = "" + + val devicesFragment = supportFragmentManager.findFragmentById( + R.id.devices_fragment, + ) as DeviceFragment + devicesFragment.updateDevices(listOf()) + + findViewById<View>(R.id.buttonLogout).visibility = View.INVISIBLE + findViewById<View>(R.id.buttonSignIn).visibility = View.VISIBLE + findViewById<View>(R.id.buttonSync).visibility = View.INVISIBLE + findViewById<View>(R.id.refreshDevice).visibility = View.INVISIBLE + findViewById<View>(R.id.sendTab).visibility = View.INVISIBLE + } + } + + override fun onAuthenticationProblems() { + logger.info("onAuthenticationProblems") + + launch { + val txtView: TextView = findViewById(R.id.fxaStatusView) + txtView.text = getString(R.string.need_reauth) + + findViewById<View>(R.id.buttonSignIn).visibility = View.VISIBLE + } + } + + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { + logger.info("onAuthenticated") + + launch { + lastAuthType = authType + + val txtView: TextView = findViewById(R.id.fxaStatusView) + txtView.text = getString(R.string.signed_in_waiting_for_profile, authType::class.simpleName) + + findViewById<View>(R.id.buttonLogout).visibility = View.VISIBLE + findViewById<View>(R.id.buttonSignIn).visibility = View.INVISIBLE + findViewById<View>(R.id.buttonSync).visibility = View.VISIBLE + findViewById<View>(R.id.refreshDevice).visibility = View.VISIBLE + findViewById<View>(R.id.sendTab).visibility = View.VISIBLE + + account.deviceConstellation().registerDeviceObserver( + deviceConstellationObserver, + this@MainActivity, + true, + ) + } + } + + override fun onProfileUpdated(profile: Profile) { + logger.info("onProfileUpdated") + + launch { + val txtView: TextView = findViewById(R.id.fxaStatusView) + txtView.text = getString( + R.string.signed_in_with_profile, + lastAuthType::class.simpleName, + "${profile.displayName ?: ""} ${profile.email}", + ) + } + } + + override fun onFlowError(error: AuthFlowError) { + launch { + val txtView: TextView = findViewById(R.id.fxaStatusView) + txtView.text = getString( + R.string.account_error, + when (error) { + AuthFlowError.FailedToBeginAuth -> "Failed to begin authentication" + AuthFlowError.FailedToCompleteAuth -> "Failed to complete authentication" + }, + ) + } + } + } + + private val syncObserver = object : SyncStatusObserver { + override fun onStarted() { + logger.info("onSyncStarted") + CoroutineScope(Dispatchers.Main).launch { + binding.syncStatus.text = getString(R.string.syncing) + } + } + + override fun onIdle() { + logger.info("onSyncIdle") + CoroutineScope(Dispatchers.Main).launch { + binding.syncStatus.text = getString(R.string.sync_idle) + + val historyResultTextView: TextView = findViewById(R.id.historySyncResult) + val visitedCount = withContext(Dispatchers.IO) { historyStorage.value.getVisited().size } + // visitedCount is passed twice: to get the correct plural form, and then as + // an argument for string formatting. + historyResultTextView.text = resources.getQuantityString( + R.plurals.visited_url_count, + visitedCount, + visitedCount, + ) + + val bookmarksResultTextView: TextView = findViewById(R.id.bookmarksSyncResult) + bookmarksResultTextView.setHorizontallyScrolling(true) + bookmarksResultTextView.movementMethod = ScrollingMovementMethod.getInstance() + bookmarksResultTextView.text = withContext(Dispatchers.IO) { + val bookmarksRoot = bookmarksStorage.value.getTree("root________", recursive = true) + if (bookmarksRoot == null) { + getString(R.string.no_bookmarks_root) + } else { + var bookmarksRootAndChildren = "BOOKMARKS\n" + fun addTreeNode(node: BookmarkNode, depth: Int) { + val desc = " ".repeat(depth * 2) + "${node.title} - ${node.url} (${node.guid})\n" + bookmarksRootAndChildren += desc + node.children?.forEach { + addTreeNode(it, depth + 1) + } + } + addTreeNode(bookmarksRoot, 0) + bookmarksRootAndChildren + } + } + } + } + + override fun onError(error: Exception?) { + logger.error("onSyncError", error) + CoroutineScope(Dispatchers.Main).launch { + binding.syncStatus.text = getString(R.string.sync_error, error) + } + } + } +} diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt new file mode 100644 index 0000000000..3ea2fe51a4 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.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.sync + +import mozilla.components.concept.sync.FxAEntryPoint + +/** + * An implementation of [FxAEntryPoint] for the sample application. + */ +enum class SampleFxAEntryPoint(override val entryName: String) : FxAEntryPoint { + HomeMenu("home-menu"), +} diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..816c5cd0e6 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml @@ -0,0 +1,146 @@ +<?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:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:id="@+id/container" + tools:context=".MainActivity"> + + <ScrollView + tools:ignore="UselessParent" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <RelativeLayout + android:id="@+id/stuff" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <Button + android:id="@+id/buttonSignIn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/sign_in" + android:textAlignment="center" /> + + <Button + android:id="@+id/buttonSync" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/buttonSignIn" + android:text="@string/sync" + android:textAlignment="center" /> + + <Button + android:id="@+id/refreshDevice" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/buttonSync" + android:text="@string/refresh_device" + android:textAlignment="center" /> + + <Button + android:id="@+id/sendTab" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/refreshDevice" + android:text="@string/send_tab" + android:textAlignment="center" /> + + <Button + android:id="@+id/buttonLogout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/sendTab" + android:text="@string/log_out" + android:textAlignment="center" /> + + <TextView + android:id="@+id/fxaStatusView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/buttonLogout" + android:text="" /> + + <TextView + android:id="@+id/syncStatus" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/fxaStatusView" + android:text="" /> + + <TextView + android:id="@+id/historySyncResult" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/syncStatus" + android:text="" /> + + <TextView + android:id="@+id/bookmarksSyncResult" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/historySyncResult" + android:text="" /> + + <TextView + android:id="@+id/currentDeviceLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/bookmarksSyncResult" + style="?android:attr/listSeparatorTextViewStyle" + android:text="@string/current_device" /> + + <TextView + android:id="@+id/currentDevice" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/currentDeviceLabel" + android:text="" /> + + <TextView + android:id="@+id/latestTabsLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/currentDevice" + style="?android:attr/listSeparatorTextViewStyle" + android:text="@string/latest_tabs" /> + + <TextView + android:id="@+id/latestTabs" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/latestTabsLabel" + android:text="" /> + + <TextView + android:id="@+id/devicesLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_below="@id/latestTabs" + style="?android:attr/listSeparatorTextViewStyle" + android:text="@string/devices" /> + + <androidx.fragment.app.FragmentContainerView android:name="org.mozilla.samples.sync.DeviceFragment" + android:id="@+id/devices_fragment" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/devicesLabel" /> + </RelativeLayout> + </ScrollView> + +</RelativeLayout> diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml new file mode 100644 index 0000000000..1cd0773ff9 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml @@ -0,0 +1,24 @@ +<?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" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <TextView + android:id="@+id/device_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/text_margin" + android:textAppearance="?attr/textAppearanceListItem" /> + + <TextView + android:id="@+id/device_type" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/text_margin" + android:textAppearance="?attr/textAppearanceListItem" /> +</LinearLayout> diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml new file mode 100644 index 0000000000..4af98d4db2 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml @@ -0,0 +1,15 @@ +<?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.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/list" + android:name="org.mozilla.samples.sync.DeviceFragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + tools:context=".DeviceFragment" + tools:listitem="@layout/fragment_device" />
\ No newline at end of file diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml new file mode 100644 index 0000000000..44536f4f68 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.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/. --> + +<WebView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/webview" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".LoginFragment" /> diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..e2edeb6cbe --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..0c12478a8e --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..abdaf95771 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..0c8508a62b --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..ba2b17573d --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml b/mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..3544b113bd --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/values/dimens.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/. --> + +<resources> + <dimen name="text_margin">16dp</dimen> +</resources> diff --git a/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml b/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml new file mode 100644 index 0000000000..b3f00174e9 --- /dev/null +++ b/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml @@ -0,0 +1,39 @@ +<?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">Firefox Sync Demo</string> + <string name="sign_in">FxA sign in</string> + <string name="signed_in_waiting_for_profile">Signed in (type=%1$s); waiting for profile</string> + <string name="signed_in_with_profile">Authenticated (type=%1$s) as %2$s</string> + <string name="account_error">FxA error: %1$s</string> + <string name="sync_idle">Sync is idle</string> + <string name="syncing">Syncing…</string> + <string name="sync_error">Sync error: %1$s</string> + <plurals name="visited_url_count"> + <item quantity="zero">There are no visited URLs</item> + <item quantity="one">There is %d visited URL</item> + <item quantity="other">There are %d visited URLs</item> + </plurals> + <string name="no_bookmarks_root">No Bookmarks Root node</string> + <string name="log_out">FxA Log Out</string> + <string name="logged_out">Logged out!</string> + <string name="need_reauth">Need to re-authenticate</string> + <string name="sync">Sync</string> + <string name="refresh_device">Refresh device</string> + <string name="send_tab">Send tab</string> + <string name="current_device">Current device</string> + <string name="current_device_unknown">Unknown</string> + <string name="full_device_details"> + ID: %1$s\n + Name: %2$s\n + Type: %3$s\n + Subscription expired: %4$b\n + Subscription: %5$s\n + Capabilities: %6$s\n + Last access: %7$d + </string> + <string name="latest_tabs">Latest tabs</string> + <string name="devices">Devices</string> +</resources> diff --git a/mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..820ae61afa --- /dev/null +++ b/mobile/android/android-components/samples/sync/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/sync/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/sync/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..55da967560 --- /dev/null +++ b/mobile/android/android-components/samples/sync/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/toolbar/build.gradle b/mobile/android/android-components/samples/toolbar/build.gradle new file mode 100644 index 0000000000..896f3c77f1 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/build.gradle @@ -0,0 +1,57 @@ +/* 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/. */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + applicationId "org.mozilla.samples.toolbar" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + viewBinding true + } + + namespace 'org.mozilla.samples.toolbar' +} + + +dependencies { + implementation project(':concept-engine') + implementation project(':concept-menu') + implementation project(':browser-toolbar') + implementation project(':browser-menu') + implementation project(':browser-menu2') + implementation project(':browser-domains') + + implementation project(':ui-colors') + implementation project(':ui-tabcounter') + implementation project(':ui-icons') + + implementation project(':feature-toolbar') + + implementation project(':support-ktx') + + implementation project(':support-utils') + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_recyclerview +} diff --git a/mobile/android/android-components/samples/toolbar/lint.xml b/mobile/android/android-components/samples/toolbar/lint.xml new file mode 100644 index 0000000000..33cf423701 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/lint.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/. --> +<lint> + <issue id="IconMissingDensityFolder" severity="ignore"> + <!-- Suppress lint warnings on mdpi --> + <ignore path="src/debug/res/drawable-mdpi"/> + </issue> + + <issue id="GoogleAppIndexingWarning" severity="ignore" /> +</lint>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/toolbar/proguard-rules.pro b/mobile/android/android-components/samples/toolbar/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/samples/toolbar/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/toolbar/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..71bbaeb7d9 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/AndroidManifest.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/. --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <uses-permission android:name="android.permission.INTERNET" /> + + <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.NoActionBar" + android:dataExtractionRules="@xml/data_extraction_rules" + tools:targetApi="s" + tools:ignore="DataExtractionRules"> + <activity android:name=".ToolbarActivity" android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/mobile/android/android-components/samples/toolbar/src/main/ic_launcher-web.png b/mobile/android/android-components/samples/toolbar/src/main/ic_launcher-web.png Binary files differnew file mode 100644 index 0000000000..b8f772f66a --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/ic_launcher-web.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/SampleToolbarHelpers.kt b/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/SampleToolbarHelpers.kt new file mode 100644 index 0000000000..7b98f77f80 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/SampleToolbarHelpers.kt @@ -0,0 +1,148 @@ +/* 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.toolbar + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.Canvas +import android.graphics.drawable.ClipDrawable +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +// Code needed for assembling the sample application - but not needed to actually explain the toolbar + +enum class ToolbarConfiguration(val label: String) { + DEFAULT("Default"), + FOCUS_TABLET("Firefox Focus (Tablet)"), + FOCUS_PHONE("Firefox Focus (Phone)"), + CUSTOM_MENU("Custom Menu"), + PRIVATE_MODE("Private Mode"), + FENIX("Fenix"), + FENIX_CUSTOMTAB("Fenix (Custom Tab)"), +} + +class ConfigurationAdapter( + private val configuration: ToolbarConfiguration, +) : RecyclerView.Adapter<ConfigurationViewHolder>() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConfigurationViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_toolbar_configuration, parent, false) + return ConfigurationViewHolder(view as TextView) + } + + override fun getItemCount() = ToolbarConfiguration.values().size + + override fun onBindViewHolder(holder: ConfigurationViewHolder, position: Int) { + val item = ToolbarConfiguration.values()[position] + holder.labelView.text = item.label + + holder.labelView.setOnClickListener { + (it.context as Activity).finish() + + val intent = Intent(it.context, ToolbarActivity::class.java) + intent.putExtra(Extra.TOOLBAR_LABEL, item.label) + it.context.startActivity(intent) + } + + if (item == configuration) { + holder.labelView.setBackgroundResource(R.color.selected_configuration) + } + } +} + +class ConfigurationViewHolder(val labelView: TextView) : RecyclerView.ViewHolder(labelView) + +fun getToolbarConfiguration(intent: Intent): ToolbarConfiguration { + val label = intent.extras?.getString(Extra.TOOLBAR_LABEL) ?: ToolbarConfiguration.DEFAULT.label + + ToolbarConfiguration.values().forEach { + if (label == it.label) { + return it + } + } + + return ToolbarConfiguration.DEFAULT +} + +object Extra { + internal const val TOOLBAR_LABEL = "toolbar_label" +} + +/** + * A custom view to be drawn behind the URL and page actions. Acts as a custom progress view. + */ +class UrlBoxProgressView( + context: Context, +) : View(context) { + var progress: Int = 0 + set(value) { + // We clip the background and progress drawable based on the new progress: + // + // progress + // v + // +---------------------+-------------------+ + // | background drawable | progress drawable | + // +---------------------+-------------------+ + // + // The drawable is clipped completely and not visible when the level is 0 and fully + // revealed when the level is 10,000. + backgroundDrawable.level = LEVEL_STEP_SIZE * (MAX_PROGRESS - value) + progressDrawable.level = MAX_LEVEL - backgroundDrawable.level + field = value + invalidate() // Force redraw + + // If the progress is 100% then we want to go back to 0 to hide the progress drawable + // again. However we want to show the full progress bar briefly so we wait 250ms before + // going back to 0. + if (value == MAX_PROGRESS) { + CoroutineScope(Dispatchers.Main).launch { + delay(PROGRESS_VISIBLE_DELAY_MS) + progress = 0 + } + } + } + + private var backgroundDrawable = ClipDrawable( + ResourcesCompat.getDrawable(resources, R.drawable.sample_url_background, context.theme), + Gravity.END, + ClipDrawable.HORIZONTAL, + ).apply { + level = MAX_LEVEL + } + + private var progressDrawable = ClipDrawable( + ResourcesCompat.getDrawable(resources, R.drawable.sample_url_progress, context.theme), + Gravity.START, + ClipDrawable.HORIZONTAL, + ).apply { + level = 0 + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + backgroundDrawable.setBounds(0, 0, w, h) + progressDrawable.setBounds(0, 0, w, h) + } + + override fun onDraw(canvas: Canvas) { + backgroundDrawable.draw(canvas) + progressDrawable.draw(canvas) + } + + companion object { + private const val MAX_PROGRESS = 100 + private const val PROGRESS_VISIBLE_DELAY_MS = 250L + private const val LEVEL_STEP_SIZE = 100 + private const val MAX_LEVEL = 10000 + } +} diff --git a/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/ToolbarActivity.kt b/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/ToolbarActivity.kt new file mode 100644 index 0000000000..58dbb8a312 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/ToolbarActivity.kt @@ -0,0 +1,539 @@ +/* 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.toolbar + +import android.content.res.Resources +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import mozilla.components.browser.domains.autocomplete.CustomDomainsProvider +import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.ext.asCandidateList +import mozilla.components.browser.menu.item.BrowserMenuItemToolbar +import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +import mozilla.components.browser.menu2.BrowserMenuController +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.browser.toolbar.display.DisplayToolbar +import mozilla.components.concept.menu.Side +import mozilla.components.concept.menu.candidate.DividerMenuCandidate +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.NestedMenuCandidate +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.toolbar.Toolbar +import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature +import mozilla.components.support.ktx.android.content.res.resolveAttribute +import mozilla.components.support.ktx.android.view.hideKeyboard +import mozilla.components.support.ktx.util.URLStringUtils +import mozilla.components.ui.tabcounter.TabCounter +import org.mozilla.samples.toolbar.databinding.ActivityToolbarBinding +import mozilla.components.browser.menu.R as menuR +import mozilla.components.browser.toolbar.R as toolbarR +import mozilla.components.ui.colors.R as colorsR +import mozilla.components.ui.icons.R as iconsR + +/** + * This sample application shows how to use and customize the browser-toolbar component. + */ +@Suppress("LargeClass") +class ToolbarActivity : AppCompatActivity() { + private val shippedDomainsProvider = ShippedDomainsProvider() + private val customDomainsProvider = CustomDomainsProvider() + private lateinit var binding: ActivityToolbarBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityToolbarBinding.inflate(layoutInflater) + + shippedDomainsProvider.initialize(this) + customDomainsProvider.initialize(this) + + setContentView(binding.root) + + val configuration = getToolbarConfiguration(intent) + + when (configuration) { + ToolbarConfiguration.DEFAULT -> setupDefaultToolbar() + ToolbarConfiguration.FOCUS_TABLET -> setupFocusTabletToolbar() + ToolbarConfiguration.FOCUS_PHONE -> setupFocusPhoneToolbar() + ToolbarConfiguration.CUSTOM_MENU -> setupCustomMenu() + ToolbarConfiguration.PRIVATE_MODE -> setupDefaultToolbar(private = true) + ToolbarConfiguration.FENIX -> setupFenixToolbar() + ToolbarConfiguration.FENIX_CUSTOMTAB -> setupFenixCustomTabToolbar() + } + + val recyclerView: RecyclerView = findViewById(R.id.recyclerView) + recyclerView.adapter = ConfigurationAdapter(configuration) + recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) + + ToolbarAutocompleteFeature(binding.toolbar).apply { + updateAutocompleteProviders( + providers = listOf(shippedDomainsProvider, customDomainsProvider), + refreshAutocomplete = false, + ) + } + } + + override fun onPause() { + super.onPause() + + binding.toolbar.hideKeyboard() + } + + /** + * A very simple toolbar with mostly default values. + */ + private fun setupDefaultToolbar(private: Boolean = false) { + binding.toolbar.setBackgroundColor( + ContextCompat.getColor(this, colorsR.color.photonBlue80), + ) + + binding.toolbar.private = private + + binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/" + } + + /** + * A toolbar that looks like Firefox Focus on tablets. + */ + private fun setupFocusTabletToolbar() { + // ////////////////////////////////////////////////////////////////////////////////////////// + // Use the iconic gradient background + // ////////////////////////////////////////////////////////////////////////////////////////// + + val background = ContextCompat.getDrawable(this, R.drawable.focus_background) + binding.toolbar.background = background + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Add "back" and "forward" navigation actions + // ////////////////////////////////////////////////////////////////////////////////////////// + + val back = BrowserToolbar.Button( + resources.getThemedDrawable(iconsR.drawable.mozac_ic_back_24)!!, + "Back", + ) { + simulateReload() + } + + binding.toolbar.addNavigationAction(back) + + val forward = BrowserToolbar.Button( + resources.getThemedDrawable(iconsR.drawable.mozac_ic_forward_24)!!, + "Forward", + ) { + simulateReload() + } + + binding.toolbar.addNavigationAction(forward) + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Add a "reload" browser action that simulates reloading the current page + // ////////////////////////////////////////////////////////////////////////////////////////// + + val reload = BrowserToolbar.TwoStateButton( + primaryImage = resources.getThemedDrawable(iconsR.drawable.mozac_ic_arrow_clockwise_24)!!, + primaryContentDescription = "Reload", + secondaryImage = resources.getThemedDrawable(iconsR.drawable.mozac_ic_stop)!!, + secondaryContentDescription = "Stop", + isInPrimaryState = { loading.value != true }, + disableInSecondaryState = false, + ) { + if (loading.value == true) { + job?.cancel() + } else { + simulateReload() + } + } + binding.toolbar.addBrowserAction(reload) + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Create a menu that looks like the one in Firefox Focus + // ////////////////////////////////////////////////////////////////////////////////////////// + + val fenix = SimpleBrowserMenuItem("POWERED BY MOZILLA") + val share = SimpleBrowserMenuItem("Share…") { /* Do nothing */ } + val homeScreen = SimpleBrowserMenuItem("Add to Home screen") { /* Do nothing */ } + val open = SimpleBrowserMenuItem("Open in…") { /* Do nothing */ } + val settings = SimpleBrowserMenuItem("Settings") { /* Do nothing */ } + + val items = listOf(fenix, share, homeScreen, open, settings) + binding.toolbar.display.menuBuilder = BrowserMenuBuilder(items) + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Display a URL + // ////////////////////////////////////////////////////////////////////////////////////////// + + binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/mobile/" + } + + /** + * A custom browser menu. + */ + private fun setupCustomMenu() { + binding.toolbar.setBackgroundColor( + ContextCompat.getColor(this, colorsR.color.photonBlue80), + ) + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Create a menu with text and icons + // ////////////////////////////////////////////////////////////////////////////////////////// + + val share = TextMenuCandidate( + "Share", + start = DrawableMenuIcon(this, iconsR.drawable.mozac_ic_share_android_24), + ) { /* Do nothing */ } + + val search = TextMenuCandidate( + "Search", + start = DrawableMenuIcon(this, iconsR.drawable.mozac_ic_search_24), + ) { /* Do nothing */ } + + binding.toolbar.display.menuController = BrowserMenuController(Side.START).apply { + submitList(listOf(share, DividerMenuCandidate(), search)) + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Display a URL + // ////////////////////////////////////////////////////////////////////////////////////////// + + binding.toolbar.url = "https://www.mozilla.org/" + } + + /** + * A toolbar that looks like Firefox Focus on phones. + */ + private fun setupFocusPhoneToolbar() { + // ////////////////////////////////////////////////////////////////////////////////////////// + // Use the iconic gradient background + // ////////////////////////////////////////////////////////////////////////////////////////// + + val background = ContextCompat.getDrawable(this, R.drawable.focus_background) + binding.toolbar.background = background + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Create a "mini" toolbar to be shown inside the menu (forward, reload) + // ////////////////////////////////////////////////////////////////////////////////////////// + + val forward = BrowserMenuItemToolbar.Button( + iconsR.drawable.mozac_ic_forward_24, + "Forward", + isEnabled = { canGoForward() }, + ) { + simulateReload() + } + + val reload = BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = iconsR.drawable.mozac_ic_arrow_clockwise_24, + primaryContentDescription = "Reload", + secondaryImageResource = iconsR.drawable.mozac_ic_stop, + secondaryContentDescription = "Stop", + isInPrimaryState = { loading.value != true }, + disableInSecondaryState = false, + ) { + if (loading.value == true) { + job?.cancel() + } else { + simulateReload() + } + } + // Redraw the reload button when loading state changes + loading.observe(this, Observer { binding.toolbar.invalidateActions() }) + + val menuToolbar = BrowserMenuItemToolbar(listOf(forward, reload)) + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Create a custom "menu item" implementation that resembles Focus' global content blocking switch. + // ////////////////////////////////////////////////////////////////////////////////////////// + + val blocking = object : BrowserMenuItem { + // Always display this item. This lambda is executed when the user clicks on the menu + // button to determine whether this item should be shown. + override val visible = { true } + + override fun getLayoutResource() = R.layout.focus_blocking_switch + + override fun bind(menu: BrowserMenu, view: View) { + // Nothing to do here. + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Create a menu that looks like the one in Firefox Focus + // ////////////////////////////////////////////////////////////////////////////////////////// + + val share = SimpleBrowserMenuItem("Share…") { /* Do nothing */ } + val homeScreen = SimpleBrowserMenuItem("Add to Home screen") { /* Do nothing */ } + val open = SimpleBrowserMenuItem("Open in…") { /* Do nothing */ } + val settings = SimpleBrowserMenuItem("Settings") { /* Do nothing */ } + + val items = listOf(menuToolbar, blocking, share, homeScreen, open, settings) + binding.toolbar.display.menuBuilder = BrowserMenuBuilder(items) + binding.toolbar.invalidateActions() + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Display a URL + // ////////////////////////////////////////////////////////////////////////////////////////// + + binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/mobile/" + } + + private class FakeTabCounterToolbarButton : Toolbar.Action { + override fun createView(parent: ViewGroup): View = TabCounter(parent.context).apply { + setCount(2) + setBackgroundResource( + parent.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless), + ) + } + + override fun bind(view: View) = Unit + } + + /** + * A toolbar that looks like the toolbar in Fenix (Light theme). + */ + @Suppress("MagicNumber") + fun setupFenixToolbar() { + binding.toolbar.setBackgroundColor(0xFFFFFFFF.toInt()) + + binding.toolbar.display.indicators = listOf( + DisplayToolbar.Indicators.SECURITY, + DisplayToolbar.Indicators.TRACKING_PROTECTION, + DisplayToolbar.Indicators.EMPTY, + ) + + binding.toolbar.display.colors = binding.toolbar.display.colors.copy( + securityIconInsecure = 0xFF20123a.toInt(), + securityIconSecure = 0xFF20123a.toInt(), + text = 0xFF0c0c0d.toInt(), + menu = 0xFF20123a.toInt(), + separator = 0x1E15141a.toInt(), + trackingProtection = 0xFF20123a.toInt(), + emptyIcon = 0xFF20123a.toInt(), + hint = 0x1E15141a.toInt(), + ) + + binding.toolbar.display.urlFormatter = { url -> + URLStringUtils.toDisplayUrl(url) + } + + binding.toolbar.display.setUrlBackground( + ContextCompat.getDrawable(this, R.drawable.fenix_url_background), + ) + binding.toolbar.display.hint = "Search or enter address" + binding.toolbar.display.setOnUrlLongClickListener { + Toast.makeText(this, "Long click!", Toast.LENGTH_SHORT).show() + true + } + + val share = TextMenuCandidate("Share…") { /* Do nothing */ } + val homeScreen = TextMenuCandidate("Add to Home screen") { /* Do nothing */ } + val open = TextMenuCandidate("Open in…") { /* Do nothing */ } + val settings = NestedMenuCandidate( + id = toolbarR.id.mozac_browser_toolbar_menu, + text = "Settings", + subMenuItems = listOf( + NestedMenuCandidate(id = menuR.id.container, text = "Back", subMenuItems = null), + TextMenuCandidate("Setting 1") { /* Do nothing */ }, + TextMenuCandidate("Setting 2") { /* Do nothing */ }, + ), + ) + + val items = listOf(share, homeScreen, open, settings) + binding.toolbar.display.menuController = BrowserMenuController().apply { + submitList(items) + } + + binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/mobile/" + + binding.toolbar.addBrowserAction(FakeTabCounterToolbarButton()) + + binding.toolbar.display.setOnSiteSecurityClickedListener { + Toast.makeText(this, "Site security", Toast.LENGTH_SHORT).show() + } + + binding.toolbar.edit.colors = binding.toolbar.edit.colors.copy( + text = 0xFF0c0c0d.toInt(), + clear = 0xFF0c0c0d.toInt(), + icon = 0xFF0c0c0d.toInt(), + ) + + binding.toolbar.edit.setUrlBackground( + ContextCompat.getDrawable(this, R.drawable.fenix_url_background), + ) + binding.toolbar.edit.setIcon( + ContextCompat.getDrawable(this, iconsR.drawable.mozac_ic_search_24)!!, + "Search", + ) + + binding.toolbar.setOnUrlCommitListener { url -> + simulateReload() + binding.toolbar.url = url + + true + } + } + + /** + * A toolbar that looks like the toolbar in Fenix in a custom tab. + */ + @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage + @Suppress("MagicNumber") + fun setupFenixCustomTabToolbar() { + binding.toolbar.setBackgroundColor(0xFFFFFFFF.toInt()) + + binding.toolbar.display.indicators = listOf( + DisplayToolbar.Indicators.SECURITY, + DisplayToolbar.Indicators.TRACKING_PROTECTION, + ) + + binding.toolbar.display.colors = binding.toolbar.display.colors.copy( + securityIconSecure = 0xFF20123a.toInt(), + securityIconInsecure = 0xFF20123a.toInt(), + text = 0xFF0c0c0d.toInt(), + title = 0xFF0c0c0d.toInt(), + menu = 0xFF20123a.toInt(), + separator = 0x1E15141a.toInt(), + trackingProtection = 0xFF20123a.toInt(), + ) + + val share = SimpleBrowserMenuItem("Share…") { /* Do nothing */ } + val homeScreen = SimpleBrowserMenuItem("Add to Home screen") { /* Do nothing */ } + val open = SimpleBrowserMenuItem("Open in…") { /* Do nothing */ } + val settings = SimpleBrowserMenuItem("Settings") { /* Do nothing */ } + + val items = listOf(share, homeScreen, open, settings) + binding.toolbar.display.menuBuilder = BrowserMenuBuilder(items) + binding.toolbar.display.menuController = BrowserMenuController().apply { + submitList(items.asCandidateList(this@ToolbarActivity)) + } + + binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/mobile/" + + val drawableIcon = ContextCompat.getDrawable(this, iconsR.drawable.mozac_ic_cross_24) + + drawableIcon?.apply { + setTint(0xFF20123a.toInt()) + }.also { + val button = Toolbar.ActionButton( + it, + "Close", + ) { + Toast.makeText(this, "Close!", Toast.LENGTH_SHORT).show() + } + binding.toolbar.addNavigationAction(button) + } + + val drawable = ContextCompat.getDrawable(this, iconsR.drawable.mozac_ic_share_android_24)?.apply { + setTint(0xFF20123a.toInt()) + } + + val button = Toolbar.ActionButton(drawable, "Share") { + Toast.makeText(this, "Share!", Toast.LENGTH_SHORT).show() + } + + binding.toolbar.addBrowserAction(button) + + binding.toolbar.display.setOnSiteSecurityClickedListener { + Toast.makeText(this, "Site security", Toast.LENGTH_SHORT).show() + } + + GlobalScope.launch(Dispatchers.Main) { + delay(2000) + binding.toolbar.title = "Mobile browsers for iOS and Android | Firefox" + } + } + + // For testing purposes + private var forward = true + private var back = true + + private fun canGoForward(): Boolean = forward + + @Suppress("UnusedPrivateMember") + private fun canGoBack(): Boolean = back + + @Suppress("UnusedPrivateMember") + private fun goBack() { + back = !(forward && back) + forward = true + } + + @Suppress("UnusedPrivateMember") + private fun goForward() { + forward = !(back && forward) + back = true + } + + private var job: Job? = null + + private var loading = MutableLiveData<Boolean>() + + @Suppress("TooGenericExceptionCaught", "LongMethod", "ComplexMethod") + private fun simulateReload(view: UrlBoxProgressView? = null) { + job?.cancel() + + loading.value = true + + job = CoroutineScope(Dispatchers.Main).launch { + try { + loop@ for (progress in PROGRESS_RANGE step RELOAD_STEP_SIZE) { + if (!isActive) { + break@loop + } + + if (view == null) { + binding.toolbar.displayProgress(progress) + } else { + view.progress = progress + } + + delay(progress * RELOAD_STEP_SIZE.toLong()) + } + } catch (t: Throwable) { + if (view == null) { + binding.toolbar.displayProgress(0) + } else { + view.progress = 0 + } + + throw t + } finally { + loading.value = false + + // Update toolbar buttons to reflect loading state + binding.toolbar.invalidateActions() + } + } + + // Update toolbar buttons to reflect loading state + binding.toolbar.invalidateActions() + } + + private fun Resources.getThemedDrawable(@DrawableRes resId: Int) = ResourcesCompat.getDrawable(this, resId, theme) + + companion object { + private val PROGRESS_RANGE = 0..100 + private const val RELOAD_STEP_SIZE = 5 + } +} diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/drawable/fenix_url_background.xml b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/fenix_url_background.xml new file mode 100644 index 0000000000..3db3318893 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/fenix_url_background.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/. --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#E6E8E8" /> + + <corners + android:bottomLeftRadius="8dp" + android:bottomRightRadius="8dp" + android:topLeftRadius="8dp" + android:topRightRadius="8dp" /> +</shape>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/drawable/focus_background.xml b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/focus_background.xml new file mode 100644 index 0000000000..c2f0193bb4 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/focus_background.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/. --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <gradient + android:angle="315" + android:startColor="#ffa01142" + android:centerColor="#ff90116D" + android:endColor="#ff5c1166" /> +</shape>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_background.xml b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_background.xml new file mode 100644 index 0000000000..5537e32d4d --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_background.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/. --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#FF2A2A2E" /> + <corners android:radius="4dp" /> +</shape> diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_progress.xml b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_progress.xml new file mode 100644 index 0000000000..a1404243be --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_progress.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/. --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#ff45a1ff" /> + <corners android:radius="4dp" /> +</shape> diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/layout/activity_toolbar.xml b/mobile/android/android-components/samples/toolbar/src/main/res/layout/activity_toolbar.xml new file mode 100644 index 0000000000..48bda7881c --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/layout/activity_toolbar.xml @@ -0,0 +1,23 @@ +<?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" + tools:context=".ToolbarActivity"> + + <mozilla.components.browser.toolbar.BrowserToolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + +</LinearLayout> diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/layout/focus_blocking_switch.xml b/mobile/android/android-components/samples/toolbar/src/main/res/layout/focus_blocking_switch.xml new file mode 100644 index 0000000000..6245c6ce46 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/layout/focus_blocking_switch.xml @@ -0,0 +1,76 @@ +<?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="80dp" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:background="#272727" + tools:ignore="Overdraw" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingEnd="0dp" + android:paddingStart="16dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center_vertical" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="2dp" + android:ellipsize="end" + android:text="@string/trackers_blocked" + android:textColor="#80FFFFFF" + android:textSize="16sp" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <TextView + android:id="@+id/trackers_count" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="#ffffff" + android:textSize="24sp" + tools:text="42" /> + + <ImageButton + android:id="@+id/help_trackers" + android:contentDescription="@string/help_trackers_description" + android:layout_width="22dp" + android:layout_height="22dp" + android:padding="4dp" + app:srcCompat="@drawable/mozac_ic_information_fill_24" + android:background="?android:attr/selectableItemBackgroundBorderless" /> + + </LinearLayout> + + </LinearLayout> + + <androidx.appcompat.widget.SwitchCompat + android:id="@+id/blocking_switch" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:layout_gravity="center_vertical|end" + android:background="?android:attr/selectableItemBackground" + android:checked="true" + android:clickable="true" + android:focusable="true" + android:ellipsize="end" + android:gravity="center" + android:lines="1" + android:paddingEnd="16dp" + android:paddingStart="16dp" + android:textSize="16sp"/> + +</LinearLayout> diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/layout/item_toolbar_configuration.xml b/mobile/android/android-components/samples/toolbar/src/main/res/layout/item_toolbar_configuration.xml new file mode 100644 index 0000000000..d7c2560465 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/layout/item_toolbar_configuration.xml @@ -0,0 +1,15 @@ +<?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/. --> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + tools:ignore="Overdraw" + android:clickable="true" + android:focusable="true" + android:textSize="14sp" + android:padding="16dp" /> diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..ff5b811c28 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/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/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/toolbar/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/toolbar/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/toolbar/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..f53686af1d --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..361ce175fe --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..eeb463471b --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..9466bb3aee --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..c158a5aa08 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..4658edf3ae --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..21410ec0f8 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..2233cd4525 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..9f62938988 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..8b0c76270e --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..ea69f75cd7 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..1414185bd2 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000000..41288de783 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000000..beccb228ea --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000000..5ab06ff36d --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/values/colors.xml b/mobile/android/android-components/samples/toolbar/src/main/res/values/colors.xml new file mode 100644 index 0000000000..abbf1bdbcc --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/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> + <color name="selected_configuration">#222222</color> +</resources> diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/values/dimens.xml b/mobile/android/android-components/samples/toolbar/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..dff0755716 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/values/dimens.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/. --> +<resources xmlns:tools="http://schemas.android.com/tools"> + <dimen tools:ignore="UnusedResources" name="mozac_browser_menu_corner_radius">20dp</dimen> + <dimen tools:ignore="UnusedResources" name="mozac_browser_menu2_corner_radius">20dp</dimen> +</resources> diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/values/ic_launcher_background.xml b/mobile/android/android-components/samples/toolbar/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000000..475311a475 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/values/ic_launcher_background.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/. --> + +<resources> + <color name="ic_launcher_background">#FF618D</color> +</resources>
\ No newline at end of file diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/values/strings.xml b/mobile/android/android-components/samples/toolbar/src/main/res/values/strings.xml new file mode 100644 index 0000000000..81a1535a0e --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/src/main/res/values/strings.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/. --> +<resources> + <string name="app_name">Toolbar Sample</string> + <string name="trackers_blocked">Trackers blocked</string> + <string name="help_trackers_description">About Trackers</string> +</resources> diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/toolbar/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..820ae61afa --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/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/toolbar/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/toolbar/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..55da967560 --- /dev/null +++ b/mobile/android/android-components/samples/toolbar/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 |