From da4c7e7ed675c3bf405668739c3012d140856109 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:34:42 +0200 Subject: Adding upstream version 126.0. Signed-off-by: Daniel Baumann --- .../android-components/samples/browser/.gitignore | 2 + .../android-components/samples/browser/README.md | 35 ++ .../samples/browser/build.gradle | 186 +++++++ .../samples/browser/proguard-rules.pro | 21 + .../browser/src/androidTest/assets/index.html | 5 + .../java/org/mozilla/samples/browser/SmokeTests.kt | 139 +++++ .../java/org/mozilla/samples/browser/Components.kt | 47 ++ .../samples/browser/src/main/AndroidManifest.xml | 177 +++++++ .../main/assets/extensions/borderify/borderify.js | 5 + .../extensions/borderify/manifest.template.json | 16 + .../src/main/assets/extensions/test/background.js | 24 + .../src/main/assets/extensions/test/icon.png | Bin 0 -> 1633 bytes .../assets/extensions/test/manifest.template.json | 22 + .../src/main/assets/extensions/test/popup.html | 13 + .../mozilla/samples/browser/BaseBrowserFragment.kt | 308 +++++++++++ .../org/mozilla/samples/browser/BrowserActivity.kt | 89 ++++ .../org/mozilla/samples/browser/BrowserFragment.kt | 190 +++++++ .../mozilla/samples/browser/DefaultComponents.kt | 515 ++++++++++++++++++ .../samples/browser/ExternalAppBrowserActivity.kt | 29 + .../samples/browser/ExternalAppBrowserFragment.kt | 132 +++++ .../samples/browser/IntentReceiverActivity.kt | 46 ++ .../mozilla/samples/browser/SampleApplication.kt | 142 +++++ .../mozilla/samples/browser/TabsTrayFragment.kt | 125 +++++ .../samples/browser/addons/AddonDetailsActivity.kt | 137 +++++ .../browser/addons/AddonSettingsActivity.kt | 94 ++++ .../samples/browser/addons/AddonsActivity.kt | 26 + .../samples/browser/addons/AddonsFragment.kt | 251 +++++++++ .../mozilla/samples/browser/addons/Extensions.kt | 12 + .../addons/InstalledAddonDetailsActivity.kt | 200 +++++++ .../browser/addons/NotYetSupportedAddonActivity.kt | 105 ++++ .../browser/addons/PermissionsDetailsActivity.kt | 54 ++ .../addons/WebExtensionActionPopupActivity.kt | 114 ++++ .../browser/autofill/AutofillConfirmActivity.kt | 19 + .../browser/autofill/AutofillSearchActivity.kt | 20 + .../samples/browser/autofill/AutofillService.kt | 19 + .../browser/autofill/AutofillUnlockActivity.kt | 20 + .../browser/awesomebar/AwesomeBarWrapper.kt | 75 +++ .../browser/customtabs/CustomTabsService.kt | 17 + .../samples/browser/downloads/DownloadService.kt | 16 + .../org/mozilla/samples/browser/ext/Context.kt | 21 + .../org/mozilla/samples/browser/ext/Fragment.kt | 14 + .../browser/integration/ContextMenuIntegration.kt | 89 ++++ .../browser/integration/FindInPageIntegration.kt | 53 ++ .../browser/integration/ReaderViewIntegration.kt | 84 +++ .../samples/browser/media/MediaSessionService.kt | 20 + .../request/SampleUrlEncodedRequestInterceptor.kt | 71 +++ .../main/res/drawable/addon_textview_selector.xml | 10 + .../res/drawable/mozac_ic_extensions_black.xml | 13 + .../src/main/res/drawable/mozac_ic_permissions.xml | 21 + .../main/res/layout/activity_add_on_details.xml | 156 ++++++ .../res/layout/activity_add_on_permissions.xml | 32 ++ .../main/res/layout/activity_add_on_settings.xml | 11 + .../layout/activity_installed_add_on_details.xml | 96 ++++ .../browser/src/main/res/layout/activity_main.xml | 11 + .../main/res/layout/fragment_add_on_settings.xml | 18 + .../src/main/res/layout/fragment_add_ons.xml | 28 + .../src/main/res/layout/fragment_browser.xml | 82 +++ .../layout/fragment_not_yet_supported_addons.xml | 44 ++ .../src/main/res/layout/fragment_tabstray.xml | 29 + .../main/res/layout/overlay_add_on_progress.xml | 44 ++ .../browser/src/main/res/menu/tabstray_menu.xml | 12 + .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 8 + .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 8 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2076 bytes .../res/mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1781 bytes .../src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4121 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1533 bytes .../res/mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1322 bytes .../src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2535 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2650 bytes .../res/mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2439 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5575 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4138 bytes .../res/mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 3674 bytes .../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8832 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5519 bytes .../res/mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 4778 bytes .../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 12481 bytes .../samples/browser/src/main/res/values/colors.xml | 7 + .../src/main/res/values/ic_launcher_background.xml | 7 + .../browser/src/main/res/values/strings.xml | 10 + .../browser/src/main/res/xml/backup_rules.xml | 8 + .../src/main/res/xml/data_extraction_rules.xml | 9 + .../src/main/res/xml/service_configuration.xml | 10 + .../java/org/mozilla/samples/browser/Components.kt | 17 + .../java/org/mozilla/samples/browser/Components.kt | 11 + .../samples/compose-browser/.gitignore | 2 + .../samples/compose-browser/README.md | 35 ++ .../samples/compose-browser/build.gradle | 86 +++ .../samples/compose-browser/proguard-rules.pro | 21 + .../compose-browser/src/main/AndroidManifest.xml | 54 ++ .../samples/compose/browser/BrowserApplication.kt | 26 + .../compose/browser/BrowserComposeActivity.kt | 43 ++ .../mozilla/samples/compose/browser/Components.kt | 70 +++ .../samples/compose/browser/app/AppAction.kt | 17 + .../samples/compose/browser/app/AppState.kt | 14 + .../samples/compose/browser/app/AppStore.kt | 22 + .../compose/browser/browser/BrowserScreen.kt | 237 +++++++++ .../compose/browser/browser/BrowserScreenAction.kt | 32 ++ .../compose/browser/browser/BrowserScreenState.kt | 22 + .../compose/browser/browser/BrowserScreenStore.kt | 29 + .../mozilla/samples/compose/browser/ext/Context.kt | 15 + .../compose/browser/settings/SettingsScreen.kt | 19 + .../main/res/drawable/ic_launcher_background.xml | 78 +++ .../main/res/drawable/ic_launcher_foreground.xml | 19 + .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 9 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2747 bytes .../src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4823 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2125 bytes .../src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2978 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3767 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6609 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6700 bytes .../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11343 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9075 bytes .../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16086 bytes .../src/main/res/values/strings.xml | 7 + .../src/main/res/xml/data_extraction_rules.xml | 9 + .../android-components/samples/crash/build.gradle | 47 ++ .../android-components/samples/crash/lint.xml | 12 + .../samples/crash/proguard-rules.pro | 21 + .../samples/crash/src/main/AndroidManifest.xml | 40 ++ .../samples/crash/src/main/ic_launcher-web.png | Bin 0 -> 5650 bytes .../org/mozilla/samples/crash/CrashActivity.kt | 168 ++++++ .../org/mozilla/samples/crash/CrashApplication.kt | 152 ++++++ .../org/mozilla/samples/crash/CrashListActivity.kt | 21 + .../java/org/mozilla/samples/crash/CrashService.kt | 82 +++ .../crash/src/main/res/layout/activity_crash.xml | 40 ++ .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 9 + .../crash/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1156 bytes .../res/mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 295 bytes .../src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3041 bytes .../crash/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 858 bytes .../res/mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 225 bytes .../src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1897 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 1600 bytes .../res/mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 403 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 4303 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 2415 bytes .../res/mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 716 bytes .../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 6677 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 3395 bytes .../res/mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 1092 bytes .../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 9675 bytes .../src/main/res/values/ic_launcher_background.xml | 8 + .../samples/crash/src/main/res/values/strings.xml | 12 + .../samples/crash/src/main/res/values/styles.xml | 19 + .../crash/src/main/res/xml/backup_rules.xml | 8 + .../src/main/res/xml/data_extraction_rules.xml | 9 + .../samples/dataprotect/build.gradle | 35 ++ .../samples/dataprotect/lint.xml | 12 + .../samples/dataprotect/proguard-rules.pro | 21 + .../dataprotect/src/main/AndroidManifest.xml | 28 + .../org/mozilla/samples/dataprotect/Constants.kt | 12 + .../mozilla/samples/dataprotect/MainActivity.kt | 44 ++ .../samples/dataprotect/ProtectedDataAdapter.kt | 43 ++ .../res/drawable-v24/ic_launcher_foreground.xml | 38 ++ .../main/res/drawable/ic_launcher_background.xml | 174 ++++++ .../src/main/res/layout/activity_main.xml | 18 + .../src/main/res/layout/protecteddata_item.xml | 21 + .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 9 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../dataprotect/src/main/res/values/colors.xml | 9 + .../dataprotect/src/main/res/values/strings.xml | 6 + .../dataprotect/src/main/res/values/styles.xml | 14 + .../src/main/res/xml/data_extraction_rules.xml | 9 + .../samples/firefox-accounts/.gitignore | 1 + .../samples/firefox-accounts/README.md | 65 +++ .../samples/firefox-accounts/build.gradle | 53 ++ .../samples/firefox-accounts/gradle.properties | 0 .../samples/firefox-accounts/proguard-rules.pro | 21 + .../firefox-accounts/src/main/AndroidManifest.xml | 50 ++ .../java/org/mozilla/samples/fxa/LoginFragment.kt | 107 ++++ .../java/org/mozilla/samples/fxa/MainActivity.kt | 220 ++++++++ .../org/mozilla/samples/fxa/SampleFxAEntryPoint.kt | 14 + .../src/main/res/layout/activity_main.xml | 76 +++ .../src/main/res/layout/fragment_view.xml | 11 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2954 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2061 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4368 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6037 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 8179 bytes .../src/main/res/values/strings.xml | 15 + .../src/main/res/xml/backup_rules.xml | 8 + .../src/main/res/xml/data_extraction_rules.xml | 9 + .../android-components/samples/glean/README.md | 20 + .../android-components/samples/glean/build.gradle | 84 +++ .../android-components/samples/glean/metrics.yaml | 116 ++++ .../android-components/samples/glean/pings.yaml | 21 + .../samples/glean/proguard-rules.pro | 21 + .../samples/glean/samples-glean-library/README.md | 5 + .../glean/samples-glean-library/build.gradle | 55 ++ .../glean/samples-glean-library/metrics.yaml | 27 + .../src/main/AndroidManifest.xml | 4 + .../samples/glean/library/SamplesGleanLibrary.kt | 31 ++ .../org/mozilla/samples/glean/MainActivityTest.kt | 39 ++ .../samples/glean/pings/BaselinePingTest.kt | 138 +++++ .../samples/glean/src/main/AndroidManifest.xml | 42 ++ .../org/mozilla/samples/glean/GleanApplication.kt | 115 ++++ .../java/org/mozilla/samples/glean/MainActivity.kt | 125 +++++ .../glean/src/main/res/layout/activity_main.xml | 75 +++ .../glean/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../glean/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../src/main/res/raw/initial_experiments.json | 60 +++ .../glean/src/main/res/values/endpoints.xml | 8 + .../samples/glean/src/main/res/values/strings.xml | 21 + .../glean/src/main/res/xml/backup_rules.xml | 8 + .../src/main/res/xml/data_extraction_rules.xml | 9 + .../ios-sample.xcodeproj/project.pbxproj | 589 +++++++++++++++++++++ .../project.xcworkspace/contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../ios-sample/Assets.xcassets/Contents.json | 6 + .../ios-sample/ios-sample/ContentView.swift | 18 + .../samples/ios-sample/ios-sample/Info.plist | 50 ++ .../Preview Assets.xcassets/Contents.json | 6 + .../ios-sample/ios-sample/ios_sampleApp.swift | 14 + .../samples/ios-sample/ios-sampleTests/Info.plist | 22 + .../ios-sampleTests/ios_sampleTests.swift | 30 ++ .../ios-sample/ios-sampleUITests/Info.plist | 22 + .../ios-sampleUITests/ios_sampleUITests.swift | 39 ++ .../samples/sync-logins/README.md | 73 +++ .../samples/sync-logins/build.gradle | 50 ++ .../samples/sync-logins/gradle.properties | 0 .../samples/sync-logins/proguard-rules.pro | 21 + .../sync-logins/src/main/AndroidManifest.xml | 41 ++ .../mozilla/samples/sync/logins/LoginFragment.kt | 108 ++++ .../mozilla/samples/sync/logins/MainActivity.kt | 181 +++++++ .../samples/sync/logins/SampleFxAEntryPoint.kt | 14 + .../src/main/res/layout/activity_main.xml | 38 ++ .../src/main/res/layout/fragment_view.xml | 11 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../sync-logins/src/main/res/values/strings.xml | 8 + .../sync-logins/src/main/res/xml/backup_rules.xml | 8 + .../src/main/res/xml/data_extraction_rules.xml | 9 + .../android-components/samples/sync/README.md | 25 + .../android-components/samples/sync/build.gradle | 58 ++ .../samples/sync/gradle.properties | 0 .../samples/sync/proguard-rules.pro | 21 + .../samples/sync/src/main/AndroidManifest.xml | 40 ++ .../org/mozilla/samples/sync/DeviceFragment.kt | 85 +++ .../samples/sync/DeviceRecyclerViewAdapter.kt | 68 +++ .../java/org/mozilla/samples/sync/LoginFragment.kt | 108 ++++ .../java/org/mozilla/samples/sync/MainActivity.kt | 464 ++++++++++++++++ .../mozilla/samples/sync/SampleFxAEntryPoint.kt | 14 + .../sync/src/main/res/layout/activity_main.xml | 146 +++++ .../sync/src/main/res/layout/fragment_device.xml | 24 + .../src/main/res/layout/fragment_device_list.xml | 15 + .../sync/src/main/res/layout/fragment_view.xml | 11 + .../sync/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2954 bytes .../sync/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2061 bytes .../sync/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4368 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6037 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 8179 bytes .../samples/sync/src/main/res/values/dimens.xml | 8 + .../samples/sync/src/main/res/values/strings.xml | 39 ++ .../samples/sync/src/main/res/xml/backup_rules.xml | 8 + .../src/main/res/xml/data_extraction_rules.xml | 9 + .../samples/toolbar/build.gradle | 57 ++ .../android-components/samples/toolbar/lint.xml | 12 + .../samples/toolbar/proguard-rules.pro | 21 + .../samples/toolbar/src/main/AndroidManifest.xml | 29 + .../samples/toolbar/src/main/ic_launcher-web.png | Bin 0 -> 5650 bytes .../samples/toolbar/SampleToolbarHelpers.kt | 148 ++++++ .../org/mozilla/samples/toolbar/ToolbarActivity.kt | 539 +++++++++++++++++++ .../src/main/res/drawable/fenix_url_background.xml | 13 + .../src/main/res/drawable/focus_background.xml | 11 + .../main/res/drawable/sample_url_background.xml | 8 + .../src/main/res/drawable/sample_url_progress.xml | 8 + .../src/main/res/layout/activity_toolbar.xml | 23 + .../src/main/res/layout/focus_blocking_switch.xml | 76 +++ .../main/res/layout/item_toolbar_configuration.xml | 15 + .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 8 + .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 8 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 862 bytes .../res/mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 295 bytes .../src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2590 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 733 bytes .../res/mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 225 bytes .../src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1737 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 1177 bytes .../res/mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 403 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 3748 bytes .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1736 bytes .../res/mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 716 bytes .../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 6057 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 2137 bytes .../res/mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 1092 bytes .../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 8695 bytes .../samples/toolbar/src/main/res/values/colors.xml | 7 + .../samples/toolbar/src/main/res/values/dimens.xml | 8 + .../src/main/res/values/ic_launcher_background.xml | 8 + .../toolbar/src/main/res/values/strings.xml | 9 + .../toolbar/src/main/res/xml/backup_rules.xml | 8 + .../src/main/res/xml/data_extraction_rules.xml | 9 + 315 files changed, 11859 insertions(+) create mode 100644 mobile/android/android-components/samples/browser/.gitignore create mode 100644 mobile/android/android-components/samples/browser/README.md create mode 100644 mobile/android/android-components/samples/browser/build.gradle create mode 100644 mobile/android/android-components/samples/browser/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/browser/src/androidTest/assets/index.html create mode 100644 mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt create mode 100644 mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js create mode 100644 mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json create mode 100644 mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js create mode 100644 mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png create mode 100644 mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json create mode 100644 mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt create mode 100644 mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/browser/src/main/res/values/colors.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml create mode 100644 mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml create mode 100644 mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt create mode 100644 mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt create mode 100644 mobile/android/android-components/samples/compose-browser/.gitignore create mode 100644 mobile/android/android-components/samples/compose-browser/README.md create mode 100644 mobile/android/android-components/samples/compose-browser/build.gradle create mode 100644 mobile/android/android-components/samples/compose-browser/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserApplication.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserComposeActivity.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/Components.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppAction.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppState.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppStore.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreen.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenAction.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenState.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenStore.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/ext/Context.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/settings/SettingsScreen.kt create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_background.xml create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/compose-browser/src/main/res/xml/data_extraction_rules.xml create mode 100644 mobile/android/android-components/samples/crash/build.gradle create mode 100644 mobile/android/android-components/samples/crash/lint.xml create mode 100644 mobile/android/android-components/samples/crash/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/crash/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/crash/src/main/ic_launcher-web.png create mode 100644 mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashActivity.kt create mode 100644 mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashApplication.kt create mode 100644 mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashListActivity.kt create mode 100644 mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashService.kt create mode 100644 mobile/android/android-components/samples/crash/src/main/res/layout/activity_crash.xml create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/crash/src/main/res/values/ic_launcher_background.xml create mode 100644 mobile/android/android-components/samples/crash/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/crash/src/main/res/values/styles.xml create mode 100644 mobile/android/android-components/samples/crash/src/main/res/xml/backup_rules.xml create mode 100644 mobile/android/android-components/samples/crash/src/main/res/xml/data_extraction_rules.xml create mode 100644 mobile/android/android-components/samples/dataprotect/build.gradle create mode 100644 mobile/android/android-components/samples/dataprotect/lint.xml create mode 100644 mobile/android/android-components/samples/dataprotect/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/Constants.kt create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/MainActivity.kt create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/ProtectedDataAdapter.kt create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/drawable/ic_launcher_background.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/layout/activity_main.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/layout/protecteddata_item.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/values/colors.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/values/styles.xml create mode 100644 mobile/android/android-components/samples/dataprotect/src/main/res/xml/data_extraction_rules.xml create mode 100644 mobile/android/android-components/samples/firefox-accounts/.gitignore create mode 100644 mobile/android/android-components/samples/firefox-accounts/README.md create mode 100644 mobile/android/android-components/samples/firefox-accounts/build.gradle create mode 100644 mobile/android/android-components/samples/firefox-accounts/gradle.properties create mode 100644 mobile/android/android-components/samples/firefox-accounts/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/LoginFragment.kt create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/MainActivity.kt create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/SampleFxAEntryPoint.kt create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/activity_main.xml create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/fragment_view.xml create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/backup_rules.xml create mode 100644 mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/data_extraction_rules.xml create mode 100644 mobile/android/android-components/samples/glean/README.md create mode 100644 mobile/android/android-components/samples/glean/build.gradle create mode 100644 mobile/android/android-components/samples/glean/metrics.yaml create mode 100644 mobile/android/android-components/samples/glean/pings.yaml create mode 100644 mobile/android/android-components/samples/glean/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/glean/samples-glean-library/README.md create mode 100644 mobile/android/android-components/samples/glean/samples-glean-library/build.gradle create mode 100644 mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml create mode 100644 mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt create mode 100644 mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt create mode 100644 mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt create mode 100644 mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt create mode 100644 mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt create mode 100644 mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml create mode 100644 mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json create mode 100644 mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml create mode 100644 mobile/android/android-components/samples/glean/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml create mode 100644 mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.pbxproj create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/Contents.json create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample/ContentView.swift create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample/Info.plist create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sample/ios_sampleApp.swift create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sampleTests/Info.plist create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sampleTests/ios_sampleTests.swift create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sampleUITests/Info.plist create mode 100644 mobile/android/android-components/samples/ios-sample/ios-sampleUITests/ios_sampleUITests.swift create mode 100644 mobile/android/android-components/samples/sync-logins/README.md create mode 100644 mobile/android/android-components/samples/sync-logins/build.gradle create mode 100644 mobile/android/android-components/samples/sync-logins/gradle.properties create mode 100644 mobile/android/android-components/samples/sync-logins/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/LoginFragment.kt create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/MainActivity.kt create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/SampleFxAEntryPoint.kt create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/layout/activity_main.xml create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/layout/fragment_view.xml create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/xml/backup_rules.xml create mode 100644 mobile/android/android-components/samples/sync-logins/src/main/res/xml/data_extraction_rules.xml create mode 100644 mobile/android/android-components/samples/sync/README.md create mode 100644 mobile/android/android-components/samples/sync/build.gradle create mode 100644 mobile/android/android-components/samples/sync/gradle.properties create mode 100644 mobile/android/android-components/samples/sync/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt create mode 100644 mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt create mode 100644 mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt create mode 100644 mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt create mode 100644 mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt create mode 100644 mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml create mode 100644 mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml create mode 100644 mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml create mode 100644 mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml create mode 100644 mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml create mode 100644 mobile/android/android-components/samples/sync/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml create mode 100644 mobile/android/android-components/samples/sync/src/main/res/xml/data_extraction_rules.xml create mode 100644 mobile/android/android-components/samples/toolbar/build.gradle create mode 100644 mobile/android/android-components/samples/toolbar/lint.xml create mode 100644 mobile/android/android-components/samples/toolbar/proguard-rules.pro create mode 100644 mobile/android/android-components/samples/toolbar/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/ic_launcher-web.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/SampleToolbarHelpers.kt create mode 100644 mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/ToolbarActivity.kt create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/drawable/fenix_url_background.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/drawable/focus_background.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_background.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_progress.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/layout/activity_toolbar.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/layout/focus_blocking_switch.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/layout/item_toolbar_configuration.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/values/colors.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/values/dimens.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/values/ic_launcher_background.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/xml/backup_rules.xml create mode 100644 mobile/android/android-components/samples/toolbar/src/main/res/xml/data_extraction_rules.xml (limited to 'mobile/android/android-components/samples') 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 @@ + + +

