diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/samples/compose-browser | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/samples/compose-browser')
33 files changed, 875 insertions, 0 deletions
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 |