summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/samples
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/samples')
-rw-r--r--mobile/android/android-components/samples/browser/.gitignore2
-rw-r--r--mobile/android/android-components/samples/browser/README.md35
-rw-r--r--mobile/android/android-components/samples/browser/build.gradle186
-rw-r--r--mobile/android/android-components/samples/browser/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/browser/src/androidTest/assets/index.html5
-rw-r--r--mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt139
-rw-r--r--mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt47
-rw-r--r--mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml177
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js5
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json16
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js24
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.pngbin0 -> 1633 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json22
-rw-r--r--mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html13
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt308
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt89
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt190
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt515
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt29
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt132
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt46
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt142
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt125
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt137
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt94
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt26
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt251
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt12
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt200
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt105
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt54
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt114
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt19
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt20
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt19
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt20
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt75
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt17
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt16
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt21
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt14
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt89
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt53
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt84
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt20
-rw-r--r--mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt71
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml10
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml13
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml21
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml156
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml32
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml11
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml96
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml11
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml18
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml28
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml82
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml44
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml29
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml44
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml12
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml8
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml8
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 2076 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 1781 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 4121 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 1533 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 1322 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2535 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 2650 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 2439 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 5575 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 4138 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 3674 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 8832 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 5519 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 4778 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 12481 bytes
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/values/colors.xml7
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml7
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/values/strings.xml10
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml10
-rw-r--r--mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt17
-rw-r--r--mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt11
-rw-r--r--mobile/android/android-components/samples/compose-browser/.gitignore2
-rw-r--r--mobile/android/android-components/samples/compose-browser/README.md35
-rw-r--r--mobile/android/android-components/samples/compose-browser/build.gradle86
-rw-r--r--mobile/android/android-components/samples/compose-browser/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/AndroidManifest.xml54
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserApplication.kt26
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserComposeActivity.kt43
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/Components.kt70
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppAction.kt17
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppState.kt14
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppStore.kt22
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreen.kt237
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenAction.kt32
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenState.kt22
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenStore.kt29
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/ext/Context.kt15
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/settings/SettingsScreen.kt19
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_background.xml78
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_foreground.xml19
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml9
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml9
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 2747 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 4823 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2125 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2978 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 3767 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 6609 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6700 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 11343 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9075 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 16086 bytes
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/values/strings.xml7
-rw-r--r--mobile/android/android-components/samples/compose-browser/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/crash/build.gradle47
-rw-r--r--mobile/android/android-components/samples/crash/lint.xml12
-rw-r--r--mobile/android/android-components/samples/crash/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/crash/src/main/AndroidManifest.xml40
-rw-r--r--mobile/android/android-components/samples/crash/src/main/ic_launcher-web.pngbin0 -> 5650 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashActivity.kt168
-rw-r--r--mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashApplication.kt152
-rw-r--r--mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashListActivity.kt21
-rw-r--r--mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashService.kt82
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/layout/activity_crash.xml40
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher.xml9
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml9
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 1156 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 295 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 3041 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 858 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 225 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 1897 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 1600 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 403 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 4303 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 2415 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 716 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 6677 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 3395 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 1092 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 9675 bytes
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/values/ic_launcher_background.xml8
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/values/strings.xml12
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/values/styles.xml19
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/crash/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/dataprotect/build.gradle35
-rw-r--r--mobile/android/android-components/samples/dataprotect/lint.xml12
-rw-r--r--mobile/android/android-components/samples/dataprotect/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/AndroidManifest.xml28
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/Constants.kt12
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/MainActivity.kt44
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/ProtectedDataAdapter.kt43
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/drawable-v24/ic_launcher_foreground.xml38
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/drawable/ic_launcher_background.xml174
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/layout/activity_main.xml18
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/layout/protecteddata_item.xml21
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher.xml9
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml9
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3056 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 5024 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2096 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2858 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4569 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 7098 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6464 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 10676 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9250 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 15523 bytes
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/values/colors.xml9
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/values/strings.xml6
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/values/styles.xml14
-rw-r--r--mobile/android/android-components/samples/dataprotect/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/.gitignore1
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/README.md65
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/build.gradle53
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/gradle.properties0
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/AndroidManifest.xml50
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/LoginFragment.kt107
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/MainActivity.kt220
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/SampleFxAEntryPoint.kt14
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/activity_main.xml76
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/fragment_view.xml11
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 2954 bytes
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2061 bytes
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4368 bytes
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6037 bytes
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 8179 bytes
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/values/strings.xml15
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/glean/README.md20
-rw-r--r--mobile/android/android-components/samples/glean/build.gradle84
-rw-r--r--mobile/android/android-components/samples/glean/metrics.yaml116
-rw-r--r--mobile/android/android-components/samples/glean/pings.yaml21
-rw-r--r--mobile/android/android-components/samples/glean/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/glean/samples-glean-library/README.md5
-rw-r--r--mobile/android/android-components/samples/glean/samples-glean-library/build.gradle55
-rw-r--r--mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml27
-rw-r--r--mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt31
-rw-r--r--mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt39
-rw-r--r--mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt138
-rw-r--r--mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml42
-rw-r--r--mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt115
-rw-r--r--mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt125
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml75
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3056 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2096 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4569 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6464 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9250 bytes
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json60
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml8
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/values/strings.xml21
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.pbxproj589
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata7
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist8
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AccentColor.colorset/Contents.json11
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AppIcon.appiconset/Contents.json98
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/Contents.json6
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample/ContentView.swift18
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample/Info.plist50
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample/Preview Content/Preview Assets.xcassets/Contents.json6
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sample/ios_sampleApp.swift14
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sampleTests/Info.plist22
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sampleTests/ios_sampleTests.swift30
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sampleUITests/Info.plist22
-rw-r--r--mobile/android/android-components/samples/ios-sample/ios-sampleUITests/ios_sampleUITests.swift39
-rw-r--r--mobile/android/android-components/samples/sync-logins/README.md73
-rw-r--r--mobile/android/android-components/samples/sync-logins/build.gradle50
-rw-r--r--mobile/android/android-components/samples/sync-logins/gradle.properties0
-rw-r--r--mobile/android/android-components/samples/sync-logins/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/AndroidManifest.xml41
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/LoginFragment.kt108
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/MainActivity.kt181
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/SampleFxAEntryPoint.kt14
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/layout/activity_main.xml38
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/layout/fragment_view.xml11
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3056 bytes
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2096 bytes
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4569 bytes
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6464 bytes
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9250 bytes
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/sync-logins/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/sync/README.md25
-rw-r--r--mobile/android/android-components/samples/sync/build.gradle58
-rw-r--r--mobile/android/android-components/samples/sync/gradle.properties0
-rw-r--r--mobile/android/android-components/samples/sync/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml40
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt85
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt68
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt108
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt464
-rw-r--r--mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt14
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml146
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml24
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml15
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml11
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 2954 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2061 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4368 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6037 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 8179 bytes
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml8
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/values/strings.xml39
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/sync/src/main/res/xml/data_extraction_rules.xml9
-rw-r--r--mobile/android/android-components/samples/toolbar/build.gradle57
-rw-r--r--mobile/android/android-components/samples/toolbar/lint.xml12
-rw-r--r--mobile/android/android-components/samples/toolbar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/AndroidManifest.xml29
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/ic_launcher-web.pngbin0 -> 5650 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/SampleToolbarHelpers.kt148
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/ToolbarActivity.kt539
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/drawable/fenix_url_background.xml13
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/drawable/focus_background.xml11
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_background.xml8
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_progress.xml8
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/layout/activity_toolbar.xml23
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/layout/focus_blocking_switch.xml76
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/layout/item_toolbar_configuration.xml15
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher.xml8
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml8
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 862 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 295 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 2590 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 733 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 225 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 1737 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 1177 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 403 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 3748 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 1736 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 716 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 6057 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 2137 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 1092 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 8695 bytes
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/values/colors.xml7
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/values/dimens.xml8
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/values/ic_launcher_background.xml8
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/values/strings.xml9
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/xml/backup_rules.xml8
-rw-r--r--mobile/android/android-components/samples/toolbar/src/main/res/xml/data_extraction_rules.xml9
315 files changed, 11859 insertions, 0 deletions
diff --git a/mobile/android/android-components/samples/browser/.gitignore b/mobile/android/android-components/samples/browser/.gitignore
new file mode 100644
index 0000000000..af6eaebcd7
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/.gitignore
@@ -0,0 +1,2 @@
+/build
+manifest.json
diff --git a/mobile/android/android-components/samples/browser/README.md b/mobile/android/android-components/samples/browser/README.md
new file mode 100644
index 0000000000..d64d19d181
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/README.md
@@ -0,0 +1,35 @@
+# [Android Components](../../README.md) > Samples > Browser
+
+![](src/main/res/mipmap-xhdpi/ic_launcher.png)
+
+A simple browser app that is composed from the browser components in this repository.
+
+⚠️ **Note**: This sample application is only a very basic browser. For a full-featured reference browser implementation see the **[reference-browser repository](https://github.com/mozilla-mobile/reference-browser)**.
+
+## Build variants
+
+The browser app uses a product flavor:
+
+* **channel**: Using different release channels of GeckoView: _nightly_, _beta_, _production_. In most cases you want to use the _nightly_ flavor as this will support all of the latest functionality.
+
+## Glean SDK support
+
+This sample application comes with Glean SDK telemetry initialized by default, but with upload disabled (no data is being sent).
+This is for creating a simpler metric testing workflow for Gecko engineers that need to add their metrics to Gecko and expose them to Mozilla mobile products.
+See [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1592935) for more context.
+
+In order to enable data upload for testing purposes, change the `Glean.setUploadEnabled(false)` to `Glean.setUploadEnabled(true)` in [`SampleApplication.kt`](src/main/java/org/mozilla/samples/browser/SampleApplication.kt).
+
+Glean will send metrics from any Glean-enabled component used in this sample application:
+
+- [engine-gecko-nightly](https://github.com/mozilla-mobile/android-components/blob/main/components/browser/engine-gecko-nightly/docs/metrics.md);
+- [engine-gecko-beta](https://github.com/mozilla-mobile/android-components/blob/main/components/browser/engine-gecko-beta/docs/metrics.md);
+- [engine-gecko](https://github.com/mozilla-mobile/android-components/blob/main/components/browser/engine-gecko/docs/metrics.md);
+
+Data review for enabling the Glean SDK for this application can be found [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1592935#c6).
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/samples/browser/build.gradle b/mobile/android/android-components/samples/browser/build.gradle
new file mode 100644
index 0000000000..351becf85b
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/build.gradle
@@ -0,0 +1,186 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+if (findProject(":geckoview") != null) {
+ buildDir "${topobjdir}/gradle/build/mobile/android/samples-browser"
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+if (findProject(":geckoview") != null) {
+ apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
+}
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.browser"
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArgument "clearPackageData", "true"
+ testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ if (findProject(":geckoview") != null) {
+ project.configureProductFlavors.delegate = it
+ project.configureProductFlavors()
+ }
+
+ flavorDimensions += "engine"
+
+ productFlavors {
+ gecko {
+ dimension "engine"
+ }
+
+ // WebView
+ system {
+ dimension "engine"
+ }
+ }
+
+ variantFilter { variant ->
+ if (variant.buildType.name == "release") {
+ // This is a sample app that we are not releasing. Save some time and do not build
+ // release versions.
+ setIgnore(true)
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'org.mozilla.samples.browser'
+}
+
+tasks.register("updateBorderifyExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/borderify')
+}
+
+tasks.register("updateTestExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/test')
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':concept-awesomebar')
+ implementation project(':concept-fetch')
+ implementation project(':concept-engine')
+ implementation project(':concept-tabstray')
+ implementation project(':concept-toolbar')
+ implementation project(':concept-storage')
+ implementation project(':concept-base')
+
+ implementation project(':compose-awesomebar')
+
+ implementation project(':browser-engine-system')
+ implementation project(':browser-domains')
+ implementation project(':browser-icons')
+ implementation project(':browser-session-storage')
+ implementation project(':browser-state')
+ implementation project(':browser-tabstray')
+ implementation project(':browser-thumbnails')
+ implementation project(':browser-toolbar')
+ implementation project(':browser-menu')
+ implementation project(':browser-storage-sync')
+
+ implementation project(':lib-fetch-httpurlconnection')
+ implementation project(":lib-crash")
+ implementation project(':lib-dataprotect')
+ implementation project(":lib-publicsuffixlist")
+
+ implementation project(':feature-awesomebar')
+ implementation project(":feature-autofill")
+ implementation project(':feature-app-links')
+ implementation project(':feature-contextmenu')
+ implementation project(':feature-customtabs')
+ implementation project(':feature-downloads')
+ implementation project(':feature-intent')
+ implementation project(':feature-media')
+ implementation project(':feature-readerview')
+ implementation project(':feature-search')
+ implementation project(':feature-session')
+ implementation project(':feature-toolbar')
+ implementation project(':feature-tabs')
+ implementation project(':feature-prompts')
+ implementation project(':feature-privatemode')
+ implementation project(':feature-pwa')
+ implementation project(':feature-findinpage')
+ implementation project(':feature-sitepermissions')
+ implementation project(':feature-webcompat')
+ implementation project(':feature-webcompat-reporter')
+ implementation project(':feature-webnotifications')
+ implementation project(':feature-addons')
+
+ implementation project(':ui-autocomplete')
+ implementation project(':ui-tabcounter')
+ implementation project(':ui-widgets')
+
+ // Add a dependency on service-glean to simplify the testing workflow
+ // for engineers that want to test Gecko metrics exfiltrated via the Glean
+ // SDK. See bug 1592935 for more context.
+ implementation project(':service-glean')
+ implementation project(':service-location')
+ implementation project(':service-digitalassetlinks')
+ implementation project(':service-sync-logins')
+
+ implementation project(':support-base')
+ implementation project(':support-locale')
+ implementation project(':support-utils')
+ implementation project(':support-ktx')
+ implementation project(':support-webextensions')
+ implementation project(':support-rustlog')
+
+ geckoImplementation project(':browser-engine-gecko')
+
+ implementation ComponentsDependencies.google_material
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_compose_ui_tooling
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_swiperefreshlayout
+ implementation ComponentsDependencies.androidx_localbroadcastmanager
+
+ debugImplementation ComponentsDependencies.leakcanary
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockito
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation project(':support-android-test')
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.androidx_test_junit
+ androidTestImplementation ComponentsDependencies.androidx_test_uiautomator
+ androidTestImplementation ComponentsDependencies.androidx_espresso_core
+ androidTestImplementation ComponentsDependencies.testing_leakcanary
+ androidTestImplementation ComponentsDependencies.testing_mockwebserver
+}
+
+preBuild.dependsOn updateBorderifyExtensionVersion
+preBuild.dependsOn updateTestExtensionVersion
diff --git a/mobile/android/android-components/samples/browser/proguard-rules.pro b/mobile/android/android-components/samples/browser/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/browser/src/androidTest/assets/index.html b/mobile/android/android-components/samples/browser/src/androidTest/assets/index.html
new file mode 100644
index 0000000000..9f5632c044
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/androidTest/assets/index.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+<h1 id="website_title">Hello World!</h1>
+</body>
+</html>
diff --git a/mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt b/mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt
new file mode 100644
index 0000000000..9c3b3e2247
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/androidTest/java/org/mozilla/samples/browser/SmokeTests.kt
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.os.SystemClock
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.pressImeActionButton
+import androidx.test.espresso.action.ViewActions.replaceText
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiSelector
+import mozilla.components.support.android.test.rules.WebserverRule
+import org.junit.Assert.assertTrue
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+
+private const val INITIAL_WAIT_SECONDS = 5L
+private const val WAIT_FOR_WEB_CONTENT_SECONDS = 15L
+
+/**
+ * A collection of "smoke tests" to verify that the basic browsing functionality is working.
+ */
+
+@LargeTest
+class SmokeTests {
+ @get:Rule
+ val activityRule: ActivityScenarioRule<BrowserActivity> = ActivityScenarioRule(BrowserActivity::class.java)
+
+ @get:Rule
+ val webserverRule: WebserverRule = WebserverRule()
+
+ /**
+ * This test loads a website from a local webserver by typing into the URL bar. After that it verifies that the
+ * web content is visible.
+ */
+
+ @Test
+ fun loadWebsiteTest() {
+ // Disable on API21 - https://github.com/mozilla-mobile/android-components/issues/6482
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.LOLLIPOP) {
+ waitForIdle()
+
+ enterUrl(webserverRule.url())
+
+ verifyWebsiteContent("Hello World!")
+ verifyUrlInToolbar(webserverRule.url())
+ }
+ }
+
+ @Ignore("Intermittent: https://bugzilla.mozilla.org/show_bug.cgi?id=1794873")
+ @Test
+ fun loadWebsitesInMultipleTabsTest() {
+ // Disable on API21 - https://github.com/mozilla-mobile/android-components/issues/6482
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.LOLLIPOP) {
+ waitForIdle()
+
+ enterUrl(webserverRule.url())
+
+ verifyWebsiteContent("Hello World!")
+ verifyUrlInToolbar(webserverRule.url())
+
+ navigateToTabsTray()
+ openNewTabInTabsTray()
+
+ enterUrl(webserverRule.url())
+
+ verifyWebsiteContent("Hello World!")
+ verifyUrlInToolbar(webserverRule.url())
+
+ navigateToTabsTray()
+ openNewTabInTabsTray()
+
+ enterUrl(webserverRule.url())
+
+ verifyWebsiteContent("Hello World!")
+ verifyUrlInToolbar(webserverRule.url())
+
+ navigateToTabsTray()
+ openNewTabInTabsTray()
+ }
+ }
+}
+
+private fun waitForIdle() {
+ // Meh! We need a better idle strategy here. Because of bug 1441059 our load request gets lost if it happens
+ // to fast and then only "about:blank" gets loaded. So a "quick" fix here is to just wait a bit.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1441059
+ SystemClock.sleep(TimeUnit.SECONDS.toMillis(INITIAL_WAIT_SECONDS))
+}
+
+private fun navigateToTabsTray() {
+ onView(withContentDescription(mozilla.components.feature.tabs.R.string.mozac_feature_tabs_toolbar_tabs_button))
+ .perform(click())
+}
+
+private fun openNewTabInTabsTray() {
+ onView(withId(R.id.newTab))
+ .perform(click())
+}
+
+private fun enterUrl(url: String) {
+ onView(withId(mozilla.components.browser.toolbar.R.id.mozac_browser_toolbar_url_view))
+ .perform(click())
+
+ onView(withId(mozilla.components.browser.toolbar.R.id.mozac_browser_toolbar_edit_url_view))
+ .perform(replaceText(url), pressImeActionButton())
+}
+
+private fun verifyUrlInToolbar(url: String) {
+ onView(withId(mozilla.components.browser.toolbar.R.id.mozac_browser_toolbar_url_view))
+ .check(matches(withText(url)))
+}
+
+private fun verifyWebsiteContent(text: String) {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.waitForIdle()
+
+ val waitingTime: Long = TimeUnit.SECONDS.toMillis(WAIT_FOR_WEB_CONTENT_SECONDS)
+
+ assertTrue(
+ device
+ .findObject(
+ UiSelector()
+ .textContains(text),
+ )
+ .waitForExists(waitingTime),
+ )
+}
diff --git a/mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt b/mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt
new file mode 100644
index 0000000000..2741064e4c
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/gecko/java/org/mozilla/samples/browser/Components.kt
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.content.Context
+import mozilla.components.browser.engine.gecko.GeckoEngine
+import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
+import mozilla.components.concept.engine.Engine
+import mozilla.components.experiment.NimbusExperimentDelegate
+import mozilla.components.feature.webcompat.WebCompatFeature
+import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
+import mozilla.components.lib.crash.handler.CrashHandlerService
+import mozilla.components.support.base.log.Log
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.geckoview.GeckoRuntimeSettings
+
+/**
+ * Helper class for lazily instantiating components needed by the application.
+ */
+class Components(private val applicationContext: Context) : DefaultComponents(applicationContext) {
+ private val runtime by lazy {
+ // Allow for exfiltrating Gecko metrics through the Glean SDK.
+ val builder = GeckoRuntimeSettings.Builder().aboutConfigEnabled(true)
+ builder.experimentDelegate(NimbusExperimentDelegate())
+ builder.crashHandler(CrashHandlerService::class.java)
+ GeckoRuntime.create(applicationContext, builder.build())
+ }
+
+ override val engine: Engine by lazy {
+ GeckoEngine(applicationContext, engineSettings, runtime).also {
+ it.installBuiltInWebExtension("borderify@mozac.org", "resource://android/assets/extensions/borderify/") {
+ throwable ->
+ Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install borderify")
+ }
+ it.installBuiltInWebExtension("testext@mozac.org", "resource://android/assets/extensions/test/") {
+ throwable ->
+ Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install testext")
+ }
+ WebCompatFeature.install(it)
+ WebCompatReporterFeature.install(it)
+ }
+ }
+
+ override val client by lazy { GeckoViewFetchClient(applicationContext, runtime) }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..36b24ebd5f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/AndroidManifest.xml
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <!-- This is needed because the android.permission.CAMERA above automatically
+ adds a requirements for camera hardware and we don't want add those restrictions -->
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.camera.autofocus"
+ android:required="false" />
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage" />
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar"
+ android:name=".SampleApplication"
+ android:usesCleartextTraffic="true"
+ tools:ignore="DataExtractionRules,UnusedAttribute"
+ android:dataExtractionRules="@xml/data_extraction_rules">
+ <activity android:name=".BrowserActivity"
+ android:launchMode="singleTask"
+ android:exported="true"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".ExternalAppBrowserActivity"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
+ android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
+ android:exported="false"
+ android:taskAffinity=""
+ android:persistableMode="persistNever"
+ android:autoRemoveFromRecents="false" />
+
+ <activity
+ android:theme="@style/Theme.AppCompat.Light"
+ android:name=".addons.AddonsActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:parentActivityName=".BrowserActivity" />
+
+ <activity
+ android:theme="@style/Theme.AppCompat.Light"
+ android:name=".addons.AddonDetailsActivity"
+ android:label="@string/mozac_feature_addons_addons" />
+
+ <activity android:name=".addons.InstalledAddonDetailsActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:parentActivityName=".addons.AddonsActivity"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".addons.PermissionsDetailsActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".addons.AddonSettingsActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".addons.NotYetSupportedAddonActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".addons.WebExtensionActionPopupActivity"
+ android:label="@string/mozac_feature_addons_addons"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
+ android:name=".IntentReceiverActivity"
+ android:relinquishTaskIdentity="true"
+ android:taskAffinity=""
+ android:exported="true"
+ android:excludeFromRecents="true" >
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="mozilla.components.pwa.category.SHORTCUT" />
+
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/plain" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="mozilla.components.feature.pwa.VIEW_PWA" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="https" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".autofill.AutofillUnlockActivity"
+ android:exported="false"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+
+ <activity android:name=".autofill.AutofillConfirmActivity"
+ android:exported="false"
+ android:theme="@style/Theme.AppCompat.Translucent" />
+
+ <activity android:name=".autofill.AutofillSearchActivity"
+ android:exported="false" />
+
+ <service
+ android:name=".autofill.AutofillService"
+ android:label="@string/app_name"
+ android:exported="true"
+ android:permission="android.permission.BIND_AUTOFILL_SERVICE">
+ <intent-filter>
+ <action android:name="android.service.autofill.AutofillService"/>
+ </intent-filter>
+ <meta-data
+ android:name="android.autofill"
+ android:resource="@xml/service_configuration" />
+ </service>
+
+ <service
+ android:name=".customtabs.CustomTabsService"
+ android:exported="true"
+ tools:ignore="ExportedService">
+ <intent-filter>
+ <action android:name="android.support.customtabs.action.CustomTabsService" />
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=".downloads.DownloadService"
+ android:foregroundServiceType="dataSync" />
+
+ <service android:name=".media.MediaSessionService"
+ android:foregroundServiceType="mediaPlayback"
+ android:exported="false" />
+
+ </application>
+
+</manifest>
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js
new file mode 100644
index 0000000000..af58957d88
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/borderify.js
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+document.body.style.border = "5px solid red"; \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json
new file mode 100644
index 0000000000..cc2db02cbf
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/borderify/manifest.template.json
@@ -0,0 +1,16 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "borderify@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - Borderify",
+ "version": "${version}",
+ "content_scripts": [
+ {
+ "matches": ["*://www.mozilla.org/*"],
+ "js": ["borderify.js"]
+ }
+ ]
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js
new file mode 100644
index 0000000000..950936be4c
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/background.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Avoid adding ID selector rules in this style sheet, since they could
+ * inadvertently match elements in the article content. */
+
+// Counts to three and sends a greeting via the browser action of a newly created tab.
+browser.tabs.onCreated.addListener((tab) => {
+ let counter = 0;
+ let intervalId = setInterval(() => {
+ var message;
+ if (++counter <= 3) {
+ message = "" + counter;
+ } else {
+ message = "Hi!";
+ clearInterval(intervalId);
+ }
+ browser.browserAction.setBadgeTextColor({tabId: tab.id, color: "#FFFFFF"});
+ browser.browserAction.setBadgeText({tabId: tab.id, text: message});
+ }, 1000);
+});
+
+browser.browserAction.setBadgeBackgroundColor({color: "#AAAAAA"}); \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png
new file mode 100644
index 0000000000..455b15fc84
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/icon.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json
new file mode 100644
index 0000000000..04dc17aa2f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/manifest.template.json
@@ -0,0 +1,22 @@
+{
+ "manifest_version": 2,
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "testext@mozac.org"
+ }
+ },
+ "name": "Mozilla Android Components - Test extension",
+ "description": "This extension is used for testing web extension functionality in Android Components",
+ "version": "${version}",
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "browser_action": {
+ "default_icon": "icon.png",
+ "default_title": "Test",
+ "default_popup": "popup.html"
+ },
+ "permissions": [
+ "tabs"
+ ]
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html
new file mode 100644
index 0000000000..40d1467569
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/assets/extensions/test/popup.html
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html>
+ <head></head>
+ <body style="font-size: 36px">
+ <h1>Hello world!</h1>
+ <p>This is a browser action default popup.</p>
+ </body>
+</html>
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt
new file mode 100644
index 0000000000..7cef15d75f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt
@@ -0,0 +1,308 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.CallSuper
+import androidx.fragment.app.Fragment
+import kotlinx.coroutines.flow.mapNotNull
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.toolbar.display.DisplayToolbar
+import mozilla.components.feature.app.links.AppLinksFeature
+import mozilla.components.feature.downloads.DownloadsFeature
+import mozilla.components.feature.downloads.manager.FetchDownloadManager
+import mozilla.components.feature.privatemode.feature.SecureWindowFeature
+import mozilla.components.feature.prompts.PromptFeature
+import mozilla.components.feature.session.CoordinateScrollingFeature
+import mozilla.components.feature.session.SessionFeature
+import mozilla.components.feature.session.SwipeRefreshFeature
+import mozilla.components.feature.sitepermissions.SitePermissionsFeature
+import mozilla.components.feature.sitepermissions.SitePermissionsRules
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
+import mozilla.components.feature.toolbar.ToolbarFeature
+import mozilla.components.lib.state.ext.consumeFlow
+import mozilla.components.support.base.feature.ActivityResultHandler
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
+import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
+import mozilla.components.support.locale.ActivityContextWrapper
+import mozilla.components.support.utils.ext.requestInPlacePermissions
+import org.mozilla.samples.browser.databinding.FragmentBrowserBinding
+import org.mozilla.samples.browser.downloads.DownloadService
+import org.mozilla.samples.browser.ext.components
+import org.mozilla.samples.browser.integration.ContextMenuIntegration
+import org.mozilla.samples.browser.integration.FindInPageIntegration
+
+/**
+ * Base fragment extended by [BrowserFragment] and [ExternalAppBrowserFragment].
+ * This class only contains shared code focused on the main browsing content.
+ * UI code specific to the app or to custom tabs can be found in the subclasses.
+ */
+@SuppressWarnings("LargeClass")
+abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler {
+ private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
+ private val toolbarFeature = ViewBoundFeatureWrapper<ToolbarFeature>()
+ private val contextMenuIntegration = ViewBoundFeatureWrapper<ContextMenuIntegration>()
+ private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
+ private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
+ private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
+ private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
+ private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
+ private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
+
+ protected val sessionId: String?
+ get() = arguments?.getString(SESSION_ID_KEY)
+
+ private val activityResultHandler: List<ViewBoundFeatureWrapper<*>> = listOf(
+ promptFeature,
+ )
+
+ private var _binding: FragmentBrowserBinding? = null
+ val binding get() = _binding!!
+
+ @CallSuper
+ @Suppress("LongMethod")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ _binding = FragmentBrowserBinding.inflate(inflater, container, false)
+
+ binding.toolbar.display.menuBuilder = components.menuBuilder
+ val originalContext = ActivityContextWrapper.getOriginalContext(requireActivity())
+ binding.engineView.setActivityContext(originalContext)
+
+ sessionFeature.set(
+ feature = SessionFeature(
+ components.store,
+ components.sessionUseCases.goBack,
+ binding.engineView,
+ sessionId,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ toolbarFeature.set(
+ feature = ToolbarFeature(
+ binding.toolbar,
+ components.store,
+ components.sessionUseCases.loadUrl,
+ components.defaultSearchUseCase,
+ sessionId,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ binding.toolbar.display.indicators += listOf(
+ DisplayToolbar.Indicators.TRACKING_PROTECTION,
+ DisplayToolbar.Indicators.HIGHLIGHT,
+ )
+
+ swipeRefreshFeature.set(
+ feature = SwipeRefreshFeature(
+ components.store,
+ components.sessionUseCases.reload,
+ binding.swipeToRefresh,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ downloadsFeature.set(
+ feature = DownloadsFeature(
+ requireContext().applicationContext,
+ store = components.store,
+ useCases = components.downloadsUseCases,
+ fragmentManager = childFragmentManager,
+ onDownloadStopped = { download, id, status ->
+ Logger.debug("Download done. ID#$id $download with status $status")
+ },
+ downloadManager = FetchDownloadManager(
+ requireContext().applicationContext,
+ components.store,
+ DownloadService::class,
+ notificationsDelegate = components.notificationsDelegate,
+ ),
+ tabId = sessionId,
+ onNeedToRequestPermissions = { permissions ->
+ requestInPlacePermissions(REQUEST_KEY_DOWNLOAD_PERMISSIONS, permissions) { result ->
+ downloadsFeature.get()?.onPermissionsResult(
+ result.keys.toTypedArray(),
+ result.values.map {
+ when (it) {
+ true -> PackageManager.PERMISSION_GRANTED
+ false -> PackageManager.PERMISSION_DENIED
+ }
+ }.toIntArray(),
+ )
+ }
+ },
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ val scrollFeature = CoordinateScrollingFeature(components.store, binding.engineView, binding.toolbar)
+
+ contextMenuIntegration.set(
+ feature = ContextMenuIntegration(
+ context = requireContext(),
+ fragmentManager = parentFragmentManager,
+ browserStore = components.store,
+ tabsUseCases = components.tabsUseCases,
+ contextMenuUseCases = components.contextMenuUseCases,
+ parentView = binding.root,
+ sessionId = sessionId,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ appLinksFeature.set(
+ feature = AppLinksFeature(
+ context = requireContext(),
+ store = components.store,
+ sessionId = sessionId,
+ fragmentManager = parentFragmentManager,
+ launchInApp = { components.preferences.getBoolean(DefaultComponents.PREF_LAUNCH_EXTERNAL_APP, false) },
+ loadUrlUseCase = components.sessionUseCases.loadUrl,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ promptFeature.set(
+ feature = PromptFeature(
+ fragment = this,
+ store = components.store,
+ customTabId = sessionId,
+ tabsUseCases = components.tabsUseCases,
+ fragmentManager = parentFragmentManager,
+ fileUploadsDirCleaner = components.fileUploadsDirCleaner,
+ onNeedToRequestPermissions = { permissions ->
+ requestInPlacePermissions(REQUEST_KEY_PROMPT_PERMISSIONS, permissions) { result ->
+ promptFeature.get()?.onPermissionsResult(
+ result.keys.toTypedArray(),
+ result.values.map {
+ when (it) {
+ true -> PackageManager.PERMISSION_GRANTED
+ false -> PackageManager.PERMISSION_DENIED
+ }
+ }.toIntArray(),
+ )
+ }
+ },
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ sitePermissionsFeature.set(
+ feature = SitePermissionsFeature(
+ context = requireContext(),
+ sessionId = sessionId,
+ storage = components.permissionStorage,
+ fragmentManager = parentFragmentManager,
+ sitePermissionsRules = SitePermissionsRules(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ camera = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ location = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ notification = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ microphone = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ persistentStorage = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ mediaKeySystemAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ crossOriginStorageAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
+ ),
+ onNeedToRequestPermissions = { permissions ->
+ requestInPlacePermissions(REQUEST_KEY_SITE_PERMISSIONS, permissions) { result ->
+ sitePermissionsFeature.get()?.onPermissionsResult(
+ result.keys.toTypedArray(),
+ result.values.map {
+ when (it) {
+ true -> PackageManager.PERMISSION_GRANTED
+ false -> PackageManager.PERMISSION_DENIED
+ }
+ }.toIntArray(),
+ )
+ }
+ },
+ onShouldShowRequestPermissionRationale = { shouldShowRequestPermissionRationale(it) },
+ store = components.store,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ findInPageIntegration.set(
+ feature = FindInPageIntegration(components.store, binding.findInPage, binding.engineView),
+ owner = this,
+ view = binding.root,
+ )
+
+ val secureWindowFeature = SecureWindowFeature(
+ window = requireActivity().window,
+ store = components.store,
+ customTabId = sessionId,
+ )
+
+ // Observe the lifecycle for supported features
+ lifecycle.addObservers(
+ scrollFeature,
+ secureWindowFeature,
+ )
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ consumeFlow(components.store) { flow ->
+ flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(sessionId) }
+ .ifAnyChanged { tab ->
+ arrayOf(
+ tab.content.loading,
+ tab.content.canGoBack,
+ tab.content.canGoForward,
+ )
+ }
+ .collect {
+ binding.toolbar.invalidateActions()
+ }
+ }
+ }
+
+ @CallSuper
+ override fun onBackPressed(): Boolean =
+ listOf(findInPageIntegration, toolbarFeature, sessionFeature).any { it.onBackPressed() }
+
+ @CallSuper
+ override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
+ return activityResultHandler.any { it.onActivityResult(requestCode, data, resultCode) }
+ }
+
+ companion object {
+ private const val SESSION_ID_KEY = "session_id"
+
+ private const val REQUEST_KEY_DOWNLOAD_PERMISSIONS = "downloadFeature"
+ private const val REQUEST_KEY_PROMPT_PERMISSIONS = "promptFeature"
+ private const val REQUEST_KEY_SITE_PERMISSIONS = "sitePermissionsFeature"
+
+ @JvmStatic
+ protected fun Bundle.putSessionId(sessionId: String?) {
+ putString(SESSION_ID_KEY, sessionId)
+ }
+ }
+ override fun onDestroyView() {
+ super.onDestroyView()
+ binding.engineView.setActivityContext(null)
+ _binding = null
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt
new file mode 100644
index 0000000000..99a05bc4f4
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.content.ComponentCallbacks2
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.util.AttributeSet
+import android.view.View
+import androidx.fragment.app.Fragment
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.contextmenu.ext.DefaultSelectionActionDelegate
+import mozilla.components.feature.intent.ext.getSessionId
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.locale.LocaleAwareAppCompatActivity
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.webextensions.WebExtensionPopupObserver
+import org.mozilla.samples.browser.addons.WebExtensionActionPopupActivity
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Activity that holds the [BrowserFragment].
+ */
+open class BrowserActivity : LocaleAwareAppCompatActivity(), ComponentCallbacks2 {
+ private val webExtensionPopupObserver by lazy {
+ WebExtensionPopupObserver(components.store, ::openPopup)
+ }
+
+ /**
+ * Returns a new instance of [BrowserFragment] to display.
+ */
+ open fun createBrowserFragment(sessionId: String?): Fragment =
+ BrowserFragment.create(sessionId)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ if (savedInstanceState == null) {
+ val sessionId = SafeIntent(intent).getSessionId()
+ supportFragmentManager.beginTransaction().apply {
+ replace(R.id.container, createBrowserFragment(sessionId))
+ commit()
+ }
+ }
+
+ lifecycle.addObserver(webExtensionPopupObserver)
+ components.historyStorage.registerStorageMaintenanceWorker()
+ components.notificationsDelegate.bindToActivity(this)
+ }
+
+ override fun onBackPressed() {
+ supportFragmentManager.fragments.forEach {
+ if (it is UserInteractionHandler && it.onBackPressed()) {
+ return
+ }
+ }
+
+ onBackPressedDispatcher.onBackPressed()
+ }
+
+ override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
+ when (name) {
+ EngineView::class.java.name -> components.engine.createView(context, attrs).apply {
+ selectionActionDelegate = DefaultSelectionActionDelegate(
+ store = components.store,
+ context = context,
+ )
+ }.asView()
+ else -> super.onCreateView(parent, name, context, attrs)
+ }
+
+ private fun openPopup(webExtensionState: WebExtensionState) {
+ val intent = Intent(this, WebExtensionActionPopupActivity::class.java)
+ intent.putExtra("web_extension_id", webExtensionState.id)
+ intent.putExtra("web_extension_name", webExtensionState.name)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ startActivity(intent)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ components.notificationsDelegate.unBindActivity(this)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt
new file mode 100644
index 0000000000..2ef0a03003
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import mozilla.components.browser.thumbnails.BrowserThumbnails
+import mozilla.components.feature.awesomebar.AwesomeBarFeature
+import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
+import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
+import mozilla.components.feature.search.SearchFeature
+import mozilla.components.feature.session.FullScreenFeature
+import mozilla.components.feature.tabs.WindowFeature
+import mozilla.components.feature.tabs.toolbar.TabsToolbarFeature
+import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
+import mozilla.components.feature.toolbar.WebExtensionToolbarFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import mozilla.components.support.ktx.android.view.enterImmersiveMode
+import mozilla.components.support.ktx.android.view.exitImmersiveMode
+import org.mozilla.samples.browser.ext.components
+import org.mozilla.samples.browser.integration.ReaderViewIntegration
+
+/**
+ * Fragment used for browsing the web within the main app.
+ */
+class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
+ private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
+ private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
+ private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
+ private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
+ private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
+ private val mediaSessionFullscreenFeature =
+ ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
+
+ @Suppress("LongMethod")
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ super.onCreateView(inflater, container, savedInstanceState)
+ val binding = super.binding
+ ToolbarAutocompleteFeature(binding.toolbar, components.engine).apply {
+ updateAutocompleteProviders(
+ providers = listOf(components.historyStorage, components.shippedDomainsProvider),
+ refreshAutocomplete = false,
+ )
+ }
+
+ TabsToolbarFeature(
+ toolbar = binding.toolbar,
+ store = components.store,
+ sessionId = sessionId,
+ lifecycleOwner = viewLifecycleOwner,
+ showTabs = ::showTabs,
+ countBasedOnSelectedTabType = false,
+ )
+
+ AwesomeBarFeature(binding.awesomeBar, binding.toolbar, binding.engineView, components.icons)
+ .addHistoryProvider(
+ components.historyStorage,
+ components.sessionUseCases.loadUrl,
+ components.engine,
+ )
+ .addSessionProvider(
+ resources,
+ components.store,
+ components.tabsUseCases.selectTab,
+ )
+ .addSearchActionProvider(
+ components.store,
+ searchUseCase = components.searchUseCases.defaultSearch,
+ )
+ .addSearchProvider(
+ requireContext(),
+ components.store,
+ components.searchUseCases.defaultSearch,
+ fetchClient = components.client,
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ engine = components.engine,
+ filterExactMatch = true,
+ )
+ .addClipboardProvider(
+ requireContext(),
+ components.sessionUseCases.loadUrl,
+ components.engine,
+ )
+
+ readerViewFeature.set(
+ feature = ReaderViewIntegration(
+ requireContext(),
+ components.engine,
+ components.store,
+ binding.toolbar,
+ binding.readerViewBar,
+ binding.readerViewAppearanceButton,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ fullScreenFeature.set(
+ feature = FullScreenFeature(
+ components.store,
+ components.sessionUseCases,
+ sessionId,
+ ) { inFullScreen ->
+ if (inFullScreen) {
+ activity?.enterImmersiveMode()
+ } else {
+ activity?.exitImmersiveMode()
+ }
+ },
+ owner = this,
+ view = binding.root,
+ )
+
+ mediaSessionFullscreenFeature.set(
+ feature = MediaSessionFullscreenFeature(
+ requireActivity(),
+ components.store,
+ sessionId,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ thumbnailsFeature.set(
+ feature = BrowserThumbnails(requireContext(), binding.engineView, components.store),
+ owner = this,
+ view = binding.root,
+ )
+
+ webExtToolbarFeature.set(
+ feature = WebExtensionToolbarFeature(
+ binding.toolbar,
+ components.store,
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ searchFeature.set(
+ feature = SearchFeature(components.store) { request, _ ->
+ if (request.isPrivate) {
+ components.searchUseCases.newPrivateTabSearch.invoke(request.query)
+ } else {
+ components.searchUseCases.newTabSearch.invoke(request.query)
+ }
+ },
+ owner = this,
+ view = binding.root,
+ )
+
+ val windowFeature = WindowFeature(components.store, components.tabsUseCases)
+ lifecycle.addObserver(windowFeature)
+
+ return binding.root
+ }
+
+ private fun showTabs() {
+ // For now we are performing manual fragment transactions here. Once we can use the new
+ // navigation support library we may want to pass navigation graphs around.
+ activity?.supportFragmentManager?.beginTransaction()?.apply {
+ replace(R.id.container, TabsTrayFragment())
+ commit()
+ }
+ }
+
+ override fun onBackPressed(): Boolean {
+ return when {
+ fullScreenFeature.onBackPressed() -> true
+ readerViewFeature.onBackPressed() -> true
+ else -> super.onBackPressed()
+ }
+ }
+
+ companion object {
+ fun create(sessionId: String? = null) = BrowserFragment().apply {
+ arguments = Bundle().apply {
+ putSessionId(sessionId)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt
new file mode 100644
index 0000000000..cda6c31982
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt
@@ -0,0 +1,515 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.widget.Toast
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
+import mozilla.components.browser.engine.system.SystemEngine
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.menu.BrowserMenuHighlight
+import mozilla.components.browser.menu.WebExtensionBrowserMenuBuilder
+import mozilla.components.browser.menu.item.BrowserMenuCheckbox
+import mozilla.components.browser.menu.item.BrowserMenuDivider
+import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
+import mozilla.components.browser.menu.item.BrowserMenuImageText
+import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.browser.session.storage.SessionStorage
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
+import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
+import mozilla.components.concept.fetch.Client
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.feature.addons.amo.AMOAddonsProvider
+import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
+import mozilla.components.feature.addons.update.DefaultAddonUpdater
+import mozilla.components.feature.app.links.AppLinksInterceptor
+import mozilla.components.feature.app.links.AppLinksUseCases
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.contextmenu.ContextMenuUseCases
+import mozilla.components.feature.customtabs.CustomTabIntentProcessor
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.downloads.DownloadMiddleware
+import mozilla.components.feature.downloads.DownloadsUseCases
+import mozilla.components.feature.intent.processing.TabIntentProcessor
+import mozilla.components.feature.media.MediaSessionFeature
+import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
+import mozilla.components.feature.prompts.PromptMiddleware
+import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.WebAppInterceptor
+import mozilla.components.feature.pwa.WebAppShortcutManager
+import mozilla.components.feature.pwa.WebAppUseCases
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
+import mozilla.components.feature.readerview.ReaderViewMiddleware
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.search.middleware.SearchMiddleware
+import mozilla.components.feature.search.region.RegionMiddleware
+import mozilla.components.feature.session.HistoryDelegate
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.session.middleware.LastAccessMiddleware
+import mozilla.components.feature.session.middleware.undo.UndoMiddleware
+import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.feature.webnotifications.WebNotificationFeature
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.service.digitalassetlinks.local.StatementApi
+import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
+import mozilla.components.service.location.LocationService
+import mozilla.components.service.sync.logins.SyncableLoginsStorage
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.worker.Frequency
+import org.mozilla.samples.browser.addons.AddonsActivity
+import org.mozilla.samples.browser.autofill.AutofillConfirmActivity
+import org.mozilla.samples.browser.autofill.AutofillSearchActivity
+import org.mozilla.samples.browser.autofill.AutofillUnlockActivity
+import org.mozilla.samples.browser.downloads.DownloadService
+import org.mozilla.samples.browser.ext.components
+import org.mozilla.samples.browser.integration.FindInPageIntegration
+import org.mozilla.samples.browser.media.MediaSessionService
+import org.mozilla.samples.browser.request.SampleUrlEncodedRequestInterceptor
+import java.util.concurrent.TimeUnit
+import mozilla.components.ui.colors.R.color as photonColors
+import mozilla.components.ui.icons.R as iconsR
+
+private const val DAY_IN_MINUTES = 24 * 60L
+
+@SuppressLint("NewApi")
+@Suppress("LargeClass")
+open class DefaultComponents(private val applicationContext: Context) {
+ companion object {
+ const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences"
+ const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app"
+ const val PREF_GLOBAL_PRIVACY_CONTROL = "sample_browser_global_privacy_control"
+ }
+
+ val preferences: SharedPreferences =
+ applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE)
+
+ private val securePreferences by lazy { SecureAbove22Preferences(applicationContext, "key_store") }
+
+ val autofillConfiguration by lazy {
+ AutofillConfiguration(
+ storage = SyncableLoginsStorage(applicationContext, lazy { securePreferences }),
+ publicSuffixList = publicSuffixList,
+ unlockActivity = AutofillUnlockActivity::class.java,
+ confirmActivity = AutofillConfirmActivity::class.java,
+ searchActivity = AutofillSearchActivity::class.java,
+ applicationName = "Sample Browser",
+ httpClient = client,
+ )
+ }
+
+ val publicSuffixList by lazy { PublicSuffixList(applicationContext) }
+
+ // Engine Settings
+ val engineSettings by lazy {
+ DefaultSettings().apply {
+ historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
+ requestInterceptor = SampleUrlEncodedRequestInterceptor(applicationContext)
+ remoteDebuggingEnabled = true
+ supportMultipleWindows = true
+ preferredColorScheme = PreferredColorScheme.Dark
+ httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
+ globalPrivacyControlEnabled = applicationContext.components.preferences.getBoolean(
+ PREF_GLOBAL_PRIVACY_CONTROL,
+ false,
+ )
+ }
+ }
+
+ private val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)
+
+ val notificationsDelegate: NotificationsDelegate by lazy {
+ NotificationsDelegate(
+ notificationManagerCompat,
+ )
+ }
+
+ val addonUpdater =
+ DefaultAddonUpdater(applicationContext, Frequency(1, TimeUnit.DAYS), notificationsDelegate)
+
+ // Engine
+ open val engine: Engine by lazy {
+ SystemEngine(applicationContext, engineSettings)
+ }
+
+ open val client: Client by lazy { HttpURLConnectionClient() }
+
+ val icons by lazy { BrowserIcons(applicationContext, client) }
+
+ // Storage
+ private val lazyHistoryStorage = lazy { PlacesHistoryStorage(applicationContext) }
+ val historyStorage by lazy { lazyHistoryStorage.value }
+
+ val sessionStorage by lazy { SessionStorage(applicationContext, engine) }
+
+ val permissionStorage by lazy { OnDiskSitePermissionsStorage(applicationContext) }
+
+ val thumbnailStorage by lazy { ThumbnailStorage(applicationContext) }
+
+ val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy {
+ FileUploadsDirCleaner { applicationContext.cacheDir }
+ }
+
+ val store by lazy {
+ BrowserStore(
+ middleware = listOf(
+ DownloadMiddleware(applicationContext, DownloadService::class.java),
+ ReaderViewMiddleware(),
+ ThumbnailsMiddleware(thumbnailStorage),
+ UndoMiddleware(),
+ RegionMiddleware(
+ applicationContext,
+ LocationService.default(),
+ ),
+ SearchMiddleware(applicationContext),
+ RecordingDevicesMiddleware(applicationContext, notificationsDelegate),
+ LastAccessMiddleware(),
+ PromptMiddleware(),
+ SessionPrioritizationMiddleware(),
+ ) + EngineMiddleware.create(engine),
+ ).apply {
+ WebNotificationFeature(
+ applicationContext,
+ engine,
+ icons,
+ R.mipmap.ic_launcher_foreground,
+ permissionStorage,
+ IntentReceiverActivity::class.java,
+ notificationsDelegate = notificationsDelegate,
+ )
+
+ MediaSessionFeature(applicationContext, MediaSessionService::class.java, this).start()
+ }
+ }
+
+ val customTabsStore by lazy { CustomTabsServiceStore() }
+
+ val sessionUseCases by lazy { SessionUseCases(store) }
+
+ val customTabsUseCases by lazy { CustomTabsUseCases(store, sessionUseCases.loadUrl) }
+
+ // Addons
+ val addonManager by lazy {
+ AddonManager(store, engine, addonsProvider, addonUpdater)
+ }
+
+ val addonsProvider by lazy {
+ AMOAddonsProvider(
+ applicationContext,
+ client,
+ collectionName = "7dfae8669acc4312a65e8ba5553036",
+ maxCacheAgeInMinutes = DAY_IN_MINUTES,
+ )
+ }
+
+ val supportedAddonsChecker by lazy {
+ DefaultSupportedAddonsChecker(applicationContext, Frequency(1, TimeUnit.DAYS))
+ }
+
+ val searchUseCases by lazy {
+ SearchUseCases(store, tabsUseCases, sessionUseCases)
+ }
+
+ val defaultSearchUseCase by lazy {
+ { searchTerms: String ->
+ searchUseCases.defaultSearch.invoke(
+ searchTerms = searchTerms,
+ searchEngine = null,
+ parentSessionId = null,
+ )
+ }
+ }
+ val appLinksUseCases by lazy { AppLinksUseCases(applicationContext) }
+
+ val appLinksInterceptor by lazy {
+ AppLinksInterceptor(
+ applicationContext,
+ interceptLinkClicks = true,
+ launchInApp = {
+ applicationContext.components.preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
+ },
+ )
+ }
+
+ val webAppInterceptor by lazy {
+ WebAppInterceptor(
+ applicationContext,
+ webAppManifestStorage,
+ )
+ }
+
+ val webAppManifestStorage by lazy { ManifestStorage(applicationContext) }
+ val webAppShortcutManager by lazy { WebAppShortcutManager(applicationContext, client, webAppManifestStorage) }
+ val webAppUseCases by lazy { WebAppUseCases(applicationContext, store, webAppShortcutManager) }
+
+ // Digital Asset Links checking
+ val relationChecker by lazy {
+ StatementRelationChecker(StatementApi(client))
+ }
+
+ // Intent
+ val tabIntentProcessor by lazy {
+ TabIntentProcessor(tabsUseCases, searchUseCases.newTabSearch)
+ }
+ val externalAppIntentProcessors by lazy {
+ listOf(
+ WebAppIntentProcessor(store, customTabsUseCases.addWebApp, sessionUseCases.loadUrl, webAppManifestStorage),
+ CustomTabIntentProcessor(customTabsUseCases.add, applicationContext.resources),
+ )
+ }
+
+ // Menu
+ val menuBuilder by lazy {
+ WebExtensionBrowserMenuBuilder(
+ menuItems,
+ store = store,
+ style = WebExtensionBrowserMenuBuilder.Style(
+ webExtIconTintColorResource = photonColors.photonGrey90,
+ ),
+ onAddonsManagerTapped = {
+ val intent = Intent(applicationContext, AddonsActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ applicationContext.startActivity(intent)
+ },
+ )
+ }
+
+ private val menuItems by lazy {
+ val items = mutableListOf(
+ menuToolbar,
+ BrowserMenuHighlightableItem(
+ "No Highlight",
+ iconsR.drawable.mozac_ic_share_android_24,
+ android.R.color.black,
+ highlight = BrowserMenuHighlight.LowPriority(
+ notificationTint = ContextCompat.getColor(applicationContext, android.R.color.holo_green_dark),
+ label = "Highlight",
+ ),
+ ) {
+ Toast.makeText(applicationContext, "Highlight", Toast.LENGTH_SHORT).show()
+ },
+ BrowserMenuImageText("Share", iconsR.drawable.mozac_ic_share_android_24, android.R.color.black) {
+ Toast.makeText(applicationContext, "Share", Toast.LENGTH_SHORT).show()
+ },
+ SimpleBrowserMenuItem("Settings") {
+ Toast.makeText(applicationContext, "Settings", Toast.LENGTH_SHORT).show()
+ },
+ SimpleBrowserMenuItem("Find In Page") {
+ FindInPageIntegration.launch?.invoke()
+ },
+ SimpleBrowserMenuItem("Save to PDF") {
+ sessionUseCases.saveToPdf.invoke()
+ },
+
+ SimpleBrowserMenuItem("Translate (auto)") {
+ var detectedFrom =
+ store.state.selectedTab?.translationsState?.translationEngineState
+ ?.detectedLanguages?.documentLangTag
+ ?: "en"
+ var detectedTo =
+ store.state.selectedTab?.translationsState?.translationEngineState
+ ?.detectedLanguages?.userPreferredLangTag
+ ?: "en"
+ sessionUseCases.translate.invoke(
+ fromLanguage = detectedFrom,
+ toLanguage = detectedTo,
+ options = null,
+ )
+ },
+ SimpleBrowserMenuItem("Print") {
+ sessionUseCases.printContent.invoke()
+ },
+ SimpleBrowserMenuItem("Restore after Translate") {
+ sessionUseCases.translateRestore.invoke()
+ },
+ SimpleBrowserMenuItem("Restore after crash") {
+ sessionUseCases.crashRecovery.invoke()
+ },
+ BrowserMenuDivider(),
+ )
+
+ items.add(
+ SimpleBrowserMenuItem("Add to homescreen") {
+ MainScope().launch {
+ webAppUseCases.addToHomescreen()
+ }
+ }.apply {
+ visible = { webAppUseCases.isPinningSupported() && store.state.selectedTabId != null }
+ },
+ )
+
+ items.add(
+ SimpleBrowserMenuItem("Open in App") {
+ val getRedirect = appLinksUseCases.appLinkRedirect
+ store.state.selectedTab?.let {
+ val redirect = getRedirect.invoke(it.content.url)
+ redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ appLinksUseCases.openAppLink.invoke(redirect.appIntent)
+ }
+ }.apply {
+ visible = {
+ store.state.selectedTab?.let {
+ appLinksUseCases.appLinkRedirect(it.content.url).hasExternalApp()
+ } ?: false
+ }
+ },
+ )
+
+ items.add(
+ BrowserMenuCheckbox(
+ "Request desktop site",
+ {
+ store.state.selectedTab?.content?.desktopMode == true
+ },
+ ) { checked ->
+ sessionUseCases.requestDesktopSite(checked)
+ }.apply {
+ visible = { store.state.selectedTab != null }
+ },
+ )
+ items.add(
+ BrowserMenuCheckbox(
+ "Open links in apps",
+ {
+ preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
+ },
+ ) { checked ->
+ preferences.edit().putBoolean(PREF_LAUNCH_EXTERNAL_APP, checked).apply()
+ },
+ )
+
+ items.add(
+ BrowserMenuCheckbox(
+ "Tell websites not to share and sell data",
+ {
+ preferences.getBoolean(PREF_GLOBAL_PRIVACY_CONTROL, false)
+ },
+ ) { checked ->
+ preferences.edit().putBoolean(PREF_GLOBAL_PRIVACY_CONTROL, checked).apply()
+ engine.settings.globalPrivacyControlEnabled = checked
+ sessionUseCases.reload()
+ },
+ )
+
+ items
+ }
+
+ private val menuToolbar by lazy {
+ val back = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = iconsR.drawable.mozac_ic_back_24,
+ primaryImageTintResource = photonColors.photonBlue90,
+ primaryContentDescription = "Back",
+ isInPrimaryState = {
+ store.state.selectedTab?.content?.canGoBack ?: true
+ },
+ disableInSecondaryState = true,
+ secondaryImageTintResource = photonColors.photonGrey40,
+ ) {
+ sessionUseCases.goBack()
+ }
+
+ val forward = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = iconsR.drawable.mozac_ic_forward_24,
+ primaryContentDescription = "Forward",
+ primaryImageTintResource = photonColors.photonBlue90,
+ isInPrimaryState = {
+ store.state.selectedTab?.content?.canGoForward ?: true
+ },
+ disableInSecondaryState = true,
+ secondaryImageTintResource = photonColors.photonGrey40,
+ ) {
+ sessionUseCases.goForward()
+ }
+
+ val refresh = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = iconsR.drawable.mozac_ic_arrow_clockwise_24,
+ primaryContentDescription = "Refresh",
+ primaryImageTintResource = photonColors.photonBlue90,
+ isInPrimaryState = {
+ store.state.selectedTab?.content?.loading == false
+ },
+ secondaryImageResource = iconsR.drawable.mozac_ic_stop,
+ secondaryContentDescription = "Stop",
+ secondaryImageTintResource = photonColors.photonBlue90,
+ disableInSecondaryState = false,
+ ) {
+ if (store.state.selectedTab?.content?.loading == true) {
+ sessionUseCases.stopLoading()
+ } else {
+ sessionUseCases.reload()
+ }
+ }
+
+ BrowserMenuItemToolbar(listOf(back, forward, refresh))
+ }
+
+ val shippedDomainsProvider by lazy {
+ // Assume this is used together with other autocomplete providers (like history) which have priority 0
+ // and set priority 1 for the domains provider to ensure other providers' results are shown first.
+ ShippedDomainsProvider(1).also { it.initialize(applicationContext) }
+ }
+
+ val tabsUseCases: TabsUseCases by lazy { TabsUseCases(store) }
+ val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) }
+ val contextMenuUseCases: ContextMenuUseCases by lazy { ContextMenuUseCases(store) }
+
+ val crashReporter: CrashReporter by lazy {
+ CrashReporter(
+ applicationContext,
+ services = listOf(
+ object : CrashReporterService {
+ override val id: String
+ get() = "xxx"
+ override val name: String
+ get() = "Test"
+
+ override fun createCrashReportUrl(identifier: String): String? {
+ return null
+ }
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? {
+ return null
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ return null
+ }
+
+ override fun report(
+ throwable: Throwable,
+ breadcrumbs: ArrayList<Breadcrumb>,
+ ): String? {
+ return null
+ }
+ },
+ ),
+ notificationsDelegate = notificationsDelegate,
+ ).install(applicationContext)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt
new file mode 100644
index 0000000000..b7d75cf8ce
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserActivity.kt
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import androidx.fragment.app.Fragment
+import mozilla.components.feature.pwa.ext.getWebAppManifest
+
+/**
+ * Activity that holds the [BrowserFragment] that is launched within an external app,
+ * such as custom tabs and progressive web apps.
+ */
+class ExternalAppBrowserActivity : BrowserActivity() {
+
+ override fun createBrowserFragment(sessionId: String?): Fragment {
+ return if (sessionId != null) {
+ val manifest = intent.getWebAppManifest()
+
+ ExternalAppBrowserFragment.create(
+ sessionId,
+ manifest = manifest,
+ )
+ } else {
+ // Fall back to browser fragment
+ super.createBrowserFragment(sessionId)
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt
new file mode 100644
index 0000000000..f397eb870f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ExternalAppBrowserFragment.kt
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.customtabs.CustomTabWindowFeature
+import mozilla.components.feature.customtabs.CustomTabsToolbarFeature
+import mozilla.components.feature.pwa.ext.getWebAppManifest
+import mozilla.components.feature.pwa.ext.putWebAppManifest
+import mozilla.components.feature.pwa.feature.ManifestUpdateFeature
+import mozilla.components.feature.pwa.feature.WebAppActivityFeature
+import mozilla.components.feature.pwa.feature.WebAppContentFeature
+import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature
+import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Fragment used for browsing within an external app, such as for custom tabs and PWAs.
+ */
+class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
+ private val customTabsToolbarFeature = ViewBoundFeatureWrapper<CustomTabsToolbarFeature>()
+ private val hideToolbarFeature = ViewBoundFeatureWrapper<WebAppHideToolbarFeature>()
+
+ private val manifest: WebAppManifest?
+ get() = arguments?.getWebAppManifest()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ super.onCreateView(inflater, container, savedInstanceState)
+ val binding = super.binding
+
+ val manifest = this.manifest
+
+ customTabsToolbarFeature.set(
+ feature = CustomTabsToolbarFeature(
+ components.store,
+ binding.toolbar,
+ sessionId,
+ components.customTabsUseCases,
+ components.menuBuilder,
+ window = activity?.window,
+ closeListener = { activity?.finish() },
+ ),
+ owner = this,
+ view = binding.root,
+ )
+
+ hideToolbarFeature.set(
+ feature = WebAppHideToolbarFeature(
+ components.store,
+ components.customTabsStore,
+ sessionId,
+ manifest,
+ ) { toolbarVisible ->
+ binding.toolbar.isVisible = toolbarVisible
+ },
+ owner = this,
+ view = binding.toolbar,
+ )
+
+ val windowFeature = CustomTabWindowFeature(
+ requireActivity(),
+ components.store,
+ sessionId!!,
+ )
+ lifecycle.addObserver(windowFeature)
+
+ if (manifest != null) {
+ activity?.lifecycle?.addObservers(
+ WebAppActivityFeature(
+ requireActivity(),
+ components.icons,
+ manifest,
+ ),
+ ManifestUpdateFeature(
+ requireContext(),
+ components.store,
+ components.webAppShortcutManager,
+ components.webAppManifestStorage,
+ sessionId!!,
+ manifest,
+ ),
+ WebAppContentFeature(
+ components.store,
+ sessionId,
+ manifest,
+ ),
+ )
+ viewLifecycleOwner.lifecycle.addObserver(
+ WebAppSiteControlsFeature(
+ context?.applicationContext!!,
+ components.store,
+ components.sessionUseCases.reload,
+ sessionId!!,
+ manifest,
+ icons = components.icons,
+ notificationsDelegate = components.notificationsDelegate,
+ ),
+ )
+ }
+
+ return binding.root
+ }
+
+ /**
+ * Calls [onBackPressed] for features in the base class first,
+ * before trying to call the external app [UserInteractionHandler].
+ */
+ override fun onBackPressed(): Boolean =
+ super.onBackPressed() || customTabsToolbarFeature.onBackPressed()
+
+ companion object {
+ fun create(
+ sessionId: String,
+ manifest: WebAppManifest?,
+ ) = ExternalAppBrowserFragment().apply {
+ arguments = Bundle().apply {
+ putSessionId(sessionId)
+ putWebAppManifest(manifest)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt
new file mode 100644
index 0000000000..7a046d6909
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/IntentReceiverActivity.kt
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import org.mozilla.samples.browser.ext.components
+
+class IntentReceiverActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ MainScope().launch {
+ val intent = intent?.let { Intent(it) } ?: Intent()
+ val intentProcessors = components.externalAppIntentProcessors + components.tabIntentProcessor
+
+ // Explicitly remove the new task and clear task flags (Our browser activity is a single
+ // task activity and we never want to start a second task here).
+ intent.flags = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
+ intent.flags = intent.flags and Intent.FLAG_ACTIVITY_CLEAR_TASK.inv()
+
+ // LauncherActivity is started with the "excludeFromRecents" flag (set in manifest). We
+ // do not want to propagate this flag from the launcher activity to the browser.
+ intent.flags = intent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.inv()
+
+ val processor = intentProcessors.firstOrNull { it.process(intent) }
+
+ val activityClass = if (processor in components.externalAppIntentProcessors) {
+ ExternalAppBrowserActivity::class
+ } else {
+ BrowserActivity::class
+ }
+
+ intent.setClassName(applicationContext, activityClass.java.name)
+
+ finish()
+ startActivity(intent)
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt
new file mode 100644
index 0000000000..a6ae1e25b7
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.app.Application
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import mozilla.appservices.Megazord
+import mozilla.components.browser.state.action.SystemAction
+import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
+import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.glean.BuildInfo
+import mozilla.components.service.glean.Glean
+import mozilla.components.service.glean.config.Configuration
+import mozilla.components.service.glean.net.ConceptFetchHttpUploader
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.base.facts.processor.LogFactProcessor
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.ktx.android.content.isMainProcess
+import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
+import mozilla.components.support.rustlog.RustLog
+import mozilla.components.support.webextensions.WebExtensionSupport
+import java.util.Calendar
+import java.util.TimeZone
+import java.util.concurrent.TimeUnit
+
+@Suppress("MagicNumber")
+internal object GleanBuildInfo {
+ val buildInfo: BuildInfo by lazy {
+ BuildInfo(
+ versionCode = "0.0.1",
+ versionName = "0.0.1",
+ buildDate = Calendar.getInstance(
+ TimeZone.getTimeZone("GMT+0"),
+ ).also { cal -> cal.set(2019, 9, 23, 12, 52, 8) },
+ )
+ }
+}
+
+class SampleApplication : Application() {
+ private val logger = Logger("SampleApplication")
+
+ val components by lazy { Components(this) }
+
+ @OptIn(DelicateCoroutinesApi::class) // Usage of GlobalScope
+ override fun onCreate() {
+ super.onCreate()
+
+ Megazord.init()
+ RustLog.enable()
+
+ Log.addSink(AndroidLogSink())
+
+ components.crashReporter.install(this)
+
+ if (!isMainProcess()) {
+ return
+ }
+
+ val httpClient = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+ val config = Configuration(httpClient = httpClient)
+ // IMPORTANT: the following lines initialize the Glean SDK but disable upload
+ // of pings. If, for testing purposes, upload is required to be on, change the
+ // next line to `uploadEnabled = true`.
+ Glean.initialize(
+ applicationContext,
+ uploadEnabled = false,
+ configuration = config,
+ buildInfo = GleanBuildInfo.buildInfo,
+ )
+
+ Facts.registerProcessor(LogFactProcessor())
+
+ components.engine.warmUp()
+ restoreBrowserState()
+
+ GlobalScope.launch(Dispatchers.IO) {
+ components.webAppManifestStorage.warmUpScopes(System.currentTimeMillis())
+ }
+ components.downloadsUseCases.restoreDownloads()
+ try {
+ GlobalPlacesDependencyProvider.initialize(components.historyStorage)
+ GlobalAddonDependencyProvider.initialize(
+ components.addonManager,
+ components.addonUpdater,
+ )
+ WebExtensionSupport.initialize(
+ components.engine,
+ components.store,
+ onNewTabOverride = {
+ _, engineSession, url ->
+ components.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession)
+ },
+ onCloseTabOverride = {
+ _, sessionId ->
+ components.tabsUseCases.removeTab(sessionId)
+ },
+ onSelectTabOverride = {
+ _, sessionId ->
+ components.tabsUseCases.selectTab(sessionId)
+ },
+ onUpdatePermissionRequest = components.addonUpdater::onUpdatePermissionRequest,
+ onExtensionsLoaded = { extensions ->
+ components.addonUpdater.registerForFutureUpdates(extensions)
+ components.supportedAddonsChecker.registerForChecks()
+ },
+ )
+ } catch (e: UnsupportedOperationException) {
+ // Web extension support is only available for engine gecko
+ Logger.error("Failed to initialize web extension support", e)
+ }
+ }
+
+ @DelicateCoroutinesApi
+ private fun restoreBrowserState() = GlobalScope.launch(Dispatchers.Main) {
+ components.tabsUseCases.restore(components.sessionStorage)
+
+ components.sessionStorage.autoSave(components.store)
+ .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
+ .whenGoingToBackground()
+ .whenSessionsChange()
+ }
+
+ override fun onTrimMemory(level: Int) {
+ super.onTrimMemory(level)
+
+ logger.debug("onTrimMemory: $level")
+
+ runOnlyInMainProcess {
+ components.store.dispatch(SystemAction.LowMemoryAction(level))
+
+ components.icons.onTrimMemory(level)
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt
new file mode 100644
index 0000000000..0abe64f243
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.snackbar.Snackbar
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.tabstray.TabsAdapter
+import mozilla.components.browser.tabstray.TabsTray
+import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.feature.tabs.tabstray.TabsFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import org.mozilla.samples.browser.databinding.FragmentTabstrayBinding
+import org.mozilla.samples.browser.ext.components
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * A fragment for displaying the tabs tray.
+ */
+class TabsTrayFragment : Fragment(), UserInteractionHandler {
+ private val tabsFeature: ViewBoundFeatureWrapper<TabsFeature> = ViewBoundFeatureWrapper()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.fragment_tabstray, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val binding = FragmentTabstrayBinding.bind(view)
+ binding.toolbar.setNavigationIcon(iconsR.drawable.mozac_ic_back_24)
+ binding.toolbar.setNavigationOnClickListener {
+ closeTabsTray()
+ }
+
+ binding.toolbar.inflateMenu(R.menu.tabstray_menu)
+ binding.toolbar.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.newTab -> {
+ components.tabsUseCases.addTab.invoke("about:blank", selectTab = true)
+ closeTabsTray()
+ }
+ }
+ true
+ }
+
+ val tabsAdapter = createTabsAdapter(view)
+ binding.tabsTray.adapter = tabsAdapter
+ binding.tabsTray.layoutManager = GridLayoutManager(context, 2)
+
+ tabsFeature.set(
+ feature = TabsFeature(
+ tabsTray = tabsAdapter,
+ store = components.store,
+ onCloseTray = ::closeTabsTray,
+ ),
+ owner = this,
+ view = view,
+ )
+ }
+
+ override fun onBackPressed(): Boolean {
+ closeTabsTray()
+ return true
+ }
+
+ private fun closeTabsTray() {
+ activity?.supportFragmentManager?.beginTransaction()?.apply {
+ replace(R.id.container, BrowserFragment.create())
+ commit()
+ }
+ }
+
+ private fun createTabsAdapter(view: View): TabsAdapter {
+ val removeUseCase = RemoveTabWithUndoUseCase(
+ components.tabsUseCases.removeTab,
+ view,
+ components.tabsUseCases.undo,
+ )
+ return TabsAdapter(
+ thumbnailLoader = ThumbnailLoader(components.thumbnailStorage),
+ delegate = object : TabsTray.Delegate {
+ override fun onTabSelected(tab: TabSessionState, source: String?) {
+ components.tabsUseCases.selectTab(tab.id)
+ closeTabsTray()
+ }
+
+ override fun onTabClosed(tab: TabSessionState, source: String?) {
+ removeUseCase.invoke(tab.id)
+ }
+ },
+ )
+ }
+}
+
+private class RemoveTabWithUndoUseCase(
+ private val actual: TabsUseCases.RemoveTabUseCase,
+ private val view: View,
+ private val undo: TabsUseCases.UndoTabRemovalUseCase,
+) : TabsUseCases.RemoveTabUseCase {
+ override fun invoke(tabId: String) {
+ actual.invoke(tabId)
+ showSnackbar()
+ }
+
+ private fun showSnackbar() {
+ Snackbar.make(
+ view,
+ "Tab removed.",
+ Snackbar.LENGTH_LONG,
+ ).setAction(
+ "Undo",
+ ) {
+ undo.invoke()
+ }.show()
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt
new file mode 100644
index 0000000000..1b8000d5b8
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonDetailsActivity.kt
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.View
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.text.HtmlCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.showInformationDialog
+import mozilla.components.feature.addons.ui.translateDescription
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.feature.addons.update.DefaultAddonUpdater
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import org.mozilla.samples.browser.R
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+import mozilla.components.feature.addons.R as addonsR
+
+/**
+ * An activity to show the details of an add-on.
+ */
+class AddonDetailsActivity : AppCompatActivity() {
+
+ private val updateAttemptStorage: DefaultAddonUpdater.UpdateAttemptStorage by lazy {
+ DefaultAddonUpdater.UpdateAttemptStorage(this)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_add_on_details)
+ val addon = requireNotNull(intent.getParcelableExtraCompat("add_on", Addon::class.java))
+ bind(addon)
+ }
+
+ private fun bind(addon: Addon) {
+ title = addon.translateName(this)
+
+ bindDetails(addon)
+
+ bindAuthor(addon)
+
+ bindVersion(addon)
+
+ bindLastUpdated(addon)
+
+ bindWebsite(addon)
+
+ bindRating(addon)
+ }
+
+ private fun bindRating(addon: Addon) {
+ addon.rating?.let {
+ val ratingView = findViewById<RatingBar>(R.id.rating_view)
+ val reviewCountView = findViewById<TextView>(R.id.users_count)
+
+ val ratingContentDescription = getString(
+ addonsR.string.mozac_feature_addons_rating_content_description,
+ )
+ ratingView.contentDescription = String.format(ratingContentDescription, it.average)
+ ratingView.rating = it.average
+
+ reviewCountView.text = getFormattedAmount(it.reviews)
+ }
+ }
+
+ private fun bindWebsite(addon: Addon) {
+ findViewById<View>(R.id.home_page_text).setOnClickListener {
+ val intent =
+ Intent(Intent.ACTION_VIEW).setData(Uri.parse(addon.homepageUrl))
+ startActivity(intent)
+ }
+ }
+
+ private fun bindLastUpdated(addon: Addon) {
+ val lastUpdatedView = findViewById<TextView>(R.id.last_updated_text)
+ lastUpdatedView.text = formatDate(addon.updatedAt)
+ }
+
+ private fun bindVersion(addon: Addon) {
+ val versionView = findViewById<TextView>(R.id.version_text)
+ versionView.text = addon.installedState?.version?.ifEmpty { addon.version } ?: addon.version
+
+ if (addon.isInstalled()) {
+ versionView.setOnLongClickListener {
+ showUpdaterDialog(addon)
+ true
+ }
+ }
+ }
+
+ private fun showUpdaterDialog(addon: Addon) {
+ val context = this@AddonDetailsActivity
+ val scope = CoroutineScope(Dispatchers.IO)
+ scope.launch {
+ val updateAttempt = updateAttemptStorage.findUpdateAttemptBy(addon.id)
+ updateAttempt?.let {
+ withContext(Dispatchers.Main) {
+ it.showInformationDialog(context)
+ }
+ }
+ }
+ }
+
+ private fun bindAuthor(addon: Addon) {
+ val authorsView = findViewById<TextView>(R.id.author_text)
+ authorsView.text = addon.author?.name.orEmpty()
+ }
+
+ private fun bindDetails(addon: Addon) {
+ val detailsView = findViewById<TextView>(R.id.details)
+ val detailsText = addon.translateDescription(this)
+
+ val parsedText = detailsText.replace("\n", "<br/>")
+ val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)
+
+ detailsView.text = text
+ detailsView.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ private fun formatDate(text: String): String {
+ val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
+ return DateFormat.getDateInstance().format(formatter.parse(text)!!)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt
new file mode 100644
index 0000000000..222a33c33e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonSettingsActivity.kt
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import android.content.Context
+import android.os.Bundle
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.support.utils.ext.getParcelableCompat
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.databinding.ActivityAddOnSettingsBinding
+import org.mozilla.samples.browser.databinding.FragmentAddOnSettingsBinding
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * An activity to show the settings of an add-on.
+ */
+class AddonSettingsActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityAddOnSettingsBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityAddOnSettingsBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ val addon = requireNotNull(intent.getParcelableExtraCompat("add_on", Addon::class.java))
+ title = addon.translateName(this)
+
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.addonSettingsContainer, AddonSettingsFragment.create(addon))
+ .commit()
+ }
+
+ override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
+ when (name) {
+ EngineView::class.java.name -> components.engine.createView(context, attrs).asView()
+ else -> super.onCreateView(parent, name, context, attrs)
+ }
+
+ /**
+ * A fragment to show the settings of an add-on with [EngineView].
+ */
+ class AddonSettingsFragment : Fragment() {
+ private lateinit var addon: Addon
+ private lateinit var engineSession: EngineSession
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ addon = requireNotNull(arguments?.getParcelableCompat("add_on", Addon::class.java))
+ engineSession = components.engine.createSession()
+
+ return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val binding = FragmentAddOnSettingsBinding.bind(view)
+ binding.addonSettingsEngineView.render(engineSession)
+ addon.installedState?.optionsPageUrl?.let {
+ engineSession.loadUrl(it)
+ }
+ }
+
+ override fun onDestroyView() {
+ engineSession.close()
+ super.onDestroyView()
+ }
+
+ companion object {
+ /**
+ * Create an [AddonSettingsFragment] with add_on as a required parameter.
+ */
+ fun create(addon: Addon) = AddonSettingsFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable("add_on", addon)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt
new file mode 100644
index 0000000000..1f30df8331
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsActivity.kt
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import org.mozilla.samples.browser.R
+
+/**
+ * An activity to manage add-ons.
+ */
+class AddonsActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ if (savedInstanceState == null) {
+ supportFragmentManager.beginTransaction().apply {
+ replace(R.id.container, AddonsFragment())
+ commit()
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt
new file mode 100644
index 0000000000..1e3115a8db
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/AddonsFragment.kt
@@ -0,0 +1,251 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManagerException
+import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
+import mozilla.components.feature.addons.ui.AddonsManagerAdapter
+import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
+import mozilla.components.feature.addons.ui.PermissionsDialogFragment
+import mozilla.components.feature.addons.ui.translateName
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.databinding.FragmentAddOnsBinding
+import org.mozilla.samples.browser.databinding.OverlayAddOnProgressBinding
+import org.mozilla.samples.browser.ext.components
+import java.util.concurrent.CancellationException
+import androidx.browser.R as androidxBrowserR
+import mozilla.components.browser.menu.R as menuR
+import mozilla.components.feature.addons.R as addonsR
+
+/**
+ * Fragment use for managing add-ons.
+ */
+class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
+ private lateinit var recyclerView: RecyclerView
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private var adapter: AddonsManagerAdapter? = null
+
+ private var _binding: FragmentAddOnsBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ _binding = FragmentAddOnsBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(rootView, savedInstanceState)
+ bindRecyclerView(rootView)
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ this@AddonsFragment.view?.let { view ->
+ bindRecyclerView(view)
+ }
+
+ findPreviousPermissionDialogFragment()?.let { dialog ->
+ dialog.onPositiveButtonClicked = onConfirmPermissionButtonClicked
+ }
+
+ findPreviousInstallationDialogFragment()?.let { dialog ->
+ dialog.onConfirmButtonClicked = onConfirmInstallationButtonClicked
+ }
+ }
+
+ private fun bindRecyclerView(rootView: View) {
+ recyclerView = rootView.findViewById(R.id.add_ons_list)
+ recyclerView.layoutManager = LinearLayoutManager(requireContext())
+ scope.launch {
+ try {
+ val context = requireContext()
+ val addons = context.components.addonManager.getAddons()
+
+ val style = AddonsManagerAdapter.Style(
+ dividerColor = androidxBrowserR.color.browser_actions_divider_color,
+ dividerHeight = menuR.dimen.mozac_browser_menu_item_divider_height,
+ )
+
+ scope.launch(Dispatchers.Main) {
+ if (adapter == null) {
+ adapter = AddonsManagerAdapter(
+ addonsManagerDelegate = this@AddonsFragment,
+ addons = addons,
+ style = style,
+ store = context.components.store,
+ )
+ recyclerView.adapter = adapter
+ } else {
+ adapter?.updateAddons(addons)
+ }
+ }
+ } catch (e: AddonManagerException) {
+ scope.launch(Dispatchers.Main) {
+ Toast.makeText(
+ activity,
+ addonsR.string.mozac_feature_addons_failed_to_query_extensions,
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+ }
+
+ override fun onAddonItemClicked(addon: Addon) {
+ val context = requireContext()
+
+ if (addon.isInstalled()) {
+ val intent = Intent(context, InstalledAddonDetailsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ context.startActivity(intent)
+ } else {
+ val intent = Intent(context, AddonDetailsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ this.startActivity(intent)
+ }
+ }
+
+ override fun onInstallAddonButtonClicked(addon: Addon) {
+ showPermissionDialog(addon)
+ }
+
+ override fun onNotYetSupportedSectionClicked(unsupportedAddons: List<Addon>) {
+ val intent = Intent(context, NotYetSupportedAddonActivity::class.java)
+ intent.putExtra("add_ons", ArrayList(unsupportedAddons))
+ requireContext().startActivity(intent)
+ }
+
+ private fun isAlreadyADialogCreated(): Boolean {
+ return findPreviousPermissionDialogFragment() != null && findPreviousInstallationDialogFragment() != null
+ }
+
+ private fun findPreviousPermissionDialogFragment(): PermissionsDialogFragment? {
+ return parentFragmentManager.findFragmentByTag(
+ PERMISSIONS_DIALOG_FRAGMENT_TAG,
+ ) as? PermissionsDialogFragment
+ }
+
+ private fun findPreviousInstallationDialogFragment(): AddonInstallationDialogFragment? {
+ return parentFragmentManager.findFragmentByTag(
+ INSTALLATION_DIALOG_FRAGMENT_TAG,
+ ) as? AddonInstallationDialogFragment
+ }
+
+ private fun showPermissionDialog(addon: Addon) {
+ if (isInstallationInProgress) {
+ return
+ }
+
+ val dialog = PermissionsDialogFragment.newInstance(
+ addon = addon,
+ onPositiveButtonClicked = onConfirmPermissionButtonClicked,
+ )
+
+ if (!isAlreadyADialogCreated() && isAdded) {
+ dialog.show(parentFragmentManager, PERMISSIONS_DIALOG_FRAGMENT_TAG)
+ }
+ }
+
+ private fun showInstallationDialog(addon: Addon) {
+ if (isInstallationInProgress) {
+ return
+ }
+ val dialog = AddonInstallationDialogFragment.newInstance(
+ addon = addon,
+ onConfirmButtonClicked = onConfirmInstallationButtonClicked,
+ )
+
+ if (!isAlreadyADialogCreated() && isAdded) {
+ dialog.show(parentFragmentManager, INSTALLATION_DIALOG_FRAGMENT_TAG)
+ }
+ }
+
+ private val onConfirmInstallationButtonClicked: ((Addon, Boolean) -> Unit) = { addon, allowInPrivateBrowsing ->
+ if (allowInPrivateBrowsing) {
+ requireContext().components.addonManager.setAddonAllowedInPrivateBrowsing(
+ addon,
+ allowInPrivateBrowsing,
+ )
+ }
+ }
+
+ private val onConfirmPermissionButtonClicked: ((Addon) -> Unit) = { addon ->
+ val includedBinding = OverlayAddOnProgressBinding.bind(binding.addonProgressOverlay.addonProgressOverlay)
+
+ includedBinding.root.visibility = View.VISIBLE
+ isInstallationInProgress = true
+
+ val installOperation = requireContext().components.addonManager.installAddon(
+ url = addon.downloadUrl,
+ onSuccess = { installedAddon ->
+ context?.let {
+ adapter?.updateAddon(installedAddon)
+ includedBinding.root.visibility = View.GONE
+ isInstallationInProgress = false
+ showInstallationDialog(installedAddon)
+ }
+ },
+ onError = { e ->
+ // No need to display an error message if installation was cancelled by the user.
+ if (e !is CancellationException) {
+ Toast.makeText(
+ requireContext(),
+ getString(
+ addonsR.string.mozac_feature_addons_failed_to_install,
+ addon.translateName(requireContext()),
+ ),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+
+ includedBinding.root.visibility = View.GONE
+ isInstallationInProgress = false
+ },
+ )
+
+ includedBinding.cancelButton.setOnClickListener {
+ MainScope().launch {
+ // Hide the installation progress overlay once cancellation is successful.
+ if (installOperation.cancel().await()) {
+ includedBinding.root.visibility = View.GONE
+ }
+ }
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ /**
+ * Whether or not an add-on installation is in progress.
+ */
+ private var isInstallationInProgress = false
+
+ companion object {
+ private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
+ private const val INSTALLATION_DIALOG_FRAGMENT_TAG = "ADDONS_INSTALLATION_DIALOG_FRAGMENT"
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt
new file mode 100644
index 0000000000..b459840911
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/Extensions.kt
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import java.text.NumberFormat
+import java.util.Locale
+
+internal fun getFormattedAmount(amount: Int): String {
+ return NumberFormat.getNumberInstance(Locale.getDefault()).format(amount)
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt
new file mode 100644
index 0000000000..1d84830d29
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/InstalledAddonDetailsActivity.kt
@@ -0,0 +1,200 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SwitchCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManagerException
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import org.mozilla.samples.browser.BrowserActivity
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.ext.components
+import mozilla.components.feature.addons.R as addonsR
+
+/**
+ * An activity to show the details of a installed add-on.
+ */
+@Suppress("LargeClass")
+class InstalledAddonDetailsActivity : AppCompatActivity() {
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_installed_add_on_details)
+ val addon = requireNotNull(intent.getParcelableExtraCompat("add_on", Addon::class.java))
+ bindAddon(addon)
+ }
+
+ private fun bindAddon(addon: Addon) {
+ scope.launch {
+ try {
+ val context = baseContext
+ val addons = context.components.addonManager.getAddons()
+ scope.launch(Dispatchers.Main) {
+ addons.find { addon.id == it.id }.let {
+ if (it == null) {
+ throw AddonManagerException(Exception("Addon ${addon.id} not found"))
+ } else {
+ bindUI(it)
+ }
+ }
+ }
+ } catch (e: AddonManagerException) {
+ scope.launch(Dispatchers.Main) {
+ Toast.makeText(
+ baseContext,
+ addonsR.string.mozac_feature_addons_failed_to_query_extensions,
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+ }
+
+ private fun bindUI(addon: Addon) {
+ title = addon.translateName(this)
+
+ bindEnableSwitch(addon)
+
+ bindSettings(addon)
+
+ bindDetails(addon)
+
+ bindPermissions(addon)
+
+ bindAllowInPrivateBrowsingSwitch(addon)
+
+ bindRemoveButton(addon)
+ }
+
+ private fun bindEnableSwitch(addon: Addon) {
+ val switch = findViewById<SwitchCompat>(R.id.enable_switch)
+ switch.isChecked = addon.isEnabled()
+ switch.setOnCheckedChangeListener { _, isChecked ->
+ if (isChecked) {
+ this.components.addonManager.enableAddon(
+ addon,
+ onSuccess = {
+ switch.isChecked = true
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_successfully_enabled,
+ addon,
+ )
+ },
+ onError = {
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_failed_to_enable,
+ addon,
+ )
+ },
+ )
+ } else {
+ this.components.addonManager.disableAddon(
+ addon,
+ onSuccess = {
+ switch.isChecked = false
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_successfully_disabled,
+ addon,
+ )
+ },
+ onError = {
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_failed_to_disable,
+ addon,
+ )
+ },
+ )
+ }
+ }
+ }
+
+ private fun bindSettings(addon: Addon) {
+ val view = findViewById<View>(R.id.settings)
+ val optionsPageUrl = addon.installedState?.optionsPageUrl
+ view.isEnabled = optionsPageUrl != null
+ view.setOnClickListener {
+ if (addon.installedState?.openOptionsPageInTab == true) {
+ components.tabsUseCases.addTab(optionsPageUrl as String)
+ val intent = Intent(this, BrowserActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ this.startActivity(intent)
+ } else {
+ val intent = Intent(this, AddonSettingsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ this.startActivity(intent)
+ }
+ }
+ }
+
+ private fun bindDetails(addon: Addon) {
+ findViewById<View>(R.id.details).setOnClickListener {
+ val intent = Intent(this, AddonDetailsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ this.startActivity(intent)
+ }
+ }
+
+ private fun bindPermissions(addon: Addon) {
+ findViewById<View>(R.id.permissions).setOnClickListener {
+ val intent = Intent(this, PermissionsDetailsActivity::class.java)
+ intent.putExtra("add_on", addon)
+ this.startActivity(intent)
+ }
+ }
+
+ private fun bindAllowInPrivateBrowsingSwitch(addon: Addon) {
+ val switch = findViewById<SwitchCompat>(R.id.allow_in_private_browsing_switch)
+ switch.isChecked = addon.isAllowedInPrivateBrowsing()
+ switch.setOnCheckedChangeListener { _, isChecked ->
+ this.components.addonManager.setAddonAllowedInPrivateBrowsing(
+ addon,
+ isChecked,
+ onSuccess = {
+ switch.isChecked = isChecked
+ },
+ )
+ }
+ }
+
+ private fun bindRemoveButton(addon: Addon) {
+ findViewById<View>(R.id.remove_add_on).setOnClickListener {
+ this.components.addonManager.uninstallAddon(
+ addon,
+ onSuccess = {
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_successfully_uninstalled,
+ addon,
+ )
+ finish()
+ },
+ onError = { _, _ ->
+ showAddonToast(
+ addonsR.string.mozac_feature_addons_failed_to_uninstall,
+ addon,
+ )
+ },
+ )
+ }
+ }
+
+ private fun showAddonToast(@StringRes textId: Int, addon: Addon) {
+ Toast.makeText(
+ this,
+ getString(textId, addon.translateName(context = this)),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt
new file mode 100644
index 0000000000..dac2e3dca1
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/NotYetSupportedAddonActivity.kt
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.UnsupportedAddonsAdapter
+import mozilla.components.feature.addons.ui.UnsupportedAddonsAdapterDelegate
+import mozilla.components.support.utils.ext.getParcelableArrayListCompat
+import mozilla.components.support.utils.ext.getParcelableArrayListExtraCompat
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.ext.components
+
+private const val LEARN_MORE_URL =
+ "https://support.mozilla.org/kb/add-compatibility-firefox-preview"
+
+/**
+ * Activity for managing unsupported add-ons.
+ */
+class NotYetSupportedAddonActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val addons = requireNotNull(intent.getParcelableArrayListExtraCompat("add_ons", Addon::class.java))
+
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.container, NotYetSupportedAddonFragment.create(addons))
+ .commit()
+ }
+
+ /**
+ * Fragment for managing add-ons that are not yet supported by the browser.
+ */
+ class NotYetSupportedAddonFragment : Fragment(), UnsupportedAddonsAdapterDelegate {
+ private lateinit var addons: List<Addon>
+ private var adapter: UnsupportedAddonsAdapter? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ addons =
+ requireNotNull(arguments?.getParcelableArrayListCompat("add_ons", Addon::class.java))
+ return inflater.inflate(R.layout.fragment_not_yet_supported_addons, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val context = requireContext()
+ val recyclerView: RecyclerView = view.findViewById(R.id.unsupported_add_ons_list)
+ adapter = UnsupportedAddonsAdapter(
+ addonManager = context.components.addonManager,
+ unsupportedAddonsAdapterDelegate = this@NotYetSupportedAddonFragment,
+ addons = addons,
+ )
+
+ recyclerView.layoutManager = LinearLayoutManager(context)
+ recyclerView.adapter = adapter
+
+ view.findViewById<View>(R.id.learn_more_label).setOnClickListener {
+ val intent = Intent(Intent.ACTION_VIEW).setData(Uri.parse(LEARN_MORE_URL))
+ startActivity(intent)
+ }
+ }
+
+ override fun onUninstallError(addonId: String, throwable: Throwable) {
+ Toast.makeText(context, "Failed to remove add-on", Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onUninstallSuccess() {
+ Toast.makeText(context, "Successfully removed add-on", Toast.LENGTH_SHORT)
+ .show()
+ if (adapter?.itemCount == 0) {
+ activity?.onBackPressedDispatcher?.onBackPressed()
+ }
+ }
+
+ companion object {
+ /**
+ * Create an [NotYetSupportedAddonFragment] with add_ons as a required parameter.
+ */
+ fun create(addons: ArrayList<Addon>) = NotYetSupportedAddonFragment().apply {
+ arguments = Bundle().apply {
+ putParcelableArrayList("add_ons", addons)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt
new file mode 100644
index 0000000000..20424be237
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/PermissionsDetailsActivity.kt
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.AddonPermissionsAdapter
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.support.utils.ext.getParcelableExtraCompat
+import org.mozilla.samples.browser.R
+
+private const val LEARN_MORE_URL =
+ "https://support.mozilla.org/kb/permission-request-messages-firefox-extensions"
+
+/**
+ * An activity to show the permissions of an add-on.
+ */
+class PermissionsDetailsActivity : AppCompatActivity(), View.OnClickListener {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_add_on_permissions)
+ val addon = requireNotNull(intent.getParcelableExtraCompat("add_on", Addon::class.java))
+ title = addon.translateName(this)
+
+ bindPermissions(addon)
+
+ bindLearnMore()
+ }
+
+ private fun bindPermissions(addon: Addon) {
+ val recyclerView = findViewById<RecyclerView>(R.id.add_ons_permissions)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ val sortedPermissions = addon.translatePermissions(this).sorted()
+ recyclerView.adapter = AddonPermissionsAdapter(sortedPermissions)
+ }
+
+ private fun bindLearnMore() {
+ findViewById<View>(R.id.learn_more_label).setOnClickListener(this)
+ }
+
+ override fun onClick(v: View?) {
+ val intent =
+ Intent(Intent.ACTION_VIEW).setData(Uri.parse(LEARN_MORE_URL))
+ startActivity(intent)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt
new file mode 100644
index 0000000000..0dc1e300c3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/addons/WebExtensionActionPopupActivity.kt
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.addons
+
+import android.content.Context
+import android.os.Bundle
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import mozilla.components.browser.state.action.WebExtensionAction
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.lib.state.ext.consumeFrom
+import org.mozilla.samples.browser.R
+import org.mozilla.samples.browser.databinding.FragmentAddOnSettingsBinding
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * An activity to show the pop up action of a web extension.
+ */
+class WebExtensionActionPopupActivity : AppCompatActivity() {
+ private lateinit var webExtensionId: String
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_add_on_settings)
+
+ webExtensionId = requireNotNull(intent.getStringExtra("web_extension_id"))
+ intent.getStringExtra("web_extension_name")?.let {
+ title = it
+ }
+
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.addonSettingsContainer, WebExtensionActionPopupFragment.create(webExtensionId))
+ .commit()
+ }
+
+ override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
+ when (name) {
+ EngineView::class.java.name -> components.engine.createView(context, attrs).asView()
+ else -> super.onCreateView(parent, name, context, attrs)
+ }
+
+ /**
+ * A fragment to show the web extension action popup with [EngineView].
+ */
+ class WebExtensionActionPopupFragment : Fragment(), EngineSession.Observer {
+ private var engineSession: EngineSession? = null
+ private lateinit var webExtensionId: String
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ webExtensionId = requireNotNull(arguments?.getString("web_extension_id"))
+ engineSession = components.store.state.extensions[webExtensionId]?.popupSession
+
+ return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val binding = FragmentAddOnSettingsBinding.bind(view)
+ val session = engineSession
+ if (session != null) {
+ binding.addonSettingsEngineView.render(session)
+ session.register(this, view)
+ consumePopupSession()
+ } else {
+ consumeFrom(requireContext().components.store) { state ->
+ state.extensions[webExtensionId]?.let { extState ->
+ extState.popupSession?.let {
+ if (engineSession == null) {
+ binding.addonSettingsEngineView.render(it)
+ it.register(this, view)
+ consumePopupSession()
+ engineSession = it
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onWindowRequest(windowRequest: WindowRequest) {
+ if (windowRequest.type == WindowRequest.Type.CLOSE) {
+ activity?.finish()
+ } else {
+ engineSession?.loadUrl(windowRequest.url)
+ }
+ }
+ private fun consumePopupSession() {
+ components.store.dispatch(
+ WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null),
+ )
+ }
+
+ companion object {
+ /**
+ * Create an [WebExtensionActionPopupFragment] with webExtensionId as a required parameter.
+ */
+ fun create(webExtensionId: String) = WebExtensionActionPopupFragment().apply {
+ arguments = Bundle().apply {
+ putString("web_extension_id", webExtensionId)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt
new file mode 100644
index 0000000000..c5fa99c09f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillConfirmActivity.kt
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.autofill
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.ui.AbstractAutofillConfirmActivity
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Activity responsible for asking the user to confirm before autofilling a third-party app.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillConfirmActivity : AbstractAutofillConfirmActivity() {
+ override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt
new file mode 100644
index 0000000000..5bf14fc138
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillSearchActivity.kt
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.autofill
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.ui.AbstractAutofillSearchActivity
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Activity responsible for letting the user manually search and pick credentials for auto-filling a
+ * third-party app.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillSearchActivity : AbstractAutofillSearchActivity() {
+ override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt
new file mode 100644
index 0000000000..a5c6811a6e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillService.kt
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.autofill
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AbstractAutofillService
+import mozilla.components.feature.autofill.AutofillConfiguration
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Service responsible for implementing Android's Autofill framework.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillService : AbstractAutofillService() {
+ override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt
new file mode 100644
index 0000000000..50bba51c1f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/autofill/AutofillUnlockActivity.kt
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.autofill
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import mozilla.components.feature.autofill.AutofillConfiguration
+import mozilla.components.feature.autofill.ui.AbstractAutofillUnlockActivity
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Activity responsible for unlocking the autofill service by asking the user to verify with a
+ * fingerprint or alternative device unlocking mechanism.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillUnlockActivity : AbstractAutofillUnlockActivity() {
+ override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt
new file mode 100644
index 0000000000..c7918e18d1
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/awesomebar/AwesomeBarWrapper.kt
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.awesomebar
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.platform.AbstractComposeView
+import mozilla.components.compose.browser.awesomebar.AwesomeBar
+import mozilla.components.concept.awesomebar.AwesomeBar
+
+/**
+ * This wrapper wraps the `AwesomeBar()` composable and exposes it as a `View` and `concept-awesomebar`
+ * implementation.
+ */
+class AwesomeBarWrapper @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : AbstractComposeView(context, attrs, defStyleAttr), AwesomeBar {
+ private val providers = mutableStateOf(emptyList<AwesomeBar.SuggestionProvider>())
+ private val text = mutableStateOf("")
+ private var onEditSuggestionListener: ((String) -> Unit)? = null
+ private var onStopListener: (() -> Unit)? = null
+
+ @Composable
+ override fun Content() {
+ AwesomeBar(
+ text = text.value,
+ providers = providers.value,
+ onSuggestionClicked = { suggestion ->
+ suggestion.onSuggestionClicked?.invoke()
+ onStopListener?.invoke()
+ },
+ onAutoComplete = { suggestion ->
+ onEditSuggestionListener?.invoke(suggestion.editSuggestion!!)
+ },
+ )
+ }
+
+ override fun addProviders(vararg providers: AwesomeBar.SuggestionProvider) {
+ val newProviders = this.providers.value.toMutableList()
+ newProviders.addAll(providers)
+ this.providers.value = newProviders
+ }
+
+ override fun containsProvider(provider: AwesomeBar.SuggestionProvider): Boolean {
+ return providers.value.any { current -> current.id == provider.id }
+ }
+
+ override fun onInputChanged(text: String) {
+ this.text.value = text
+ }
+
+ override fun removeAllProviders() {
+ providers.value = emptyList()
+ }
+
+ override fun removeProviders(vararg providers: AwesomeBar.SuggestionProvider) {
+ val newProviders = this.providers.value.toMutableList()
+ newProviders.removeAll(providers)
+ this.providers.value = newProviders
+ }
+
+ override fun setOnEditSuggestionListener(listener: (String) -> Unit) {
+ onEditSuggestionListener = listener
+ }
+
+ override fun setOnStopListener(listener: () -> Unit) {
+ onStopListener = listener
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt
new file mode 100644
index 0000000000..225cfee4d4
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/customtabs/CustomTabsService.kt
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.customtabs
+
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.customtabs.AbstractCustomTabsService
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import org.mozilla.samples.browser.ext.components
+
+class CustomTabsService : AbstractCustomTabsService() {
+ override val engine: Engine by lazy { components.engine }
+ override val customTabsServiceStore: CustomTabsServiceStore by lazy { components.customTabsStore }
+ override val relationChecker: RelationChecker by lazy { components.relationChecker }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt
new file mode 100644
index 0000000000..cc9897c90f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/downloads/DownloadService.kt
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.downloads
+
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.downloads.AbstractFetchDownloadService
+import mozilla.components.support.base.android.NotificationsDelegate
+import org.mozilla.samples.browser.ext.components
+
+class DownloadService : AbstractFetchDownloadService() {
+ override val httpClient by lazy { components.client }
+ override val store: BrowserStore by lazy { components.store }
+ override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt
new file mode 100644
index 0000000000..2dc924356a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Context.kt
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.ext
+
+import android.content.Context
+import org.mozilla.samples.browser.Components
+import org.mozilla.samples.browser.SampleApplication
+
+/**
+ * Get the SampleApplication object from a context.
+ */
+val Context.application: SampleApplication
+ get() = applicationContext as SampleApplication
+
+/**
+ * Get the components of this application.
+ */
+val Context.components: Components
+ get() = application.components
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt
new file mode 100644
index 0000000000..0a07e89d57
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/ext/Fragment.kt
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.ext
+
+import androidx.fragment.app.Fragment
+import org.mozilla.samples.browser.Components
+
+/**
+ * Get the components of this application.
+ */
+val Fragment.components: Components
+ get() = context!!.components
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt
new file mode 100644
index 0000000000..d2b6c07b83
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ContextMenuIntegration.kt
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.integration
+
+import android.content.Context
+import android.view.View
+import androidx.fragment.app.FragmentManager
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.app.links.AppLinksUseCases
+import mozilla.components.feature.contextmenu.ContextMenuCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createAddContactCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createCopyEmailAddressCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createCopyImageLocationCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createCopyLinkCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createOpenImageInNewTabCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createSaveImageCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createShareEmailAddressCandidate
+import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.createShareLinkCandidate
+import mozilla.components.feature.contextmenu.ContextMenuFeature
+import mozilla.components.feature.contextmenu.ContextMenuUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.ui.widgets.DefaultSnackbarDelegate
+import org.mozilla.samples.browser.databinding.FragmentBrowserBinding
+
+@Suppress("LongParameterList", "UndocumentedPublicClass")
+class ContextMenuIntegration(
+ context: Context,
+ fragmentManager: FragmentManager,
+ browserStore: BrowserStore,
+ tabsUseCases: TabsUseCases,
+ contextMenuUseCases: ContextMenuUseCases,
+ parentView: View,
+ sessionId: String? = null,
+) : LifecycleAwareFeature {
+
+ private val candidates = run {
+ if (sessionId != null) {
+ val snackbarDelegate = DefaultSnackbarDelegate()
+ listOf(
+ createCopyLinkCandidate(context, parentView, snackbarDelegate),
+ createShareLinkCandidate(context),
+ createOpenImageInNewTabCandidate(
+ context,
+ tabsUseCases,
+ parentView,
+ snackbarDelegate,
+ ),
+ createSaveImageCandidate(context, contextMenuUseCases),
+ createCopyImageLocationCandidate(context, parentView, snackbarDelegate),
+ createAddContactCandidate(context),
+ createShareEmailAddressCandidate(context),
+ createCopyEmailAddressCandidate(context, parentView, snackbarDelegate),
+ )
+ } else {
+ val appLinksCandidate = ContextMenuCandidate.createOpenInExternalAppCandidate(
+ context = context,
+ appLinksUseCases = AppLinksUseCases(
+ context = context,
+ launchInApp = { true },
+ ),
+ )
+ ContextMenuCandidate.defaultCandidates(
+ context,
+ tabsUseCases,
+ contextMenuUseCases,
+ parentView,
+ ) + appLinksCandidate
+ }
+ }
+
+ private val feature = ContextMenuFeature(
+ fragmentManager,
+ browserStore,
+ candidates,
+ FragmentBrowserBinding.bind(parentView).engineView,
+ contextMenuUseCases,
+ )
+
+ override fun start() {
+ feature.start()
+ }
+
+ override fun stop() {
+ feature.stop()
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt
new file mode 100644
index 0000000000..3e56201ce0
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/FindInPageIntegration.kt
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.integration
+
+import android.view.View
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.feature.findinpage.FindInPageFeature
+import mozilla.components.feature.findinpage.view.FindInPageView
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+
+@Suppress("UndocumentedPublicClass")
+class FindInPageIntegration(
+ private val store: BrowserStore,
+ private val view: FindInPageView,
+ engineView: EngineView,
+) : LifecycleAwareFeature, UserInteractionHandler {
+ private val feature = FindInPageFeature(store, view, engineView, ::onClose)
+
+ override fun start() {
+ feature.start()
+ launch = this::launch
+ }
+
+ override fun stop() {
+ feature.stop()
+ launch = null
+ }
+
+ override fun onBackPressed(): Boolean {
+ return feature.onBackPressed()
+ }
+
+ private fun onClose() {
+ view.asView().visibility = View.GONE
+ }
+
+ private fun launch() {
+ val session = store.state.selectedTab ?: return
+
+ view.asView().visibility = View.VISIBLE
+ feature.bind(session)
+ }
+
+ companion object {
+ var launch: (() -> Unit)? = null
+ private set
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt
new file mode 100644
index 0000000000..76bcf69d5f
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/integration/ReaderViewIntegration.kt
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.integration
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.core.content.ContextCompat
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.readerview.ReaderViewFeature
+import mozilla.components.feature.readerview.view.ReaderViewControlsView
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import org.mozilla.samples.browser.R
+import mozilla.components.ui.colors.R as colorsR
+import mozilla.components.ui.icons.R as iconsR
+
+@Suppress("UndocumentedPublicClass")
+class ReaderViewIntegration(
+ context: Context,
+ engine: Engine,
+ store: BrowserStore,
+ toolbar: BrowserToolbar,
+ view: ReaderViewControlsView,
+ readerViewAppearanceButton: FloatingActionButton,
+) : LifecycleAwareFeature, UserInteractionHandler {
+
+ private var readerViewButtonVisible = false
+
+ private val readerViewButton: BrowserToolbar.ToggleButton = BrowserToolbar.ToggleButton(
+ image = getReaderDrawable(context),
+ imageSelected = getReaderDrawable(context).mutate().apply {
+ setTint(ContextCompat.getColor(context, colorsR.color.photonBlue40))
+ },
+ contentDescription = context.getString(R.string.mozac_reader_view_description),
+ contentDescriptionSelected = context.getString(R.string.mozac_reader_view_description_selected),
+ selected = store.state.selectedTab?.readerState?.active ?: false,
+ visible = { readerViewButtonVisible },
+ ) { enabled ->
+ if (enabled) {
+ feature.showReaderView()
+ readerViewAppearanceButton.show()
+ } else {
+ feature.hideReaderView()
+ feature.hideControls()
+ readerViewAppearanceButton.hide()
+ }
+ }
+
+ init {
+ toolbar.addPageAction(readerViewButton)
+ readerViewAppearanceButton.setOnClickListener { feature.showControls() }
+ }
+
+ private val feature = ReaderViewFeature(context, engine, store, view) { available, active ->
+ readerViewButtonVisible = available
+ readerViewButton.setSelected(active)
+
+ if (active) readerViewAppearanceButton.show() else readerViewAppearanceButton.hide()
+ toolbar.invalidateActions()
+ }
+
+ override fun start() {
+ feature.start()
+ }
+
+ override fun stop() {
+ feature.stop()
+ }
+
+ override fun onBackPressed(): Boolean {
+ return feature.onBackPressed()
+ }
+}
+
+private fun getReaderDrawable(context: Context): Drawable {
+ val drawable = iconsR.drawable.mozac_ic_reader_view_24
+ return ContextCompat.getDrawable(context, drawable)!!
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt
new file mode 100644
index 0000000000..352330e925
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/media/MediaSessionService.kt
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.media
+
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.base.crash.CrashReporting
+import mozilla.components.feature.media.service.AbstractMediaSessionService
+import mozilla.components.support.base.android.NotificationsDelegate
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * See [AbstractMediaSessionService].
+ */
+class MediaSessionService : AbstractMediaSessionService() {
+ override val crashReporter: CrashReporting? by lazy { components.crashReporter }
+ override val store: BrowserStore by lazy { components.store }
+ override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt
new file mode 100644
index 0000000000..509efe9e7e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/request/SampleUrlEncodedRequestInterceptor.kt
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.browser.request
+
+import android.content.Context
+import mozilla.components.browser.errorpages.ErrorPages
+import mozilla.components.browser.errorpages.ErrorType
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.concept.engine.request.RequestInterceptor.ErrorResponse
+import mozilla.components.concept.engine.request.RequestInterceptor.InterceptionResponse
+import org.mozilla.samples.browser.ext.components
+
+/**
+ * Example of a request interceptor that loads error pages with URL encoding (images)
+ */
+class SampleUrlEncodedRequestInterceptor(val context: Context) : RequestInterceptor {
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): InterceptionResponse? {
+ return when (uri) {
+ "sample:about" -> InterceptionResponse.Content("<h1>I am the sample browser</h1>")
+ else -> {
+ var response = context.components.appLinksInterceptor.onLoadRequest(
+ engineSession,
+ uri,
+ lastUri,
+ hasUserGesture,
+ isSameDomain,
+ isRedirect,
+ isDirectNavigation,
+ isSubframeRequest,
+ )
+
+ if (response == null && !isDirectNavigation) {
+ response = context.components.webAppInterceptor.onLoadRequest(
+ engineSession,
+ uri,
+ lastUri,
+ hasUserGesture,
+ isSameDomain,
+ isRedirect,
+ isDirectNavigation,
+ isSubframeRequest,
+ )
+ }
+
+ response
+ }
+ }
+ }
+
+ override fun onErrorRequest(
+ session: EngineSession,
+ errorType: ErrorType,
+ uri: String?,
+ ): ErrorResponse {
+ val errorPage = ErrorPages.createUrlEncodedErrorPage(context, errorType, uri)
+ return ErrorResponse(errorPage)
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml b/mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml
new file mode 100644
index 0000000000..8e89011a37
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/drawable/addon_textview_selector.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ This Source Code Form is subject to the terms of the Mozilla Public
+ ~ License, v. 2.0. If a copy of the MPL was not distributed with this
+ ~ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="true" android:color="@android:color/black" />
+ <item android:state_checked="false" android:color="@color/photonGrey40" />
+</selector> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml b/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml
new file mode 100644
index 0000000000..673ffa98a7
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_extensions_black.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:pathData="M14.5,8c-0.971,0 -1,1 -1.75,1a0.765,0.765 0,0 1,-0.75 -0.75V5a1,1 0,0 0,-1 -1H7.75A0.765,0.765 0,0 1,7 3.25c0,-0.75 1,-0.779 1,-1.75C8,0.635 7.1,0 6,0S4,0.635 4,1.5c0,0.971 1,1 1,1.75a0.765,0.765 0,0 1,-0.75 0.75H1a1,1 0,0 0,-1 1v2.25A0.765,0.765 0,0 0,0.75 8c0.75,0 0.779,-1 1.75,-1C3.365,7 4,7.9 4,9s-0.635,2 -1.5,2c-0.971,0 -1,-1 -1.75,-1a0.765,0.765 0,0 0,-0.75 0.75V15a1,1 0,0 0,1 1h3.25a0.765,0.765 0,0 0,0.75 -0.75c0,-0.75 -1,-0.779 -1,-1.75 0,-0.865 0.9,-1.5 2,-1.5s2,0.635 2,1.5c0,0.971 -1,1 -1,1.75a0.765,0.765 0,0 0,0.75 0.75H11a1,1 0,0 0,1 -1v-3.25a0.765,0.765 0,0 1,0.75 -0.75c0.75,0 0.779,1 1.75,1 0.865,0 1.5,-0.9 1.5,-2s-0.635,-2 -1.5,-2z"
+ android:fillColor="@android:color/black"/>
+</vector>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml b/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml
new file mode 100644
index 0000000000..cf50e31dbf
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/drawable/mozac_ic_permissions.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:autoMirrored="true"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M2,1h20c1.1,0 2,0.9 2,2v18c0,1.1 -0.9,2 -2,2H2c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2z" />
+ <path
+ android:fillColor="#FFF"
+ android:pathData="M12,3h9c0.6,0 1,0.4 1,1v16c0,0.6 -0.4,1 -1,1h-9L12,3zM5.5,12.5l2.7,-3.7c0.2,-0.3 0.6,-0.3 0.8,-0.1l0.7,0.5c0.2,0.2 0.2,0.5 0,0.7L5.8,15c-0.2,0.2 -0.5,0.3 -0.8,0.1l-2.2,-2.2c-0.2,-0.2 -0.2,-0.5 0,-0.7l0.8,-0.8c0.2,-0.2 0.5,-0.2 0.7,0l1.2,1.1z" />
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M15,9l-1,1 2,2 -2,2 1,1 2,-2 2,2 1,-1 -2,-2 2,-2 -1,-1 -2,2.01L15,9z" />
+</vector>
+
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml
new file mode 100644
index 0000000000..5a3896533a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_details.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="6dp">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <TextView
+ android:id="@+id/details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ tools:text="@tools:sample/lorem/random" />
+
+ <TextView
+ android:id="@+id/author_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/details"
+ android:text="@string/mozac_feature_addons_author" />
+
+ <TextView
+ android:id="@+id/author_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/details"
+ android:layout_alignParentEnd="true"
+ tools:text="@tools:sample/full_names" />
+
+ <View
+ android:id="@+id/author_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/author_label"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/photonGrey40"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/version_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/author_divider"
+ android:text="@string/mozac_feature_addons_version" />
+
+ <TextView
+ android:id="@+id/version_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/author_divider"
+ android:layout_alignParentEnd="true"
+ tools:text="1.2.3" />
+
+ <View
+ android:id="@+id/version_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/version_label"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/photonGrey40"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/last_updated_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/version_divider"
+ android:text="@string/mozac_feature_addons_last_updated" />
+
+ <TextView
+ android:id="@+id/last_updated_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/version_divider"
+ android:layout_alignParentEnd="true"
+ tools:text="Oct 16, 2019" />
+
+ <View
+ android:id="@+id/last_updated_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/last_updated_label"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/photonGrey40"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/home_page_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/last_updated_divider"
+ android:text="@string/mozac_feature_addons_home_page" />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/home_page_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/last_updated_divider"
+ android:layout_alignParentEnd="true"
+ android:contentDescription="@string/mozac_feature_addons_home_page"
+ app:srcCompat="@drawable/mozac_ic_link_24"
+ app:tint="@android:color/black" />
+
+ <View
+ android:id="@+id/home_page_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/home_page_label"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/photonGrey40"
+ android:importantForAccessibility="no" />
+
+ <TextView
+ android:id="@+id/rating_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/home_page_divider"
+ android:text="@string/mozac_feature_addons_rating" />
+
+ <RatingBar
+ android:id="@+id/rating_view"
+ style="@style/Widget.AppCompat.RatingBar.Small"
+ android:layout_width="wrap_content"
+ android:layout_height="20dp"
+ android:layout_below="@+id/home_page_divider"
+ android:layout_toStartOf="@+id/users_count"
+ android:isIndicator="true"
+ android:numStars="5" />
+
+ <TextView
+ android:id="@+id/users_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/home_page_divider"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="6dp"
+ tools:text="591,642" />
+
+ </RelativeLayout>
+</ScrollView>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml
new file mode 100644
index 0000000000..003949ec45
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_permissions.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:context=".addons.PermissionsDetailsActivity">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/add_ons_permissions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:id="@+id/learn_more_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/add_ons_permissions"
+ android:background="?attr/selectableItemBackground"
+ android:padding="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:text="@string/mozac_feature_addons_learn_more"
+ android:textColor="@android:color/black"
+ app:drawableEndCompat="@drawable/mozac_ic_link_24"
+ app:drawableTint="@android:color/black" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml
new file mode 100644
index 0000000000..f2a64edaf8
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_add_on_settings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/addonSettingsContainer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:ignore="MergeRootFrame" />
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml
new file mode 100644
index 0000000000..1c713c13b3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_installed_add_on_details.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="6dp">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/enable_switch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ android:background="?android:attr/selectableItemBackground"
+ android:checked="true"
+ android:clickable="true"
+ android:focusable="true"
+ android:text="@string/mozac_feature_addons_enabled"
+ android:padding="16dp"
+ android:textSize="18sp"/>
+
+ <TextView
+ android:id="@+id/settings"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/enable_switch"
+ android:background="?android:attr/selectableItemBackground"
+ android:drawablePadding="10dp"
+ android:padding="16dp"
+ android:text="@string/mozac_feature_addons_settings"
+ android:textColor="@drawable/addon_textview_selector"
+ android:textSize="18sp"
+ app:drawableStartCompat="@drawable/mozac_ic_preferences"
+ app:drawableTint="@android:color/black" />
+
+ <TextView
+ android:id="@+id/details"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/settings"
+ android:background="?android:attr/selectableItemBackground"
+ android:drawablePadding="6dp"
+ android:padding="16dp"
+ android:text="@string/mozac_feature_addons_details"
+ android:textColor="@drawable/addon_textview_selector"
+ android:textSize="18sp"
+ app:drawableStartCompat="@drawable/mozac_ic_information_24"
+ app:drawableTint="@android:color/black" />
+
+ <TextView
+ android:id="@+id/permissions"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/details"
+ android:background="?android:attr/selectableItemBackground"
+ android:drawablePadding="6dp"
+ android:padding="16dp"
+ android:text="@string/mozac_feature_addons_permissions"
+ android:textColor="@drawable/addon_textview_selector"
+ android:textSize="18sp"
+ app:drawableStartCompat="@drawable/mozac_ic_permissions" />
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/allow_in_private_browsing_switch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ android:layout_below="@+id/permissions"
+ android:background="?android:attr/selectableItemBackground"
+ android:checked="true"
+ android:clickable="true"
+ android:focusable="true"
+ android:text="@string/mozac_feature_addons_settings_allow_in_private_browsing"
+ android:padding="16dp"
+ android:textSize="18sp"/>
+
+ <Button
+ android:id="@+id/remove_add_on"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/allow_in_private_browsing_switch"
+ android:layout_marginTop="16dp"
+ android:text="@string/mozac_feature_addons_remove"
+ android:textAlignment="center"
+ android:textColor="@color/photonRed50" />
+ </RelativeLayout>
+</ScrollView>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..05ab3e0238
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/activity_main.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:ignore="MergeRootFrame" />
+
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml
new file mode 100644
index 0000000000..d4bf988d33
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_on_settings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <mozilla.components.concept.engine.EngineView
+ tools:ignore="Instantiatable"
+ android:id="@+id/addonSettingsEngineView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml
new file mode 100644
index 0000000000..f4d63f6fc3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_add_ons.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/add_ons_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".BrowserActivity"/>
+
+
+ <include
+ android:id="@+id/addonProgressOverlay"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:visibility="gone"
+ layout="@layout/overlay_add_on_progress" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml
new file mode 100644
index 0000000000..0403dbea20
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_browser.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".BrowserActivity">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <mozilla.components.browser.toolbar.BrowserToolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:background="#aaaaaa" />
+
+ <mozilla.components.feature.findinpage.view.FindInPageBar
+ android:id="@+id/findInPage"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:background="#FFFFFFFF"
+ android:elevation="10dp"
+ android:padding="4dp"
+ android:visibility="gone"
+ app:findInPageNoMatchesTextColor="@color/photonRed50" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <mozilla.components.ui.widgets.VerticalSwipeRefreshLayout
+ android:id="@+id/swipeToRefresh"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <mozilla.components.concept.engine.EngineView
+ tools:ignore="Instantiatable"
+ android:id="@+id/engineView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ </mozilla.components.ui.widgets.VerticalSwipeRefreshLayout>
+
+ <org.mozilla.samples.browser.awesomebar.AwesomeBarWrapper
+ android:id="@+id/awesomeBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="4dp"
+ android:visibility="gone" />
+
+ <mozilla.components.feature.readerview.view.ReaderViewControlsBar
+ android:id="@+id/readerViewBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:background="#FFFFFFFF"
+ android:elevation="10dp"
+ android:paddingBottom="55dp"
+ android:visibility="gone" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/readerViewAppearanceButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|bottom"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginBottom="72dp"
+ android:src="@drawable/mozac_ic_font"
+ android:visibility="gone"
+ tools:ignore="ContentDescription" />
+
+ </FrameLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml
new file mode 100644
index 0000000000..3dddf561db
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_not_yet_supported_addons.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:text="@string/mozac_feature_addons_not_yet_supported_caption2" />
+
+ <TextView
+ android:id="@+id/learn_more_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:background="?attr/selectableItemBackground"
+ android:text="@string/mozac_feature_addons_unsupported_learn_more" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/photonGrey30" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/unsupported_add_ons_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".BrowserActivity"/>
+
+</LinearLayout>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml
new file mode 100644
index 0000000000..c16d4e3ea3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/fragment_tabstray.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:mozac="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:background="#aaaaaa"/>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/tabsTray"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/toolbar" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml b/mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml
new file mode 100644
index 0000000000..77cc30fa5e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/layout/overlay_add_on_progress.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/addonProgressOverlay"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:elevation="1dp">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/install_hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:drawablePadding="8dp"
+ android:gravity="start|center_vertical"
+ android:padding="16dp"
+ android:text="@string/mozac_extension_install_progress_caption"
+ app:drawableStartCompat="@drawable/mozac_ic_extensions_black" />
+
+ <Button
+ android:id="@+id/cancel_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/install_hint"
+ android:layout_alignParentEnd="true"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/mozac_feature_addons_install_addon_dialog_cancel"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+ </RelativeLayout>
+
+</androidx.cardview.widget.CardView>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml b/mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml
new file mode 100644
index 0000000000..b918b60307
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/menu/tabstray_menu.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<menu xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/newTab"
+ android:icon="@drawable/mozac_ic_tab_new"
+ android:title="@string/menu_action_add_tab"
+ app:showAsAction="ifRoom" />
+</menu> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..ff5b811c28
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..ff5b811c28
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..cdc89f3dee
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..84ef408f6a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..e6df10d76b
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..c01ea2a106
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..0a810a25a3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..3909d6df1e
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..40a7e0cc99
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..15b39d10eb
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..190b2d260a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..0e9b1e4e1b
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..7b7aa5dfdb
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..6ad22ca834
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..73759f1a06
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..646c51a8ae
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..e249dfc1d3
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/browser/src/main/res/values/colors.xml b/mobile/android/android-components/samples/browser/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..b1560d4ab9
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <color name="mozac_ui_tabcounter_default_tint" tools:ignore="UnusedResources">#FFFFFFFF</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml b/mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000000..92e7745041
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <color name="ic_launcher_background">#45A1FF</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/values/strings.xml b/mobile/android/android-components/samples/browser/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..8789005cc6
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">Sample Browser</string>
+
+ <string name="menu_action_add_tab">Add New Tab</string>
+ <string name="mozac_reader_view_description">Enable Reader View</string>
+ <string name="mozac_reader_view_description_selected">Disable Reader View</string>
+</resources>
diff --git a/mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml b/mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml
new file mode 100644
index 0000000000..0a338b877a
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/main/res/xml/service_configuration.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<autofill-service
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:supportsInlineSuggestions="true"
+ tools:targetApi="r" /> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt b/mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt
new file mode 100644
index 0000000000..41afeb2c30
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/servo/java/org/mozilla/samples/browser/Components.kt
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.samples.browser
+
+import android.content.Context
+import mozilla.components.browser.engine.servo.ServoEngine
+import mozilla.components.concept.engine.Engine
+
+/**
+ * Helper class for lazily instantiating components needed by the application.
+ */
+class Components(applicationContext: Context) : DefaultComponents(applicationContext) {
+ override val engine: Engine by lazy {
+ ServoEngine()
+ }
+}
diff --git a/mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt b/mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt
new file mode 100644
index 0000000000..220e859f6b
--- /dev/null
+++ b/mobile/android/android-components/samples/browser/src/system/java/org/mozilla/samples/browser/Components.kt
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.samples.browser
+
+import android.content.Context
+
+/**
+ * Helper class for lazily instantiating components needed by the application.
+ */
+class Components(applicationContext: Context) : DefaultComponents(applicationContext)
diff --git a/mobile/android/android-components/samples/compose-browser/.gitignore b/mobile/android/android-components/samples/compose-browser/.gitignore
new file mode 100644
index 0000000000..af6eaebcd7
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/.gitignore
@@ -0,0 +1,2 @@
+/build
+manifest.json
diff --git a/mobile/android/android-components/samples/compose-browser/README.md b/mobile/android/android-components/samples/compose-browser/README.md
new file mode 100644
index 0000000000..a8cd90148e
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/README.md
@@ -0,0 +1,35 @@
+# [Android Components](../../README.md) > Samples > Browser
+
+![](src/main/res/mipmap-xhdpi/ic_launcher.png)
+
+A simple browser app that is composed from the browser components in this repository.
+
+⚠️ **Note**: This sample application is only a very basic browser. For a full-featured reference browser implementation see the **[reference-browser repository](https://github.com/mozilla-mobile/reference-browser)**.
+
+## Build variants
+
+The browser app uses a product flavor:
+
+* **channel**: Using different release channels of GeckoView: _nightly_, _beta_, _production_. In most cases you want to use the _nightly_ flavor as this will support all of the latest functionality.
+
+## Glean SDK support
+
+This sample application comes with Glean SDK telemetry initialized by default, but with upload disabled (no data is being sent).
+This is for creating a simpler metric testing workflow for Gecko engineers that need to add their metrics to Gecko and expose them to Mozilla mobile products.
+See [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1592935) for more context.
+
+In order to enable data upload for testing purposes, change the `Glean.setUploadEnabled(false)` to `Glean.setUploadEnabled(true)` in [`SampleApplication.kt`](src/main/java/org/mozilla/samples/browser/SampleApplication.kt).
+
+Glean will send metrics from any Glean-enabled component used in this sample application:
+
+- [engine-gecko-nightly](https://github.com/mozilla-mobile/android-components/blob/master/components/browser/engine-gecko-nightly/docs/metrics.md);
+- [engine-gecko-beta](https://github.com/mozilla-mobile/android-components/blob/master/components/browser/engine-gecko-beta/docs/metrics.md);
+- [engine-gecko](https://github.com/mozilla-mobile/android-components/blob/master/components/browser/engine-gecko/docs/metrics.md);
+
+Data review for enabling the Glean SDK for this application can be found [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1592935#c6).
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/samples/compose-browser/build.gradle b/mobile/android/android-components/samples/compose-browser/build.gradle
new file mode 100644
index 0000000000..1dd2422ec2
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/build.gradle
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.compose.browser"
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArgument "clearPackageData", "true"
+ testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'org.mozilla.samples.compose.browser'
+}
+
+tasks.register("updateBorderifyExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/borderify')
+}
+
+tasks.register("updateTestExtensionVersion", Copy) { task ->
+ updateExtensionVersion(task, 'src/main/assets/extensions/test')
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':concept-engine')
+ implementation project(':concept-awesomebar')
+ implementation project(':concept-tabstray')
+
+ implementation project(':browser-engine-gecko')
+ implementation project(':browser-state')
+ implementation project(':browser-icons')
+
+ implementation project(':compose-awesomebar')
+ implementation project(':compose-browser-toolbar')
+ implementation project(':compose-engine')
+ implementation project(':compose-tabstray')
+
+ implementation project(':feature-awesomebar')
+ implementation project(':feature-fxsuggest')
+ implementation project(':feature-search')
+ implementation project(':feature-session')
+ implementation project(':feature-tabs')
+
+ implementation project(':service-location')
+ implementation project(':support-rusthttp')
+
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_activity_compose
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_ui_tooling
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+ implementation ComponentsDependencies.androidx_compose_navigation
+}
+
+preBuild.dependsOn updateBorderifyExtensionVersion
+preBuild.dependsOn updateTestExtensionVersion
diff --git a/mobile/android/android-components/samples/compose-browser/proguard-rules.pro b/mobile/android/android-components/samples/compose-browser/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/compose-browser/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..cb77ad9280
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <!-- This is needed because the android.permission.CAMERA above automatically
+ adds a requirements for camera hardware and we don't want add those restrictions -->
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.camera.autofocus"
+ android:required="false" />
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage" />
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+ <application
+ android:allowBackup="false"
+ android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
+ android:name=".BrowserApplication"
+ android:usesCleartextTraffic="true"
+ tools:ignore="DataExtractionRules,UnusedAttribute"
+ android:dataExtractionRules="@xml/data_extraction_rules">
+ <activity android:name=".BrowserComposeActivity"
+ android:launchMode="singleTask"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserApplication.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserApplication.kt
new file mode 100644
index 0000000000..c4b13b1e3e
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserApplication.kt
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser
+
+import android.app.Application
+import mozilla.appservices.Megazord
+import mozilla.components.feature.fxsuggest.GlobalFxSuggestDependencyProvider
+import mozilla.components.support.rusthttp.RustHttpConfig
+
+/**
+ * The global [Application] class of this browser application.
+ */
+class BrowserApplication : Application() {
+ val components by lazy { Components(this) }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ Megazord.init()
+ RustHttpConfig.setClient(lazy { components.client })
+
+ GlobalFxSuggestDependencyProvider.initialize(components.fxSuggestStorage)
+ }
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserComposeActivity.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserComposeActivity.kt
new file mode 100644
index 0000000000..1b46e4f2af
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/BrowserComposeActivity.kt
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.material.MaterialTheme
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import org.mozilla.samples.compose.browser.browser.BrowserScreen
+import org.mozilla.samples.compose.browser.ext.components
+import org.mozilla.samples.compose.browser.settings.SettingsScreen
+
+/**
+ * Ladies and gentleman, the browser. ¯\_(ツ)_/¯
+ */
+class BrowserComposeActivity : AppCompatActivity() {
+ companion object {
+ const val ROUTE_BROWSER = "browser"
+ const val ROUTE_SETTINGS = "settings"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ val navController = rememberNavController()
+
+ MaterialTheme {
+ NavHost(navController, startDestination = ROUTE_BROWSER) {
+ composable(ROUTE_BROWSER) { BrowserScreen(navController) }
+ composable(ROUTE_SETTINGS) { SettingsScreen() }
+ }
+ }
+ }
+
+ components.fxSuggestIngestionScheduler.startPeriodicIngestion()
+ }
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/Components.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/Components.kt
new file mode 100644
index 0000000000..874b4081de
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/Components.kt
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import mozilla.components.browser.engine.gecko.GeckoEngine
+import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
+import mozilla.components.browser.state.engine.EngineMiddleware
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.fetch.Client
+import mozilla.components.feature.fxsuggest.FxSuggestIngestionScheduler
+import mozilla.components.feature.fxsuggest.FxSuggestStorage
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.search.middleware.SearchMiddleware
+import mozilla.components.feature.search.region.RegionMiddleware
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.service.location.LocationService
+import org.mozilla.geckoview.GeckoRuntime
+import org.mozilla.samples.compose.browser.app.AppStore
+
+/**
+ * Global components of the sample browser.
+ */
+class Components(
+ context: Context,
+) {
+ private val runtime by lazy { GeckoRuntime.create(context) }
+
+ val engine: Engine by lazy { GeckoEngine(context, runtime = runtime) }
+ val client: Client by lazy { GeckoViewFetchClient(context, runtime = runtime) }
+
+ val store: BrowserStore by lazy {
+ BrowserStore(
+ middleware = listOf(
+ RegionMiddleware(context, locationService),
+ SearchMiddleware(context),
+ ) + EngineMiddleware.create(engine),
+ )
+ }
+
+ val appStore: AppStore by lazy { AppStore() }
+
+ val sessionUseCases by lazy { SessionUseCases(store) }
+ val tabsUseCases by lazy { TabsUseCases(store) }
+ val searchUseCases by lazy { SearchUseCases(store, tabsUseCases, sessionUseCases) }
+
+ val locationService by lazy { LocationService.default() }
+
+ val fxSuggestStorage: FxSuggestStorage by lazy {
+ FxSuggestStorage(context)
+ }
+
+ val fxSuggestIngestionScheduler: FxSuggestIngestionScheduler by lazy {
+ FxSuggestIngestionScheduler(context)
+ }
+}
+
+/**
+ * Returns the global [Components] object from within a `@Composable` context.
+ */
+@Composable
+fun components(): Components {
+ return (LocalContext.current.applicationContext as BrowserApplication).components
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppAction.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppAction.kt
new file mode 100644
index 0000000000..cf36ac991b
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppAction.kt
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.app
+
+import mozilla.components.lib.state.Action
+
+/**
+ * Actions for updating the global [AppState] via [AppStore].
+ */
+sealed class AppAction : Action {
+ /**
+ * Toggles the theme of the app (only for testing purposes).
+ */
+ object ToggleTheme : AppAction()
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppState.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppState.kt
new file mode 100644
index 0000000000..163dddb8ec
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppState.kt
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.app
+
+import mozilla.components.lib.state.State
+
+/**
+ * Global state the browser is in (regardless of the currently displayed screen).
+ */
+data class AppState(
+ val theme: Int = 1,
+) : State
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppStore.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppStore.kt
new file mode 100644
index 0000000000..d75e5f3787
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/app/AppStore.kt
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.app
+
+import mozilla.components.lib.state.Store
+
+/**
+ * [Store] for the global [AppState].
+ */
+class AppStore : Store<AppState, AppAction>(
+ initialState = AppState(),
+ reducer = ::reduce,
+)
+
+private fun reduce(appState: AppState, appAction: AppAction): AppState {
+ if (appAction is AppAction.ToggleTheme) {
+ return appState.copy(theme = (appState.theme + 1) % 2)
+ }
+ return appState
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreen.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreen.kt
new file mode 100644
index 0000000000..2e5a66312d
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreen.kt
@@ -0,0 +1,237 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.browser
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Button
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.navigation.NavController
+import mozilla.components.browser.state.helper.Target
+import mozilla.components.compose.browser.awesomebar.AwesomeBar
+import mozilla.components.compose.browser.toolbar.BrowserToolbar
+import mozilla.components.compose.engine.WebContent
+import mozilla.components.compose.tabstray.TabCounterButton
+import mozilla.components.compose.tabstray.TabList
+import mozilla.components.concept.awesomebar.AwesomeBar
+import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
+import mozilla.components.feature.awesomebar.provider.SearchActionProvider
+import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
+import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
+import mozilla.components.feature.fxsuggest.FxSuggestSuggestionProvider
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.ext.composableStore
+import mozilla.components.lib.state.ext.observeAsComposableState
+import org.mozilla.samples.compose.browser.BrowserComposeActivity.Companion.ROUTE_SETTINGS
+import org.mozilla.samples.compose.browser.components
+
+/**
+ * The main browser screen.
+ */
+@Composable
+fun BrowserScreen(navController: NavController) {
+ val target = Target.SelectedTab
+
+ val store = composableStore<BrowserScreenState, BrowserScreenAction> { restoredState ->
+ BrowserScreenStore(restoredState ?: BrowserScreenState())
+ }
+
+ val editState = store.observeAsComposableState { state -> state.editMode }
+ val editUrl = store.observeAsComposableState { state -> state.editText }
+ val loadUrl = components().sessionUseCases.loadUrl
+ val showTabs = store.observeAsComposableState { state -> state.showTabs }
+
+ BackHandler(enabled = editState.value == true) {
+ store.dispatch(BrowserScreenAction.ToggleEditMode(false))
+ }
+
+ Box {
+ Column {
+ BrowserToolbar(
+ components().store,
+ target,
+ editMode = editState.value!!,
+ onDisplayMenuClicked = {
+ navController.navigate(ROUTE_SETTINGS)
+ },
+ onTextCommit = { text ->
+ store.dispatch(BrowserScreenAction.ToggleEditMode(false))
+ loadUrl(text)
+ },
+ onTextEdit = { text -> store.dispatch(BrowserScreenAction.UpdateEditText(text)) },
+ onDisplayToolbarClick = {
+ store.dispatch(BrowserScreenAction.ToggleEditMode(true))
+ },
+ editText = editUrl.value,
+ hint = "Search or enter address",
+ browserActions = {
+ TabCounterButton(
+ components().store,
+ onClicked = { store.dispatch(BrowserScreenAction.ShowTabs) },
+ )
+ },
+ )
+
+ Box {
+ WebContent(
+ components().engine,
+ components().store,
+ Target.SelectedTab,
+ )
+
+ val url = editUrl.value
+ if (editState.value == true && url != null) {
+ Suggestions(
+ url,
+ onSuggestionClicked = { suggestion ->
+ store.dispatch(BrowserScreenAction.ToggleEditMode(false))
+ suggestion.onSuggestionClicked?.invoke()
+ },
+ onAutoComplete = { suggestion ->
+ store.dispatch(BrowserScreenAction.UpdateEditText(suggestion.editSuggestion!!))
+ },
+ )
+ }
+ }
+ }
+
+ if (showTabs.value == true) {
+ TabsTray(store)
+ }
+ }
+}
+
+/**
+ * Shows the lit of tabs.
+ */
+@Composable
+fun TabsTray(
+ store: Store<BrowserScreenState, BrowserScreenAction>,
+) {
+ val components = components()
+
+ BackHandler(onBack = { store.dispatch(BrowserScreenAction.HideTabs) })
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ .background(Color.Black.copy(alpha = ContentAlpha.medium))
+ .clickable {
+ store.dispatch(BrowserScreenAction.HideTabs)
+ },
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxHeight(fraction = 0.8f)
+ .align(Alignment.BottomStart),
+ ) {
+ TabList(
+ store = components().store,
+ onTabSelected = { tab ->
+ components.tabsUseCases.selectTab(tab.id)
+ store.dispatch(BrowserScreenAction.HideTabs)
+ },
+ onTabClosed = { tab ->
+ components.tabsUseCases.removeTab(tab.id)
+ },
+ modifier = Modifier.weight(1f),
+ )
+ Button(
+ onClick = {
+ components.tabsUseCases.addTab(
+ url = "about:blank",
+ selectTab = true,
+ )
+ store.dispatch(BrowserScreenAction.HideTabs)
+ store.dispatch(BrowserScreenAction.ToggleEditMode(true))
+ },
+ ) {
+ Text("+")
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun Suggestions(
+ url: String,
+ onSuggestionClicked: (AwesomeBar.Suggestion) -> Unit,
+ onAutoComplete: (AwesomeBar.Suggestion) -> Unit,
+) {
+ val context = LocalContext.current
+ val components = components()
+
+ val sessionSuggestionProvider = remember(context) {
+ SessionSuggestionProvider(
+ context.resources,
+ components.store,
+ components.tabsUseCases.selectTab,
+ )
+ }
+
+ val searchActionProvider = remember {
+ SearchActionProvider(components.store, components.searchUseCases.defaultSearch)
+ }
+
+ val fxSuggestSuggestionProvider = remember(context) {
+ FxSuggestSuggestionProvider(
+ context.resources,
+ loadUrlUseCase = components.sessionUseCases.loadUrl,
+ includeSponsoredSuggestions = false,
+ includeNonSponsoredSuggestions = true,
+ )
+ }
+
+ val searchSuggestionProvider = remember(context) {
+ SearchSuggestionProvider(
+ context,
+ components.store,
+ components.searchUseCases.defaultSearch,
+ components.client,
+ mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
+ engine = components.engine,
+ filterExactMatch = true,
+ )
+ }
+
+ val clipboardSuggestionProvider = remember(context) {
+ ClipboardSuggestionProvider(
+ context,
+ components.sessionUseCases.loadUrl,
+ )
+ }
+
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ AwesomeBar(
+ url,
+ providers = listOf(
+ sessionSuggestionProvider,
+ searchActionProvider,
+ fxSuggestSuggestionProvider,
+ searchSuggestionProvider,
+ clipboardSuggestionProvider,
+ ),
+ onSuggestionClicked = { suggestion -> onSuggestionClicked(suggestion) },
+ onAutoComplete = { suggestion -> onAutoComplete(suggestion) },
+ onScroll = { keyboardController?.hide() },
+ )
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenAction.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenAction.kt
new file mode 100644
index 0000000000..c7da237d5a
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenAction.kt
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.browser
+
+import mozilla.components.lib.state.Action
+
+/**
+ * Actions for updating the [BrowserScreenState] via [BrowserScreenStore].
+ */
+sealed class BrowserScreenAction : Action {
+ /**
+ * Updates whether the toolbar is in "display" or "edit" mode.
+ */
+ data class ToggleEditMode(val editMode: Boolean) : BrowserScreenAction()
+
+ /**
+ * Updates the text of the toolbar that is currently being edited (in "edit" mode).
+ */
+ data class UpdateEditText(val text: String) : BrowserScreenAction()
+
+ /**
+ * Shows the list of tabs on top of the web content.
+ */
+ object ShowTabs : BrowserScreenAction()
+
+ /**
+ * Hides the list of tabs.
+ */
+ object HideTabs : BrowserScreenAction()
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenState.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenState.kt
new file mode 100644
index 0000000000..d3bae7578d
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenState.kt
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.browser
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import mozilla.components.lib.state.State
+
+/**
+ * The state the browser screen is in.
+ *
+ * @param editMode Whether the toolbar is in "edit" or "display" mode.
+ * @param editText The text in the toolbar that is being edited by the user.
+ */
+@Parcelize
+data class BrowserScreenState(
+ val editMode: Boolean = false,
+ val editText: String? = null,
+ val showTabs: Boolean = false,
+) : State, Parcelable
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenStore.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenStore.kt
new file mode 100644
index 0000000000..6d7dcbb3ac
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/browser/BrowserScreenStore.kt
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.browser
+
+import mozilla.components.lib.state.Store
+
+/**
+ * [Store] for maintaining the state of the browser screen.
+ */
+class BrowserScreenStore(
+ initialState: BrowserScreenState = BrowserScreenState(),
+) : Store<BrowserScreenState, BrowserScreenAction>(
+ initialState = initialState,
+ reducer = ::reduce,
+)
+
+private fun reduce(state: BrowserScreenState, action: BrowserScreenAction): BrowserScreenState {
+ return when (action) {
+ is BrowserScreenAction.ToggleEditMode -> state.copy(
+ editMode = action.editMode,
+ editText = if (action.editMode) null else state.editText,
+ )
+ is BrowserScreenAction.UpdateEditText -> state.copy(editText = action.text)
+ is BrowserScreenAction.ShowTabs -> state.copy(showTabs = true)
+ is BrowserScreenAction.HideTabs -> state.copy(showTabs = false)
+ }
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/ext/Context.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/ext/Context.kt
new file mode 100644
index 0000000000..a847394795
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/ext/Context.kt
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.ext
+
+import android.content.Context
+import org.mozilla.samples.compose.browser.BrowserApplication
+import org.mozilla.samples.compose.browser.Components
+
+val Context.application: BrowserApplication
+ get() = applicationContext as BrowserApplication
+
+val Context.components: Components
+ get() = application.components
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/settings/SettingsScreen.kt b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/settings/SettingsScreen.kt
new file mode 100644
index 0000000000..0e83658e7a
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/java/org/mozilla/samples/compose/browser/settings/SettingsScreen.kt
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.compose.browser.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+
+/**
+ * Screen displaying the settings of the browser.
+ */
+@Composable
+fun SettingsScreen() {
+ Column {
+ Text("Settings")
+ }
+}
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_background.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..61f5b8183f
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<vector
+ android:height="108dp"
+ android:width="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z"/>
+ <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+</vector>
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_foreground.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..8a7cd0aa55
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,19 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108"
+ android:tint="#FFFFFF">
+ <group android:scaleX="2.9232"
+ android:scaleY="2.9232"
+ android:translateX="18.9216"
+ android:translateY="18.9216">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
+ </group>
+</vector>
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..c7743a9582
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..c7743a9582
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..3782c0799b
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..8b8c8e4041
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..7a42b483fd
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..3ff74c1d9d
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..497337793e
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..f1c35726b8
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..cd006d2f57
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..9db209fcbc
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..3ca4e817f7
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..0d5a5a266a
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/values/strings.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..2441d14625
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">Compose Browser</string>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/compose-browser/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/compose-browser/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/compose-browser/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/crash/build.gradle b/mobile/android/android-components/samples/crash/build.gradle
new file mode 100644
index 0000000000..9ea07c4d9f
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/build.gradle
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.crash"
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'org.mozilla.samples.crash'
+}
+
+
+dependencies {
+ implementation project(':lib-crash')
+ implementation project(':lib-fetch-httpurlconnection')
+ implementation project(':service-glean')
+ implementation project(':support-base')
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.google_material
+ implementation ComponentsDependencies.androidx_recyclerview
+}
diff --git a/mobile/android/android-components/samples/crash/lint.xml b/mobile/android/android-components/samples/crash/lint.xml
new file mode 100644
index 0000000000..33cf423701
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/lint.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<lint>
+ <issue id="IconMissingDensityFolder" severity="ignore">
+ <!-- Suppress lint warnings on mdpi -->
+ <ignore path="src/debug/res/drawable-mdpi"/>
+ </issue>
+
+ <issue id="GoogleAppIndexingWarning" severity="ignore" />
+</lint> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/crash/proguard-rules.pro b/mobile/android/android-components/samples/crash/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/crash/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/crash/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..29c01303bd
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
+
+ <application
+ android:name="org.mozilla.samples.crash.CrashApplication"
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ tools:targetApi="s"
+ tools:ignore="DataExtractionRules">
+ <activity android:name="org.mozilla.samples.crash.CrashActivity"
+ android:launchMode="singleTask"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="org.mozilla.samples.crash.CrashListActivity" android:exported="false" />
+
+ <service android:name="org.mozilla.samples.crash.CrashService"
+ android:foregroundServiceType="specialUse"
+ android:process=":samples.crash.service" />
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/samples/crash/src/main/ic_launcher-web.png b/mobile/android/android-components/samples/crash/src/main/ic_launcher-web.png
new file mode 100644
index 0000000000..b8f772f66a
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/ic_launcher-web.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashActivity.kt b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashActivity.kt
new file mode 100644
index 0000000000..2bcce48acc
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashActivity.kt
@@ -0,0 +1,168 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.crash
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import com.google.android.material.snackbar.Snackbar
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.support.utils.ext.registerReceiverCompat
+import org.mozilla.samples.crash.databinding.ActivityCrashBinding
+
+class CrashActivity : AppCompatActivity(), View.OnClickListener {
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (!Crash.isCrashIntent(intent)) {
+ return
+ }
+
+ val crash = Crash.fromIntent(intent)
+
+ Snackbar.make(
+ findViewById(android.R.id.content),
+ "Sorry. We crashed.",
+ Snackbar.LENGTH_LONG,
+ )
+ .setAction("Report") { crashReporter.submitReport(crash) }
+ .show()
+ }
+ }
+ private lateinit var binding: ActivityCrashBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityCrashBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ binding.fatalCrashButton.setOnClickListener(this)
+ binding.crashButton.setOnClickListener(this)
+ binding.fatalServiceCrashButton.setOnClickListener(this)
+ binding.crashList.setOnClickListener(this)
+
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ "CrashActivity onCreate",
+ emptyMap(),
+ "sample",
+ Breadcrumb.Level.DEBUG,
+ Breadcrumb.Type.NAVIGATION,
+ ),
+ )
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ registerReceiverCompat(
+ receiver,
+ IntentFilter(CrashApplication.NON_FATAL_CRASH_BROADCAST),
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ "CrashActivity onResume",
+ emptyMap(),
+ "sample",
+ Breadcrumb.Level.DEBUG,
+ Breadcrumb.Type.NAVIGATION,
+ ),
+ )
+ }
+
+ override fun onPause() {
+ super.onPause()
+ unregisterReceiver(receiver)
+
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ "CrashActivity onPause",
+ emptyMap(),
+ "sample",
+ Breadcrumb.Level.DEBUG,
+ Breadcrumb.Type.NAVIGATION,
+ ),
+ )
+ }
+
+ @Suppress("TooGenericExceptionThrown")
+ override fun onClick(view: View) {
+ when (view) {
+ binding.fatalCrashButton -> {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ "fatal crash button clicked",
+ emptyMap(),
+ "sample",
+ Breadcrumb.Level.INFO,
+ Breadcrumb.Type.USER,
+ ),
+ )
+
+ throw RuntimeException("Boom!")
+ }
+
+ binding.crashButton -> {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ "crash button clicked",
+ emptyMap(),
+ "sample",
+ Breadcrumb.Level.INFO,
+ Breadcrumb.Type.USER,
+ ),
+ )
+
+ // Pretend GeckoView has crashed by re-building a crash Intent and launching the CrashHandlerService.
+ val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+ intent.component = ComponentName(
+ packageName,
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "${filesDir.path}/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("fatal", false)
+ intent.putExtra(
+ "extrasPath",
+ "${filesDir.path}/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+
+ ContextCompat.startForegroundService(this, intent)
+ }
+
+ binding.fatalServiceCrashButton -> {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ "fatal service crash button clicked",
+ emptyMap(),
+ "sample",
+ Breadcrumb.Level.INFO,
+ Breadcrumb.Type.USER,
+ ),
+ )
+
+ startService(Intent(this, CrashService::class.java))
+ finish()
+ }
+
+ binding.crashList -> {
+ startActivity(Intent(this, CrashListActivity::class.java))
+ }
+
+ else -> throw java.lang.RuntimeException("Unknown ID")
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashApplication.kt b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashApplication.kt
new file mode 100644
index 0000000000..2d50615dd7
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashApplication.kt
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.crash
+
+import android.app.Application
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.widget.Toast
+import androidx.core.app.NotificationManagerCompat
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.lib.crash.service.GleanCrashReporterService
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.glean.BuildInfo
+import mozilla.components.service.glean.Glean
+import mozilla.components.service.glean.config.Configuration
+import mozilla.components.service.glean.net.ConceptFetchHttpUploader
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.utils.PendingIntentUtils
+import java.util.Calendar
+import java.util.TimeZone
+import java.util.UUID
+
+@Suppress("MagicNumber")
+internal object GleanBuildInfo {
+ val buildInfo: BuildInfo by lazy {
+ BuildInfo(
+ versionCode = "0.0.1",
+ versionName = "0.0.1",
+ buildDate = Calendar.getInstance(
+ TimeZone.getTimeZone("GMT+0"),
+ ).also { cal -> cal.set(2019, 9, 23, 12, 52, 8) },
+ )
+ }
+}
+
+class CrashApplication : Application() {
+ internal lateinit var crashReporter: CrashReporter
+
+ override fun onCreate() {
+ super.onCreate()
+
+ // We want the log messages of all builds to go to Android logcat
+ Log.addSink(AndroidLogSink())
+
+ val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)
+
+ val notificationsDelegate: NotificationsDelegate by lazy {
+ NotificationsDelegate(
+ notificationManagerCompat,
+ )
+ }
+
+ crashReporter = CrashReporter(
+ context = this,
+ services = listOf(
+ createDummyCrashService(this),
+ ),
+ telemetryServices = listOf(GleanCrashReporterService(applicationContext)),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ promptConfiguration = CrashReporter.PromptConfiguration(
+ appName = "Sample App",
+ organizationName = "Mozilla",
+ message = "As a private browser, we never save and cannot restore your last browsing session.",
+ theme = R.style.CrashDialogTheme,
+ ),
+ nonFatalCrashIntent = createNonFatalPendingIntent(this),
+ enabled = true,
+ notificationsDelegate = notificationsDelegate,
+ ).install(this)
+
+ // Initialize Glean for recording by the GleanCrashReporterService
+ val httpClient = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+ val config = Configuration(httpClient = httpClient)
+ Glean.initialize(
+ applicationContext,
+ uploadEnabled = true,
+ configuration = config,
+ buildInfo = GleanBuildInfo.buildInfo,
+ )
+ }
+
+ companion object {
+ const val NON_FATAL_CRASH_BROADCAST = "org.mozilla.samples.crash.CRASH"
+ }
+}
+
+@OptIn(DelicateCoroutinesApi::class)
+private fun createDummyCrashService(context: Context): CrashReporterService {
+ // For this sample we create a dummy service. In a real application this would be an instance of SentryCrashService
+ // or SocorroCrashService.
+ return object : CrashReporterService {
+ override val id: String = "dummy"
+
+ override val name: String = "Dummy"
+
+ override fun createCrashReportUrl(identifier: String): String? {
+ return "https://example.org/$identifier"
+ }
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? {
+ GlobalScope.launch(Dispatchers.Main) {
+ Toast.makeText(context, "Uploading uncaught exception crash...", Toast.LENGTH_SHORT).show()
+ }
+ return createDummyId()
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ GlobalScope.launch(Dispatchers.Main) {
+ Toast.makeText(context, "Uploading native crash...", Toast.LENGTH_SHORT).show()
+ }
+ return createDummyId()
+ }
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? {
+ GlobalScope.launch(Dispatchers.Main) {
+ Toast.makeText(context, "Uploading caught exception...", Toast.LENGTH_SHORT).show()
+ }
+ return createDummyId()
+ }
+
+ private fun createDummyId(): String {
+ return "dummy${UUID.randomUUID().toString().hashCode()}"
+ }
+ }
+}
+
+private fun createNonFatalPendingIntent(context: Context): PendingIntent {
+ // The PendingIntent can launch whatever you want - an activity, a service... Here we pick a broadcast. Our main
+ // activity will listener for the broadcast and show an in-app snackbar to ask the user whether we should send
+ // this crash report.
+ return PendingIntent.getBroadcast(
+ context,
+ 0,
+ Intent(CrashApplication.NON_FATAL_CRASH_BROADCAST),
+ PendingIntentUtils.defaultFlags,
+ )
+}
+
+val Context.crashReporter: CrashReporter
+ get() = (applicationContext as CrashApplication).crashReporter
diff --git a/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashListActivity.kt b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashListActivity.kt
new file mode 100644
index 0000000000..9a0d4d1d1d
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashListActivity.kt
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.crash
+
+import android.widget.Toast
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.ui.AbstractCrashListActivity
+
+/**
+ * Activity showing list of past crashes.
+ */
+class CrashListActivity : AbstractCrashListActivity() {
+ override val crashReporter: CrashReporter
+ get() = (application as CrashApplication).crashReporter
+
+ override fun onCrashServiceSelected(url: String) {
+ Toast.makeText(this, "Go to: $url", Toast.LENGTH_SHORT).show()
+ }
+}
diff --git a/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashService.kt b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashService.kt
new file mode 100644
index 0000000000..270f9d505d
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/java/org/mozilla/samples/crash/CrashService.kt
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.crash
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.widget.Toast
+import androidx.core.app.NotificationCompat
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import mozilla.components.support.base.ids.SharedIdsHelper
+
+private const val NOTIFICATION_CHANNEL_ID = "mozac.lib.crash.notification"
+private const val NOTIFICATION_TAG = "mozac.lib.crash.foreground-service"
+private const val DELAY_CRASH_MS = 10000L
+
+/**
+ * This service will wait 10 seconds and then crash. We need to wait some time because Android still allows to launch
+ * an activity from a background service if the app was in the foreground a couple of seconds ago.
+ */
+class CrashService : Service() {
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ @Suppress("TooGenericExceptionThrown")
+ override fun onCreate() {
+ Toast.makeText(this, "Crashing from background soonish...", Toast.LENGTH_SHORT).show()
+
+ // We need to put this service into foreground because otherwise Android may kill it (with no visible app UI)
+ // before we can crash.
+ startForeground(SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG), createNotification())
+
+ GlobalScope.launch(Dispatchers.Main) {
+ delay(DELAY_CRASH_MS)
+
+ throw RuntimeException("Background crash")
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ return START_NOT_STICKY
+ }
+
+ private fun createNotification(): Notification {
+ val channel = ensureChannelExists()
+
+ return NotificationCompat.Builder(this, channel)
+ .setContentTitle("Crash Service")
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+ .build()
+ }
+
+ private fun ensureChannelExists(): String {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager: NotificationManager = getSystemService(
+ Context.NOTIFICATION_SERVICE,
+ ) as NotificationManager
+
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ "Crash Service",
+ NotificationManager.IMPORTANCE_DEFAULT,
+ )
+
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ return NOTIFICATION_CHANNEL_ID
+ }
+}
diff --git a/mobile/android/android-components/samples/crash/src/main/res/layout/activity_crash.xml b/mobile/android/android-components/samples/crash/src/main/res/layout/activity_crash.xml
new file mode 100644
index 0000000000..4921aa4849
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/layout/activity_crash.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/crashButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/crash_nonfatal"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/fatalCrashButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/crash_fatal"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/fatalServiceCrashButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/crash_fatal_service"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/crashList"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/list_of_crashes"
+ android:textAlignment="center" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..fb7d4e724b
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..fb7d4e724b
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..506f4c3598
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..361ce175fe
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..302220d5c0
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..777f4f5b51
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..c158a5aa08
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..3f578142cd
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..fd7337a647
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..2233cd4525
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..91bc706ebe
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..7863ad9b3f
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..ea69f75cd7
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..7fdbb4bede
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..6e28647885
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..beccb228ea
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..83a223b8a8
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/crash/src/main/res/values/ic_launcher_background.xml b/mobile/android/android-components/samples/crash/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000000..bce8f507b6
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <color name="ic_launcher_background">#FFF31A</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/crash/src/main/res/values/strings.xml b/mobile/android/android-components/samples/crash/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..ac69e68e60
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">Crash Sample</string>
+
+ <string name="crash_fatal">Crash (Fatal)</string>
+ <string name="crash_nonfatal">Crash (Non-Fatal)</string>
+ <string name="crash_fatal_service">Crash (Fatal; background service)</string>
+ <string name="list_of_crashes">List of crashes</string>
+</resources>
diff --git a/mobile/android/android-components/samples/crash/src/main/res/values/styles.xml b/mobile/android/android-components/samples/crash/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..bea300304b
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/values/styles.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <!-- inherit from the AppCompat theme -->
+ <style name="CrashDialogTheme" parent="Theme.Mozac.CrashReporter">
+
+ <!-- your app branding color for the app bar -->
+ <item name="colorPrimary">#8BC34A</item>
+
+ <!-- darker variant for the status bar and contextual app bars -->
+ <item name="colorPrimaryDark">#689F38</item>
+
+ <!-- theme UI controls like checkboxes and text fields -->
+ <item name="colorAccent">#E040FB</item>
+
+ </style>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/crash/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/crash/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/crash/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/crash/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/crash/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/dataprotect/build.gradle b/mobile/android/android-components/samples/dataprotect/build.gradle
new file mode 100644
index 0000000000..f9b70a7368
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/build.gradle
@@ -0,0 +1,35 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.dataprotect"
+
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'org.mozilla.samples.dataprotect'
+}
+
+dependencies {
+ implementation project(':support-ktx')
+ implementation project(':lib-dataprotect')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_recyclerview
+}
diff --git a/mobile/android/android-components/samples/dataprotect/lint.xml b/mobile/android/android-components/samples/dataprotect/lint.xml
new file mode 100644
index 0000000000..33cf423701
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/lint.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<lint>
+ <issue id="IconMissingDensityFolder" severity="ignore">
+ <!-- Suppress lint warnings on mdpi -->
+ <ignore path="src/debug/res/drawable-mdpi"/>
+ </issue>
+
+ <issue id="GoogleAppIndexingWarning" severity="ignore" />
+</lint> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/dataprotect/proguard-rules.pro b/mobile/android/android-components/samples/dataprotect/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/dataprotect/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..72969290ec
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application
+ android:allowBackup="false"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ tools:ignore="DataExtractionRules"
+ tools:targetApi="s">
+ <activity android:name="org.mozilla.samples.dataprotect.MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/Constants.kt b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/Constants.kt
new file mode 100644
index 0000000000..ab648d4fff
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/Constants.kt
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.dataprotect
+
+import android.util.Base64
+
+object Constants {
+ const val B64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_PADDING
+ const val KEYSTORE_LABEL = "samples-dataprotect"
+}
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/MainActivity.kt b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/MainActivity.kt
new file mode 100644
index 0000000000..12e30e638d
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/MainActivity.kt
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.dataprotect
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+
+class MainActivity : AppCompatActivity() {
+ @Suppress("MagicNumber")
+ private val itemKeys: List<String> = List(5) { "protected item ${it + 1}" }
+
+ private lateinit var listView: RecyclerView
+ private lateinit var listAdapter: ProtectedDataAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val prefs = SecureAbove22Preferences(this, "secret-data-storage")
+
+ prepareProtectedData(prefs)
+
+ // setup recycler
+ listAdapter = ProtectedDataAdapter(prefs, itemKeys)
+ listView = findViewById(R.id.protecteddata_list)
+ listView.apply {
+ setHasFixedSize(true)
+ layoutManager = LinearLayoutManager(this@MainActivity)
+ adapter = listAdapter
+ }
+ }
+
+ private fun prepareProtectedData(prefs: SecureAbove22Preferences) {
+ for (datakey in itemKeys) {
+ val plain = "value for $datakey"
+ prefs.putString(datakey, plain)
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/ProtectedDataAdapter.kt b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/ProtectedDataAdapter.kt
new file mode 100644
index 0000000000..366f4a9059
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/java/org/mozilla/samples/dataprotect/ProtectedDataAdapter.kt
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.dataprotect
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+
+class ProtectedDataAdapter(
+ private val prefs: SecureAbove22Preferences,
+ private val itemKeys: List<String>,
+) : RecyclerView.Adapter<ProtectedDataAdapter.Holder>() {
+ override fun getItemCount(): Int = itemKeys.size
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.protecteddata_item, parent, false)
+
+ return Holder(view)
+ }
+
+ override fun onBindViewHolder(holder: Holder, position: Int) {
+ val key = itemKeys[position]
+ var value = prefs.getString(key)
+ holder.keyView.text = key
+ holder.valView.text = value
+ }
+
+ class Holder(val view: View) : RecyclerView.ViewHolder(view) {
+ var keyView: TextView
+ var valView: TextView
+
+ init {
+ keyView = view.findViewById(R.id.protecteddata_item_key_view)
+ valView = view.findViewById(R.id.protecteddata_item_val_view)
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..772d73c1a8
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,38 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path
+ android:fillType="evenOdd"
+ android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="78.5885"
+ android:endY="90.9159"
+ android:startX="48.7653"
+ android:startY="61.0927"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+</vector>
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/drawable/ic_launcher_background.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..84af1fd596
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path
+ android:fillColor="#26A69A"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+</vector>
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..96d6ab81da
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context="org.mozilla.samples.dataprotect.MainActivity">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/protecteddata_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/layout/protecteddata_item.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/layout/protecteddata_item.xml
new file mode 100644
index 0000000000..4ad43964ab
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/layout/protecteddata_item.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="50sp">
+
+ <TextView
+ android:id="@+id/protecteddata_item_key_view"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight=".25" />
+
+ <TextView
+ android:id="@+id/protecteddata_item_val_view"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight=".75" />
+</LinearLayout> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..f39d507313
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..f39d507313
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..a2f5908281
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..1b52399808
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..ff10afd6e1
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..115a4c768a
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..dcd3cd8083
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..459ca609d3
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..8ca12fe024
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..8e19b410a1
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b824ebdd48
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..4c19a13c23
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/values/colors.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..3a96673022
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/values/colors.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/values/strings.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..2883c42227
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">DataProtect Demo App</string>
+</resources>
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/values/styles.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..1f33369bc5
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/values/styles.xml
@@ -0,0 +1,14 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/android-components/samples/dataprotect/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/dataprotect/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/dataprotect/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/firefox-accounts/.gitignore b/mobile/android/android-components/samples/firefox-accounts/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/mobile/android/android-components/samples/firefox-accounts/README.md b/mobile/android/android-components/samples/firefox-accounts/README.md
new file mode 100644
index 0000000000..1e81a3c358
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/README.md
@@ -0,0 +1,65 @@
+# [Android Components](../../README.md) > Samples > Firefox Accounts (FxA)
+
+![](src/main/res/mipmap-xhdpi/ic_launcher.png)
+
+A simple app showcasing the service-firefox-account component.
+
+## Concepts
+
+The main concepts shown in the sample app are:
+
+* Usage of the asynchronous result type `Deferred`
+* Setting up a`FirefoxAccount` object, from a previous session or from scratch
+* Spawning a custom tab or a WebView to handle the user's authentication flow
+
+A minimal walkthrough is also provided in the [component README](https://github.com/mozilla-mobile/android-components/tree/main/components/service/firefox-accounts).
+
+## Setting up the account
+
+### From a previous session
+
+`FirefoxAccount` is a representation of the authentication state for the current client. It provides two methods for saving and restoring state: `toJSONString` and `fromJSONString`.
+
+> The state provided by `toJSONString` should be stored securely, as the credentials inside could in theory let a user stay authenticated forever.
+
+To restore an account from an existing state in shared preferences:
+
+```kotlin
+// Inside a `launch` or `async` block:
+getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).getString(FXA_STATE_KEY, "").let {
+ FirefoxAccount.fromJSONString(it)
+}
+```
+
+To persist an account's state in shared preferences:
+
+```kotlin
+account.toJSONString().let {
+ getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).edit().putString(FXA_STATE_KEY, it).apply()
+}
+```
+
+### From scratch
+
+If no previous auth state was found, we have to create a new one using some default OAuth parameters. Find the hostname, or `CONFIG_URL` for your OAuth provider, then create a `CLIENT_ID` and `REDIRECT_URL` for your application. From there, we can create a `Config` object, and finally our `FirefoxAccount` object:
+
+```kotlin
+val config = Config(CONFIG_URL, CLIENT_ID, REDIRECT_URL)
+// Some helpers such as Config.release(CLIENT_ID, REDIRECT_URL)
+// are also provided for well-known Firefox Accounts servers.
+val account = FirefoxAccount(config)
+```
+
+## Viewing the web pages
+
+In order to complete the OAuth flow, the app can spawn a view and capture the code/state parameters in one of three ways:
+
+* Opening a custom tab, then capturing params via intent filters
+* Spawning a WebView with a page load hook
+* Spawning an EngineView (WebView/GeckoView) [WIP]
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/samples/firefox-accounts/build.gradle b/mobile/android/android-components/samples/firefox-accounts/build.gradle
new file mode 100644
index 0000000000..c6a7d616e9
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/build.gradle
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.fxa"
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'x86', 'arm64-v8a', 'armeabi-v7a'
+ }
+ }
+
+ namespace 'org.mozilla.samples.fxa'
+
+}
+
+dependencies {
+ implementation project(':service-firefox-accounts')
+ implementation project(':feature-qr')
+ implementation project(':support-base')
+ implementation project(':support-rustlog')
+ implementation project(':support-rusthttp')
+ implementation project(':lib-fetch-httpurlconnection')
+
+ implementation ComponentsDependencies.androidx_constraintlayout
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_browser
+}
diff --git a/mobile/android/android-components/samples/firefox-accounts/gradle.properties b/mobile/android/android-components/samples/firefox-accounts/gradle.properties
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/gradle.properties
diff --git a/mobile/android/android-components/samples/firefox-accounts/proguard-rules.pro b/mobile/android/android-components/samples/firefox-accounts/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..16a41799c4
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/AndroidManifest.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <!-- This is needed because the android.permission.CAMERA above automatically
+ adds a requirements for camera hardware and we don't want add those restrictions -->
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.camera.autofocus"
+ android:required="false" />
+
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ tools:targetApi="s">
+ <activity
+ android:name="org.mozilla.samples.fxa.MainActivity"
+ android:launchMode="singleTask"
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data
+ android:host="*"
+ android:scheme="fxaclient" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/LoginFragment.kt b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/LoginFragment.kt
new file mode 100644
index 0000000000..2db927dfa8
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/LoginFragment.kt
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.fxa
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.fragment.app.Fragment
+
+class LoginFragment : Fragment() {
+
+ private lateinit var authUrl: String
+ private lateinit var redirectUrl: String
+ private var mWebView: WebView? = null
+ private var listener: OnLoginCompleteListener? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ arguments?.let {
+ authUrl = it.getString(AUTH_URL)!!
+ redirectUrl = it.getString(REDIRECT_URL)!!
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val view: View = inflater.inflate(R.layout.fragment_view, container, false)
+ val webView = view.findViewById<WebView>(R.id.webview)
+ // Need JS, cookies and localStorage.
+ webView.settings.domStorageEnabled = true
+ webView.settings.javaScriptEnabled = true
+ CookieManager.getInstance().setAcceptCookie(true)
+
+ webView.webViewClient = object : WebViewClient() {
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ if (url != null && url.startsWith(redirectUrl)) {
+ val uri = Uri.parse(url)
+ val code = uri.getQueryParameter("code")
+ val state = uri.getQueryParameter("state")
+ if (code != null && state != null) {
+ listener?.onLoginComplete(code, state, this@LoginFragment)
+ }
+ }
+
+ super.onPageStarted(view, url, favicon)
+ }
+ }
+ webView.loadUrl(authUrl)
+
+ mWebView?.destroy()
+ mWebView = webView
+
+ return view
+ }
+
+ @Suppress("TooGenericExceptionThrown")
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is OnLoginCompleteListener) {
+ listener = context
+ } else {
+ throw IllegalStateException("$context must implement OnLoginCompleteListener")
+ }
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ listener = null
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mWebView?.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mWebView?.onResume()
+ }
+
+ interface OnLoginCompleteListener {
+ fun onLoginComplete(code: String, state: String, fragment: LoginFragment)
+ }
+
+ companion object {
+ const val AUTH_URL = "authUrl"
+ const val REDIRECT_URL = "redirectUrl"
+
+ fun create(authUrl: String, redirectUrl: String): LoginFragment =
+ LoginFragment().apply {
+ arguments = Bundle().apply {
+ putString(AUTH_URL, authUrl)
+ putString(REDIRECT_URL, redirectUrl)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/MainActivity.kt b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/MainActivity.kt
new file mode 100644
index 0000000000..4103577318
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/MainActivity.kt
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.fxa
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.core.app.ActivityCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import mozilla.appservices.fxaclient.FxaConfig
+import mozilla.appservices.fxaclient.FxaServer
+import mozilla.components.concept.sync.Profile
+import mozilla.components.feature.qr.QrFeature
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.fxa.FirefoxAccount
+import mozilla.components.service.fxa.FxaException
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.rusthttp.RustHttpConfig
+import mozilla.components.support.rustlog.RustLog
+import kotlin.coroutines.CoroutineContext
+
+open class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener, CoroutineScope {
+ private lateinit var account: FirefoxAccount
+ private var scopesWithoutKeys: Set<String> = setOf("profile")
+ private var scopesWithKeys: Set<String> = setOf("profile", "https://identity.mozilla.com/apps/oldsync")
+ private var scopes: Set<String> = scopesWithoutKeys
+
+ private lateinit var qrFeature: QrFeature
+
+ private lateinit var job: Job
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
+ companion object {
+ const val CLIENT_ID = "3c49430b43dfba77"
+ const val CONFIG_URL = "https://accounts.firefox.com"
+ const val REDIRECT_URL = "$CONFIG_URL/oauth/success/3c49430b43dfba77"
+ const val FXA_STATE_PREFS_KEY = "fxaAppState"
+ const val FXA_STATE_KEY = "fxaState"
+ private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ RustLog.disable()
+ RustLog.enable()
+ RustHttpConfig.setClient(lazy { HttpURLConnectionClient() })
+
+ Log.addSink(AndroidLogSink())
+
+ setContentView(R.layout.activity_main)
+ job = Job()
+ account = initAccount()
+
+ qrFeature = QrFeature(
+ this,
+ fragmentManager = supportFragmentManager,
+ onNeedToRequestPermissions = { permissions ->
+ ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
+ },
+ onScanResult = { pairingUrl ->
+ launch {
+ val url = account.beginPairingFlow(pairingUrl, scopes, SampleFxAEntryPoint.HomeMenu)
+ if (url == null) {
+ Log.log(
+ Log.Priority.ERROR,
+ tag = "mozac-samples-fxa",
+ message = "Pairing flow failed for $pairingUrl",
+ )
+ return@launch
+ }
+ openWebView(url.url)
+ }
+ },
+ scanMessage = R.string.pair_instructions_message,
+ )
+
+ lifecycle.addObserver(qrFeature)
+
+ findViewById<View>(R.id.buttonCustomTabs).setOnClickListener {
+ launch {
+ account.beginOAuthFlow(scopes, SampleFxAEntryPoint.HomeMenu)?.let {
+ openTab(it.url)
+ }
+ }
+ }
+
+ findViewById<View>(R.id.buttonWebView).setOnClickListener {
+ launch {
+ account.beginOAuthFlow(scopes, SampleFxAEntryPoint.HomeMenu)?.let {
+ openWebView(it.url)
+ }
+ }
+ }
+
+ findViewById<View>(R.id.buttonPair).setOnClickListener {
+ qrFeature.scan()
+ }
+
+ findViewById<View>(R.id.buttonLogout).setOnClickListener {
+ getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).edit().putString(FXA_STATE_KEY, "").apply()
+ val txtView: TextView = findViewById(R.id.txtView)
+ txtView.text = getString(R.string.logged_out)
+ }
+
+ findViewById<CheckBox>(R.id.checkboxKeys).setOnCheckedChangeListener { _, isChecked ->
+ scopes = if (isChecked) scopesWithKeys else scopesWithoutKeys
+ }
+ }
+
+ private fun initAccount(): FirefoxAccount {
+ getAuthenticatedAccount()?.let {
+ launch {
+ it.getProfile(true)?.let { profile ->
+ displayProfile(profile)
+ }
+ }
+ return it
+ }
+
+ val config = FxaConfig(FxaServer.Custom(CONFIG_URL), CLIENT_ID, REDIRECT_URL)
+ return FirefoxAccount(config)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ account.close()
+ job.cancel()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ val action = intent.action
+ val data = intent.dataString
+
+ if (Intent.ACTION_VIEW == action && data != null) {
+ val url = Uri.parse(data)
+ val code = url.getQueryParameter("code")!!
+ val state = url.getQueryParameter("state")!!
+ displayAndPersistProfile(code, state)
+ }
+ }
+
+ override fun onLoginComplete(code: String, state: String, fragment: LoginFragment) {
+ displayAndPersistProfile(code, state)
+ supportFragmentManager.popBackStack()
+ }
+
+ private fun getAuthenticatedAccount(): FirefoxAccount? {
+ val savedJSON = getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE).getString(FXA_STATE_KEY, "")
+ return savedJSON?.let {
+ try {
+ FirefoxAccount.fromJSONString(it, null)
+ } catch (e: FxaException) {
+ null
+ }
+ }
+ }
+
+ private fun openTab(url: String) {
+ val customTabsIntent = CustomTabsIntent.Builder()
+ .setShareState(CustomTabsIntent.SHARE_STATE_ON)
+ .setShowTitle(true)
+ .build()
+
+ customTabsIntent.intent.data = Uri.parse(url)
+ customTabsIntent.launchUrl(this@MainActivity, Uri.parse(url))
+ }
+
+ private fun openWebView(url: String) {
+ supportFragmentManager.beginTransaction().apply {
+ replace(R.id.container, LoginFragment.create(url, REDIRECT_URL))
+ addToBackStack(null)
+ commit()
+ }
+ }
+
+ private fun displayAndPersistProfile(code: String, state: String) {
+ launch {
+ account.completeOAuthFlow(code, state)
+ account.getProfile()?.let {
+ displayProfile(it)
+ }
+ account.toJSONString().let {
+ getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE)
+ .edit().putString(FXA_STATE_KEY, it).apply()
+ }
+ }
+ }
+
+ private fun displayProfile(profile: Profile) {
+ val txtView: TextView = findViewById(R.id.txtView)
+ txtView.text = getString(R.string.signed_in, "${profile.displayName ?: ""} ${profile.email}")
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
+ when (requestCode) {
+ REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.onPermissionsResult(permissions, grantResults)
+ else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+ }
+
+ override fun onBackPressed() {
+ if (!qrFeature.onBackPressed()) {
+ onBackPressedDispatcher.onBackPressed()
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/SampleFxAEntryPoint.kt b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/SampleFxAEntryPoint.kt
new file mode 100644
index 0000000000..2e587ee04e
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/java/org/mozilla/samples/fxa/SampleFxAEntryPoint.kt
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.fxa
+
+import mozilla.components.concept.sync.FxAEntryPoint
+
+/**
+ * An implementation of [FxAEntryPoint] for the sample application.
+ */
+enum class SampleFxAEntryPoint(override val entryName: String) : FxAEntryPoint {
+ HomeMenu("home-menu"),
+}
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..d3bdad5ca7
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/activity_main.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:id="@+id/container"
+ tools:context="org.mozilla.samples.fxa.MainActivity">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:orientation="vertical"
+ android:id="@+id/buttonList"
+ tools:context="org.mozilla.samples.fxa.MainActivity">
+
+ <Button
+ android:id="@+id/buttonCustomTabs"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sign_in_customtabs"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/buttonWebView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sign_in_webview"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/buttonPair"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sign_in_pair"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/buttonLogout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/log_out"
+ android:textAlignment="center" />
+ </LinearLayout>
+
+ <CheckBox
+ android:id="@+id/checkboxKeys"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/buttonList"
+ android:text="@string/wants_keys" />
+
+ <TextView
+ android:id="@+id/txtView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/checkboxKeys"
+ android:layout_gravity="center"
+ android:text="" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/scanresult"
+ android:textColor="#000"
+ android:layout_marginTop="10dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text=""/>
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/fragment_view.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/fragment_view.xml
new file mode 100644
index 0000000000..44536f4f68
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/layout/fragment_view.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<WebView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".LoginFragment" />
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..e2edeb6cbe
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..0c12478a8e
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..abdaf95771
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..0c8508a62b
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..ba2b17573d
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/values/strings.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..987e1042d1
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">FxA Android Demo</string>
+ <string name="sign_in_webview">FxA sign in: webview</string>
+ <string name="sign_in_customtabs">FxA sign in: custom tab</string>
+ <string name="sign_in_pair">FxA sign in: pair</string>
+ <string name="signed_in">Signed in: %1$s</string>
+ <string name="log_out">FxA Log Out</string>
+ <string name="logged_out">Logged out!</string>
+ <string name="wants_keys">Request sync scope?</string>
+ <string name="pair_instructions_message"><![CDATA[Scan the QR code shown at <b>firefox.com/pair</b>]]></string>
+</resources>
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/firefox-accounts/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/glean/README.md b/mobile/android/android-components/samples/glean/README.md
new file mode 100644
index 0000000000..eb2f5abbc1
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/README.md
@@ -0,0 +1,20 @@
+# [Android Components](../../README.md) > Samples > Glean
+
+![](src/main/res/mipmap-xhdpi/ic_launcher.png)
+
+A simple app showcasing Glean, the mobile telemetry SDK & the Experiments library.
+
+## Concepts
+
+The main concepts shown in the sample app are:
+
+* Usage of the `metrics.yaml` file.
+* Integration between Glean and the application's build process to generate the specific metrics API.
+* Usage of the generated specific metrics API.
+* Integration of the Nimbus experimentation library.
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/samples/glean/build.gradle b/mobile/android/android-components/samples/glean/build.gradle
new file mode 100644
index 0000000000..b4922d6b53
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/build.gradle
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+buildscript {
+ repositories {
+ gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
+ maven {
+ url repository
+ if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
+ allowInsecureProtocol = true
+ }
+ }
+ }
+
+ dependencies {
+ classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}"
+ }
+ }
+}
+
+plugins {
+ id "com.jetbrains.python.envs" version "$python_envs_plugin"
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.glean"
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ applicationIdSuffix ".debug"
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+
+ namespace 'org.mozilla.samples.glean'
+}
+
+dependencies {
+ implementation project(':service-glean')
+ implementation project(':service-nimbus')
+ implementation project(':support-base')
+ implementation project(':support-rusthttp')
+ implementation project(':support-rustlog')
+ implementation project(':lib-fetch-httpurlconnection')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_browser
+
+ implementation project(':samples-glean-library')
+
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+ androidTestImplementation ComponentsDependencies.androidx_test_junit
+ androidTestImplementation ComponentsDependencies.androidx_test_uiautomator
+ androidTestImplementation ComponentsDependencies.androidx_espresso_core
+ androidTestImplementation ComponentsDependencies.androidx_work_testing
+ androidTestImplementation ComponentsDependencies.testing_mockwebserver
+}
+
+apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
diff --git a/mobile/android/android-components/samples/glean/metrics.yaml b/mobile/android/android-components/samples/glean/metrics.yaml
new file mode 100644
index 0000000000..5759c626b2
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/metrics.yaml
@@ -0,0 +1,116 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file defines the metrics that are recorded by glean telemetry. They are
+# automatically converted to Kotlin code at build time using the `glean_parser`
+# PyPI package.
+---
+
+$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0
+
+browser.engagement:
+ click:
+ type: event
+ description: >
+ Just testing events
+ bugs:
+ - https://bugzilla.mozilla.org/123456789
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@example.com
+ extra_keys:
+ key1:
+ type: string
+ description: "This is key one"
+ key2:
+ type: string
+ description: "This is key two"
+ expires: 2100-01-01
+
+ event_no_keys:
+ type: event
+ description: >
+ Just testing events without keys
+ bugs:
+ - https://bugzilla.mozilla.org/123456789
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@example.com
+ expires: 2100-01-01
+
+basic:
+ os:
+ type: string
+ description: >
+ The name of the os
+ bugs:
+ - https://bugzilla.mozilla.org/123456789
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@example.com
+ expires: 2100-01-01
+
+test:
+ string_list:
+ type: string_list
+ description: >
+ Testing StringList ping
+ send_in_pings:
+ - test-string-list
+ lifetime: user
+ bugs:
+ - https://bugzilla.mozilla.org/123456789
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@example.com
+ expires: 2100-01-01
+
+ counter:
+ type: counter
+ description: >
+ Testing counter
+ send_in_pings:
+ - test-string-list
+ lifetime: user
+ bugs:
+ - https://bugzilla.mozilla.org/123456789
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@example.com
+ expires: 2100-01-01
+
+ timespan:
+ type: timespan
+ description: >
+ Testing a timespan
+ time_unit: microsecond
+ lifetime: application
+ bugs:
+ - https://bugzilla.mozilla.org/1508948
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@example.com
+ expires: 2100-01-01
+
+custom:
+ counter:
+ type: counter
+ description: >
+ A custom counter that goes on a custom ping
+ lifetime: ping
+ send_in_pings:
+ - sample
+ bugs:
+ - https://bugzilla.mozilla.org/1547330
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@test-only.com
+ expires: 2100-01-01
diff --git a/mobile/android/android-components/samples/glean/pings.yaml b/mobile/android/android-components/samples/glean/pings.yaml
new file mode 100644
index 0000000000..7f998f7748
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/pings.yaml
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file defines the built-in pings that are recorded by glean telemetry.
+# They are # automatically converted to Kotlin code at build time
+# using the `glean_parser` PyPI package.
+---
+
+$schema: moz://mozilla.org/schemas/glean/pings/2-0-0
+
+sample:
+ description: |
+ A sample custom ping.
+ include_client_id: true
+ bugs:
+ - https://bugzilla.mozilla.org/123456789
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@example.com
diff --git a/mobile/android/android-components/samples/glean/proguard-rules.pro b/mobile/android/android-components/samples/glean/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/README.md b/mobile/android/android-components/samples/glean/samples-glean-library/README.md
new file mode 100644
index 0000000000..97a4de14b6
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/samples-glean-library/README.md
@@ -0,0 +1,5 @@
+samples_glean_library is a toy library that uses glean to record metrics. It
+exists simply to show how libraries that are neither the application or glean
+itself can record metrics, and those metrics will be stored and sent as part of
+the [main pings](../../../components/service/glean/docs/pings.md) that glean
+provides.
diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle b/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle
new file mode 100644
index 0000000000..68df279fab
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/samples-glean-library/build.gradle
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+buildscript {
+ repositories {
+ gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
+ maven {
+ url repository
+ if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
+ allowInsecureProtocol = true
+ }
+ }
+ }
+
+ dependencies {
+ classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}"
+ }
+ }
+}
+
+plugins {
+ id "com.jetbrains.python.envs" version "$python_envs_plugin"
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ namespace 'mozilla.samples.glean.library'
+}
+
+dependencies {
+ implementation project(':service-glean')
+}
+
+apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
+
diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml b/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml
new file mode 100644
index 0000000000..c999c586cf
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/samples-glean-library/metrics.yaml
@@ -0,0 +1,27 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file defines the built-in pings that are recorded by glean telemetry.
+# They are # automatically converted to Kotlin code at build time
+# using the `glean_parser` PyPI package.
+---
+
+$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0
+
+sample_metrics:
+ test:
+ type: counter
+ description: >
+ A simple counter defined in a third-party library
+ # To see the result immediately when pressing the "send ping" button in the
+ # interface, uncomment the following:
+ # send_in_pings:
+ # - baseline
+ bugs:
+ - https://bugzilla.mozilla.org/123456789
+ data_reviews:
+ - N/A
+ notification_emails:
+ - CHANGE-ME@example.com
+ expires: 2100-01-01
diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest />
diff --git a/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt
new file mode 100644
index 0000000000..d9d635ddd4
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/samples-glean-library/src/main/java/org/mozilla/samples/glean/library/SamplesGleanLibrary.kt
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.glean.library
+
+import mozilla.components.service.glean.Glean
+import mozilla.samples.glean.library.GleanMetrics.SampleMetrics
+
+/**
+ * These are just simple functions to test calling the Glean API
+ * from a third-party library.
+ */
+object SamplesGleanLibrary {
+ /**
+ * Record to a metric defined in *this* library's metrics.yaml file.
+ */
+ fun recordMetric() {
+ SampleMetrics.test.add()
+ }
+
+ /**
+ * Notate an active experiment.
+ */
+ fun recordExperiment() {
+ Glean.setExperimentActive(
+ "third_party_library",
+ "enabled",
+ )
+ }
+}
diff --git a/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt
new file mode 100644
index 0000000000..1a145e61c6
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/MainActivityTest.kt
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.glean
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.samples.glean.GleanMetrics.Test as GleanTestMetrics
+
+class MainActivityTest {
+ @get:Rule
+ val activityRule: ActivityScenarioRule<MainActivity> = ActivityScenarioRule(MainActivity::class.java)
+
+ @Test
+ fun checkGleanClickData() {
+ // We don't reset the storage in this test as the GleanTestRule does not
+ // work nicely in instrumented test. Just check the current value, increment
+ // by one and make it the expected value.
+ val expectedValue = if (GleanTestMetrics.counter.testGetValue() != null) {
+ GleanTestMetrics.counter.testGetValue()!! + 1
+ } else {
+ 1
+ }
+
+ // Simulate a click on the button.
+ onView(withId(R.id.buttonGenerateData)).perform(click())
+
+ // Use the Glean testing API to check if the expected data was recorded.
+ assertNotNull(GleanTestMetrics.counter.testGetValue())
+ assertEquals(expectedValue, GleanTestMetrics.counter.testGetValue())
+ }
+}
diff --git a/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt
new file mode 100644
index 0000000000..61057039d0
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/androidTest/java/org/mozilla/samples/glean/pings/BaselinePingTest.kt
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.glean.pings
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import mozilla.components.service.glean.testing.GleanTestLocalServer
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
+import org.json.JSONObject
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.samples.glean.MainActivity
+import java.io.BufferedReader
+import java.io.ByteArrayInputStream
+import java.util.concurrent.TimeUnit
+import java.util.zip.GZIPInputStream
+
+/**
+ * Decompress the GZIP returned by the glean-core layer.
+ *
+ * @param data the gzipped [ByteArray] to decompress
+ * @return a [String] containing the uncompressed data.
+ */
+fun decompressGZIP(data: ByteArray): String {
+ return GZIPInputStream(ByteArrayInputStream(data)).bufferedReader().use(BufferedReader::readText)
+}
+
+/**
+ * Convenience method to get the body of a request as a String.
+ * The UTF8 representation of the request body will be returned.
+ * If the request body is gzipped, it will be decompressed first.
+ *
+ * @return a [String] containing the body of the request.
+ */
+fun RecordedRequest.getPlainBody(): String {
+ return if (this.getHeader("Content-Encoding") == "gzip") {
+ val bodyInBytes = this.body.readByteArray()
+ decompressGZIP(bodyInBytes)
+ } else {
+ this.body.readUtf8()
+ }
+}
+
+class BaselinePingTest {
+ private val server = createMockWebServer()
+
+ @get:Rule
+ val activityRule: ActivityScenarioRule<MainActivity> = ActivityScenarioRule(MainActivity::class.java)
+
+ @get:Rule
+ val gleanRule = GleanTestLocalServer(context, server.port)
+
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ /**
+ * Create a mock webserver that accepts all requests and replies with "OK".
+ * @return a [MockWebServer] instance
+ */
+ private fun createMockWebServer(): MockWebServer {
+ val server = MockWebServer()
+ server.dispatcher =
+ object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse {
+ return MockResponse().setBody("OK")
+ }
+ }
+
+ return server
+ }
+
+ private fun waitForPingContent(
+ pingName: String,
+ pingReason: String?,
+ maxAttempts: Int = 3,
+ ): JSONObject? {
+ var attempts = 0
+ do {
+ attempts += 1
+ val request = server.takeRequest(20L, TimeUnit.SECONDS)
+ val docType = request?.path?.split("/")?.get(3)
+ if (pingName == docType) {
+ val parsedPayload = JSONObject(request.getPlainBody())
+ if (pingReason == null) {
+ return parsedPayload
+ }
+
+ // If we requested a specific ping reason, look for it.
+ val reason = parsedPayload.getJSONObject("ping_info").getString("reason")
+ if (reason == pingReason) {
+ return parsedPayload
+ }
+ }
+ } while (attempts < maxAttempts)
+
+ return null
+ }
+
+ @Test
+ fun validateBaselinePing() {
+ // Wait for the app to be idle/ready.
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.waitForIdle()
+
+ // Wait for 1 second: this should guarantee we have some valid duration in the
+ // ping.
+ Thread.sleep(1000)
+
+ // Move it to background.
+ device.pressHome()
+
+ // Validate the received data.
+ val baselinePing = waitForPingContent("baseline", "inactive")!!
+ val metrics = baselinePing.getJSONObject("metrics")
+
+ // Make sure we have a 'duration' field with a reasonable value: it should be >= 1, since
+ // we slept for 1000ms.
+ val timespans = metrics.getJSONObject("timespan")
+ assertTrue(timespans.getJSONObject("glean.baseline.duration").getLong("value") >= 1L)
+
+ // Make sure there's no errors.
+ val errors = metrics.optJSONObject("labeled_counter")?.keys()
+ errors?.forEach {
+ assertFalse(it.startsWith("glean.error."))
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..0fb5a873cb
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage" />
+
+ <!-- Note: the usesCleartextTraffic is only required for making instrumentation
+ tests work on API 23+. Also note that this requires tools:ignore="UnusedAttribute"
+ for stopping the linter from complaining on API 21 <= x < 23. -->
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+ android:usesCleartextTraffic="true"
+ tools:ignore="DataExtractionRules,UnusedAttribute"
+ android:name=".GleanApplication"
+ android:dataExtractionRules="@xml/data_extraction_rules">
+ <activity android:name="org.mozilla.samples.glean.MainActivity"
+ android:windowSoftInputMode="adjustResize"
+ android:launchMode="singleTask"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt
new file mode 100644
index 0000000000..bf0ff5f641
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/GleanApplication.kt
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.glean
+
+import android.app.Application
+import android.content.Context
+import android.net.Uri
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.glean.Glean
+import mozilla.components.service.glean.config.Configuration
+import mozilla.components.service.glean.net.ConceptFetchHttpUploader
+import mozilla.components.service.nimbus.Nimbus
+import mozilla.components.service.nimbus.NimbusApi
+import mozilla.components.service.nimbus.NimbusAppInfo
+import mozilla.components.service.nimbus.NimbusServerSettings
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.rusthttp.RustHttpConfig
+import mozilla.components.support.rustlog.RustLog
+import org.mozilla.samples.glean.GleanMetrics.Basic
+import org.mozilla.samples.glean.GleanMetrics.Custom
+import org.mozilla.samples.glean.GleanMetrics.GleanBuildInfo
+import org.mozilla.samples.glean.GleanMetrics.Pings
+import org.mozilla.samples.glean.GleanMetrics.Test
+
+class GleanApplication : Application() {
+
+ companion object {
+ lateinit var nimbus: NimbusApi
+
+ const val SAMPLE_GLEAN_PREFERENCES = "sample_glean_preferences"
+ const val PREF_IS_FIRST_RUN = "isFirstRun"
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ val settings = applicationContext.getSharedPreferences(SAMPLE_GLEAN_PREFERENCES, Context.MODE_PRIVATE)
+ val isFirstRun = settings.getBoolean(PREF_IS_FIRST_RUN, true)
+
+ // We want the log messages of all builds to go to Android logcat
+
+ Log.addSink(AndroidLogSink())
+
+ // Register the sample application's custom pings.
+ Glean.registerPings(Pings)
+
+ // Initialize the Glean library. Ideally, this is the first thing that
+ // must be done right after enabling logging.
+ val client by lazy { HttpURLConnectionClient() }
+ val httpClient = ConceptFetchHttpUploader.fromClient(client)
+ val config = Configuration(httpClient = httpClient)
+ Glean.initialize(
+ applicationContext,
+ uploadEnabled = true,
+ configuration = config,
+ buildInfo = GleanBuildInfo.buildInfo,
+ )
+
+ /** Begin Nimbus component specific code. Note: this is not relevant to Glean */
+ initNimbus(isFirstRun)
+ /** End Nimbus specific code. */
+
+ Test.timespan.start()
+
+ Custom.counter.add()
+
+ // Set a sample value for a metric.
+ Basic.os.set("Android")
+
+ settings
+ .edit()
+ .putBoolean(PREF_IS_FIRST_RUN, false)
+ .apply()
+ }
+
+ /**
+ * Initialize the Nimbus experiments library. This is only relevant to the Nimbus library, aside
+ * from recording the experiment in Glean.
+ */
+ private fun initNimbus(isFirstRun: Boolean) {
+ RustLog.enable()
+ RustHttpConfig.setClient(lazy { HttpURLConnectionClient() })
+ val url = Uri.parse(getString(R.string.nimbus_default_endpoint))
+ val appInfo = NimbusAppInfo(
+ appName = "samples-glean",
+ channel = "samples",
+ )
+ nimbus = Nimbus(
+ context = this,
+ appInfo = appInfo,
+ server = NimbusServerSettings(url),
+ ).also { nimbus ->
+ if (isFirstRun) {
+ // This file is bundled with the app, but derived from the server at build time.
+ // We'll use it now, on first run.
+ nimbus.setExperimentsLocally(R.raw.initial_experiments)
+ }
+ // Apply the experiments downloaded on last run, but on first run, it will
+ // use the contents of `R.raw.initial_experiments`.
+ nimbus.applyPendingExperiments()
+
+ // In a real application, we might want to fetchExperiments() here.
+ //
+ // We won't do that in this app because:
+ // * the server's experiments will overwrite the current ones
+ // * it's not clear that the server will have a `test-color` experiment
+ // by the time you run this
+ // * an update experiments button is in `MainActivity`
+ //
+ // nimbus.fetchExperiments()
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt
new file mode 100644
index 0000000000..8020022d87
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/java/org/mozilla/samples/glean/MainActivity.kt
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.glean
+
+import android.graphics.Color
+import android.os.Bundle
+import androidx.annotation.MainThread
+import androidx.appcompat.app.AppCompatActivity
+import org.mozilla.experiments.nimbus.EnrolledExperiment
+import org.mozilla.experiments.nimbus.NimbusInterface
+import org.mozilla.samples.glean.GleanMetrics.BrowserEngagement
+import org.mozilla.samples.glean.GleanMetrics.Test
+import org.mozilla.samples.glean.databinding.ActivityMainBinding
+import org.mozilla.samples.glean.library.SamplesGleanLibrary
+
+/**
+ * Main Activity of the glean-sample-app
+ */
+open class MainActivity : AppCompatActivity(), NimbusInterface.Observer {
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+
+ setContentView(binding.root)
+
+ // Generate an event when user clicks on the button.
+ binding.buttonGenerateData.setOnClickListener {
+ // These first two actions, adding to the string list and incrementing the counter are
+ // tied to a user lifetime metric which is persistent from launch to launch.
+
+ // Adds the EditText's text content as a new string in the string list metric from the
+ // metrics.yaml file.
+ Test.stringList.add(binding.etStringListInput.text.toString())
+ // Clear current text to help indicate something happened
+ binding.etStringListInput.setText("")
+
+ // Increments the test_counter metric from the metrics.yaml file.
+ Test.counter.add()
+
+ // This is referencing the event ping named 'click' from the metrics.yaml file. In
+ // order to illustrate adding extra information to the event, it is also adding to the
+ // 'extras' field a dictionary of values. Note that the dictionary keys must be
+ // declared in the metrics.yaml file under the 'extra_keys' section of an event metric.
+ BrowserEngagement.click.record(
+ BrowserEngagement.ClickExtra(
+ key1 = "extra_value_1",
+ key2 = "extra_value_2",
+ ),
+ )
+ }
+
+ Test.timespan.stop()
+
+ // Update some metrics from a third-party library
+ SamplesGleanLibrary.recordMetric()
+ SamplesGleanLibrary.recordExperiment()
+
+ // The following is not relevant to the Glean SDK, but to the Nimbus experiments library
+ setupNimbusExperiments()
+ }
+
+ /** Begin Nimbus component specific functions */
+
+ /**
+ * This sets up the update receiver and sets the onClickListener for the "Update Experiments"
+ * button. This is not relevant to the Glean SDK, but to the Nimbus experiments library.
+ */
+ private fun setupNimbusExperiments() {
+ // Register the main activity as a Nimbus observer
+ GleanApplication.nimbus.register(this)
+
+ // Attach the click listener for the experiments button to the updateExperiments function
+ binding.buttonCheckExperiments.setOnClickListener {
+ // Once the experiments are fetched, then the activity's (a Nimbus observer)
+ // `onExperimentFetched()` method is called.
+ GleanApplication.nimbus.fetchExperiments()
+ }
+
+ configureButton()
+ }
+
+ /**
+ * Event to indicate that the experiments have been fetched from the endpoint
+ */
+ override fun onExperimentsFetched() {
+ println("Experiments fetched")
+ GleanApplication.nimbus.applyPendingExperiments()
+ }
+
+ /**
+ * Event to indicate that the experiment enrollments have been applied. Developers normally
+ * shouldn't care to observe this and rather rely on `onExperimentsFetched` and `withExperiment`
+ */
+ override fun onUpdatesApplied(updated: List<EnrolledExperiment>) {
+ runOnUiThread {
+ configureButton()
+ }
+ }
+
+ @MainThread
+ private fun configureButton() {
+ val nimbus = GleanApplication.nimbus
+ val branch = nimbus.getExperimentBranch("sample-experiment-feature")
+
+ val color = when (branch) {
+ "blue" -> Color.BLUE
+ "red" -> Color.RED
+ "control" -> Color.DKGRAY
+ else -> Color.WHITE
+ }
+ val text = when (branch) {
+ null -> getString(R.string.experiment_not_active)
+ else -> getString(R.string.experiment_active_branch, branch)
+ }
+
+ binding.textViewExperimentStatus.setBackgroundColor(color)
+ binding.textViewExperimentStatus.text = text
+ }
+
+ /** End Nimbus component functions */
+}
diff --git a/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..87e3025d0f
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/layout/activity_main.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/buttonList"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp"
+ tools:context="org.mozilla.samples.glean.MainActivity">
+
+ <!-- This is a dummy linear layout to capture focus and prevent the keyboard from popping
+ when the app first launches. This is a known issue of linear layouts and a common
+ workaround. -->
+ <LinearLayout android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:layout_width="0px"
+ android:layout_height="0px" >
+ <requestFocus />
+ </LinearLayout>
+
+ <EditText
+ android:id="@+id/etStringListInput"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:hint="@string/string_list_input_hint"
+ android:inputType="textPersonName"
+ android:autofillHints=""
+ tools:ignore="UnusedAttribute" />
+
+ <Button
+ android:id="@+id/buttonGenerateData"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:text="@string/generate_data"
+ android:textAlignment="center" />
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:text="@string/counter_metric_info" />
+
+ <Button
+ android:id="@+id/buttonCheckExperiments"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/check_experiments"
+ android:textAlignment="center" />
+
+ <TextView
+ android:id="@+id/textViewExperimentStatus"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/white"
+ android:text="@string/experiment_not_active"
+ android:textStyle="italic" />
+
+ <TextView
+ android:id="@+id/textView3"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/check_experiments_btn_description" />
+
+</LinearLayout>
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..a2f5908281
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..ff10afd6e1
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..dcd3cd8083
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..8ca12fe024
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b824ebdd48
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json b/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json
new file mode 100644
index 0000000000..e4eadb052b
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/raw/initial_experiments.json
@@ -0,0 +1,60 @@
+{
+ "data": [{
+ "slug": "test-color",
+ "endDate": null,
+ "branches": [
+ {
+ "slug": "control",
+ "ratio": 1,
+ "feature": {
+ "featureId": "sample-experiment-feature",
+ "enabled": true,
+ "value": null
+ }
+ },
+ {
+ "slug": "red",
+ "ratio": 1,
+ "feature": {
+ "featureId": "sample-experiment-feature",
+ "enabled": true,
+ "value": null
+ }
+ },
+ {
+ "slug": "blue",
+ "ratio": 1,
+ "feature": {
+ "featureId": "sample-experiment-feature",
+ "enabled": true,
+ "value": null
+ }
+ }
+ ],
+ "featureIds": [
+ "sample-experiment-feature"
+ ],
+ "probeSets": [],
+ "startDate": null,
+ "targeting": "true",
+ "appName": "samples-glean",
+ "appId": "org.mozilla.samples.glean.debug",
+ "channel": "samples",
+ "bucketConfig": {
+ "count": 10000,
+ "start": 0,
+ "total": 10000,
+ "namespace": "test-color-1",
+ "randomizationUnit": "nimbus_id"
+ },
+ "schemaVersion": "1.1.0",
+ "userFacingName": "Button color sample experiment",
+ "referenceBranch": "control",
+ "proposedDuration": null,
+ "isEnrollmentPaused": false,
+ "proposedEnrollment": 7,
+ "userFacingDescription": "A sample experiment to determine the color of a button",
+ "id": "test-color",
+ "last_modified": 1607613885917
+ }]
+}
diff --git a/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml b/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml
new file mode 100644
index 0000000000..1a6c4363ca
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/values/endpoints.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <string name="nimbus_default_endpoint" translatable="false">https://settings.stage.mozaws.net</string>
+</resources>
diff --git a/mobile/android/android-components/samples/glean/src/main/res/values/strings.xml b/mobile/android/android-components/samples/glean/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..ed86142323
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">Glean - Metrics Demo</string>
+ <string name="generate_data">Record Text</string>
+ <string name="string_list_input_hint">Enter some text here</string>
+ <string name="counter_metric_info">
+ Every time you click on the button above, the text is added to the test_string_list metric
+ and a counter metric type called testCounter is incremented. Both of these will be included
+ when the ping is sent. The counter and the string list are utilizing the `user` lifetime
+ and should persist from launch to launch of the app.\n\nAn event metric is also being used
+ to attach a dictionary of values to the extras of the event ping. See MainActivity for
+ where all of this is happening.
+ </string>
+ <string name="experiment_not_active">Experiment not active</string>
+ <string name="experiment_active_branch">Experiment active, branch: %1$s</string>
+ <string name="check_experiments">Check experiments</string>
+ <string name="check_experiments_btn_description">The CHECK EXPERIMENTS button checks the experiments status of the test-colorexperiment and sets the color accordingly.</string>
+</resources>
diff --git a/mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/glean/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.pbxproj b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..dd311b051a
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.pbxproj
@@ -0,0 +1,589 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 227BF22525C2EAD500DB0AB9 /* ios_sampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BF22425C2EAD500DB0AB9 /* ios_sampleApp.swift */; };
+ 227BF22725C2EAD500DB0AB9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BF22625C2EAD500DB0AB9 /* ContentView.swift */; };
+ 227BF22925C2EAD600DB0AB9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 227BF22825C2EAD600DB0AB9 /* Assets.xcassets */; };
+ 227BF22C25C2EAD600DB0AB9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 227BF22B25C2EAD600DB0AB9 /* Preview Assets.xcassets */; };
+ 227BF23725C2EAD600DB0AB9 /* ios_sampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BF23625C2EAD600DB0AB9 /* ios_sampleTests.swift */; };
+ 227BF24225C2EAD600DB0AB9 /* ios_sampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BF24125C2EAD600DB0AB9 /* ios_sampleUITests.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 227BF23325C2EAD600DB0AB9 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 227BF21925C2EAD500DB0AB9 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 227BF22025C2EAD500DB0AB9;
+ remoteInfo = "ios-sample";
+ };
+ 227BF23E25C2EAD600DB0AB9 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 227BF21925C2EAD500DB0AB9 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 227BF22025C2EAD500DB0AB9;
+ remoteInfo = "ios-sample";
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 227BF22125C2EAD500DB0AB9 /* ios-sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ios-sample.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 227BF22425C2EAD500DB0AB9 /* ios_sampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ios_sampleApp.swift; sourceTree = "<group>"; };
+ 227BF22625C2EAD500DB0AB9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+ 227BF22825C2EAD600DB0AB9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 227BF22B25C2EAD600DB0AB9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
+ 227BF22D25C2EAD600DB0AB9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 227BF23225C2EAD600DB0AB9 /* ios-sampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ios-sampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 227BF23625C2EAD600DB0AB9 /* ios_sampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ios_sampleTests.swift; sourceTree = "<group>"; };
+ 227BF23825C2EAD600DB0AB9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 227BF23D25C2EAD600DB0AB9 /* ios-sampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ios-sampleUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 227BF24125C2EAD600DB0AB9 /* ios_sampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ios_sampleUITests.swift; sourceTree = "<group>"; };
+ 227BF24325C2EAD600DB0AB9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 227BF25325C2EBFE00DB0AB9 /* dummy.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = dummy.framework; path = "../../components/multiplatform/lib-dummy/build/xcode-frameworks/dummy.framework"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 227BF21E25C2EAD500DB0AB9 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 227BF22F25C2EAD600DB0AB9 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 227BF23A25C2EAD600DB0AB9 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 227BF21825C2EAD500DB0AB9 = {
+ isa = PBXGroup;
+ children = (
+ 227BF22325C2EAD500DB0AB9 /* ios-sample */,
+ 227BF23525C2EAD600DB0AB9 /* ios-sampleTests */,
+ 227BF24025C2EAD600DB0AB9 /* ios-sampleUITests */,
+ 227BF22225C2EAD500DB0AB9 /* Products */,
+ 227BF25225C2EBFE00DB0AB9 /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 227BF22225C2EAD500DB0AB9 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 227BF22125C2EAD500DB0AB9 /* ios-sample.app */,
+ 227BF23225C2EAD600DB0AB9 /* ios-sampleTests.xctest */,
+ 227BF23D25C2EAD600DB0AB9 /* ios-sampleUITests.xctest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 227BF22325C2EAD500DB0AB9 /* ios-sample */ = {
+ isa = PBXGroup;
+ children = (
+ 227BF22425C2EAD500DB0AB9 /* ios_sampleApp.swift */,
+ 227BF22625C2EAD500DB0AB9 /* ContentView.swift */,
+ 227BF22825C2EAD600DB0AB9 /* Assets.xcassets */,
+ 227BF22D25C2EAD600DB0AB9 /* Info.plist */,
+ 227BF22A25C2EAD600DB0AB9 /* Preview Content */,
+ );
+ path = "ios-sample";
+ sourceTree = "<group>";
+ };
+ 227BF22A25C2EAD600DB0AB9 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 227BF22B25C2EAD600DB0AB9 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "<group>";
+ };
+ 227BF23525C2EAD600DB0AB9 /* ios-sampleTests */ = {
+ isa = PBXGroup;
+ children = (
+ 227BF23625C2EAD600DB0AB9 /* ios_sampleTests.swift */,
+ 227BF23825C2EAD600DB0AB9 /* Info.plist */,
+ );
+ path = "ios-sampleTests";
+ sourceTree = "<group>";
+ };
+ 227BF24025C2EAD600DB0AB9 /* ios-sampleUITests */ = {
+ isa = PBXGroup;
+ children = (
+ 227BF24125C2EAD600DB0AB9 /* ios_sampleUITests.swift */,
+ 227BF24325C2EAD600DB0AB9 /* Info.plist */,
+ );
+ path = "ios-sampleUITests";
+ sourceTree = "<group>";
+ };
+ 227BF25225C2EBFE00DB0AB9 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 227BF25325C2EBFE00DB0AB9 /* dummy.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 227BF22025C2EAD500DB0AB9 /* ios-sample */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 227BF24625C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sample" */;
+ buildPhases = (
+ 227BF21D25C2EAD500DB0AB9 /* Sources */,
+ 227BF21E25C2EAD500DB0AB9 /* Frameworks */,
+ 227BF21F25C2EAD500DB0AB9 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "ios-sample";
+ productName = "ios-sample";
+ productReference = 227BF22125C2EAD500DB0AB9 /* ios-sample.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 227BF23125C2EAD600DB0AB9 /* ios-sampleTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 227BF24925C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sampleTests" */;
+ buildPhases = (
+ 227BF22E25C2EAD600DB0AB9 /* Sources */,
+ 227BF22F25C2EAD600DB0AB9 /* Frameworks */,
+ 227BF23025C2EAD600DB0AB9 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 227BF23425C2EAD600DB0AB9 /* PBXTargetDependency */,
+ );
+ name = "ios-sampleTests";
+ productName = "ios-sampleTests";
+ productReference = 227BF23225C2EAD600DB0AB9 /* ios-sampleTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 227BF23C25C2EAD600DB0AB9 /* ios-sampleUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 227BF24C25C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sampleUITests" */;
+ buildPhases = (
+ 227BF23925C2EAD600DB0AB9 /* Sources */,
+ 227BF23A25C2EAD600DB0AB9 /* Frameworks */,
+ 227BF23B25C2EAD600DB0AB9 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 227BF23F25C2EAD600DB0AB9 /* PBXTargetDependency */,
+ );
+ name = "ios-sampleUITests";
+ productName = "ios-sampleUITests";
+ productReference = 227BF23D25C2EAD600DB0AB9 /* ios-sampleUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 227BF21925C2EAD500DB0AB9 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 1210;
+ LastUpgradeCheck = 1210;
+ TargetAttributes = {
+ 227BF22025C2EAD500DB0AB9 = {
+ CreatedOnToolsVersion = 12.1;
+ };
+ 227BF23125C2EAD600DB0AB9 = {
+ CreatedOnToolsVersion = 12.1;
+ TestTargetID = 227BF22025C2EAD500DB0AB9;
+ };
+ 227BF23C25C2EAD600DB0AB9 = {
+ CreatedOnToolsVersion = 12.1;
+ TestTargetID = 227BF22025C2EAD500DB0AB9;
+ };
+ };
+ };
+ buildConfigurationList = 227BF21C25C2EAD500DB0AB9 /* Build configuration list for PBXProject "ios-sample" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 227BF21825C2EAD500DB0AB9;
+ productRefGroup = 227BF22225C2EAD500DB0AB9 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 227BF22025C2EAD500DB0AB9 /* ios-sample */,
+ 227BF23125C2EAD600DB0AB9 /* ios-sampleTests */,
+ 227BF23C25C2EAD600DB0AB9 /* ios-sampleUITests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 227BF21F25C2EAD500DB0AB9 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 227BF22C25C2EAD600DB0AB9 /* Preview Assets.xcassets in Resources */,
+ 227BF22925C2EAD600DB0AB9 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 227BF23025C2EAD600DB0AB9 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 227BF23B25C2EAD600DB0AB9 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 227BF21D25C2EAD500DB0AB9 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 227BF22725C2EAD500DB0AB9 /* ContentView.swift in Sources */,
+ 227BF22525C2EAD500DB0AB9 /* ios_sampleApp.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 227BF22E25C2EAD600DB0AB9 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 227BF23725C2EAD600DB0AB9 /* ios_sampleTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 227BF23925C2EAD600DB0AB9 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 227BF24225C2EAD600DB0AB9 /* ios_sampleUITests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 227BF23425C2EAD600DB0AB9 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 227BF22025C2EAD500DB0AB9 /* ios-sample */;
+ targetProxy = 227BF23325C2EAD600DB0AB9 /* PBXContainerItemProxy */;
+ };
+ 227BF23F25C2EAD600DB0AB9 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 227BF22025C2EAD500DB0AB9 /* ios-sample */;
+ targetProxy = 227BF23E25C2EAD600DB0AB9 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 227BF24425C2EAD600DB0AB9 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.1;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 227BF24525C2EAD600DB0AB9 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.1;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 227BF24725C2EAD600DB0AB9 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"ios-sample/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = "ios-sample/Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sample";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 227BF24825C2EAD600DB0AB9 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"ios-sample/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = "ios-sample/Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sample";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 227BF24A25C2EAD600DB0AB9 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "ios-sampleTests/Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios-sample.app/ios-sample";
+ };
+ name = Debug;
+ };
+ 227BF24B25C2EAD600DB0AB9 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "ios-sampleTests/Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sampleTests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios-sample.app/ios-sample";
+ };
+ name = Release;
+ };
+ 227BF24D25C2EAD600DB0AB9 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "ios-sampleUITests/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sampleUITests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = "ios-sample";
+ };
+ name = Debug;
+ };
+ 227BF24E25C2EAD600DB0AB9 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "ios-sampleUITests/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.samples.ios-sampleUITests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = "ios-sample";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 227BF21C25C2EAD500DB0AB9 /* Build configuration list for PBXProject "ios-sample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 227BF24425C2EAD600DB0AB9 /* Debug */,
+ 227BF24525C2EAD600DB0AB9 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 227BF24625C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 227BF24725C2EAD600DB0AB9 /* Debug */,
+ 227BF24825C2EAD600DB0AB9 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 227BF24925C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sampleTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 227BF24A25C2EAD600DB0AB9 /* Debug */,
+ 227BF24B25C2EAD600DB0AB9 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 227BF24C25C2EAD600DB0AB9 /* Build configuration list for PBXNativeTarget "ios-sampleUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 227BF24D25C2EAD600DB0AB9 /* Debug */,
+ 227BF24E25C2EAD600DB0AB9 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 227BF21925C2EAD500DB0AB9 /* Project object */;
+}
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..919434a625
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:">
+ </FileRef>
+</Workspace>
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AccentColor.colorset/Contents.json b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000000..eb87897008
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..9221b9bb1a
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,98 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "83.5x83.5"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/Contents.json b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/ContentView.swift b/mobile/android/android-components/samples/ios-sample/ios-sample/ContentView.swift
new file mode 100644
index 0000000000..91f9fb1266
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample/ContentView.swift
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import SwiftUI
+
+struct ContentView: View {
+ var body: some View {
+ Text("Sample app")
+ .padding()
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView()
+ }
+}
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Info.plist b/mobile/android/android-components/samples/ios-sample/ios-sample/Info.plist
new file mode 100644
index 0000000000..efc211a0c1
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Info.plist
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UIApplicationSceneManifest</key>
+ <dict>
+ <key>UIApplicationSupportsMultipleScenes</key>
+ <true/>
+ </dict>
+ <key>UIApplicationSupportsIndirectInputEvents</key>
+ <true/>
+ <key>UILaunchScreen</key>
+ <dict/>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/Preview Content/Preview Assets.xcassets/Contents.json b/mobile/android/android-components/samples/ios-sample/ios-sample/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sample/ios_sampleApp.swift b/mobile/android/android-components/samples/ios-sample/ios-sample/ios_sampleApp.swift
new file mode 100644
index 0000000000..6143209faa
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sample/ios_sampleApp.swift
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import SwiftUI
+
+@main
+struct ios_sampleApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sampleTests/Info.plist b/mobile/android/android-components/samples/ios-sample/ios-sampleTests/Info.plist
new file mode 100644
index 0000000000..64d65ca495
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sampleTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sampleTests/ios_sampleTests.swift b/mobile/android/android-components/samples/ios-sample/ios-sampleTests/ios_sampleTests.swift
new file mode 100644
index 0000000000..a5bc5eebe0
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sampleTests/ios_sampleTests.swift
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import XCTest
+@testable import ios_sample
+
+class ios_sampleTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ func testExample() throws {
+ // This is an example of a functional test case.
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
+ }
+
+ func testPerformanceExample() throws {
+ // This is an example of a performance test case.
+ self.measure {
+ // Put the code you want to measure the time of here.
+ }
+ }
+
+}
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/Info.plist b/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/Info.plist
new file mode 100644
index 0000000000..64d65ca495
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/ios_sampleUITests.swift b/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/ios_sampleUITests.swift
new file mode 100644
index 0000000000..06147c6596
--- /dev/null
+++ b/mobile/android/android-components/samples/ios-sample/ios-sampleUITests/ios_sampleUITests.swift
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import XCTest
+
+class ios_sampleUITests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+
+ // In UI tests it is usually best to stop immediately when a failure occurs.
+ continueAfterFailure = false
+
+ // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ func testExample() throws {
+ // UI tests must launch the application that they test.
+ let app = XCUIApplication()
+ app.launch()
+
+ // Use recording to get started writing UI tests.
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
+ }
+
+ func testLaunchPerformance() throws {
+ if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
+ // This measures how long it takes to launch your application.
+ measure(metrics: [XCTApplicationLaunchMetric()]) {
+ XCUIApplication().launch()
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/sync-logins/README.md b/mobile/android/android-components/samples/sync-logins/README.md
new file mode 100644
index 0000000000..c3784c15fe
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/README.md
@@ -0,0 +1,73 @@
+# [Android Components](../../README.md) > Samples > Firefox Sync - Logins
+
+![](src/main/res/mipmap-xhdpi/ic_launcher.png)
+
+A simple app showcasing the service-sync-logins component.
+
+## Concepts
+
+The main concepts shown in the sample app are:
+
+* Usage of the asynchronous result type `SyncResult`.
+* Login to Firefox Accounts that provides the necessary information to fetch the Logins from Firefox Sync.
+* Getting the list of Logins from Firefox Sync.
+
+## `SyncResult` usage
+
+`SyncResult` represents a chainable asynchronous result, and is used as a convenient method of running potentially long-running tasks (eg. network requests, crypto operations) on threads outside of the UI thread.
+
+A value or exception can be wrapped in an SyncResult:
+
+```kotlin
+val syncValue = SyncResult.fromValue(42)
+val syncException = SyncResult.fromException(Exception("Something went wrong"))
+```
+
+One can attach `OnValueListener`s or `OnExceptionListener`s to an `SyncResult`. There are a few ways of chaining results in Kotlin:
+
+* Passing the listeners directly via `then`, with object expressions or otherwise:
+
+ ```kotlin
+ SyncResult.fromValue(42).then(object : OnValueListener<Integer, Void> {
+ override fun onValue(value: Integer): SyncResult<Void>? {
+ // handle the value
+ return SyncResult<Void>()
+ }
+ }, object : OnExceptionListener<Void> {
+ override fun onException(exception: Exception): SyncResult<Void>? {
+ // handle the exception
+ return SyncResult<Void>()
+ }
+ })
+ ```
+
+ Since Java 6 does not support simple lambda syntax, this is one of the main ways to chain `SyncResult`s in Java.
+
+* Passing lambdas via `then`:
+
+ ```kotlin
+ SyncResult.fromValue(42).then({ value: Int -> // valueListener
+ // handle the value
+ return SyncResult<Void>()
+ }, { exception: Exception ->
+ // handle the exception
+ return SyncResult<Void>()
+ })
+ ```
+
+* Completing a chain via `whenComplete`:
+
+ ```kotlin
+ SyncResult.fromValue(42).whenComplete { value: Integer ->
+ // handle the value
+ }
+ ```
+
+ Since `whenComplete` implies that the chain of promises has come to an end, there is no need to return another SyncResult at the end.
+
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/samples/sync-logins/build.gradle b/mobile/android/android-components/samples/sync-logins/build.gradle
new file mode 100644
index 0000000000..fb654517f0
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/build.gradle
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.sync.logins"
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'x86', 'arm64-v8a', 'armeabi-v7a'
+ }
+ }
+
+ namespace 'org.mozilla.samples.sync.logins'
+}
+
+dependencies {
+ implementation project(':concept-storage')
+ implementation project(':service-firefox-accounts')
+ implementation project(':service-sync-logins')
+ implementation project(':support-rustlog')
+ implementation project(':support-rusthttp')
+ implementation project(':lib-dataprotect')
+ implementation project(':lib-fetch-httpurlconnection')
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_browser
+ implementation ComponentsDependencies.androidx_recyclerview
+}
diff --git a/mobile/android/android-components/samples/sync-logins/gradle.properties b/mobile/android/android-components/samples/sync-logins/gradle.properties
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/gradle.properties
diff --git a/mobile/android/android-components/samples/sync-logins/proguard-rules.pro b/mobile/android/android-components/samples/sync-logins/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/sync-logins/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..d6fd659746
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage" />
+
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ tools:targetApi="s">
+ <activity android:name="org.mozilla.samples.sync.logins.MainActivity"
+ android:windowSoftInputMode="adjustResize"
+ android:launchMode="singleTask"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data
+ android:host="*"
+ android:scheme="fxaclient" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/LoginFragment.kt b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/LoginFragment.kt
new file mode 100644
index 0000000000..b6eecc5f9d
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/LoginFragment.kt
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.sync.logins
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.fragment.app.Fragment
+
+class LoginFragment : Fragment() {
+
+ private lateinit var authUrl: String
+ private lateinit var redirectUrl: String
+ private var mWebView: WebView? = null
+ private var listener: OnLoginCompleteListener? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ arguments?.let {
+ authUrl = it.getString(AUTH_URL)!!
+ redirectUrl = it.getString(REDIRECT_URL)!!
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val view: View = inflater.inflate(R.layout.fragment_view, container, false)
+ val webView = view.findViewById<WebView>(R.id.webview)
+ // Need JS, cookies and localStorage.
+ webView.settings.domStorageEnabled = true
+ webView.settings.javaScriptEnabled = true
+ CookieManager.getInstance().setAcceptCookie(true)
+
+ webView.webViewClient = object : WebViewClient() {
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ if (url != null && url.startsWith(redirectUrl)) {
+ val uri = Uri.parse(url)
+ val code = uri.getQueryParameter("code")
+ val state = uri.getQueryParameter("state")
+ val action = uri.getQueryParameter("action")
+ if (code != null && state != null && action != null) {
+ listener?.onLoginComplete(code, state, action, this@LoginFragment)
+ }
+ }
+
+ super.onPageStarted(view, url, favicon)
+ }
+ }
+ webView.loadUrl(authUrl)
+
+ mWebView?.destroy()
+ mWebView = webView
+
+ return view
+ }
+
+ @Suppress("TooGenericExceptionThrown")
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is OnLoginCompleteListener) {
+ listener = context
+ } else {
+ throw IllegalStateException("$context must implement OnLoginCompleteListener")
+ }
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ listener = null
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mWebView?.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mWebView?.onResume()
+ }
+
+ interface OnLoginCompleteListener {
+ fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment)
+ }
+
+ companion object {
+ const val AUTH_URL = "authUrl"
+ const val REDIRECT_URL = "redirectUrl"
+
+ fun create(authUrl: String, redirectUrl: String): LoginFragment =
+ LoginFragment().apply {
+ arguments = Bundle().apply {
+ putString(AUTH_URL, authUrl)
+ putString(REDIRECT_URL, redirectUrl)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/MainActivity.kt b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/MainActivity.kt
new file mode 100644
index 0000000000..c2f5244f07
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/MainActivity.kt
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.sync.logins
+
+import android.os.Bundle
+import android.view.View
+import android.widget.ArrayAdapter
+import android.widget.ListView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import mozilla.appservices.fxaclient.FxaConfig
+import mozilla.appservices.fxaclient.FxaServer
+import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.fxa.FirefoxAccount
+import mozilla.components.service.fxa.FxaAuthData
+import mozilla.components.service.fxa.PeriodicSyncConfig
+import mozilla.components.service.fxa.SyncConfig
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
+import mozilla.components.service.fxa.sync.SyncReason
+import mozilla.components.service.fxa.sync.SyncStatusObserver
+import mozilla.components.service.fxa.toAuthType
+import mozilla.components.service.sync.logins.SyncableLoginsStorage
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.rusthttp.RustHttpConfig
+import mozilla.components.support.rustlog.RustLog
+import kotlin.coroutines.CoroutineContext
+
+const val CLIENT_ID = "3c49430b43dfba77"
+const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID"
+
+class MainActivity : AppCompatActivity(), LoginFragment.OnLoginCompleteListener, CoroutineScope, SyncStatusObserver {
+ private lateinit var keyStorage: SecureAbove22Preferences
+
+ private val loginsStorage = lazy {
+ SyncableLoginsStorage(this, lazy { keyStorage })
+ }
+
+ private lateinit var listView: ListView
+ private lateinit var adapter: ArrayAdapter<String>
+ private lateinit var activityContext: MainActivity
+ private lateinit var account: FirefoxAccount
+ private val accountManager by lazy {
+ FxaAccountManager(
+ applicationContext,
+ FxaConfig(FxaServer.Release, CLIENT_ID, REDIRECT_URL),
+ DeviceConfig("A-C Logins Sync Sample", DeviceType.MOBILE, setOf()),
+ SyncConfig(setOf(SyncEngine.Passwords), PeriodicSyncConfig()),
+ )
+ }
+
+ private lateinit var job: Job
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ RustLog.enable()
+ RustHttpConfig.setClient(lazy { HttpURLConnectionClient() })
+
+ Log.addSink(AndroidLogSink())
+
+ setContentView(R.layout.activity_main)
+ job = Job()
+
+ // Observe sync state changes.
+ accountManager.registerForSyncEvents(observer = this, owner = this, autoPause = true)
+
+ listView = findViewById(R.id.logins_list_view)
+ adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1)
+ listView.adapter = adapter
+ activityContext = this
+
+ keyStorage = SecureAbove22Preferences(this, "secret-data-storage")
+ keyStorage.putString(SyncEngine.Passwords.nativeName, "my-not-so-secret-password")
+
+ accountManager.register(accountObserver, owner = this, autoPause = true)
+
+ launch {
+ // Initializing loginsStorage is an expensive operation, and is thus a deferred function.
+ // In order to avoid race conditions with the sync workers trying to access loginsStorage
+ // that's not fully initialized, we 'await' on loginsStorage initialization before
+ // kicking off the accountManager.
+ GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to loginsStorage)
+
+ accountManager.start()
+ }
+
+ findViewById<View>(R.id.buttonWebView).setOnClickListener {
+ launch {
+ val authUrl = accountManager.beginAuthentication(entrypoint = SampleFxAEntryPoint.HomeMenu)
+ if (authUrl == null) {
+ Toast.makeText(this@MainActivity, "Account auth error", Toast.LENGTH_LONG).show()
+ return@launch
+ }
+ openWebView(authUrl)
+ }
+ }
+ }
+
+ private val accountObserver = object : AccountObserver {
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onLoggedOut() {}
+
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ launch { accountManager.syncNow(SyncReason.User) }
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onProfileUpdated(profile: Profile) {}
+
+ override fun onAuthenticationProblems() {
+ launch {
+ Toast.makeText(this@MainActivity, "Account auth problem", Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ account.close()
+ job.cancel()
+ }
+
+ private fun openWebView(url: String) {
+ supportFragmentManager.beginTransaction().apply {
+ replace(R.id.container, LoginFragment.create(url, REDIRECT_URL))
+ addToBackStack(null)
+ commit()
+ }
+ }
+
+ override fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) {
+ launch {
+ accountManager.finishAuthentication(
+ FxaAuthData(action.toAuthType(), code = code, state = state),
+ )
+ supportFragmentManager.popBackStack()
+ }
+ }
+
+ // SyncManager observable interface:
+ override fun onStarted() {
+ Toast.makeText(this@MainActivity, "Syncing...", Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onIdle() {
+ Toast.makeText(this@MainActivity, "Logins sync success", Toast.LENGTH_SHORT).show()
+
+ launch {
+ val syncedLogins = loginsStorage.value.list()
+ adapter.addAll(syncedLogins.map { "Login: " + it.origin })
+ adapter.notifyDataSetChanged()
+ }
+ }
+
+ override fun onError(error: java.lang.Exception?) {
+ Toast.makeText(
+ this@MainActivity,
+ "Logins sync error ${error?.localizedMessage}",
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+}
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/SampleFxAEntryPoint.kt b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/SampleFxAEntryPoint.kt
new file mode 100644
index 0000000000..773b5ee844
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/java/org/mozilla/samples/sync/logins/SampleFxAEntryPoint.kt
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.sync.logins
+
+import mozilla.components.concept.sync.FxAEntryPoint
+
+/**
+ * An implementation of [FxAEntryPoint] for the sample application.
+ */
+enum class SampleFxAEntryPoint(override val entryName: String) : FxAEntryPoint {
+ HomeMenu("home-menu"),
+}
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..c4add9e840
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/layout/activity_main.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:id="@+id/container"
+ tools:context="org.mozilla.samples.sync.logins.MainActivity">
+
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:orientation="vertical"
+ android:id="@+id/buttonList"
+ tools:context="org.mozilla.samples.sync.logins.MainActivity"
+ tools:ignore="UselessParent">
+
+ <ListView
+ android:id="@+id/logins_list_view"
+ android:layout_width="fill_parent"
+ android:layout_height="150dp"
+ />
+ <Button
+ android:id="@+id/buttonWebView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sign_in_webview"
+ android:textAlignment="center" />
+
+ </LinearLayout>
+</RelativeLayout>
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/layout/fragment_view.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/layout/fragment_view.xml
new file mode 100644
index 0000000000..44536f4f68
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/layout/fragment_view.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<WebView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".LoginFragment" />
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..a2f5908281
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..ff10afd6e1
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..dcd3cd8083
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..8ca12fe024
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b824ebdd48
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/values/strings.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..82cbb84eb1
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">Sync - Logins Demo</string>
+ <string name="sign_in_webview">FxA sign in</string>
+</resources>
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/sync-logins/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/sync-logins/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/sync-logins/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/sync/README.md b/mobile/android/android-components/samples/sync/README.md
new file mode 100644
index 0000000000..1790aaca5b
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/README.md
@@ -0,0 +1,25 @@
+# [Android Components](../../README.md) > Samples > Firefox Sync
+
+![](src/main/res/mipmap-xhdpi/ic_launcher.png)
+
+A simple app showcasing how to use `FxaAccountManager` together with `BackgroundSyncManager` and `browser-storage-sync` components
+to (periodically) synchronize FxA data in a background worker.
+
+## Concepts
+
+This app demonstrates how to synchronize Firefox Account data (bookmarks, history, ...).
+
+This app could be "easily" generalized to synchronize other types of data stores, if another implementation of `concept-storage`
+is used.
+
+Following basic bits of functionality are present:
+
+* Configuring `FxaAccountManager` and `BackgroundSyncManager`
+* Making `Syncable` stores (such as `PlacesHistoryStorage`) available to background workers via `GlobalSyncableStoreProvider`
+* Configuring listeners to monitor account status (logged in, logged out) and sync status (in-progress, finished, querying local data)
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/samples/sync/build.gradle b/mobile/android/android-components/samples/sync/build.gradle
new file mode 100644
index 0000000000..bb99c4220b
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/build.gradle
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.sync"
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'x86', 'arm64-v8a', 'armeabi-v7a'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+
+ namespace 'org.mozilla.samples.sync'
+}
+
+dependencies {
+ implementation project(':concept-storage')
+ implementation project(':concept-toolbar')
+ implementation project(':browser-storage-sync')
+ implementation project(':service-firefox-accounts')
+ implementation project(':service-sync-logins')
+ implementation project(':service-sync-autofill')
+ implementation project(':support-rustlog')
+ implementation project(':support-rusthttp')
+ implementation project(':lib-fetch-httpurlconnection')
+ implementation project(':lib-dataprotect')
+
+ implementation ComponentsDependencies.kotlin_reflect
+ implementation ComponentsDependencies.androidx_fragment
+ implementation ComponentsDependencies.androidx_recyclerview
+}
diff --git a/mobile/android/android-components/samples/sync/gradle.properties b/mobile/android/android-components/samples/sync/gradle.properties
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/gradle.properties
diff --git a/mobile/android/android-components/samples/sync/proguard-rules.pro b/mobile/android/android-components/samples/sync/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..eb03b1d407
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.Light"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ tools:targetApi="s"
+ tools:ignore="DataExtractionRules">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTask"
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data
+ android:host="*"
+ android:scheme="fxaclient" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt
new file mode 100644
index 0000000000..a2fa4c15b6
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceFragment.kt
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.sync
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.sync.Device
+
+/**
+ * A fragment representing a list of Items.
+ * Activities containing this fragment MUST implement the
+ * [DeviceFragment.OnDeviceListInteractionListener] interface.
+ */
+class DeviceFragment : Fragment() {
+
+ private var listenerDevice: OnDeviceListInteractionListener? = null
+
+ private val adapter = DeviceRecyclerViewAdapter(listenerDevice)
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ val view = inflater.inflate(R.layout.fragment_device_list, container, false)
+
+ // Set the adapter
+ if (view is RecyclerView) {
+ with(view) {
+ layoutManager = LinearLayoutManager(context)
+ adapter = this@DeviceFragment.adapter
+ }
+ }
+ return view
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is OnDeviceListInteractionListener) {
+ listenerDevice = context
+ adapter.mListenerDevice = context
+ } else {
+ throw IllegalArgumentException("$context must implement OnDeviceListInteractionListener")
+ }
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ listenerDevice = null
+ }
+
+ /**
+ * Updates the list of devices.
+ */
+ @SuppressLint("NotifyDataSetChanged")
+ fun updateDevices(devices: List<Device>) {
+ adapter.devices.clear()
+ adapter.devices.addAll(devices)
+ adapter.notifyDataSetChanged()
+ }
+
+ /**
+ * This interface must be implemented by activities that contain this
+ * fragment to allow an interaction in this fragment to be communicated
+ * to the activity and potentially other fragments contained in that
+ * activity.
+ *
+ *
+ * See the Android Training lesson
+ * [Communicating with Other Fragments](http://developer.android.com/training/basics/fragments/communicating.html)
+ * for more information.
+ */
+ interface OnDeviceListInteractionListener {
+ fun onDeviceInteraction(item: Device)
+ }
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt
new file mode 100644
index 0000000000..f240b793b9
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/DeviceRecyclerViewAdapter.kt
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.sync
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceType
+import org.mozilla.samples.sync.DeviceFragment.OnDeviceListInteractionListener
+import org.mozilla.samples.sync.databinding.FragmentDeviceBinding
+
+/**
+ * [RecyclerView.Adapter] that can display a [DummyItem] and makes a call to the
+ * specified [OnDeviceListInteractionListener].
+ */
+class DeviceRecyclerViewAdapter(
+ var mListenerDevice: OnDeviceListInteractionListener?,
+) : RecyclerView.Adapter<DeviceRecyclerViewAdapter.ViewHolder>() {
+
+ val devices = mutableListOf<Device>()
+
+ private val mOnClickListener: View.OnClickListener
+
+ init {
+ mOnClickListener = View.OnClickListener { v ->
+ val item = v.tag as Device
+ // Notify the active callbacks interface (the activity, if the fragment is attached to
+ // one) that an item has been selected.
+ mListenerDevice?.onDeviceInteraction(item)
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = FragmentDeviceBinding
+ .inflate(LayoutInflater.from(parent.context), parent, false)
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = devices[position]
+ holder.nameView.text = item.displayName
+ holder.typeView.text = when (item.deviceType) {
+ DeviceType.DESKTOP -> "Desktop"
+ DeviceType.MOBILE -> "Mobile"
+ DeviceType.TABLET -> "Tablet"
+ DeviceType.TV -> "TV"
+ DeviceType.VR -> "VR"
+ DeviceType.UNKNOWN -> "Unknown"
+ }
+
+ with(holder.itemView) {
+ tag = item
+ setOnClickListener(mOnClickListener)
+ }
+ }
+
+ override fun getItemCount(): Int = devices.size
+
+ inner class ViewHolder(binding: FragmentDeviceBinding) : RecyclerView.ViewHolder(binding.root) {
+ val nameView: TextView = binding.deviceName
+ val typeView: TextView = binding.deviceType
+ }
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt
new file mode 100644
index 0000000000..ac3802c0c6
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/LoginFragment.kt
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.sync
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.fragment.app.Fragment
+
+class LoginFragment : Fragment() {
+
+ private lateinit var authUrl: String
+ private lateinit var redirectUrl: String
+ private var mWebView: WebView? = null
+ private var listener: OnLoginCompleteListener? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ arguments?.let {
+ authUrl = it.getString(AUTH_URL)!!
+ redirectUrl = it.getString(REDIRECT_URL)!!
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val view: View = inflater.inflate(R.layout.fragment_view, container, false)
+ val webView = view.findViewById<WebView>(R.id.webview)
+ // Need JS, cookies and localStorage.
+ webView.settings.domStorageEnabled = true
+ webView.settings.javaScriptEnabled = true
+ CookieManager.getInstance().setAcceptCookie(true)
+
+ webView.webViewClient = object : WebViewClient() {
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ if (url != null && url.startsWith(redirectUrl)) {
+ val uri = Uri.parse(url)
+ val code = uri.getQueryParameter("code")
+ val state = uri.getQueryParameter("state")
+ val action = uri.getQueryParameter("action")
+ if (code != null && state != null && action != null) {
+ listener?.onLoginComplete(code, state, action, this@LoginFragment)
+ }
+ }
+
+ super.onPageStarted(view, url, favicon)
+ }
+ }
+ webView.loadUrl(authUrl)
+
+ mWebView?.destroy()
+ mWebView = webView
+
+ return view
+ }
+
+ @Suppress("TooGenericExceptionThrown")
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is OnLoginCompleteListener) {
+ listener = context
+ } else {
+ throw IllegalStateException("$context must implement OnLoginCompleteListener")
+ }
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ listener = null
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mWebView?.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mWebView?.onResume()
+ }
+
+ interface OnLoginCompleteListener {
+ fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment)
+ }
+
+ companion object {
+ const val AUTH_URL = "authUrl"
+ const val REDIRECT_URL = "redirectUrl"
+
+ fun create(authUrl: String, redirectUrl: String): LoginFragment =
+ LoginFragment().apply {
+ arguments = Bundle().apply {
+ putString(AUTH_URL, authUrl)
+ putString(REDIRECT_URL, redirectUrl)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt
new file mode 100644
index 0000000000..06f889d441
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt
@@ -0,0 +1,464 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.sync
+
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.view.View
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.appservices.fxaclient.FxaServer
+import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import mozilla.components.concept.storage.BookmarkNode
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthFlowError
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceCommandIncoming
+import mozilla.components.concept.sync.DeviceCommandOutgoing
+import mozilla.components.concept.sync.DeviceConfig
+import mozilla.components.concept.sync.DeviceConstellationObserver
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.lib.dataprotect.SecureAbove22Preferences
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.fxa.FxaAuthData
+import mozilla.components.service.fxa.PeriodicSyncConfig
+import mozilla.components.service.fxa.ServerConfig
+import mozilla.components.service.fxa.SyncConfig
+import mozilla.components.service.fxa.SyncEngine
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
+import mozilla.components.service.fxa.sync.SyncReason
+import mozilla.components.service.fxa.sync.SyncStatusObserver
+import mozilla.components.service.fxa.toAuthType
+import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage
+import mozilla.components.service.sync.logins.SyncableLoginsStorage
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.rusthttp.RustHttpConfig
+import mozilla.components.support.rustlog.RustLog
+import org.mozilla.samples.sync.databinding.ActivityMainBinding
+import java.lang.Exception
+import kotlin.coroutines.CoroutineContext
+
+class MainActivity :
+ AppCompatActivity(),
+ LoginFragment.OnLoginCompleteListener,
+ DeviceFragment.OnDeviceListInteractionListener,
+ CoroutineScope {
+ private val historyStorage = lazy {
+ PlacesHistoryStorage(this)
+ }
+
+ private val bookmarksStorage = lazy {
+ PlacesBookmarksStorage(this)
+ }
+
+ private val securePreferences by lazy { SecureAbove22Preferences(this, "key_store") }
+
+ private val passwordsStorage = lazy {
+ SyncableLoginsStorage(this, lazy { securePreferences })
+ }
+
+ private val creditCardsAddressesStorage = lazy {
+ AutofillCreditCardsAddressesStorage(this, lazy { securePreferences })
+ }
+
+ private val creditCardKeyProvider by lazy { creditCardsAddressesStorage.value.crypto }
+ private val passwordsKeyProvider by lazy { passwordsStorage.value.crypto }
+
+ private val accountManager by lazy {
+ FxaAccountManager(
+ this,
+ ServerConfig(FxaServer.Release, CLIENT_ID, REDIRECT_URL),
+ DeviceConfig(
+ name = "A-C Sync Sample - ${System.currentTimeMillis()}",
+ type = DeviceType.MOBILE,
+ capabilities = setOf(DeviceCapability.SEND_TAB),
+ secureStateAtRest = true,
+ ),
+ SyncConfig(
+ setOf(
+ SyncEngine.History,
+ SyncEngine.Bookmarks,
+ SyncEngine.Passwords,
+ SyncEngine.Addresses,
+ SyncEngine.CreditCards,
+ ),
+ periodicSyncConfig = PeriodicSyncConfig(periodMinutes = 15, initialDelayMinutes = 5),
+ ),
+ )
+ }
+
+ private var job = Job()
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
+ companion object {
+ const val CLIENT_ID = "3c49430b43dfba77"
+ const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID"
+ }
+
+ private val logger = Logger("SampleSync")
+
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+
+ RustLog.enable()
+ RustHttpConfig.setClient(lazy { HttpURLConnectionClient() })
+
+ Log.addSink(AndroidLogSink())
+
+ setContentView(binding.root)
+
+ findViewById<View>(R.id.buttonSignIn).setOnClickListener {
+ launch {
+ accountManager.beginAuthentication(entrypoint = SampleFxAEntryPoint.HomeMenu)?.let { openWebView(it) }
+ }
+ }
+
+ findViewById<View>(R.id.buttonLogout).setOnClickListener {
+ launch { accountManager.logout() }
+ }
+
+ findViewById<View>(R.id.refreshDevice).setOnClickListener {
+ launch { accountManager.authenticatedAccount()?.deviceConstellation()?.refreshDevices() }
+ }
+
+ findViewById<View>(R.id.sendTab).setOnClickListener {
+ launch {
+ accountManager.authenticatedAccount()?.deviceConstellation()?.let { constellation ->
+ // Ignore devices that can't receive tabs.
+ val targets = constellation.state()?.otherDevices?.filter {
+ it.capabilities.contains(DeviceCapability.SEND_TAB)
+ }
+
+ targets?.forEach {
+ constellation.sendCommandToDevice(
+ it.id,
+ DeviceCommandOutgoing.SendTab("Sample tab", "https://www.mozilla.org"),
+ )
+ }
+
+ Toast.makeText(
+ this@MainActivity,
+ "Sent sample tab to ${targets?.size ?: 0} device(s)",
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+
+ // NB: ObserverRegistry takes care of unregistering this observer when appropriate, and
+ // cleaning up any internal references to 'observer' and 'owner'.
+ // Observe changes to the account and profile.
+ accountManager.register(accountObserver, owner = this, autoPause = true)
+ // Observe sync state changes.
+ accountManager.registerForSyncEvents(syncObserver, owner = this, autoPause = true)
+ // Observe incoming device commands.
+ accountManager.registerForAccountEvents(accountEventsObserver, owner = this, autoPause = true)
+
+ GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
+ GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage)
+ GlobalSyncableStoreProvider.configureStore(
+ storePair = SyncEngine.Passwords to passwordsStorage,
+ keyProvider = lazy { passwordsKeyProvider },
+ )
+ GlobalSyncableStoreProvider.configureStore(
+ storePair = SyncEngine.CreditCards to creditCardsAddressesStorage,
+ keyProvider = lazy { creditCardKeyProvider },
+ )
+ GlobalSyncableStoreProvider.configureStore(SyncEngine.Addresses to creditCardsAddressesStorage)
+
+ launch {
+ // Now that our account state observer is registered, we can kick off the account manager.
+ accountManager.start()
+ }
+
+ findViewById<View>(R.id.buttonSync).setOnClickListener {
+ launch {
+ accountManager.syncNow(SyncReason.User)
+ accountManager.authenticatedAccount()?.deviceConstellation()?.pollForCommands()
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ accountManager.close()
+ job.cancel()
+ }
+
+ override fun onLoginComplete(code: String, state: String, action: String, fragment: LoginFragment) {
+ launch {
+ supportFragmentManager.popBackStack()
+ accountManager.finishAuthentication(
+ FxaAuthData(action.toAuthType(), code = code, state = state),
+ )
+ }
+ }
+
+ override fun onDeviceInteraction(item: Device) {
+ Toast.makeText(
+ this@MainActivity,
+ getString(
+ R.string.full_device_details,
+ item.id,
+ item.displayName,
+ item.deviceType,
+ item.subscriptionExpired,
+ item.subscription,
+ item.capabilities,
+ item.lastAccessTime,
+ ),
+ Toast.LENGTH_LONG,
+ ).show()
+ }
+
+ private fun openWebView(url: String) {
+ supportFragmentManager.beginTransaction().apply {
+ replace(R.id.container, LoginFragment.create(url, REDIRECT_URL))
+ addToBackStack(null)
+ commit()
+ }
+ }
+
+ private val deviceConstellationObserver = object : DeviceConstellationObserver {
+ override fun onDevicesUpdate(constellation: ConstellationState) {
+ launch {
+ val currentDevice = constellation.currentDevice
+
+ val currentDeviceView: TextView = findViewById(R.id.currentDevice)
+ if (currentDevice != null) {
+ currentDeviceView.text = getString(
+ R.string.full_device_details,
+ currentDevice.id,
+ currentDevice.displayName,
+ currentDevice.deviceType,
+ currentDevice.subscriptionExpired,
+ currentDevice.subscription,
+ currentDevice.capabilities,
+ currentDevice.lastAccessTime,
+ )
+ } else {
+ currentDeviceView.text = getString(R.string.current_device_unknown)
+ }
+
+ val devicesFragment = supportFragmentManager.findFragmentById(R.id.devices_fragment) as DeviceFragment
+ devicesFragment.updateDevices(constellation.otherDevices)
+
+ Toast.makeText(this@MainActivity, "Devices updated", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ @Suppress("SetTextI18n", "NestedBlockDepth")
+ private val accountEventsObserver = object : AccountEventsObserver {
+ override fun onEvents(events: List<AccountEvent>) {
+ val txtView: TextView = findViewById(R.id.latestTabs)
+ events.forEach {
+ when (it) {
+ is AccountEvent.DeviceCommandIncoming -> {
+ when (it.command) {
+ is DeviceCommandIncoming.TabReceived -> {
+ val cmd = it.command as DeviceCommandIncoming.TabReceived
+ var tabsStringified = "Tab(s) from: ${cmd.from?.displayName}\n"
+ cmd.entries.forEach { tab ->
+ tabsStringified += "${tab.title}: ${tab.url}\n"
+ }
+ txtView.text = tabsStringified
+ }
+ }
+ }
+ is AccountEvent.ProfileUpdated -> {
+ txtView.text = "The user's profile was updated"
+ }
+ is AccountEvent.AccountAuthStateChanged -> {
+ txtView.text = "The account auth state changed"
+ }
+ is AccountEvent.AccountDestroyed -> {
+ txtView.text = "The account was destroyed"
+ }
+ is AccountEvent.DeviceConnected -> {
+ txtView.text = "Another device connected to the account"
+ }
+ is AccountEvent.DeviceDisconnected -> {
+ if (it.isLocalDevice) {
+ txtView.text = "This device disconnected"
+ } else {
+ txtView.text = "The device ${it.deviceId} disconnected"
+ }
+ }
+ is AccountEvent.Unknown -> {
+ // Unknown events are ignored to allow supporting new
+ // account events
+ }
+ }
+ }
+ }
+ }
+
+ private val accountObserver = object : AccountObserver {
+ lateinit var lastAuthType: AuthType
+
+ override fun onLoggedOut() {
+ logger.info("onLoggedOut")
+
+ launch {
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(R.string.logged_out)
+
+ val historyResultTextView: TextView = findViewById(R.id.historySyncResult)
+ historyResultTextView.text = ""
+ val bookmarksResultTextView: TextView = findViewById(R.id.bookmarksSyncResult)
+ bookmarksResultTextView.text = ""
+ val currentDeviceTextView: TextView = findViewById(R.id.currentDevice)
+ currentDeviceTextView.text = ""
+
+ val devicesFragment = supportFragmentManager.findFragmentById(
+ R.id.devices_fragment,
+ ) as DeviceFragment
+ devicesFragment.updateDevices(listOf())
+
+ findViewById<View>(R.id.buttonLogout).visibility = View.INVISIBLE
+ findViewById<View>(R.id.buttonSignIn).visibility = View.VISIBLE
+ findViewById<View>(R.id.buttonSync).visibility = View.INVISIBLE
+ findViewById<View>(R.id.refreshDevice).visibility = View.INVISIBLE
+ findViewById<View>(R.id.sendTab).visibility = View.INVISIBLE
+ }
+ }
+
+ override fun onAuthenticationProblems() {
+ logger.info("onAuthenticationProblems")
+
+ launch {
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(R.string.need_reauth)
+
+ findViewById<View>(R.id.buttonSignIn).visibility = View.VISIBLE
+ }
+ }
+
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ logger.info("onAuthenticated")
+
+ launch {
+ lastAuthType = authType
+
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(R.string.signed_in_waiting_for_profile, authType::class.simpleName)
+
+ findViewById<View>(R.id.buttonLogout).visibility = View.VISIBLE
+ findViewById<View>(R.id.buttonSignIn).visibility = View.INVISIBLE
+ findViewById<View>(R.id.buttonSync).visibility = View.VISIBLE
+ findViewById<View>(R.id.refreshDevice).visibility = View.VISIBLE
+ findViewById<View>(R.id.sendTab).visibility = View.VISIBLE
+
+ account.deviceConstellation().registerDeviceObserver(
+ deviceConstellationObserver,
+ this@MainActivity,
+ true,
+ )
+ }
+ }
+
+ override fun onProfileUpdated(profile: Profile) {
+ logger.info("onProfileUpdated")
+
+ launch {
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(
+ R.string.signed_in_with_profile,
+ lastAuthType::class.simpleName,
+ "${profile.displayName ?: ""} ${profile.email}",
+ )
+ }
+ }
+
+ override fun onFlowError(error: AuthFlowError) {
+ launch {
+ val txtView: TextView = findViewById(R.id.fxaStatusView)
+ txtView.text = getString(
+ R.string.account_error,
+ when (error) {
+ AuthFlowError.FailedToBeginAuth -> "Failed to begin authentication"
+ AuthFlowError.FailedToCompleteAuth -> "Failed to complete authentication"
+ },
+ )
+ }
+ }
+ }
+
+ private val syncObserver = object : SyncStatusObserver {
+ override fun onStarted() {
+ logger.info("onSyncStarted")
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.syncStatus.text = getString(R.string.syncing)
+ }
+ }
+
+ override fun onIdle() {
+ logger.info("onSyncIdle")
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.syncStatus.text = getString(R.string.sync_idle)
+
+ val historyResultTextView: TextView = findViewById(R.id.historySyncResult)
+ val visitedCount = withContext(Dispatchers.IO) { historyStorage.value.getVisited().size }
+ // visitedCount is passed twice: to get the correct plural form, and then as
+ // an argument for string formatting.
+ historyResultTextView.text = resources.getQuantityString(
+ R.plurals.visited_url_count,
+ visitedCount,
+ visitedCount,
+ )
+
+ val bookmarksResultTextView: TextView = findViewById(R.id.bookmarksSyncResult)
+ bookmarksResultTextView.setHorizontallyScrolling(true)
+ bookmarksResultTextView.movementMethod = ScrollingMovementMethod.getInstance()
+ bookmarksResultTextView.text = withContext(Dispatchers.IO) {
+ val bookmarksRoot = bookmarksStorage.value.getTree("root________", recursive = true)
+ if (bookmarksRoot == null) {
+ getString(R.string.no_bookmarks_root)
+ } else {
+ var bookmarksRootAndChildren = "BOOKMARKS\n"
+ fun addTreeNode(node: BookmarkNode, depth: Int) {
+ val desc = " ".repeat(depth * 2) + "${node.title} - ${node.url} (${node.guid})\n"
+ bookmarksRootAndChildren += desc
+ node.children?.forEach {
+ addTreeNode(it, depth + 1)
+ }
+ }
+ addTreeNode(bookmarksRoot, 0)
+ bookmarksRootAndChildren
+ }
+ }
+ }
+ }
+
+ override fun onError(error: Exception?) {
+ logger.error("onSyncError", error)
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.syncStatus.text = getString(R.string.sync_error, error)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt
new file mode 100644
index 0000000000..3ea2fe51a4
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/java/org/mozilla/samples/sync/SampleFxAEntryPoint.kt
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.sync
+
+import mozilla.components.concept.sync.FxAEntryPoint
+
+/**
+ * An implementation of [FxAEntryPoint] for the sample application.
+ */
+enum class SampleFxAEntryPoint(override val entryName: String) : FxAEntryPoint {
+ HomeMenu("home-menu"),
+}
diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..816c5cd0e6
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/layout/activity_main.xml
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:id="@+id/container"
+ tools:context=".MainActivity">
+
+ <ScrollView
+ tools:ignore="UselessParent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <RelativeLayout
+ android:id="@+id/stuff"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/buttonSignIn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sign_in"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/buttonSync"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/buttonSignIn"
+ android:text="@string/sync"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/refreshDevice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/buttonSync"
+ android:text="@string/refresh_device"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/sendTab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/refreshDevice"
+ android:text="@string/send_tab"
+ android:textAlignment="center" />
+
+ <Button
+ android:id="@+id/buttonLogout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/sendTab"
+ android:text="@string/log_out"
+ android:textAlignment="center" />
+
+ <TextView
+ android:id="@+id/fxaStatusView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/buttonLogout"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/syncStatus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/fxaStatusView"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/historySyncResult"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/syncStatus"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/bookmarksSyncResult"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/historySyncResult"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/currentDeviceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/bookmarksSyncResult"
+ style="?android:attr/listSeparatorTextViewStyle"
+ android:text="@string/current_device" />
+
+ <TextView
+ android:id="@+id/currentDevice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/currentDeviceLabel"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/latestTabsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/currentDevice"
+ style="?android:attr/listSeparatorTextViewStyle"
+ android:text="@string/latest_tabs" />
+
+ <TextView
+ android:id="@+id/latestTabs"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/latestTabsLabel"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/devicesLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_below="@id/latestTabs"
+ style="?android:attr/listSeparatorTextViewStyle"
+ android:text="@string/devices" />
+
+ <androidx.fragment.app.FragmentContainerView android:name="org.mozilla.samples.sync.DeviceFragment"
+ android:id="@+id/devices_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/devicesLabel" />
+ </RelativeLayout>
+ </ScrollView>
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml
new file mode 100644
index 0000000000..1cd0773ff9
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/device_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem" />
+
+ <TextView
+ android:id="@+id/device_type"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem" />
+</LinearLayout>
diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml
new file mode 100644
index 0000000000..4af98d4db2
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_device_list.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/list"
+ android:name="org.mozilla.samples.sync.DeviceFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ tools:context=".DeviceFragment"
+ tools:listitem="@layout/fragment_device" /> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml
new file mode 100644
index 0000000000..44536f4f68
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/layout/fragment_view.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<WebView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".LoginFragment" />
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..e2edeb6cbe
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..0c12478a8e
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..abdaf95771
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..0c8508a62b
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..ba2b17573d
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml b/mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..3544b113bd
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <dimen name="text_margin">16dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml b/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..b3f00174e9
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/values/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">Firefox Sync Demo</string>
+ <string name="sign_in">FxA sign in</string>
+ <string name="signed_in_waiting_for_profile">Signed in (type=%1$s); waiting for profile</string>
+ <string name="signed_in_with_profile">Authenticated (type=%1$s) as %2$s</string>
+ <string name="account_error">FxA error: %1$s</string>
+ <string name="sync_idle">Sync is idle</string>
+ <string name="syncing">Syncing&#8230;</string>
+ <string name="sync_error">Sync error: %1$s</string>
+ <plurals name="visited_url_count">
+ <item quantity="zero">There are no visited URLs</item>
+ <item quantity="one">There is %d visited URL</item>
+ <item quantity="other">There are %d visited URLs</item>
+ </plurals>
+ <string name="no_bookmarks_root">No Bookmarks Root node</string>
+ <string name="log_out">FxA Log Out</string>
+ <string name="logged_out">Logged out!</string>
+ <string name="need_reauth">Need to re-authenticate</string>
+ <string name="sync">Sync</string>
+ <string name="refresh_device">Refresh device</string>
+ <string name="send_tab">Send tab</string>
+ <string name="current_device">Current device</string>
+ <string name="current_device_unknown">Unknown</string>
+ <string name="full_device_details">
+ ID: %1$s\n
+ Name: %2$s\n
+ Type: %3$s\n
+ Subscription expired: %4$b\n
+ Subscription: %5$s\n
+ Capabilities: %6$s\n
+ Last access: %7$d
+ </string>
+ <string name="latest_tabs">Latest tabs</string>
+ <string name="devices">Devices</string>
+</resources>
diff --git a/mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/sync/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/sync/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/sync/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/toolbar/build.gradle b/mobile/android/android-components/samples/toolbar/build.gradle
new file mode 100644
index 0000000000..896f3c77f1
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/build.gradle
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ applicationId "org.mozilla.samples.toolbar"
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'org.mozilla.samples.toolbar'
+}
+
+
+dependencies {
+ implementation project(':concept-engine')
+ implementation project(':concept-menu')
+ implementation project(':browser-toolbar')
+ implementation project(':browser-menu')
+ implementation project(':browser-menu2')
+ implementation project(':browser-domains')
+
+ implementation project(':ui-colors')
+ implementation project(':ui-tabcounter')
+ implementation project(':ui-icons')
+
+ implementation project(':feature-toolbar')
+
+ implementation project(':support-ktx')
+
+ implementation project(':support-utils')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_recyclerview
+}
diff --git a/mobile/android/android-components/samples/toolbar/lint.xml b/mobile/android/android-components/samples/toolbar/lint.xml
new file mode 100644
index 0000000000..33cf423701
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/lint.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<lint>
+ <issue id="IconMissingDensityFolder" severity="ignore">
+ <!-- Suppress lint warnings on mdpi -->
+ <ignore path="src/debug/res/drawable-mdpi"/>
+ </issue>
+
+ <issue id="GoogleAppIndexingWarning" severity="ignore" />
+</lint> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/toolbar/proguard-rules.pro b/mobile/android/android-components/samples/toolbar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/samples/toolbar/src/main/AndroidManifest.xml b/mobile/android/android-components/samples/toolbar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..71bbaeb7d9
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat.NoActionBar"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ tools:targetApi="s"
+ tools:ignore="DataExtractionRules">
+ <activity android:name=".ToolbarActivity" android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/mobile/android/android-components/samples/toolbar/src/main/ic_launcher-web.png b/mobile/android/android-components/samples/toolbar/src/main/ic_launcher-web.png
new file mode 100644
index 0000000000..b8f772f66a
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/ic_launcher-web.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/SampleToolbarHelpers.kt b/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/SampleToolbarHelpers.kt
new file mode 100644
index 0000000000..7b98f77f80
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/SampleToolbarHelpers.kt
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.toolbar
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.Canvas
+import android.graphics.drawable.ClipDrawable
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.core.content.res.ResourcesCompat
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+// Code needed for assembling the sample application - but not needed to actually explain the toolbar
+
+enum class ToolbarConfiguration(val label: String) {
+ DEFAULT("Default"),
+ FOCUS_TABLET("Firefox Focus (Tablet)"),
+ FOCUS_PHONE("Firefox Focus (Phone)"),
+ CUSTOM_MENU("Custom Menu"),
+ PRIVATE_MODE("Private Mode"),
+ FENIX("Fenix"),
+ FENIX_CUSTOMTAB("Fenix (Custom Tab)"),
+}
+
+class ConfigurationAdapter(
+ private val configuration: ToolbarConfiguration,
+) : RecyclerView.Adapter<ConfigurationViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConfigurationViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.item_toolbar_configuration, parent, false)
+ return ConfigurationViewHolder(view as TextView)
+ }
+
+ override fun getItemCount() = ToolbarConfiguration.values().size
+
+ override fun onBindViewHolder(holder: ConfigurationViewHolder, position: Int) {
+ val item = ToolbarConfiguration.values()[position]
+ holder.labelView.text = item.label
+
+ holder.labelView.setOnClickListener {
+ (it.context as Activity).finish()
+
+ val intent = Intent(it.context, ToolbarActivity::class.java)
+ intent.putExtra(Extra.TOOLBAR_LABEL, item.label)
+ it.context.startActivity(intent)
+ }
+
+ if (item == configuration) {
+ holder.labelView.setBackgroundResource(R.color.selected_configuration)
+ }
+ }
+}
+
+class ConfigurationViewHolder(val labelView: TextView) : RecyclerView.ViewHolder(labelView)
+
+fun getToolbarConfiguration(intent: Intent): ToolbarConfiguration {
+ val label = intent.extras?.getString(Extra.TOOLBAR_LABEL) ?: ToolbarConfiguration.DEFAULT.label
+
+ ToolbarConfiguration.values().forEach {
+ if (label == it.label) {
+ return it
+ }
+ }
+
+ return ToolbarConfiguration.DEFAULT
+}
+
+object Extra {
+ internal const val TOOLBAR_LABEL = "toolbar_label"
+}
+
+/**
+ * A custom view to be drawn behind the URL and page actions. Acts as a custom progress view.
+ */
+class UrlBoxProgressView(
+ context: Context,
+) : View(context) {
+ var progress: Int = 0
+ set(value) {
+ // We clip the background and progress drawable based on the new progress:
+ //
+ // progress
+ // v
+ // +---------------------+-------------------+
+ // | background drawable | progress drawable |
+ // +---------------------+-------------------+
+ //
+ // The drawable is clipped completely and not visible when the level is 0 and fully
+ // revealed when the level is 10,000.
+ backgroundDrawable.level = LEVEL_STEP_SIZE * (MAX_PROGRESS - value)
+ progressDrawable.level = MAX_LEVEL - backgroundDrawable.level
+ field = value
+ invalidate() // Force redraw
+
+ // If the progress is 100% then we want to go back to 0 to hide the progress drawable
+ // again. However we want to show the full progress bar briefly so we wait 250ms before
+ // going back to 0.
+ if (value == MAX_PROGRESS) {
+ CoroutineScope(Dispatchers.Main).launch {
+ delay(PROGRESS_VISIBLE_DELAY_MS)
+ progress = 0
+ }
+ }
+ }
+
+ private var backgroundDrawable = ClipDrawable(
+ ResourcesCompat.getDrawable(resources, R.drawable.sample_url_background, context.theme),
+ Gravity.END,
+ ClipDrawable.HORIZONTAL,
+ ).apply {
+ level = MAX_LEVEL
+ }
+
+ private var progressDrawable = ClipDrawable(
+ ResourcesCompat.getDrawable(resources, R.drawable.sample_url_progress, context.theme),
+ Gravity.START,
+ ClipDrawable.HORIZONTAL,
+ ).apply {
+ level = 0
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ backgroundDrawable.setBounds(0, 0, w, h)
+ progressDrawable.setBounds(0, 0, w, h)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ backgroundDrawable.draw(canvas)
+ progressDrawable.draw(canvas)
+ }
+
+ companion object {
+ private const val MAX_PROGRESS = 100
+ private const val PROGRESS_VISIBLE_DELAY_MS = 250L
+ private const val LEVEL_STEP_SIZE = 100
+ private const val MAX_LEVEL = 10000
+ }
+}
diff --git a/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/ToolbarActivity.kt b/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/ToolbarActivity.kt
new file mode 100644
index 0000000000..58dbb8a312
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/java/org/mozilla/samples/toolbar/ToolbarActivity.kt
@@ -0,0 +1,539 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.samples.toolbar
+
+import android.content.res.Resources
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.annotation.DrawableRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import mozilla.components.browser.domains.autocomplete.CustomDomainsProvider
+import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
+import mozilla.components.browser.menu.BrowserMenu
+import mozilla.components.browser.menu.BrowserMenuBuilder
+import mozilla.components.browser.menu.BrowserMenuItem
+import mozilla.components.browser.menu.ext.asCandidateList
+import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
+import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
+import mozilla.components.browser.menu2.BrowserMenuController
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.browser.toolbar.display.DisplayToolbar
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.ktx.android.view.hideKeyboard
+import mozilla.components.support.ktx.util.URLStringUtils
+import mozilla.components.ui.tabcounter.TabCounter
+import org.mozilla.samples.toolbar.databinding.ActivityToolbarBinding
+import mozilla.components.browser.menu.R as menuR
+import mozilla.components.browser.toolbar.R as toolbarR
+import mozilla.components.ui.colors.R as colorsR
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * This sample application shows how to use and customize the browser-toolbar component.
+ */
+@Suppress("LargeClass")
+class ToolbarActivity : AppCompatActivity() {
+ private val shippedDomainsProvider = ShippedDomainsProvider()
+ private val customDomainsProvider = CustomDomainsProvider()
+ private lateinit var binding: ActivityToolbarBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityToolbarBinding.inflate(layoutInflater)
+
+ shippedDomainsProvider.initialize(this)
+ customDomainsProvider.initialize(this)
+
+ setContentView(binding.root)
+
+ val configuration = getToolbarConfiguration(intent)
+
+ when (configuration) {
+ ToolbarConfiguration.DEFAULT -> setupDefaultToolbar()
+ ToolbarConfiguration.FOCUS_TABLET -> setupFocusTabletToolbar()
+ ToolbarConfiguration.FOCUS_PHONE -> setupFocusPhoneToolbar()
+ ToolbarConfiguration.CUSTOM_MENU -> setupCustomMenu()
+ ToolbarConfiguration.PRIVATE_MODE -> setupDefaultToolbar(private = true)
+ ToolbarConfiguration.FENIX -> setupFenixToolbar()
+ ToolbarConfiguration.FENIX_CUSTOMTAB -> setupFenixCustomTabToolbar()
+ }
+
+ val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
+ recyclerView.adapter = ConfigurationAdapter(configuration)
+ recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
+
+ ToolbarAutocompleteFeature(binding.toolbar).apply {
+ updateAutocompleteProviders(
+ providers = listOf(shippedDomainsProvider, customDomainsProvider),
+ refreshAutocomplete = false,
+ )
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ binding.toolbar.hideKeyboard()
+ }
+
+ /**
+ * A very simple toolbar with mostly default values.
+ */
+ private fun setupDefaultToolbar(private: Boolean = false) {
+ binding.toolbar.setBackgroundColor(
+ ContextCompat.getColor(this, colorsR.color.photonBlue80),
+ )
+
+ binding.toolbar.private = private
+
+ binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/"
+ }
+
+ /**
+ * A toolbar that looks like Firefox Focus on tablets.
+ */
+ private fun setupFocusTabletToolbar() {
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Use the iconic gradient background
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val background = ContextCompat.getDrawable(this, R.drawable.focus_background)
+ binding.toolbar.background = background
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Add "back" and "forward" navigation actions
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val back = BrowserToolbar.Button(
+ resources.getThemedDrawable(iconsR.drawable.mozac_ic_back_24)!!,
+ "Back",
+ ) {
+ simulateReload()
+ }
+
+ binding.toolbar.addNavigationAction(back)
+
+ val forward = BrowserToolbar.Button(
+ resources.getThemedDrawable(iconsR.drawable.mozac_ic_forward_24)!!,
+ "Forward",
+ ) {
+ simulateReload()
+ }
+
+ binding.toolbar.addNavigationAction(forward)
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Add a "reload" browser action that simulates reloading the current page
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val reload = BrowserToolbar.TwoStateButton(
+ primaryImage = resources.getThemedDrawable(iconsR.drawable.mozac_ic_arrow_clockwise_24)!!,
+ primaryContentDescription = "Reload",
+ secondaryImage = resources.getThemedDrawable(iconsR.drawable.mozac_ic_stop)!!,
+ secondaryContentDescription = "Stop",
+ isInPrimaryState = { loading.value != true },
+ disableInSecondaryState = false,
+ ) {
+ if (loading.value == true) {
+ job?.cancel()
+ } else {
+ simulateReload()
+ }
+ }
+ binding.toolbar.addBrowserAction(reload)
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Create a menu that looks like the one in Firefox Focus
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val fenix = SimpleBrowserMenuItem("POWERED BY MOZILLA")
+ val share = SimpleBrowserMenuItem("Share…") { /* Do nothing */ }
+ val homeScreen = SimpleBrowserMenuItem("Add to Home screen") { /* Do nothing */ }
+ val open = SimpleBrowserMenuItem("Open in…") { /* Do nothing */ }
+ val settings = SimpleBrowserMenuItem("Settings") { /* Do nothing */ }
+
+ val items = listOf(fenix, share, homeScreen, open, settings)
+ binding.toolbar.display.menuBuilder = BrowserMenuBuilder(items)
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Display a URL
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/mobile/"
+ }
+
+ /**
+ * A custom browser menu.
+ */
+ private fun setupCustomMenu() {
+ binding.toolbar.setBackgroundColor(
+ ContextCompat.getColor(this, colorsR.color.photonBlue80),
+ )
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Create a menu with text and icons
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val share = TextMenuCandidate(
+ "Share",
+ start = DrawableMenuIcon(this, iconsR.drawable.mozac_ic_share_android_24),
+ ) { /* Do nothing */ }
+
+ val search = TextMenuCandidate(
+ "Search",
+ start = DrawableMenuIcon(this, iconsR.drawable.mozac_ic_search_24),
+ ) { /* Do nothing */ }
+
+ binding.toolbar.display.menuController = BrowserMenuController(Side.START).apply {
+ submitList(listOf(share, DividerMenuCandidate(), search))
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Display a URL
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ binding.toolbar.url = "https://www.mozilla.org/"
+ }
+
+ /**
+ * A toolbar that looks like Firefox Focus on phones.
+ */
+ private fun setupFocusPhoneToolbar() {
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Use the iconic gradient background
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val background = ContextCompat.getDrawable(this, R.drawable.focus_background)
+ binding.toolbar.background = background
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Create a "mini" toolbar to be shown inside the menu (forward, reload)
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val forward = BrowserMenuItemToolbar.Button(
+ iconsR.drawable.mozac_ic_forward_24,
+ "Forward",
+ isEnabled = { canGoForward() },
+ ) {
+ simulateReload()
+ }
+
+ val reload = BrowserMenuItemToolbar.TwoStateButton(
+ primaryImageResource = iconsR.drawable.mozac_ic_arrow_clockwise_24,
+ primaryContentDescription = "Reload",
+ secondaryImageResource = iconsR.drawable.mozac_ic_stop,
+ secondaryContentDescription = "Stop",
+ isInPrimaryState = { loading.value != true },
+ disableInSecondaryState = false,
+ ) {
+ if (loading.value == true) {
+ job?.cancel()
+ } else {
+ simulateReload()
+ }
+ }
+ // Redraw the reload button when loading state changes
+ loading.observe(this, Observer { binding.toolbar.invalidateActions() })
+
+ val menuToolbar = BrowserMenuItemToolbar(listOf(forward, reload))
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Create a custom "menu item" implementation that resembles Focus' global content blocking switch.
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val blocking = object : BrowserMenuItem {
+ // Always display this item. This lambda is executed when the user clicks on the menu
+ // button to determine whether this item should be shown.
+ override val visible = { true }
+
+ override fun getLayoutResource() = R.layout.focus_blocking_switch
+
+ override fun bind(menu: BrowserMenu, view: View) {
+ // Nothing to do here.
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Create a menu that looks like the one in Firefox Focus
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ val share = SimpleBrowserMenuItem("Share…") { /* Do nothing */ }
+ val homeScreen = SimpleBrowserMenuItem("Add to Home screen") { /* Do nothing */ }
+ val open = SimpleBrowserMenuItem("Open in…") { /* Do nothing */ }
+ val settings = SimpleBrowserMenuItem("Settings") { /* Do nothing */ }
+
+ val items = listOf(menuToolbar, blocking, share, homeScreen, open, settings)
+ binding.toolbar.display.menuBuilder = BrowserMenuBuilder(items)
+ binding.toolbar.invalidateActions()
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Display a URL
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/mobile/"
+ }
+
+ private class FakeTabCounterToolbarButton : Toolbar.Action {
+ override fun createView(parent: ViewGroup): View = TabCounter(parent.context).apply {
+ setCount(2)
+ setBackgroundResource(
+ parent.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless),
+ )
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ /**
+ * A toolbar that looks like the toolbar in Fenix (Light theme).
+ */
+ @Suppress("MagicNumber")
+ fun setupFenixToolbar() {
+ binding.toolbar.setBackgroundColor(0xFFFFFFFF.toInt())
+
+ binding.toolbar.display.indicators = listOf(
+ DisplayToolbar.Indicators.SECURITY,
+ DisplayToolbar.Indicators.TRACKING_PROTECTION,
+ DisplayToolbar.Indicators.EMPTY,
+ )
+
+ binding.toolbar.display.colors = binding.toolbar.display.colors.copy(
+ securityIconInsecure = 0xFF20123a.toInt(),
+ securityIconSecure = 0xFF20123a.toInt(),
+ text = 0xFF0c0c0d.toInt(),
+ menu = 0xFF20123a.toInt(),
+ separator = 0x1E15141a.toInt(),
+ trackingProtection = 0xFF20123a.toInt(),
+ emptyIcon = 0xFF20123a.toInt(),
+ hint = 0x1E15141a.toInt(),
+ )
+
+ binding.toolbar.display.urlFormatter = { url ->
+ URLStringUtils.toDisplayUrl(url)
+ }
+
+ binding.toolbar.display.setUrlBackground(
+ ContextCompat.getDrawable(this, R.drawable.fenix_url_background),
+ )
+ binding.toolbar.display.hint = "Search or enter address"
+ binding.toolbar.display.setOnUrlLongClickListener {
+ Toast.makeText(this, "Long click!", Toast.LENGTH_SHORT).show()
+ true
+ }
+
+ val share = TextMenuCandidate("Share…") { /* Do nothing */ }
+ val homeScreen = TextMenuCandidate("Add to Home screen") { /* Do nothing */ }
+ val open = TextMenuCandidate("Open in…") { /* Do nothing */ }
+ val settings = NestedMenuCandidate(
+ id = toolbarR.id.mozac_browser_toolbar_menu,
+ text = "Settings",
+ subMenuItems = listOf(
+ NestedMenuCandidate(id = menuR.id.container, text = "Back", subMenuItems = null),
+ TextMenuCandidate("Setting 1") { /* Do nothing */ },
+ TextMenuCandidate("Setting 2") { /* Do nothing */ },
+ ),
+ )
+
+ val items = listOf(share, homeScreen, open, settings)
+ binding.toolbar.display.menuController = BrowserMenuController().apply {
+ submitList(items)
+ }
+
+ binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/mobile/"
+
+ binding.toolbar.addBrowserAction(FakeTabCounterToolbarButton())
+
+ binding.toolbar.display.setOnSiteSecurityClickedListener {
+ Toast.makeText(this, "Site security", Toast.LENGTH_SHORT).show()
+ }
+
+ binding.toolbar.edit.colors = binding.toolbar.edit.colors.copy(
+ text = 0xFF0c0c0d.toInt(),
+ clear = 0xFF0c0c0d.toInt(),
+ icon = 0xFF0c0c0d.toInt(),
+ )
+
+ binding.toolbar.edit.setUrlBackground(
+ ContextCompat.getDrawable(this, R.drawable.fenix_url_background),
+ )
+ binding.toolbar.edit.setIcon(
+ ContextCompat.getDrawable(this, iconsR.drawable.mozac_ic_search_24)!!,
+ "Search",
+ )
+
+ binding.toolbar.setOnUrlCommitListener { url ->
+ simulateReload()
+ binding.toolbar.url = url
+
+ true
+ }
+ }
+
+ /**
+ * A toolbar that looks like the toolbar in Fenix in a custom tab.
+ */
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ @Suppress("MagicNumber")
+ fun setupFenixCustomTabToolbar() {
+ binding.toolbar.setBackgroundColor(0xFFFFFFFF.toInt())
+
+ binding.toolbar.display.indicators = listOf(
+ DisplayToolbar.Indicators.SECURITY,
+ DisplayToolbar.Indicators.TRACKING_PROTECTION,
+ )
+
+ binding.toolbar.display.colors = binding.toolbar.display.colors.copy(
+ securityIconSecure = 0xFF20123a.toInt(),
+ securityIconInsecure = 0xFF20123a.toInt(),
+ text = 0xFF0c0c0d.toInt(),
+ title = 0xFF0c0c0d.toInt(),
+ menu = 0xFF20123a.toInt(),
+ separator = 0x1E15141a.toInt(),
+ trackingProtection = 0xFF20123a.toInt(),
+ )
+
+ val share = SimpleBrowserMenuItem("Share…") { /* Do nothing */ }
+ val homeScreen = SimpleBrowserMenuItem("Add to Home screen") { /* Do nothing */ }
+ val open = SimpleBrowserMenuItem("Open in…") { /* Do nothing */ }
+ val settings = SimpleBrowserMenuItem("Settings") { /* Do nothing */ }
+
+ val items = listOf(share, homeScreen, open, settings)
+ binding.toolbar.display.menuBuilder = BrowserMenuBuilder(items)
+ binding.toolbar.display.menuController = BrowserMenuController().apply {
+ submitList(items.asCandidateList(this@ToolbarActivity))
+ }
+
+ binding.toolbar.url = "https://www.mozilla.org/en-US/firefox/mobile/"
+
+ val drawableIcon = ContextCompat.getDrawable(this, iconsR.drawable.mozac_ic_cross_24)
+
+ drawableIcon?.apply {
+ setTint(0xFF20123a.toInt())
+ }.also {
+ val button = Toolbar.ActionButton(
+ it,
+ "Close",
+ ) {
+ Toast.makeText(this, "Close!", Toast.LENGTH_SHORT).show()
+ }
+ binding.toolbar.addNavigationAction(button)
+ }
+
+ val drawable = ContextCompat.getDrawable(this, iconsR.drawable.mozac_ic_share_android_24)?.apply {
+ setTint(0xFF20123a.toInt())
+ }
+
+ val button = Toolbar.ActionButton(drawable, "Share") {
+ Toast.makeText(this, "Share!", Toast.LENGTH_SHORT).show()
+ }
+
+ binding.toolbar.addBrowserAction(button)
+
+ binding.toolbar.display.setOnSiteSecurityClickedListener {
+ Toast.makeText(this, "Site security", Toast.LENGTH_SHORT).show()
+ }
+
+ GlobalScope.launch(Dispatchers.Main) {
+ delay(2000)
+ binding.toolbar.title = "Mobile browsers for iOS and Android | Firefox"
+ }
+ }
+
+ // For testing purposes
+ private var forward = true
+ private var back = true
+
+ private fun canGoForward(): Boolean = forward
+
+ @Suppress("UnusedPrivateMember")
+ private fun canGoBack(): Boolean = back
+
+ @Suppress("UnusedPrivateMember")
+ private fun goBack() {
+ back = !(forward && back)
+ forward = true
+ }
+
+ @Suppress("UnusedPrivateMember")
+ private fun goForward() {
+ forward = !(back && forward)
+ back = true
+ }
+
+ private var job: Job? = null
+
+ private var loading = MutableLiveData<Boolean>()
+
+ @Suppress("TooGenericExceptionCaught", "LongMethod", "ComplexMethod")
+ private fun simulateReload(view: UrlBoxProgressView? = null) {
+ job?.cancel()
+
+ loading.value = true
+
+ job = CoroutineScope(Dispatchers.Main).launch {
+ try {
+ loop@ for (progress in PROGRESS_RANGE step RELOAD_STEP_SIZE) {
+ if (!isActive) {
+ break@loop
+ }
+
+ if (view == null) {
+ binding.toolbar.displayProgress(progress)
+ } else {
+ view.progress = progress
+ }
+
+ delay(progress * RELOAD_STEP_SIZE.toLong())
+ }
+ } catch (t: Throwable) {
+ if (view == null) {
+ binding.toolbar.displayProgress(0)
+ } else {
+ view.progress = 0
+ }
+
+ throw t
+ } finally {
+ loading.value = false
+
+ // Update toolbar buttons to reflect loading state
+ binding.toolbar.invalidateActions()
+ }
+ }
+
+ // Update toolbar buttons to reflect loading state
+ binding.toolbar.invalidateActions()
+ }
+
+ private fun Resources.getThemedDrawable(@DrawableRes resId: Int) = ResourcesCompat.getDrawable(this, resId, theme)
+
+ companion object {
+ private val PROGRESS_RANGE = 0..100
+ private const val RELOAD_STEP_SIZE = 5
+ }
+}
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/drawable/fenix_url_background.xml b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/fenix_url_background.xml
new file mode 100644
index 0000000000..3db3318893
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/fenix_url_background.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#E6E8E8" />
+
+ <corners
+ android:bottomLeftRadius="8dp"
+ android:bottomRightRadius="8dp"
+ android:topLeftRadius="8dp"
+ android:topRightRadius="8dp" />
+</shape> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/drawable/focus_background.xml b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/focus_background.xml
new file mode 100644
index 0000000000..c2f0193bb4
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/focus_background.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:angle="315"
+ android:startColor="#ffa01142"
+ android:centerColor="#ff90116D"
+ android:endColor="#ff5c1166" />
+</shape> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_background.xml b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_background.xml
new file mode 100644
index 0000000000..5537e32d4d
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#FF2A2A2E" />
+ <corners android:radius="4dp" />
+</shape>
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_progress.xml b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_progress.xml
new file mode 100644
index 0000000000..a1404243be
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/drawable/sample_url_progress.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#ff45a1ff" />
+ <corners android:radius="4dp" />
+</shape>
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/layout/activity_toolbar.xml b/mobile/android/android-components/samples/toolbar/src/main/res/layout/activity_toolbar.xml
new file mode 100644
index 0000000000..48bda7881c
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/layout/activity_toolbar.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context=".ToolbarActivity">
+
+ <mozilla.components.browser.toolbar.BrowserToolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/layout/focus_blocking_switch.xml b/mobile/android/android-components/samples/toolbar/src/main/res/layout/focus_blocking_switch.xml
new file mode 100644
index 0000000000..6245c6ce46
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/layout/focus_blocking_switch.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="80dp"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:background="#272727"
+ tools:ignore="Overdraw"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingEnd="0dp"
+ android:paddingStart="16dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:ellipsize="end"
+ android:text="@string/trackers_blocked"
+ android:textColor="#80FFFFFF"
+ android:textSize="16sp" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/trackers_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="#ffffff"
+ android:textSize="24sp"
+ tools:text="42" />
+
+ <ImageButton
+ android:id="@+id/help_trackers"
+ android:contentDescription="@string/help_trackers_description"
+ android:layout_width="22dp"
+ android:layout_height="22dp"
+ android:padding="4dp"
+ app:srcCompat="@drawable/mozac_ic_information_fill_24"
+ android:background="?android:attr/selectableItemBackgroundBorderless" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/blocking_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_gravity="center_vertical|end"
+ android:background="?android:attr/selectableItemBackground"
+ android:checked="true"
+ android:clickable="true"
+ android:focusable="true"
+ android:ellipsize="end"
+ android:gravity="center"
+ android:lines="1"
+ android:paddingEnd="16dp"
+ android:paddingStart="16dp"
+ android:textSize="16sp"/>
+
+</LinearLayout>
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/layout/item_toolbar_configuration.xml b/mobile/android/android-components/samples/toolbar/src/main/res/layout/item_toolbar_configuration.xml
new file mode 100644
index 0000000000..d7c2560465
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/layout/item_toolbar_configuration.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ tools:ignore="Overdraw"
+ android:clickable="true"
+ android:focusable="true"
+ android:textSize="14sp"
+ android:padding="16dp" />
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..ff5b811c28
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..ff5b811c28
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..f53686af1d
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..361ce175fe
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..eeb463471b
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..9466bb3aee
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..c158a5aa08
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..4658edf3ae
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..21410ec0f8
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..2233cd4525
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..9f62938988
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..8b0c76270e
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..ea69f75cd7
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..1414185bd2
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..41288de783
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..beccb228ea
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..5ab06ff36d
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/values/colors.xml b/mobile/android/android-components/samples/toolbar/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..abbf1bdbcc
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <color name="selected_configuration">#222222</color>
+</resources>
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/values/dimens.xml b/mobile/android/android-components/samples/toolbar/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..dff0755716
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <dimen tools:ignore="UnusedResources" name="mozac_browser_menu_corner_radius">20dp</dimen>
+ <dimen tools:ignore="UnusedResources" name="mozac_browser_menu2_corner_radius">20dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/values/ic_launcher_background.xml b/mobile/android/android-components/samples/toolbar/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000000..475311a475
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <color name="ic_launcher_background">#FF618D</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/values/strings.xml b/mobile/android/android-components/samples/toolbar/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..81a1535a0e
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <string name="app_name">Toolbar Sample</string>
+ <string name="trackers_blocked">Trackers blocked</string>
+ <string name="help_trackers_description">About Trackers</string>
+</resources>
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/xml/backup_rules.xml b/mobile/android/android-components/samples/toolbar/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..820ae61afa
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<full-backup-content>
+ <include domain="sharedpref" path="."/>
+</full-backup-content> \ No newline at end of file
diff --git a/mobile/android/android-components/samples/toolbar/src/main/res/xml/data_extraction_rules.xml b/mobile/android/android-components/samples/toolbar/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..55da967560
--- /dev/null
+++ b/mobile/android/android-components/samples/toolbar/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<data-extraction-rules>
+ <cloud-backup>
+ <include domain="sharedpref" path="."/>
+ </cloud-backup>
+</data-extraction-rules> \ No newline at end of file