Hello World!

+ + 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 = 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js new file mode 100644 index 0000000000..af58957d88 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +document.body.style.border = "5px solid red"; \ No newline at end of file diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json new file mode 100644 index 0000000000..cc2db02cbf --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json @@ -0,0 +1,16 @@ +{ + "manifest_version": 2, + "browser_specific_settings": { + "gecko": { + "id": "borderify@mozac.org" + } + }, + "name": "Mozilla Android Components - Borderify", + "version": "${version}", + "content_scripts": [ + { + "matches": ["*://www.mozilla.org/*"], + "js": ["borderify.js"] + } + ] +} diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js new file mode 100644 index 0000000000..950936be4c --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Avoid adding ID selector rules in this style sheet, since they could + * inadvertently match elements in the article content. */ + +// Counts to three and sends a greeting via the browser action of a newly created tab. +browser.tabs.onCreated.addListener((tab) => { + let counter = 0; + let intervalId = setInterval(() => { + var message; + if (++counter <= 3) { + message = "" + counter; + } else { + message = "Hi!"; + clearInterval(intervalId); + } + browser.browserAction.setBadgeTextColor({tabId: tab.id, color: "#FFFFFF"}); + browser.browserAction.setBadgeText({tabId: tab.id, text: message}); + }, 1000); +}); + +browser.browserAction.setBadgeBackgroundColor({color: "#AAAAAA"}); \ No newline at end of file diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png new file mode 100644 index 0000000000..455b15fc84 Binary files /dev/null and b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png differ diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json new file mode 100644 index 0000000000..04dc17aa2f --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json @@ -0,0 +1,22 @@ +{ + "manifest_version": 2, + "browser_specific_settings": { + "gecko": { + "id": "testext@mozac.org" + } + }, + "name": "Mozilla Android Components - Test extension", + "description": "This extension is used for testing web extension functionality in Android Components", + "version": "${version}", + "background": { + "scripts": ["background.js"] + }, + "browser_action": { + "default_icon": "icon.png", + "default_title": "Test", + "default_popup": "popup.html" + }, + "permissions": [ + "tabs" + ] +} diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html new file mode 100644 index 0000000000..40d1467569 --- /dev/null +++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html @@ -0,0 +1,13 @@ + + + + + + + +

Hello world!

+

This is a browser action default popup.

+ + 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() + private val toolbarFeature = ViewBoundFeatureWrapper() + private val contextMenuIntegration = ViewBoundFeatureWrapper() + private val downloadsFeature = ViewBoundFeatureWrapper() + private val appLinksFeature = ViewBoundFeatureWrapper() + private val promptFeature = ViewBoundFeatureWrapper() + private val findInPageIntegration = ViewBoundFeatureWrapper() + private val sitePermissionsFeature = ViewBoundFeatureWrapper() + private val swipeRefreshFeature = ViewBoundFeatureWrapper() + + protected val sessionId: String? + get() = arguments?.getString(SESSION_ID_KEY) + + private val activityResultHandler: List> = 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() + private val readerViewFeature = ViewBoundFeatureWrapper() + private val webExtToolbarFeature = ViewBoundFeatureWrapper() + private val searchFeature = ViewBoundFeatureWrapper() + private val fullScreenFeature = ViewBoundFeatureWrapper() + private val mediaSessionFullscreenFeature = + ViewBoundFeatureWrapper() + + @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, + ): 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() + private val hideToolbarFeature = ViewBoundFeatureWrapper() + + 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 = 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(R.id.rating_view) + val reviewCountView = findViewById(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(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(R.id.last_updated_text) + lastUpdatedView.text = formatDate(addon.updatedAt) + } + + private fun bindVersion(addon: Addon) { + val versionView = findViewById(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(R.id.author_text) + authorsView.text = addon.author?.name.orEmpty() + } + + private fun bindDetails(addon: Addon) { + val detailsView = findViewById(R.id.details) + val detailsText = addon.translateDescription(this) + + val parsedText = detailsText.replace("\n", "
") + 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) { + 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(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(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(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(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(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(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 + 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(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) = 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(R.id.add_ons_permissions) + recyclerView.layoutManager = LinearLayoutManager(this) + val sortedPermissions = addon.translatePermissions(this).sorted() + recyclerView.adapter = AddonPermissionsAdapter(sortedPermissions) + } + + private fun bindLearnMore() { + findViewById(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()) + 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("

I am the sample browser

") + 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 @@ + + + + + + \ 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 @@ + + + + + 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 @@ + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + 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 @@ + + + + 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 @@ + + + + + + + + + + + + + + + +