summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/feature/pwa/src
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/feature/pwa/src')
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/androidTest/java/mozilla/components/feature/pwa/db/ManifestDatabaseMigrationTest.kt113
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/AndroidManifest.xml22
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ManifestStorage.kt159
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ProgressiveWebAppFacts.kt50
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppInterceptor.kt62
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppLauncherActivity.kt105
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppShortcutManager.kt287
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppUseCases.kt88
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestConverter.kt26
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDao.kt61
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDatabase.kt70
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestEntity.kt50
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Activity.kt26
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Bundle.kt31
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/CustomTabState.kt22
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Intent.kt48
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/SessionState.kt28
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Uri.kt25
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/WebAppManifest.kt98
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeature.kt94
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/SiteControlsBuilder.kt130
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppActivityFeature.kt54
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppContentFeature.kt30
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeature.kt117
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeature.kt198
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt96
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt95
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_pwa.xml13
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_refresh.xml13
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-am/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-an/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ar/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ast/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-az/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-azb/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ban/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-be/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-bg/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-bn/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-br/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-bs/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ca/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-cak/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ceb/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ckb/strings.xml12
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-co/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-cs/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-cy/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-da/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-de/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-dsb/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-el/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rCA/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rGB/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-eo/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rAR/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rCL/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rES/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rMX/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-es/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-et/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-eu/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fa/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fi/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fur/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-fy-rNL/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-gd/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-gl/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-gn/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-gu-rIN/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hi-rIN/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hil/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hsb/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hu/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-hy-rAM/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ia/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-in/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-is/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-it/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-iw/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ja/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ka/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kaa/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kab/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kk/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kmr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-kn/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ko/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-lo/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-lt/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-mix/strings.xml8
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-mr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-my/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-nb-rNO/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ne-rNP/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-nl/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-nn-rNO/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-oc/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-or/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rIN/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rPK/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pl/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rBR/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rPT/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-rm/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ro/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ru/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sat/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sc/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-si/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sk/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-skr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sl/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sq/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-su/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-sv-rSE/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ta/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-te/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tg/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-th/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tl/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tr/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-trs/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tt/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-tzm/strings.xml6
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ug/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-uk/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-ur/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-uz/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-vi/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-yo/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rCN/strings.xml15
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rTW/strings.xml14
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values/strings.xml17
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/main/res/values/styles.xml16
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt305
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppInterceptorTest.kt102
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppLauncherActivityTest.kt113
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt355
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt149
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/ActivityKtTest.kt89
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/CustomTabStateKtTest.kt59
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/SessionStateKtTest.kt169
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/UriKtTest.kt56
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/WebAppManifestKtTest.kt172
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt255
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppActivityFeatureTest.kt95
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppContentFeatureTest.kt53
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeatureTest.kt343
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeatureTest.kt147
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt97
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt154
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/pwa/src/test/resources/robolectric.properties1
157 files changed, 6414 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/pwa/src/androidTest/java/mozilla/components/feature/pwa/db/ManifestDatabaseMigrationTest.kt b/mobile/android/android-components/components/feature/pwa/src/androidTest/java/mozilla/components/feature/pwa/db/ManifestDatabaseMigrationTest.kt
new file mode 100644
index 0000000000..00659e3af2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/androidTest/java/mozilla/components/feature/pwa/db/ManifestDatabaseMigrationTest.kt
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.db
+
+import androidx.core.database.getStringOrNull
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import java.io.IOException
+
+class ManifestDatabaseMigrationTest {
+ private val TEST_DB = "migration-test"
+
+ @Rule
+ @JvmField
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ ManifestDatabase::class.java,
+ )
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate2To3() {
+ helper.createDatabase(TEST_DB, 2).apply {
+ // db has schema version 2. insert some data using SQL queries.
+ // You cannot use DAO classes because they expect the latest schema.
+ execSQL("INSERT INTO manifests (start_url, created_at, updated_at, manifest, used_at, scope) VALUES ('https://mozilla.org', 1, 2, '{}', 3, 'https://mozilla.org')")
+
+ // Prepare for the next version.
+ close()
+ }
+
+ // Re-open the database with version 2 and provide
+ // MIGRATION_1_2 as the migration process.
+ helper.runMigrationsAndValidate(TEST_DB, 3, true, ManifestDatabase.MIGRATION_2_3).apply {
+ val result = query("SELECT scope, has_share_targets FROM manifests WHERE start_url = 'https://mozilla.org'")
+
+ result.moveToNext()
+
+ assertEquals(1, result.count)
+ assertTrue(result.isFirst)
+ assertTrue(result.isLast)
+ assertEquals("https://mozilla.org", result.getStringOrNull(0))
+ assertEquals(0, result.getInt(1))
+
+ close()
+ }
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate1To2() {
+ helper.createDatabase(TEST_DB, 1).apply {
+ // db has schema version 1. insert some data using SQL queries.
+ // You cannot use DAO classes because they expect the latest schema.
+ execSQL("INSERT INTO manifests (start_url, created_at, updated_at, manifest, used_at, scope) VALUES ('https://mozilla.org', 1, 2, '{}', 3, 'https://mozilla.org')")
+
+ // Prepare for the next version.
+ close()
+ }
+
+ // Re-open the database with version 2 and provide
+ // MIGRATION_1_2 as the migration process.
+ helper.runMigrationsAndValidate(TEST_DB, 2, true, ManifestDatabase.MIGRATION_1_2).apply {
+ val result = query("SELECT scope, used_at FROM manifests WHERE start_url = 'https://mozilla.org'")
+
+ result.moveToNext()
+
+ assertEquals(1, result.count)
+ assertTrue(result.isFirst)
+ assertTrue(result.isLast)
+ assertEquals("https://mozilla.org", result.getStringOrNull(0))
+ assertEquals(3, result.getLong(1))
+
+ close()
+ }
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun migrate0To2() {
+ helper.createDatabase(TEST_DB, 0).apply {
+ // db has schema version 0 which was the original version 1. insert some data using SQL queries.
+ // You cannot use DAO classes because they expect the latest schema.
+ execSQL("INSERT INTO manifests (start_url, created_at, updated_at, manifest) VALUES ('https://mozilla.org', 1, 2, '{}')")
+
+ // Prepare for the next version.
+ close()
+ }
+
+ // Re-open the database with version 2 and provide
+ // MIGRATION_1_2 as the migration process.
+ helper.runMigrationsAndValidate(TEST_DB, 2, true, ManifestDatabase.MIGRATION_1_2).apply {
+ val result = query("SELECT scope, used_at FROM manifests WHERE start_url = 'https://mozilla.org'")
+
+ result.moveToNext()
+
+ assertEquals(1, result.count)
+ assertTrue(result.isFirst)
+ assertTrue(result.isLast)
+ assertNull(result.getStringOrNull(0))
+ assertEquals(2, result.getLong(1))
+
+ close()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/pwa/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..289f2ba72c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/AndroidManifest.xml
@@ -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/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission
+ android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
+ android:maxSdkVersion="26" />
+
+ <application>
+
+ <activity android:name=".WebAppLauncherActivity"
+ android:theme="@style/Theme.AppCompat.Translucent"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="mozilla.components.feature.pwa.PWA_LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ManifestStorage.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ManifestStorage.kt
new file mode 100644
index 0000000000..acf793cd88
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ManifestStorage.kt
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.db.ManifestDatabase
+import mozilla.components.feature.pwa.db.ManifestEntity
+
+/**
+ * Disk storage for [WebAppManifest]. Other components use this class to reload a saved manifest.
+ *
+ * @param context the application context this storage is associated with
+ * @param activeThresholdMs a timeout in milliseconds after which the storage will consider a manifest
+ * as unused. By default this is [ACTIVE_THRESHOLD_MS].
+ */
+class ManifestStorage(context: Context, private val activeThresholdMs: Long = ACTIVE_THRESHOLD_MS) {
+
+ @VisibleForTesting
+ internal var manifestDao = lazy { ManifestDatabase.get(context).manifestDao() }
+ internal var installedScopes: MutableMap<String, String>? = null
+
+ /**
+ * Load a Web App Manifest for the given URL from disk.
+ * If no manifest is found, null is returned.
+ *
+ * @param startUrl URL of site. Should correspond to manifest's start_url.
+ */
+ suspend fun loadManifest(startUrl: String): WebAppManifest? = withContext(IO) {
+ manifestDao.value.getManifest(startUrl)?.manifest
+ }
+
+ /**
+ * Load all Web App Manifests with a matching scope for the given URL from disk.
+ * If no manifests are found, an empty list is returned.
+ *
+ * @param url URL of site. Should correspond to an url covered by the scope of a stored manifest.
+ */
+ suspend fun loadManifestsByScope(url: String): List<WebAppManifest> = withContext(IO) {
+ manifestDao.value.getManifestsByScope(url).map { it.manifest }
+ }
+
+ /**
+ * Checks whether there is a currently used manifest with a scope that matches the url.
+ *
+ * @param url the url to match with manifest scopes.
+ * @param currentTimeMs the current time in milliseconds.
+ */
+ suspend fun hasRecentManifest(
+ url: String,
+ @VisibleForTesting currentTimeMs: Long = System.currentTimeMillis(),
+ ): Boolean = withContext(IO) {
+ manifestDao.value.hasRecentManifest(url, thresholdMs = currentTimeMs - activeThresholdMs) > 0
+ }
+
+ /**
+ * Counts number of recently used manifests, as configured by [activeThresholdMs].
+ *
+ * @param currentTimeMs current time, exposed for testing
+ * @param activeThresholdMs a time threshold within which manifests are considered to be recently used.
+ */
+ suspend fun recentManifestsCount(
+ activeThresholdMs: Long = this.activeThresholdMs,
+ @VisibleForTesting currentTimeMs: Long = System.currentTimeMillis(),
+ ): Int = withContext(IO) {
+ manifestDao.value.recentManifestsCount(thresholdMs = currentTimeMs - activeThresholdMs)
+ }
+
+ /**
+ * Returns the cached scope for an url if the url falls into a web app scope that has been installed by the user.
+ *
+ * @param url the url to match against installed web app scopes.
+ */
+ fun getInstalledScope(url: String) = installedScopes?.keys?.sortedDescending()?.find { url.startsWith(it) }
+
+ /**
+ * Returns a cached start url for an installed web app scope.
+ *
+ * @param scope the scope url to look up.
+ */
+ fun getStartUrlForInstalledScope(scope: String) = installedScopes?.get(scope)
+
+ /**
+ * Populates a cache of currently installed web app scopes and their start urls.
+ *
+ * @param currentTime the current time is used to determine which web apps are still installed.
+ */
+ suspend fun warmUpScopes(currentTime: Long) = withContext(IO) {
+ installedScopes = manifestDao.value
+ .getInstalledScopes(deadline(currentTime))
+ .mapNotNull { manifest -> manifest.scope?.let { scope -> Pair(scope, manifest.startUrl) } }
+ .toMap()
+ .toMutableMap()
+ }
+
+ /**
+ * Load all Web App Manifests that contain share targets.
+ * If no manifests are found, an empty list is returned.
+ *
+ * @param currentTime the current time in milliseconds.
+ */
+ suspend fun loadShareableManifests(currentTime: Long): List<WebAppManifest> = withContext(IO) {
+ manifestDao.value.getRecentShareableManifests(deadline(currentTime)).map { it.manifest }
+ }
+
+ /**
+ * Save a Web App Manifest to disk.
+ */
+ suspend fun saveManifest(manifest: WebAppManifest) = withContext(IO) {
+ val entity = ManifestEntity(manifest, currentTime = System.currentTimeMillis())
+ manifestDao.value.insertManifest(entity)
+ }
+
+ /**
+ * Update an existing Web App Manifest on disk.
+ */
+ suspend fun updateManifest(manifest: WebAppManifest) = withContext(IO) {
+ manifestDao.value.getManifest(manifest.startUrl)?.let { existing ->
+ val update = existing.copy(manifest = manifest, updatedAt = System.currentTimeMillis())
+ manifestDao.value.updateManifest(update)
+ }
+ }
+
+ /**
+ * Update the last time a web app was used.
+ *
+ * @param manifest the manifest to update
+ */
+ suspend fun updateManifestUsedAt(manifest: WebAppManifest) = withContext(IO) {
+ manifestDao.value.getManifest(manifest.startUrl)?.let { existing ->
+ val update = existing.copy(usedAt = System.currentTimeMillis())
+ manifestDao.value.updateManifest(update)
+
+ existing.scope?.let { scope ->
+ installedScopes?.put(scope, existing.startUrl)
+ }
+
+ return@let
+ }
+ }
+
+ /**
+ * Delete all manifests associated with the list of URLs.
+ */
+ suspend fun removeManifests(startUrls: List<String>) = withContext(IO) {
+ manifestDao.value.deleteManifests(startUrls)
+ }
+
+ private fun deadline(currentTime: Long) = currentTime - activeThresholdMs
+
+ companion object {
+ const val ACTIVE_THRESHOLD_MS = 86400000 * 30L // 30 days
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ProgressiveWebAppFacts.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ProgressiveWebAppFacts.kt
new file mode 100644
index 0000000000..0abbbfc6e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ProgressiveWebAppFacts.kt
@@ -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/. */
+
+package mozilla.components.feature.pwa
+
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+
+/**
+ * Facts emitted for telemetry related to [PwaFeature]
+ */
+class ProgressiveWebAppFacts {
+ /**
+ * Items that specify which portion of the [PwaFeature] was interacted with
+ */
+ object Items {
+ const val INSTALL_SHORTCUT = "install_shortcut"
+ const val HOMESCREEN_ICON_TAP = "homescreen_icon_tap"
+ }
+}
+
+private fun emitPwaFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.FEATURE_PWA,
+ action,
+ item,
+ value,
+ metadata,
+ ).collect()
+}
+
+internal fun emitPwaInstallFact() =
+ emitPwaFact(
+ Action.CLICK,
+ ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT,
+ )
+
+internal fun emitHomescreenIconTapFact() =
+ emitPwaFact(
+ Action.CLICK,
+ ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP,
+ )
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppInterceptor.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppInterceptor.kt
new file mode 100644
index 0000000000..169a90c2a7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppInterceptor.kt
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.feature.pwa.ext.putUrlOverride
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
+
+/**
+ * This feature will intercept requests and reopen them in the corresponding installed PWA, if any.
+ *
+ * @param shortcutManager current shortcut manager instance to lookup web app install states
+ */
+class WebAppInterceptor(
+ private val context: Context,
+ private val manifestStorage: ManifestStorage,
+ private val launchFromInterceptor: Boolean = true,
+) : RequestInterceptor {
+
+ @Suppress("ReturnCount")
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ val scope = manifestStorage.getInstalledScope(uri) ?: return null
+ val startUrl = manifestStorage.getStartUrlForInstalledScope(scope) ?: return null
+ val intent = createIntentFromUri(startUrl, uri)
+
+ if (!launchFromInterceptor) {
+ return RequestInterceptor.InterceptionResponse.AppIntent(intent, uri)
+ }
+
+ intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(intent)
+
+ return RequestInterceptor.InterceptionResponse.Deny
+ }
+
+ /**
+ * Creates a new VIEW_PWA intent for a URL.
+ *
+ * @param uri target URL for the new intent
+ */
+ private fun createIntentFromUri(startUrl: String, urlOverride: String = startUrl): Intent {
+ return Intent(WebAppIntentProcessor.ACTION_VIEW_PWA, Uri.parse(startUrl)).apply {
+ this.addCategory(Intent.CATEGORY_DEFAULT)
+ this.putUrlOverride(urlOverride)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppLauncherActivity.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppLauncherActivity.kt
new file mode 100644
index 0000000000..fe4eb94897
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppLauncherActivity.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 mozilla.components.feature.pwa
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import androidx.appcompat.app.AppCompatActivity
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * This activity is launched by Web App shortcuts on the home screen.
+ *
+ * Based on the Web App Manifest (display) it will decide whether the app is launched in the
+ * browser or in a standalone activity.
+ */
+class WebAppLauncherActivity : AppCompatActivity() {
+
+ private val scope = MainScope()
+ private val logger = Logger("WebAppLauncherActivity")
+ private lateinit var storage: ManifestStorage
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ storage = ManifestStorage(applicationContext)
+
+ val startUrl = intent.data ?: return finish()
+
+ scope.launch {
+ val manifest = loadManifest(startUrl.toString())
+ routeManifest(startUrl, manifest)
+
+ finish()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ scope.cancel()
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun routeManifest(startUrl: Uri, manifest: WebAppManifest?) {
+ when (manifest?.display) {
+ WebAppManifest.DisplayMode.FULLSCREEN,
+ WebAppManifest.DisplayMode.STANDALONE,
+ WebAppManifest.DisplayMode.MINIMAL_UI,
+ -> {
+ emitHomescreenIconTapFact()
+ launchWebAppShell(startUrl)
+ }
+
+ // If no manifest is saved for this site, just open the browser.
+ WebAppManifest.DisplayMode.BROWSER, null -> launchBrowser(startUrl)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun launchBrowser(startUrl: Uri) {
+ val intent = Intent(Intent.ACTION_VIEW, startUrl).apply {
+ addCategory(SHORTCUT_CATEGORY)
+ `package` = packageName
+ }
+
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ logger.error("Package does not handle VIEW intent. Can't launch browser.")
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun launchWebAppShell(startUrl: Uri) {
+ val intent = Intent(ACTION_VIEW_PWA, startUrl).apply {
+ `package` = packageName
+ }
+
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ logger.error("Packages does not handle ACTION_VIEW_PWA intent. Can't launch as web app.", e)
+ // Fall back to normal browser
+ launchBrowser(startUrl)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal suspend fun loadManifest(startUrl: String): WebAppManifest? {
+ return storage.loadManifest(startUrl)
+ }
+
+ companion object {
+ internal const val ACTION_PWA_LAUNCHER = "mozilla.components.feature.pwa.PWA_LAUNCHER"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppShortcutManager.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppShortcutManager.kt
new file mode 100644
index 0000000000..a97b3b8415
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppShortcutManager.kt
@@ -0,0 +1,287 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_MAIN
+import android.content.Intent.CATEGORY_HOME
+import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.pm.ShortcutManager
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.getSystemService
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+import androidx.core.net.toUri
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.decoder.ICOIconDecoder
+import mozilla.components.browser.icons.extension.toIconRequest
+import mozilla.components.browser.icons.generator.DefaultIconGenerator
+import mozilla.components.browser.icons.loader.DataUriIconLoader
+import mozilla.components.browser.icons.loader.HttpIconLoader
+import mozilla.components.browser.icons.loader.MemoryIconLoader
+import mozilla.components.browser.icons.preparer.MemoryIconPreparer
+import mozilla.components.browser.icons.processor.AdaptiveIconProcessor
+import mozilla.components.browser.icons.processor.ColorProcessor
+import mozilla.components.browser.icons.processor.MemoryIconProcessor
+import mozilla.components.browser.icons.processor.ResizingProcessor
+import mozilla.components.browser.icons.utils.IconMemoryCache
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.feature.pwa.WebAppLauncherActivity.Companion.ACTION_PWA_LAUNCHER
+import mozilla.components.feature.pwa.ext.hasLargeIcons
+import mozilla.components.feature.pwa.ext.installableManifest
+import mozilla.components.support.images.decoder.AndroidImageDecoder
+import mozilla.components.support.utils.PendingIntentUtils
+import java.util.UUID
+
+private val pwaIconMemoryCache = IconMemoryCache()
+
+const val SHORTCUT_CATEGORY = mozilla.components.feature.customtabs.SHORTCUT_CATEGORY
+
+/**
+ * Helper to manage pinned shortcuts for websites.
+ *
+ * @param httpClient Fetch client used to load website icons.
+ * @param storage Storage used to save web app manifests to disk.
+ * @param supportWebApps If true, Progressive Web Apps will be pinnable.
+ * If false, all web sites will be bookmark shortcuts even if they have a manifest.
+ */
+class WebAppShortcutManager(
+ context: Context,
+ httpClient: Client,
+ private val storage: ManifestStorage,
+ internal val supportWebApps: Boolean = true,
+) {
+
+ internal val icons = webAppIcons(context, httpClient)
+
+ private val fallbackLabel = context.getString(R.string.mozac_feature_pwa_default_shortcut_label)
+
+ /**
+ * Request to create a new shortcut on the home screen.
+ * @param context The current context.
+ * @param session The session to create the shortcut for.
+ * @param overrideShortcutName (optional) The name of the shortcut. Ignored for PWAs.
+ */
+ suspend fun requestPinShortcut(
+ context: Context,
+ session: SessionState,
+ overrideShortcutName: String? = null,
+ ) {
+ if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
+ val manifest = session.installableManifest()
+ val shortcut = if (supportWebApps && manifest != null) {
+ emitPwaInstallFact()
+ buildWebAppShortcut(context, manifest)
+ } else {
+ buildBasicShortcut(context, session, overrideShortcutName)
+ }
+
+ if (shortcut != null) {
+ val intent = Intent(ACTION_MAIN).apply {
+ addCategory(CATEGORY_HOME)
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntentUtils.defaultFlags or FLAG_UPDATE_CURRENT,
+ )
+ val intentSender = pendingIntent.intentSender
+
+ ShortcutManagerCompat.requestPinShortcut(context, shortcut, intentSender)
+ }
+ }
+ }
+
+ /**
+ * Update existing PWA shortcuts with the latest info from web app manifests.
+ *
+ * Devices before 7.1 do not allow shortcuts to be dynamically updated,
+ * so this method will do nothing.
+ */
+ suspend fun updateShortcuts(context: Context, manifests: List<WebAppManifest>) {
+ if (SDK_INT >= VERSION_CODES.N_MR1) {
+ context.getSystemService<ShortcutManager>()?.apply {
+ val shortcuts = manifests.mapNotNull { buildWebAppShortcut(context, it)?.toShortcutInfo() }
+ updateShortcuts(shortcuts)
+ }
+ }
+ }
+
+ /**
+ * Create a new basic pinned website shortcut using info from the session.
+ * Consuming `SHORTCUT_CATEGORY` in `AndroidManifest` is required for the package to be launched
+ */
+ suspend fun buildBasicShortcut(
+ context: Context,
+ session: SessionState,
+ overrideShortcutName: String? = null,
+ ): ShortcutInfoCompat {
+ val shortcutIntent = Intent(Intent.ACTION_VIEW, session.content.url.toUri()).apply {
+ addCategory(SHORTCUT_CATEGORY)
+ `package` = context.packageName
+ }
+
+ val manifest = session.content.webAppManifest
+ val shortLabel = overrideShortcutName
+ ?: manifest?.shortName
+ ?: manifest?.name
+ ?: session.content.title
+
+ val fallback = fallbackLabel
+ val fixedLabel = shortLabel.ifBlank { fallback }
+
+ val builder = ShortcutInfoCompat.Builder(context, UUID.randomUUID().toString())
+ .setShortLabel(fixedLabel)
+ .setIntent(shortcutIntent)
+
+ val icon = if (manifest != null && manifest.hasLargeIcons()) {
+ buildIconFromManifest(manifest)
+ } else {
+ session.content.icon?.let { IconCompat.createWithBitmap(it) }
+ }
+ icon?.let {
+ builder.setIcon(it)
+ }
+
+ return builder.build()
+ }
+
+ /**
+ * Create a new Progressive Web App shortcut using a web app manifest.
+ */
+ suspend fun buildWebAppShortcut(
+ context: Context,
+ manifest: WebAppManifest,
+ ): ShortcutInfoCompat? {
+ val shortcutIntent = Intent(context, WebAppLauncherActivity::class.java).apply {
+ action = ACTION_PWA_LAUNCHER
+ data = manifest.startUrl.toUri()
+ flags = FLAG_ACTIVITY_NEW_DOCUMENT
+ `package` = context.packageName
+ }
+
+ val shortLabel = manifest.shortName ?: manifest.name
+ storage.saveManifest(manifest)
+
+ return ShortcutInfoCompat.Builder(context, manifest.startUrl)
+ .setLongLabel(manifest.name)
+ .setShortLabel(shortLabel.ifBlank { fallbackLabel })
+ .setIcon(buildIconFromManifest(manifest))
+ .setIntent(shortcutIntent)
+ .build()
+ }
+
+ @VisibleForTesting
+ internal suspend fun buildIconFromManifest(manifest: WebAppManifest): IconCompat {
+ val request = manifest.toIconRequest()
+ val icon = icons.loadIcon(request).await()
+ return if (icon.maskable) {
+ IconCompat.createWithAdaptiveBitmap(icon.bitmap)
+ } else {
+ IconCompat.createWithBitmap(icon.bitmap)
+ }
+ }
+
+ /**
+ * Finds the shortcut associated with the given startUrl.
+ * This method can be used to check if a web app was added to the homescreen.
+ */
+ fun findShortcut(context: Context, startUrl: String) =
+ if (SDK_INT >= VERSION_CODES.N_MR1) {
+ context.getSystemService<ShortcutManager>()?.pinnedShortcuts?.find { it.id == startUrl }
+ } else {
+ null
+ }
+
+ /**
+ * Checks if there is a currently installed web app to which this URL belongs.
+ *
+ * @param url url that is covered by the scope of a web app installed by the user
+ * @param currentTimeMs the current time in milliseconds, exposed for testing
+ */
+ suspend fun getWebAppInstallState(
+ url: String,
+ @VisibleForTesting currentTimeMs: Long = System.currentTimeMillis(),
+ ): WebAppInstallState {
+ if (storage.hasRecentManifest(url, currentTimeMs = currentTimeMs)) {
+ return WebAppInstallState.Installed
+ }
+
+ return WebAppInstallState.NotInstalled
+ }
+
+ /**
+ * Counts number of recently used web apps. See [ManifestStorage.activeThresholdMs].
+ *
+ * @param activeThresholdMs defines a time window within which a web app is considered recently used.
+ * Defaults to [ManifestStorage.ACTIVE_THRESHOLD_MS].
+ * @return count of recently used web apps
+ */
+ suspend fun recentlyUsedWebAppsCount(
+ activeThresholdMs: Long = ManifestStorage.ACTIVE_THRESHOLD_MS,
+ ): Int {
+ return storage.recentManifestsCount(activeThresholdMs = activeThresholdMs)
+ }
+
+ /**
+ * Updates the usedAt timestamp of the web app this url is associated with.
+ *
+ * @param manifest the manifest to update
+ */
+ suspend fun reportWebAppUsed(manifest: WebAppManifest): Unit? {
+ return storage.updateManifestUsedAt(manifest)
+ }
+
+ /**
+ * Possible install states of a Web App.
+ */
+ enum class WebAppInstallState {
+ NotInstalled,
+ Installed,
+ }
+}
+
+/**
+ * Creates custom version of [BrowserIcons] for loading web app icons.
+ *
+ * This version has its own cache to avoid affecting tab icons.
+ */
+private fun webAppIcons(
+ context: Context,
+ httpClient: Client,
+) = BrowserIcons(
+ context = context,
+ httpClient = httpClient,
+ generator = DefaultIconGenerator(cornerRadiusDimen = null),
+ preparers = listOf(
+ MemoryIconPreparer(pwaIconMemoryCache),
+ ),
+ loaders = listOf(
+ MemoryIconLoader(pwaIconMemoryCache),
+ HttpIconLoader(httpClient),
+ DataUriIconLoader(),
+ ),
+ decoders = listOf(
+ AndroidImageDecoder(),
+ ICOIconDecoder(),
+ ),
+ processors = listOf(
+ MemoryIconProcessor(pwaIconMemoryCache),
+ ResizingProcessor(),
+ ColorProcessor(),
+ AdaptiveIconProcessor(),
+ ),
+)
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppUseCases.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppUseCases.kt
new file mode 100644
index 0000000000..b598706d03
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/WebAppUseCases.kt
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import android.content.Context
+import androidx.core.content.pm.ShortcutManagerCompat
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.pwa.ext.installableManifest
+
+/**
+ * These use cases allow for adding a web app or web site to the homescreen.
+ */
+class WebAppUseCases(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val shortcutManager: WebAppShortcutManager,
+) {
+ /**
+ * Checks if the launcher supports adding shortcuts.
+ */
+ fun isPinningSupported() =
+ ShortcutManagerCompat.isRequestPinShortcutSupported(applicationContext)
+
+ /**
+ * Checks to see if the current session can be installed as a Progressive Web App.
+ */
+ fun isInstallable() =
+ store.state.selectedTab?.installableManifest() != null && shortcutManager.supportWebApps
+
+ /**
+ * Let the user add the selected session to the homescreen.
+ *
+ * If the selected session represents a Progressive Web App, then the
+ * manifest will be saved and the web app will be launched based on the
+ * manifest values.
+ *
+ * Otherwise, the pinned shortcut will act like a simple bookmark for the site.
+ */
+ class AddToHomescreenUseCase internal constructor(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val shortcutManager: WebAppShortcutManager,
+ ) {
+
+ /**
+ * @param overrideBasicShortcutName (optional) Custom label used if the current session
+ * is NOT a Progressive Web App
+ */
+ suspend operator fun invoke(overrideBasicShortcutName: String? = null) {
+ val session = store.state.selectedTab ?: return
+ shortcutManager.requestPinShortcut(applicationContext, session, overrideBasicShortcutName)
+ }
+ }
+
+ val addToHomescreen by lazy {
+ AddToHomescreenUseCase(applicationContext, store, shortcutManager)
+ }
+
+ /**
+ * Checks the current install state of a Web App.
+ *
+ * Returns WebAppShortcutManager.InstallState.Installed if the user has installed
+ * or used the web app in the past 30 days.
+ *
+ * Otherwise, WebAppShortcutManager.InstallState.NotInstalled is returned.
+ */
+ class GetInstallStateUseCase internal constructor(
+ private val store: BrowserStore,
+ private val shortcutManager: WebAppShortcutManager,
+ ) {
+ /**
+ * @param currentTimeMs the current time against which manifest usage timeouts will be validated
+ */
+ suspend operator fun invoke(
+ currentTimeMs: Long = System.currentTimeMillis(),
+ ): WebAppShortcutManager.WebAppInstallState? {
+ val session = store.state.selectedTab ?: return null
+ return shortcutManager.getWebAppInstallState(session.content.url, currentTimeMs)
+ }
+ }
+
+ val getInstallState by lazy {
+ GetInstallStateUseCase(store, shortcutManager)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestConverter.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestConverter.kt
new file mode 100644
index 0000000000..2f93f373db
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestConverter.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 mozilla.components.feature.pwa.db
+
+import androidx.room.TypeConverter
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifestParser
+
+/**
+ * Converts a web app manifest to and from JSON strings
+ */
+internal class ManifestConverter {
+ private val parser = WebAppManifestParser()
+
+ @TypeConverter
+ fun fromJsonString(json: String): WebAppManifest =
+ when (val result = parser.parse(json)) {
+ is WebAppManifestParser.Result.Success -> result.manifest
+ is WebAppManifestParser.Result.Failure -> throw result.exception
+ }
+
+ @TypeConverter
+ fun toJsonString(manifest: WebAppManifest): String = parser.serialize(manifest).toString()
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDao.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDao.kt
new file mode 100644
index 0000000000..5db04cc8fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDao.kt
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.db
+
+import androidx.annotation.WorkerThread
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+
+/**
+ * Internal DAO for accessing [ManifestEntity] instances.
+ */
+@Dao
+internal interface ManifestDao {
+ @WorkerThread
+ @Query("SELECT * from manifests WHERE start_url = :startUrl")
+ fun getManifest(startUrl: String): ManifestEntity?
+
+ @WorkerThread
+ @Query("SELECT * from manifests WHERE :url LIKE scope||'%' ORDER BY LENGTH(scope) DESC")
+ fun getManifestsByScope(url: String): List<ManifestEntity>
+
+ @WorkerThread
+ @Query("SELECT count(start_url) from manifests WHERE :url LIKE scope||'%' AND used_at > :thresholdMs")
+ fun hasRecentManifest(url: String, thresholdMs: Long): Int
+
+ @WorkerThread
+ @Query("SELECT count(start_url) from manifests WHERE used_at > :thresholdMs")
+ fun recentManifestsCount(thresholdMs: Long): Int
+
+ @WorkerThread
+ @Query(
+ """
+ SELECT * from manifests
+ WHERE has_share_targets == 1
+ AND used_at > :deadline
+ ORDER BY used_at DESC
+ """,
+ )
+ fun getRecentShareableManifests(deadline: Long): List<ManifestEntity>
+
+ @WorkerThread
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertManifest(manifest: ManifestEntity): Long
+
+ @WorkerThread
+ @Update
+ fun updateManifest(manifest: ManifestEntity)
+
+ @WorkerThread
+ @Query("DELETE FROM manifests WHERE start_url IN (:startUrls)")
+ fun deleteManifests(startUrls: List<String>)
+
+ @WorkerThread
+ @Query("SELECT * from manifests WHERE used_at > :expiresAt ORDER BY LENGTH(scope)")
+ fun getInstalledScopes(expiresAt: Long): List<ManifestEntity>
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDatabase.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDatabase.kt
new file mode 100644
index 0000000000..fdf36d5734
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestDatabase.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 mozilla.components.feature.pwa.db
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+/**
+ * Internal database for storing web app manifests and metadata.
+ */
+@Database(entities = [ManifestEntity::class], version = 3)
+@TypeConverters(ManifestConverter::class)
+internal abstract class ManifestDatabase : RoomDatabase() {
+ abstract fun manifestDao(): ManifestDao
+
+ @Suppress("MagicNumber")
+ companion object {
+ @Volatile private var instance: ManifestDatabase? = null
+
+ @VisibleForTesting
+ internal val MIGRATION_1_2: Migration = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ val cursor = db.query("SELECT * FROM manifests LIMIT 0,1")
+
+ if (cursor.getColumnIndex("used_at") < 0) {
+ db.execSQL("ALTER TABLE manifests ADD COLUMN used_at INTEGER NOT NULL DEFAULT 0")
+ }
+
+ if (cursor.getColumnIndex("scope") < 0) {
+ db.execSQL("ALTER TABLE manifests ADD COLUMN scope TEXT")
+ }
+
+ db.execSQL("CREATE INDEX IF NOT EXISTS index_manifests_scope ON manifests (scope)")
+ db.execSQL("UPDATE manifests SET used_at = updated_at WHERE used_at = 0")
+ }
+ }
+
+ @VisibleForTesting
+ internal val MIGRATION_2_3: Migration = object : Migration(2, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("ALTER TABLE manifests ADD COLUMN has_share_targets INTEGER NOT NULL DEFAULT 0")
+
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS index_manifests_has_share_targets ON manifests (has_share_targets)",
+ )
+ }
+ }
+
+ @Synchronized
+ fun get(context: Context): ManifestDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ ManifestDatabase::class.java,
+ "manifests",
+ ).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build().also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestEntity.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestEntity.kt
new file mode 100644
index 0000000000..84ab17ac27
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/db/ManifestEntity.kt
@@ -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/. */
+
+package mozilla.components.feature.pwa.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.concept.engine.manifest.WebAppManifest
+
+/**
+ * Internal entity representing a web app manifest.
+ */
+@Entity(tableName = "manifests")
+internal data class ManifestEntity(
+ val manifest: WebAppManifest,
+
+ @PrimaryKey
+ @ColumnInfo(name = "start_url")
+ val startUrl: String,
+
+ @ColumnInfo(name = "scope", index = true)
+ val scope: String?,
+
+ @ColumnInfo(name = "has_share_targets", index = true)
+ val hasShareTargets: Int,
+
+ @ColumnInfo(name = "created_at")
+ val createdAt: Long,
+
+ @ColumnInfo(name = "updated_at")
+ val updatedAt: Long,
+
+ @ColumnInfo(name = "used_at")
+ val usedAt: Long,
+) {
+ constructor(
+ manifest: WebAppManifest,
+ currentTime: Long = System.currentTimeMillis(),
+ ) : this(
+ manifest,
+ startUrl = manifest.startUrl,
+ scope = manifest.scope,
+ hasShareTargets = if (manifest.shareTarget != null) 1 else 0,
+ createdAt = currentTime,
+ updatedAt = currentTime,
+ usedAt = currentTime,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Activity.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Activity.kt
new file mode 100644
index 0000000000..a64d4c4c86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Activity.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 mozilla.components.feature.pwa.ext
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifest.Orientation
+
+/**
+ * Sets the requested orientation of the [Activity] to the orientation provided by the given [WebAppManifest] (See
+ * [WebAppManifest.orientation] and [WebAppManifest.Orientation].
+ */
+fun Activity.applyOrientation(manifest: WebAppManifest?) {
+ requestedOrientation = when (manifest?.orientation) {
+ Orientation.NATURAL, Orientation.ANY, null -> ActivityInfo.SCREEN_ORIENTATION_USER
+ Orientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ Orientation.LANDSCAPE_PRIMARY -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ Orientation.LANDSCAPE_SECONDARY -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
+ Orientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+ Orientation.PORTRAIT_PRIMARY -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ Orientation.PORTRAIT_SECONDARY -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Bundle.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Bundle.kt
new file mode 100644
index 0000000000..44670d4b10
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Bundle.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 mozilla.components.feature.pwa.ext
+
+import android.os.Bundle
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifestParser
+import mozilla.components.concept.engine.manifest.getOrNull
+
+internal const val EXTRA_WEB_APP_MANIFEST = "mozilla.components.feature.pwa.EXTRA_WEB_APP_MANIFEST"
+
+/**
+ * Serializes and inserts a [WebAppManifest] value into the mapping of this [Bundle],
+ * replacing any existing web app manifest.
+ */
+fun Bundle.putWebAppManifest(webAppManifest: WebAppManifest?) {
+ val json = webAppManifest?.let { WebAppManifestParser().serialize(it).toString() }
+ putString(EXTRA_WEB_APP_MANIFEST, json)
+}
+
+/**
+ * Parses and returns the [WebAppManifest] associated with this [Bundle],
+ * or null if no mapping of the desired type exists.
+ */
+fun Bundle.getWebAppManifest(): WebAppManifest? {
+ return getString(EXTRA_WEB_APP_MANIFEST)?.let { json ->
+ WebAppManifestParser().parse(json).getOrNull()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/CustomTabState.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/CustomTabState.kt
new file mode 100644
index 0000000000..2b949cabe4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/CustomTabState.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 mozilla.components.feature.pwa.ext
+
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.VerificationStatus.PENDING
+import mozilla.components.feature.customtabs.store.VerificationStatus.SUCCESS
+
+/**
+ * Returns a list of trusted (or pending) origins.
+ */
+val CustomTabState.trustedOrigins
+ get() = relationships.mapNotNull { (pair, status) ->
+ if (pair.relation == RELATION_HANDLE_ALL_URLS && (status == PENDING || status == SUCCESS)) {
+ pair.origin
+ } else {
+ null
+ }
+ }
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Intent.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Intent.kt
new file mode 100644
index 0000000000..7258a7341c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Intent.kt
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.ext
+
+import android.content.Intent
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifestParser
+
+internal const val EXTRA_URL_OVERRIDE = "mozilla.components.feature.pwa.EXTRA_URL_OVERRIDE"
+
+/**
+ * Add extended [WebAppManifest] data to the intent.
+ */
+fun Intent.putWebAppManifest(webAppManifest: WebAppManifest) {
+ val json = WebAppManifestParser().serialize(webAppManifest)
+ putExtra(EXTRA_WEB_APP_MANIFEST, json.toString())
+}
+
+/**
+ * Retrieve extended [WebAppManifest] data from the intent.
+ */
+fun Intent.getWebAppManifest(): WebAppManifest? {
+ return extras?.getWebAppManifest()
+}
+
+/**
+ * Add [String] URL override to the intent.
+ *
+ * @param url The URL override value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see [getUrlOverride]
+ */
+fun Intent.putUrlOverride(url: String?): Intent {
+ return putExtra(EXTRA_URL_OVERRIDE, url)
+}
+
+/**
+ * Retrieves [String] Url override from the intent.
+ *
+ * @return The URL override previously added with [putUrlOverride],
+ * or null if no URL was found.
+ */
+fun Intent.getUrlOverride(): String? = getStringExtra(EXTRA_URL_OVERRIDE)
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/SessionState.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/SessionState.kt
new file mode 100644
index 0000000000..27cb4cf780
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/SessionState.kt
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.ext
+
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifest.DisplayMode.BROWSER
+
+/**
+ * Checks if the current session represents an installable web app.
+ * If so, return the web app manifest. Otherwise, return null.
+ *
+ * Websites are installable if:
+ * - The site is served over HTTPS
+ * - The site has a valid manifest with a name or short_name
+ * - The manifest display mode is standalone, fullscreen, or minimal-ui
+ * - The icons array in the manifest contains an icon of at least 192x192
+ */
+fun SessionState.installableManifest(): WebAppManifest? {
+ val manifest = content.webAppManifest ?: return null
+ return if (content.securityInfo.secure && manifest.display != BROWSER && manifest.hasLargeIcons()) {
+ manifest
+ } else {
+ null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Uri.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Uri.kt
new file mode 100644
index 0000000000..4fac485fd1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/Uri.kt
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.ext
+
+import android.net.Uri
+
+/**
+ * Returns just the origin of the [Uri].
+ *
+ * The origin is a URL that contains only the scheme, host, and port.
+ * https://html.spec.whatwg.org/multipage/origin.html#concept-origin
+ *
+ * Null is returned if the URI was invalid (i.e.: `"/foo/bar".toUri()`)
+ */
+fun Uri.toOrigin(): Uri? {
+ var authority = host
+ if (port != -1) {
+ authority += ":$port"
+ }
+
+ val result = Uri.Builder().scheme(scheme).encodedAuthority(authority).build().normalizeScheme()
+ return if (result.toString().isNotEmpty()) result else null
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/WebAppManifest.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/WebAppManifest.kt
new file mode 100644
index 0000000000..c222a3c73d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/ext/WebAppManifest.kt
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.ext
+
+import android.app.ActivityManager.TaskDescription
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.net.Uri
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.core.net.toUri
+import mozilla.components.browser.state.state.ColorSchemeParams
+import mozilla.components.browser.state.state.ColorSchemes
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.engine.manifest.WebAppManifest.Icon.Purpose
+import mozilla.components.support.utils.ColorUtils.isDark
+
+private const val MIN_INSTALLABLE_ICON_SIZE = 192
+
+/**
+ * Checks if the web app manifest can be used to create a shortcut icon.
+ *
+ * Websites have an installable icon if the manifest contains an icon of at least 192x192.
+ * @see [installableManifest]
+ */
+fun WebAppManifest.hasLargeIcons() = icons.any { icon ->
+ (Purpose.ANY in icon.purpose || Purpose.MASKABLE in icon.purpose) &&
+ icon.sizes.any { size ->
+ size.minLength >= MIN_INSTALLABLE_ICON_SIZE
+ }
+}
+
+/**
+ * Creates a [TaskDescription] for the activity manager based on the manifest.
+ *
+ * Since the web app icon is provided dynamically by the web site, we can't provide a resource ID.
+ * Instead we use the deprecated constructor.
+ */
+@Suppress("Deprecation")
+fun WebAppManifest.toTaskDescription(icon: Bitmap?) =
+ TaskDescription(name, icon, themeColor ?: 0)
+
+/**
+ * Creates a [CustomTabConfig] that styles a custom tab toolbar to match the manifest theme.
+ */
+fun WebAppManifest.toCustomTabConfig(): CustomTabConfig {
+ val backgroundColor = this.backgroundColor
+ val colorSchemes = if (themeColor != null && backgroundColor != null) {
+ ColorSchemes(
+ ColorSchemeParams(
+ toolbarColor = themeColor,
+ navigationBarColor = getVersionSafeNavBarColor(backgroundColor),
+ ),
+ )
+ } else {
+ null
+ }
+
+ return CustomTabConfig(
+ colorSchemes = colorSchemes,
+ closeButtonIcon = null,
+ enableUrlbarHiding = true,
+ actionButtonConfig = null,
+ showCloseButton = false,
+ showShareMenuItem = true,
+ menuItems = emptyList(),
+ externalAppType = ExternalAppType.PROGRESSIVE_WEB_APP,
+ )
+}
+
+private fun getVersionSafeNavBarColor(backgroundColor: Int) = if (SDK_INT >= Build.VERSION_CODES.O) {
+ if (isDark(backgroundColor)) Color.BLACK else Color.WHITE
+} else {
+ null
+}
+
+/**
+ * Returns the scope of the manifest as a [Uri] for use
+ * with [mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature].
+ *
+ * Null is returned when the scope should be ignored, such as with display: minimal-ui,
+ * where the toolbar should always be displayed.
+ */
+fun WebAppManifest.getTrustedScope(): Uri? {
+ return when (display) {
+ WebAppManifest.DisplayMode.FULLSCREEN,
+ WebAppManifest.DisplayMode.STANDALONE,
+ -> (scope ?: startUrl).toUri()
+
+ WebAppManifest.DisplayMode.MINIMAL_UI,
+ WebAppManifest.DisplayMode.BROWSER,
+ -> null
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeature.kt
new file mode 100644
index 0000000000..9b9c30cbbd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeature.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 mozilla.components.feature.pwa.feature
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.WebAppShortcutManager
+import mozilla.components.lib.state.ext.flow
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Feature used to update the existing web app manifest and web app shortcut.
+ *
+ * @param shortcutManager Shortcut manager used to update pinned shortcuts.
+ * @param storage Manifest storage used to have updated manifests.
+ * @param sessionId ID of the web app session to observe.
+ * @param initialManifest Loaded manifest for the current web app.
+ */
+class ManifestUpdateFeature(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val shortcutManager: WebAppShortcutManager,
+ private val storage: ManifestStorage,
+ private val sessionId: String,
+ private var initialManifest: WebAppManifest,
+) : LifecycleAwareFeature {
+
+ private var scope: CoroutineScope? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var updateJob: Job? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var updateUsageJob: Job? = null
+
+ /**
+ * Updates the manifest on disk then updates the pinned shortcut to reflect changes.
+ */
+ @VisibleForTesting
+ internal suspend fun updateStoredManifest(manifest: WebAppManifest) {
+ storage.updateManifest(manifest)
+ shortcutManager.updateShortcuts(applicationContext, listOf(manifest))
+ initialManifest = manifest
+ }
+
+ override fun start() {
+ scope = MainScope().also { observeManifestChanges(it) }
+ updateUsageJob?.cancel()
+
+ updateUsageJob = scope?.launch {
+ storage.updateManifestUsedAt(initialManifest)
+ }
+ }
+
+ private fun observeManifestChanges(scope: CoroutineScope) = scope.launch {
+ store.flow()
+ .mapNotNull { state -> state.findCustomTab(sessionId) }
+ .map { tab -> tab.content.webAppManifest }
+ .distinctUntilChanged()
+ .collect { manifest -> onWebAppManifestChanged(manifest) }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ /**
+ * When the manifest is changed, compare it to the existing manifest.
+ * If it is different, update the disk and shortcut. Ignore if called with a null
+ * manifest or a manifest with a different start URL.
+ */
+ private fun onWebAppManifestChanged(manifest: WebAppManifest?) {
+ if (manifest?.startUrl == initialManifest.startUrl && manifest != initialManifest) {
+ updateJob?.cancel()
+ updateUsageJob?.cancel()
+
+ updateJob = scope?.launch { updateStoredManifest(manifest) }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/SiteControlsBuilder.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/SiteControlsBuilder.kt
new file mode 100644
index 0000000000..f7c1f3c1a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/SiteControlsBuilder.kt
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.feature
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.widget.Toast
+import androidx.core.content.getSystemService
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.feature.pwa.R
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.utils.PendingIntentUtils
+
+/**
+ * Callback for [WebAppSiteControlsFeature] that lets the displayed notification be customized.
+ */
+interface SiteControlsBuilder {
+
+ /**
+ * Create the notification to be displayed. Initial values are set in the provided [builder]
+ * and additional actions can be added here. Actions should be represented as [PendingIntent]
+ * that are filtered by [getFilter] and handled in [onReceiveBroadcast].
+ */
+ fun buildNotification(context: Context, builder: Notification.Builder)
+
+ /**
+ * Return an intent filter that matches the actions specified in [buildNotification].
+ */
+ fun getFilter(): IntentFilter
+
+ /**
+ * Handle actions the user selected in the site controls notification.
+ */
+ fun onReceiveBroadcast(context: Context, tab: CustomTabSessionState, intent: Intent)
+
+ /**
+ * Default implementation of [SiteControlsBuilder] that copies the URL of the site when tapped.
+ */
+ open class Default : SiteControlsBuilder {
+
+ override fun getFilter() = IntentFilter().apply {
+ addAction(ACTION_COPY)
+ }
+
+ override fun buildNotification(context: Context, builder: Notification.Builder) {
+ val copyIntent = createPendingIntent(context, ACTION_COPY, 1)
+
+ builder.setContentText(context.getString(R.string.mozac_feature_pwa_site_controls_notification_text))
+ builder.setContentIntent(copyIntent)
+ }
+
+ override fun onReceiveBroadcast(context: Context, tab: CustomTabSessionState, intent: Intent) {
+ when (intent.action) {
+ ACTION_COPY -> {
+ context.getSystemService<ClipboardManager>()?.let { clipboardManager ->
+ clipboardManager.setPrimaryClip(ClipData.newPlainText(tab.content.url, tab.content.url))
+ Toast.makeText(
+ context,
+ context.getString(R.string.mozac_feature_pwa_copy_success),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+ }
+
+ protected fun createPendingIntent(context: Context, action: String, requestCode: Int): PendingIntent {
+ val intent = Intent(action)
+ intent.setPackage(context.packageName)
+ return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntentUtils.defaultFlags)
+ }
+
+ companion object {
+ private const val ACTION_COPY = "mozilla.components.feature.pwa.COPY"
+ }
+ }
+
+ /**
+ * Implementation of [SiteControlsBuilder] that adds a Refresh button and
+ * copies the URL of the site when tapped.
+ */
+ class CopyAndRefresh(
+ private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
+ ) : Default() {
+
+ override fun getFilter() = super.getFilter().apply {
+ addAction(ACTION_REFRESH)
+ }
+
+ override fun buildNotification(context: Context, builder: Notification.Builder) {
+ super.buildNotification(context, builder)
+
+ val title = context.getString(R.string.mozac_feature_pwa_site_controls_refresh)
+ val intent = createPendingIntent(context, ACTION_REFRESH, 2)
+ val refreshAction = if (SDK_INT >= Build.VERSION_CODES.M) {
+ Notification.Action.Builder(
+ Icon.createWithResource(context, R.drawable.ic_refresh),
+ title,
+ intent,
+ )
+ } else {
+ @Suppress("Deprecation")
+ Notification.Action.Builder(R.drawable.ic_refresh, title, intent)
+ }.build()
+
+ builder.addAction(refreshAction)
+ }
+
+ override fun onReceiveBroadcast(context: Context, tab: CustomTabSessionState, intent: Intent) {
+ when (intent.action) {
+ ACTION_REFRESH -> reloadUrlUseCase(tab.id)
+ else -> super.onReceiveBroadcast(context, tab, intent)
+ }
+ }
+
+ companion object {
+ private const val ACTION_REFRESH = "mozilla.components.feature.pwa.REFRESH"
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppActivityFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppActivityFeature.kt
new file mode 100644
index 0000000000..81c6424641
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppActivityFeature.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 mozilla.components.feature.pwa.feature
+
+import android.app.Activity
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.extension.toIconRequest
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.ext.applyOrientation
+import mozilla.components.feature.pwa.ext.toTaskDescription
+import mozilla.components.support.ktx.android.view.enterImmersiveMode
+
+/**
+ * Feature used to handle window effects for "standalone" and "fullscreen" web apps.
+ */
+class WebAppActivityFeature(
+ private val activity: Activity,
+ private val icons: BrowserIcons,
+ private val manifest: WebAppManifest,
+) : DefaultLifecycleObserver {
+
+ private val scope = MainScope()
+
+ override fun onResume(owner: LifecycleOwner) {
+ if (manifest.display == WebAppManifest.DisplayMode.FULLSCREEN) {
+ activity.enterImmersiveMode()
+ }
+
+ activity.applyOrientation(manifest)
+
+ scope.launch {
+ updateRecentEntry()
+ }
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ scope.cancel()
+ }
+
+ @VisibleForTesting
+ internal suspend fun updateRecentEntry() {
+ val icon = icons.loadIcon(manifest.toIconRequest()).await()
+
+ activity.setTaskDescription(manifest.toTaskDescription(icon.bitmap))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppContentFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppContentFeature.kt
new file mode 100644
index 0000000000..975c774716
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppContentFeature.kt
@@ -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/. */
+
+package mozilla.components.feature.pwa.feature
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+
+/**
+ * Feature used to handle web content settings from manifest file.
+ */
+class WebAppContentFeature(
+ private val store: BrowserStore,
+ private val tabId: String? = null,
+ private val manifest: WebAppManifest,
+) : DefaultLifecycleObserver {
+
+ override fun onCreate(owner: LifecycleOwner) {
+ setDisplayMode(manifest.display)
+ }
+
+ private fun setDisplayMode(display: WebAppManifest.DisplayMode) {
+ val tab = store.state.findTabOrCustomTabOrSelectedTab(tabId)
+ tab?.engineState?.engineSession?.setDisplayMode(display)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeature.kt
new file mode 100644
index 0000000000..954793bb80
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeature.kt
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.feature
+
+import androidx.core.net.toUri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.pwa.ext.getTrustedScope
+import mozilla.components.feature.pwa.ext.trustedOrigins
+import mozilla.components.lib.state.ext.flow
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.ktx.android.net.isInScope
+
+/**
+ * Hides a custom tab toolbar for Progressive Web Apps and Trusted Web Activities.
+ *
+ * When the tab with [tabId] is inside a trusted scope, the toolbar will be hidden.
+ * Once the tab with [tabId] navigates to another scope, the toolbar will be revealed.
+ * The toolbar is also hidden in fullscreen mode or picture in picture mode.
+ *
+ * In standard custom tabs, no scopes are trusted.
+ * As a result the URL has no impact on toolbar visibility.
+ *
+ * @param store Reference to the browser store where tab state is located.
+ * @param customTabsStore Reference to the store that communicates with the custom tabs service.
+ * @param tabId ID of the tab session, or null if the selected session should be used.
+ * @param manifest Reference to the cached [WebAppManifest] for the current PWA.
+ * Null if this feature is not used in a PWA context.
+ * @param setToolbarVisibility Callback to show or hide the toolbar.
+ */
+class WebAppHideToolbarFeature(
+ private val store: BrowserStore,
+ private val customTabsStore: CustomTabsServiceStore,
+ private val tabId: String? = null,
+ manifest: WebAppManifest? = null,
+ private val setToolbarVisibility: (Boolean) -> Unit,
+) : LifecycleAwareFeature {
+
+ private val manifestScope = listOfNotNull(manifest?.getTrustedScope())
+ private var scope: CoroutineScope? = null
+
+ init {
+ // Hide the toolbar by default to prevent a flash.
+ val tab = store.state.findTabOrCustomTabOrSelectedTab(tabId)
+ val customTabState = customTabsStore.state.getCustomTabStateForTab(tab)
+ setToolbarVisibility(shouldToolbarBeVisible(tab, customTabState))
+ }
+
+ override fun start() {
+ scope = MainScope().apply {
+ launch {
+ // Since we subscribe to both store and customTabsStore,
+ // we don't extend another non-external-apps feature for hiding the toolbar
+ // as very little code would be shared.
+ val sessionFlow = store.flow()
+ .map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) }
+ .distinctUntilChanged()
+ val customTabServiceMapFlow = customTabsStore.flow()
+
+ sessionFlow.combine(customTabServiceMapFlow) { tab, customTabServiceState ->
+ tab to customTabServiceState.getCustomTabStateForTab(tab)
+ }
+ .map { (tab, customTabState) -> shouldToolbarBeVisible(tab, customTabState) }
+ .distinctUntilChanged()
+ .collect { toolbarVisible ->
+ setToolbarVisibility(toolbarVisible)
+ }
+ }
+ }
+ }
+
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ /**
+ * Reports if the toolbar should be shown for the given external app session.
+ * If the URL is in the same scope as the [WebAppManifest]
+ */
+ private fun shouldToolbarBeVisible(
+ session: SessionState?,
+ customTabState: CustomTabState?,
+ ): Boolean {
+ val url = session?.content?.url?.toUri() ?: return true
+
+ val trustedOrigins = customTabState?.trustedOrigins.orEmpty()
+ val inScope = url.isInScope(manifestScope + trustedOrigins)
+
+ return !inScope && !session.content.fullScreen && !session.content.pictureInPictureEnabled
+ }
+
+ /**
+ * Find corresponding custom tab state, if any.
+ */
+ private fun CustomTabsServiceState.getCustomTabStateForTab(
+ tab: SessionState?,
+ ): CustomTabState? {
+ return (tab as? CustomTabSessionState)?.config?.sessionToken?.let { sessionToken ->
+ tabs[sessionToken]
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeature.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeature.kt
new file mode 100644
index 0000000000..b53ce3ec5a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeature.kt
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.feature
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.BADGE_ICON_NONE
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.launch
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.extension.toMonochromeIconRequest
+import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.R
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.utils.ext.registerReceiverCompat
+
+/**
+ * Displays site controls notification for fullscreen web apps.
+ * @param sessionId ID of the web app session to observe.
+ * @param manifest Web App Manifest reference used to populate the notification.
+ * @param controlsBuilder Customizes the created notification.
+ */
+class WebAppSiteControlsFeature(
+ private val applicationContext: Context,
+ private val store: BrowserStore,
+ private val sessionId: String,
+ private val manifest: WebAppManifest? = null,
+ private val controlsBuilder: SiteControlsBuilder = SiteControlsBuilder.Default(),
+ private val icons: BrowserIcons? = null,
+ private val notificationsDelegate: NotificationsDelegate,
+) : BroadcastReceiver(), DefaultLifecycleObserver {
+
+ constructor(
+ applicationContext: Context,
+ store: BrowserStore,
+ reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
+ sessionId: String,
+ manifest: WebAppManifest? = null,
+ controlsBuilder: SiteControlsBuilder = SiteControlsBuilder.CopyAndRefresh(reloadUrlUseCase),
+ icons: BrowserIcons? = null,
+ notificationsDelegate: NotificationsDelegate,
+ ) : this(
+ applicationContext,
+ store,
+ sessionId,
+ manifest,
+ controlsBuilder,
+ icons,
+ notificationsDelegate,
+ )
+
+ private var notificationIcon: Deferred<mozilla.components.browser.icons.Icon>? = null
+
+ /**
+ * Starts loading the [notificationIcon] on create.
+ */
+ override fun onCreate(owner: LifecycleOwner) {
+ if (SDK_INT >= Build.VERSION_CODES.M && manifest != null && icons != null) {
+ val request = manifest.toMonochromeIconRequest()
+ if (request.resources.isNotEmpty()) {
+ notificationIcon = icons.loadIcon(request)
+ }
+ }
+ }
+
+ /**
+ * Displays a notification from the given [SiteControlsBuilder.buildNotification] that will be
+ * shown as long as the lifecycle is in the foreground. Registers this class as a broadcast
+ * receiver to receive events from the notification and call [SiteControlsBuilder.onReceiveBroadcast].
+ */
+ override fun onResume(owner: LifecycleOwner) {
+ val filter = controlsBuilder.getFilter()
+ registerReceiver(filter)
+
+ val iconAsync = notificationIcon
+ if (iconAsync != null) {
+ owner.lifecycleScope.launch {
+ val bitmap = iconAsync.await().bitmap
+ notificationsDelegate.notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification(bitmap))
+ }
+ } else {
+ notificationsDelegate.notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification(null))
+ }
+ }
+
+ @VisibleForTesting
+ internal fun registerReceiver(filter: IntentFilter) {
+ applicationContext.registerReceiverCompat(
+ this,
+ filter,
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+ }
+
+ /**
+ * Cancels the site controls notification and unregisters the broadcast receiver.
+ */
+ override fun onPause(owner: LifecycleOwner) {
+ applicationContext.unregisterReceiver(this)
+
+ NotificationManagerCompat.from(applicationContext)
+ .cancel(NOTIFICATION_TAG, NOTIFICATION_ID)
+ }
+
+ /**
+ * Cancels the [notificationIcon] loading job on destroy.
+ */
+ override fun onDestroy(owner: LifecycleOwner) {
+ notificationIcon?.cancel()
+ }
+
+ /**
+ * Responds to [PendingIntent]s fired by the site controls notification.
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ store.state.findCustomTab(sessionId)?.also { tab ->
+ controlsBuilder.onReceiveBroadcast(context, tab, intent)
+ }
+ }
+
+ /**
+ * Build the notification with site controls to be displayed while the web app is active.
+ */
+ private fun buildNotification(icon: Bitmap?): Notification {
+ val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
+ val channelId = ensureChannelExists()
+ Notification.Builder(applicationContext, channelId).apply {
+ setBadgeIconType(BADGE_ICON_NONE)
+ }
+ } else {
+ @Suppress("Deprecation")
+ Notification.Builder(applicationContext).apply {
+ setPriority(Notification.PRIORITY_MIN)
+ }
+ }
+ if (icon != null && SDK_INT >= Build.VERSION_CODES.M) {
+ builder.setSmallIcon(Icon.createWithBitmap(icon))
+ } else {
+ builder.setSmallIcon(R.drawable.ic_pwa)
+ }
+ builder.setContentTitle(manifest?.name ?: manifest?.shortName)
+ builder.setColor(manifest?.themeColor ?: NotificationCompat.COLOR_DEFAULT)
+ builder.setShowWhen(false)
+ builder.setOngoing(true)
+ controlsBuilder.buildNotification(applicationContext, builder)
+ return builder.build()
+ }
+
+ /**
+ * Make sure a notification channel for site controls notifications exists.
+ *
+ * Returns the channel id to be used for notifications.
+ */
+ private fun ensureChannelExists(): String {
+ if (SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager: NotificationManager = applicationContext.getSystemService()!!
+
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ applicationContext.getString(R.string.mozac_feature_pwa_site_controls_notification_channel),
+ NotificationManager.IMPORTANCE_MIN,
+ )
+
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ return NOTIFICATION_CHANNEL_ID
+ }
+
+ companion object {
+ private const val NOTIFICATION_CHANNEL_ID = "Site Controls"
+ private const val NOTIFICATION_TAG = "SiteControls"
+ private const val NOTIFICATION_ID = 1
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt
new file mode 100644
index 0000000000..8038ad7a88
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.intent
+
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsSessionToken
+import androidx.browser.trusted.TrustedWebActivityIntentBuilder.EXTRA_ADDITIONAL_TRUSTED_ORIGINS
+import androidx.core.net.toUri
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.feature.customtabs.createCustomTabConfigFromIntent
+import mozilla.components.feature.customtabs.feature.OriginVerifierFeature
+import mozilla.components.feature.customtabs.isTrustedWebActivityIntent
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.intent.ext.putSessionId
+import mozilla.components.feature.intent.processing.IntentProcessor
+import mozilla.components.feature.pwa.ext.toOrigin
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.service.digitalassetlinks.RelationChecker
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.utils.toSafeIntent
+
+/**
+ * Processor for intents which open Trusted Web Activities.
+ */
+@Deprecated("TWAs are not supported. See https://github.com/mozilla-mobile/android-components/issues/12024")
+class TrustedWebActivityIntentProcessor(
+ private val addNewTabUseCase: CustomTabsUseCases.AddCustomTabUseCase,
+ packageManager: PackageManager,
+ relationChecker: RelationChecker,
+ private val store: CustomTabsServiceStore,
+) : IntentProcessor {
+
+ private val verifier = OriginVerifierFeature(packageManager, relationChecker) { store.dispatch(it) }
+ private val scope = MainScope()
+
+ private fun matches(intent: Intent): Boolean {
+ val safeIntent = intent.toSafeIntent()
+ return safeIntent.action == ACTION_VIEW && isTrustedWebActivityIntent(safeIntent)
+ }
+
+ override fun process(intent: Intent): Boolean {
+ val safeIntent = SafeIntent(intent)
+ val url = safeIntent.dataString
+
+ return if (!url.isNullOrEmpty() && matches(intent)) {
+ val customTabConfig = createCustomTabConfigFromIntent(intent, null).copy(
+ externalAppType = ExternalAppType.TRUSTED_WEB_ACTIVITY,
+ )
+
+ val tabId = addNewTabUseCase.invoke(
+ url,
+ source = SessionState.Source.Internal.HomeScreen,
+ customTabConfig = customTabConfig,
+ )
+
+ intent.putSessionId(tabId)
+
+ customTabConfig.sessionToken?.let { token ->
+ val origin = listOfNotNull(safeIntent.data?.toOrigin())
+ val additionalOrigins = safeIntent
+ .getStringArrayListExtra(EXTRA_ADDITIONAL_TRUSTED_ORIGINS)
+ .orEmpty()
+ .mapNotNull { it.toUri().toOrigin() }
+
+ // Launch verification separately so the intent processing isn't held up
+ scope.launch {
+ verify(token, origin + additionalOrigins)
+ }
+ }
+
+ true
+ } else {
+ false
+ }
+ }
+
+ private suspend fun verify(token: CustomTabsSessionToken, origins: List<Uri>) {
+ val tabState = store.state.tabs[token] ?: return
+ origins.map { origin ->
+ scope.async {
+ verifier.verify(tabState, token, RELATION_HANDLE_ALL_URLS, origin)
+ }
+ }.awaitAll()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt
new file mode 100644
index 0000000000..5669638355
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.intent
+
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.browser.state.state.SessionState.Source
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.intent.ext.putSessionId
+import mozilla.components.feature.intent.processing.IntentProcessor
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.ext.getUrlOverride
+import mozilla.components.feature.pwa.ext.putWebAppManifest
+import mozilla.components.feature.pwa.ext.toCustomTabConfig
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.utils.toSafeIntent
+
+/**
+ * Processor for intents which trigger actions related to web apps.
+ */
+class WebAppIntentProcessor(
+ private val store: BrowserStore,
+ private val addTabUseCase: CustomTabsUseCases.AddWebAppTabUseCase,
+ private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
+ private val storage: ManifestStorage,
+) : IntentProcessor {
+
+ /**
+ * Returns true if this intent should launch a progressive web app.
+ */
+ private fun matches(intent: Intent) =
+ intent.toSafeIntent().action == ACTION_VIEW_PWA
+
+ /**
+ * Processes the given [Intent] by creating a [Session] with a corresponding web app manifest.
+ *
+ * A custom tab config is also set so a custom tab toolbar can be shown when the user leaves
+ * the scope defined in the manifest.
+ */
+ override fun process(intent: Intent): Boolean {
+ val url = intent.toSafeIntent().dataString
+
+ return if (!url.isNullOrEmpty() && matches(intent)) {
+ val webAppManifest = runBlocking { storage.loadManifest(url) } ?: return false
+ val targetUrl = intent.getUrlOverride() ?: url
+
+ val id = findExistingSession(webAppManifest) ?: createSession(webAppManifest, url)
+
+ if (targetUrl !== url) {
+ loadUrlUseCase(targetUrl, id, EngineSession.LoadUrlFlags.external())
+ }
+
+ intent.flags = FLAG_ACTIVITY_NEW_DOCUMENT
+ intent.putSessionId(id)
+ intent.putWebAppManifest(webAppManifest)
+
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Returns an existing web app session that matches the manifest.
+ */
+ private fun findExistingSession(webAppManifest: WebAppManifest): String? {
+ return store.state.customTabs.find { tab ->
+ tab.config.externalAppType == ExternalAppType.PROGRESSIVE_WEB_APP &&
+ tab.content.webAppManifest?.startUrl == webAppManifest.startUrl
+ }?.id
+ }
+
+ /**
+ * Returns a new web app session.
+ */
+ private fun createSession(webAppManifest: WebAppManifest, url: String): String {
+ return addTabUseCase.invoke(
+ url = url,
+ source = Source.Internal.HomeScreen,
+ webAppManifest = webAppManifest,
+ customTabConfig = webAppManifest.toCustomTabConfig(),
+ )
+ }
+
+ companion object {
+ const val ACTION_VIEW_PWA = "mozilla.components.feature.pwa.VIEW_PWA"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_pwa.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_pwa.xml
new file mode 100644
index 0000000000..1c5e00215d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_pwa.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="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M16.72 14.42l0.58-1.46h1.67l-0.8-2.22 1-2.5L22 15.76h-2.1l-0.48-1.35h-2.7zM14.94 15.77l3.03-7.54h-2.01l-2.08 4.87-1.48-4.86h-1.54L9.27 13.1l-1.12-2.22-1 3.12 1.02 1.77h1.98l1.43-4.37 1.37 4.37h1.99zM3.91 13.18h1.24c0.38 0 0.71-0.04 1-0.13l0.32-0.98 0.9-2.76A2.2 2.2 0 0 0 7.14 9c-0.46-0.51-1.14-0.77-2.02-0.77H2v7.54h1.91v-2.59zm1.64-3.21c0.18 0.18 0.27 0.42 0.27 0.72s-0.08 0.55-0.24 0.73c-0.17 0.2-0.49 0.3-0.95 0.3H3.9V9.7h0.72c0.44 0 0.74 0.09 0.92 0.27z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_refresh.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_refresh.xml
new file mode 100644
index 0000000000..9f9d3120f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/drawable/ic_refresh.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="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M21,4.04a0.96,0.96 0,0 0,-0.96 0.96V8a8.981,8.981 0,1 0,-1.676 10.361A1,1 0,0 0,16.95 16.95a7.031,7.031 0,1 1,1.72 -6.91H15a0.96,0.96 0,0 0,0 1.92h6a0.96,0.96 0,0 0,0.96 -0.96V5A0.96,0.96 0,0 0,21 4.04Z"/>
+</vector>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..7a7d6451cd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-am/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ድረ-ገፅ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">የሙሉ ማያ ገጽ መቆጣጠሪያዎች</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">የዚህን መተግበሪያ URL ለመቅዳት ይንኩ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">አድስ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ተቀድቷል።</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..0220920827
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-an/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Puesto web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controls d\'o puesto en pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca pa copiar la URL d\'esta aplicación</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refrescar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..39989440c7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ar/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">الموقع</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">تحكّمات الموقع حين يملأ الشاشة</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">انقر لنسخ مسار هذا التطبيق</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">أعِد التحميل</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">نُسِخَ المسار.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..5befbc6461
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ast/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitiu web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles de sitios a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca pa copiar la URL d\'esta aplicación</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Anovar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Copióse la URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..3405a6766e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-az/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sayt</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Tam ekran sayt idarəsi</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Bu tətbiqin ünvanını köçürtmək üçün toxunun</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Yenilə</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Ünvan köçürüldü.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..b7d75d181f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-azb/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">وبسایت</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">بوتون صفحه سایت کونترول‌لاری</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">بو اپ‌ین اینترنت آدرسینی کوپی ائتمک اوچون توخونون.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">رفرش</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">اینترنت آدرسی کوپی اولدو.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..15441343cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ban/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Situs web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrol situs layar penuh</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ketuk anggen nyalin URL ring aplikasi niki</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Segerang</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kasalin.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..29222db78a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-be/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Вэб-сайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Элементы кіравання поўнаэкранным сайтам</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Націсніце, каб скапіяваць URL для гэтай праграмы</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Абнавіць</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL скапіяваны.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..ad6a638ee7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bg/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Страница</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Управление сайт на цял екран</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">При докосване се копира адреса на приложението</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Презареждане</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Адресът е копиран.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..70fc4ae777
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bn/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ওয়েবসাইট</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">পূর্ণ পর্দার সাইট নিয়ন্ত্রণ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">অ্যাপের ইউআরএল অনুলিপি করাতে ট্যাপ করুন</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">রিফ্রেশ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL কপি হয়েছে</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..f54073d383
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-br/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Lec’hienn</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Reoliadurioù al lec’hienn er skramm a-bezh</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Stokit da eilañ an URL evit an arload-mañ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Azbevaat</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL eilet.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..0966d3a0e2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-bs/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Web stranica</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen kontrole sajta</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Dodirnite za kopiranje URL-a za ovu aplikaciju</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Osvježi</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopiran.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..16131de509
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ca/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Lloc web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controls de lloc en pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toqueu per copiar l’URL d’aquesta aplicació</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualitza</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">S’ha copiat l’URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..91544b69e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cak/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Ruxaq Ajk\'amaya\'l</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Ruchajixik ruxaq pa tz\'aqät ruwa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tachapa\' richin nawachib\'ej ri URL richin re chokoy re\'</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Titzolïx</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL xwachib\'ëx</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..6083a4c297
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen nga control sa site</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">i-Tap para makopya ang URL ani nga app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL nakopya na.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..a3ce4aa97a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ماڵپەڕ</string>
+
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">پەنجە دابگرە بۆ لەبەرگرتنەوەی بەستەری ئەم بەرنامەیە</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">نوێکردنەوە</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">بەستەر لەبەرگیرا.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..bce7b4f5fa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-co/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Situ web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Cuntrolli di situ di u screnu sanu</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Picchichjà per cupià l’indirizzu per st’appiecazione</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Attualizà</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Indirizzu web cupiatu.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..03818cb038
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cs/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Server</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Ovládací prvky režimu celé obrazovky</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Klepnutím zkopírujte URL adresu pro tuto aplikaci</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Znovu načíst</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL adresa zkopírována.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..97cc456da3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-cy/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Gwefan</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Rheolaethau gwefan sgrin lawn</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tapio i gopïo’r URL ar gyfer yr ap hwn</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Adnewyddu</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL wedi ei gopïo</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..39491424b5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-da/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Websted</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrolelementer til websted i fuld skærm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tryk for at kopiere webadressen til denne app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Opdater</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopieret.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..71f1d22443
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-de/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Website-Steuerelemente im Vollbildmodus</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tippen Sie hier, um die URL für diese App zu kopieren</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Aktualisieren</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Adresse kopiert.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..6cde676900
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Websedło</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Wóźeńske elementy sedła w połnej wobrazowce</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Pótusniśo how, aby URL za toś to nałoženje kopěrował</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Aktualizěrowaś</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL jo kopěrowany.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..c558ee23a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-el/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Ιστότοπος</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Κουμπιά ελέγχου πλήρους οθόνης</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Πατήστε για αντιγραφή URL για αυτήν την εφαρμογή</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Ανανέωση</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Το URL αντιγράφτηκε.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..35087f8f0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen site controls</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tap to copy the URL for this app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copied.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..3db32067f2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Web site</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen site controls</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tap to copy the URL for this app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copied.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..05a6de8646
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-eo/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Retejo</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Regiloj por retejo en plenekrana reĝimo</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tuŝetu por kopii la retadreson por tiu ĉi programo</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refreŝigi</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Retadreso kopiita.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..bcc3bd196e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio para pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocá para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Recargar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..ae5c98253c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refrescar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..ae5c98253c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refrescar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..9e298da4ab
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Recargar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..ae5c98253c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-es/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles del sitio a pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca para copiar la URL de esta aplicación.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refrescar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..69e329519f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-et/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sait</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Täisekraanil saidi juhtnupud</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Puuduta selle äpi URLi kopeerimiseks</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Värskenda</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopeeritud.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..242ee3b357
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-eu/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Webgunea</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Pantaila osoaren gune-kontrolak</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Sakatu aplikazio honen URLa kopiatzeko</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Berritu</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URLa kopiatu da</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..efbe1dcc1b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fa/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">پایگاه اینترنتی</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">واپایش‌های پایگاه در حالت تمام‌صفحه</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">برای رونوشت از نشانی این کاره ضربه بزنید</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">بازخوانی</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">نشانی رونوشت شد.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..908a906e22
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fi/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Verkkosivusto</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Koko näytön sivuston säätimet</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Napauta kopioidaksesi tämän sovelluksen verkko-osoitteen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Päivitä</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Osoite kopioitu.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..15df8f7e02
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Contrôles du site en plein écran</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Appuyez pour copier l’URL de cette application</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualiser</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Adresse web copiée.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..f0c272c217
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fur/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sît web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controi dal sît a plen schermi</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocje par copiâ l’URL par cheste aplicazion</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Inzorne</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiât.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..c2afbbb476
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sitebetsjinning folslein skerm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tik om de URL foar dizze app te kopiearjen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Fernije</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopiearre.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..1f2a1324cc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gd/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Làrach-lìn</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Uidheaman-smachd na làraich airson na làn-sgrìn</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Thoir gnogag airson lethbhreac a dhèanamh de URL na h-aplacaid seo</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Ath-nuadhaich</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Chaidh lethbhreac dhen URL a dhèanamh.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..4aa4400b79
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gl/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controis do sitio en pantalla completa</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toque para copiar o URL desta aplicación</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualizar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Copiouse o URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..e57bf2ce43
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gn/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Ñanduti renda</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Tenda ñeñangreko mba’erechaha tuichavévape</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Eikutu embohasa hag̃ua URL ko tembiporu’ípe.</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Mbopiro’y</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL monguatiapyre</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..ada97dea46
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">વેબસાઇટ</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..39ed3f939e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">वेबसाइट</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">पूर्ण स्क्रीन साइट नियंत्रक </string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">इस ऐप के लिए URL को कॉपी करने के लिए टैप करें</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">रीफ़्रेश करें</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL कॉपी हो गया।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..568bf7f541
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hil/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..03f463821d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Web stranica</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrole stranice u prikazu preko cijelog ekrana</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Pritisnite za kopiranje URLa za ovu aplikaciju</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Osvježi</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL je kopiran.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..633468894a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Websydło</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Wodźenske elementy sydła w połnej wobrazowce</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Podótkńće so tu, zo byšće URL za tute nałoženje kopěrował</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Aktualizować</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL je kopěrowany.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..61641ae97b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hu/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Webhely</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Teljes képernyős webhelyvezérlők</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Koppintson az alkalmazás URL-jének másolásához</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Frissítés</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL másolva.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..003da434df
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Կայք</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Լիաէկրան կայքի կառավարում</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Հպեք՝ այս հավելվածի համար URL-ն պատճենելու համար</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Թարմացնել</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL-ն պատճենվել է</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..c0aa5b6764
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ia/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sito web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controlos del sito a plen schermo</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocca pro copiar le URL de iste app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualisar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiate.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..803706cbf5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-in/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Situs web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kendali situs layar penuh</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ketuk untuk menyalin URL untuk apl ini</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Segarkan</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL tersalin.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..d0a050f9e9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-is/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Vefsvæði</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Stýringar vefsvæðis á öllum skjánum</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ýttu til að afrita slóðina fyrir þetta forrit</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Endurlesa</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Vistfang afritað.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..2988de3743
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-it/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sito web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controlli del sito a schermo intero</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocca per copiare l’indirizzo per questa app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Aggiorna</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Indirizzo copiato.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..f498515972
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-iw/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">אתר</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">פקדי אתר במסך מלא</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">יש להקיש כדי להעתיק את הכתובת עבור יישומון זה</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">רענון</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">הקישור הועתק.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..02fc90da65
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ja/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ウェブサイト</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">全画面サイトコントロール</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">タップしてこのアプリの URL をコピー</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">再読み込み</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL をコピーしました。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..4f1536cdea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ka/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ვებსაიტი</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">საიტის სამართავი სრულ ეკრანზე</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">შეეხეთ, ბმულის ასლის ასაღებად ამ აპისთვის</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">განახლება</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">ბმული აღებულია.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..9541854019
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sayt</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sayttı tolıq ekranda kórıw basqarıwları</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text"> Bul baǵdarlamaǵa siltemeni kóshirip alıw ushın basıń</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Jańalaw</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kóshirip alındı.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..1eb4cd2882
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kab/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Asmel Web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Isenqaden n ugdil aččuran n usmel</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Senned akken ad tneɣleḍ tansa URL i usnas-a</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Smiren</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Tansa URL tettwanɣel.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..d9eba97a18
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kk/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Веб-сайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Толық экрандағы сайттарды басқару элементтері</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Бұл қолданбаның URL сілтемесін көшіру үшін шертіңіз</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Жаңарту</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL сілтемесі көшірілді.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..91c7e53cac
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Malper</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrolên malpera ekrana tije</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ji bo navnîşana vê sepanê kopî bikî, bitikîne</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Nû bike</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Navnîşan hate kopîkirin.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..14b0a62186
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-kn/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ಜಾಲತಾಣ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">ಪೂರ್ಣ ಪರದೆ ಜಾಲ ನಿಯಂತ್ರಣಗಳು</string>
+ </resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..fa6ca5c1e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ko/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">웹 사이트</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">전체 화면 사이트 컨트롤</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">이 앱의 URL을 복사하려면 누르세요</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">새로 고침</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL 복사됨.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..a2b17fe149
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-lo/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ເວັບໄຊທ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">ການຄວບຄຸມເວັບໄຊທແບບເຕັ້ມຫນ້າຈໍ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ແຕະເພື່ອສຳເນົາ URL ສຳລັບແອັບນີ້</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ລີເຟສ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">ສຳເນົາ URL ແລ້ວ</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..078d9b4364
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-lt/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Svetainė</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Svetainės valdikliai viso ekrano veiksenoje</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Bakstelėkite, norėdami nukopijuoti šios programos URL</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Atnaujinti</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL nukopijuotas.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..ae0c204c72
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-mix/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio Web</string>
+
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Ndatava URL</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..e7a5b0bb4c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-mr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">संकेतस्थळ</string>
+
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">पुनःदाखल करा</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ची प्रत बनवली.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..a47c9238b4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-my/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ဝဘ်ဆိုက်</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">မျက်နှာပြင်အပြည့် ဆိုက် ထိန်းချုပ်မှု</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ဤ အက်ပ် အတွက် URL ကူးယူရန်  ထိတို့ ပါ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ပြန်ရယူရန်</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ကူးပြီး။</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..e474cf3d04
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Nettsted</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrollelementer til nettsted i fullskjerm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Trykk for å kopiere nettadressen for denne appen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Oppdater</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopiert.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..91dc6c3930
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">वेबसाइट</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">पूर्ण स्क्रिन साइट नियन्त्रणहरू</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">यो एपको URL कपि गर्नका लागि ट्याप गर्नुहोस्</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ताजा गर्नुहोस्</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL कपि गरियो।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..6001040634
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nl/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sitebediening volledig scherm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tik om de URL voor deze app te kopiëren</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Vernieuwen</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL gekopieerd.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..137d50a6ad
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Nettstad</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrollelement til nettstad i fullskjerm</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Trykk for å kopiere nettadressa for denne appen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Oppdater</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopiert.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..8467e79ffe
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-oc/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Contraròtles del plen ecran</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tocatz per copiar l’URL d’aquesta aplicacion</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualizar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..5879423c5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-or/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL କପି କରିନିଆଗଲା।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..8d5d5ed3c9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ਵੈੱਬਸਾਈਟ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">ਪੂਰੀ ਸਕਰੀਨ ਸਾਈਟ ਕੰਟਰੋਲ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ਇਸ ਐਪ ਲਈ URL ਨੂੰ ਕਾਪੀ ਕਰਨ ਲਈ ਛੂਹੋ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ਤਾਜ਼ਾ ਕਰੋ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ਕਾਪੀ ਕੀਤਾ।</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..8b74c1300b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ویب‌سائٹ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">پوری سکرین دیاں چوݨاں</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ایس ایپ لئی پتے نوں کاپی کرن لئی چھوہو</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">تازہ کرو</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">کاپی کیتا گیا۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..ee86506d6e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pl/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Witryna</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sterowanie witryną pełnoekranową</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Stuknij, aby skopiować adres tej aplikacji</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Odśwież</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Skopiowano adres</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..3190b55f61
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controles de site em tela inteira</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toque para copiar a URL deste aplicativo</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Atualizar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiada.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..a1c384e152
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controlos de site em ecrã completo</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toque para copiar o endereço para esta aplicação</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Atualizar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Endereço copiado.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..5fe99cd6fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-rm/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controllas da la website en maletg entir</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tutgar per copiar l\'URL da questa app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Actualisar</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Copià l\'URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..6505043d1f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ro/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Site web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Comenzi pentru site la vizualizare pe tot ecranul</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Atinge pentru copierea URL-ului acestei aplicații</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Reîmprospătează</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiat.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..b06c6827b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ru/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Сайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Управление полноэкранным просмотром сайта</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Нажмите, чтобы скопировать ссылку для этого приложения</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Обновить</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Сетевой адрес скопирован.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..574d4473ff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sat/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ᱣᱮᱵᱥᱟᱭᱤᱴ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">ᱯᱩᱨᱟᱹ ᱤᱥᱠᱨᱤᱱ ᱥᱟᱭᱤᱴ ᱥᱟᱸᱢᱲᱟᱣ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ᱱᱚᱶᱟ ᱮᱯ ᱨᱮᱭᱟᱜ URL ᱱᱚᱠᱚᱞ ᱞᱟᱹᱜᱤᱫ ᱛᱮ ᱡᱚᱴᱮᱫ ᱢᱮ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">ᱱᱟᱶᱟ ᱟᱹᱨᱩ</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ᱱᱚᱠᱚᱞᱮᱱᱟ ᱾</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..7e4fd21077
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sc/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Situ web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Controllos de sitos a ischermu intreu</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toca pro copiare s’URL pro custa aplicatzione</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Atualiza</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copiadu.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..1b5d601500
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-si/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">අඩවිය</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">පූර්ණ තිර අඩවි පාලන</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">මෙම යෙදුම සඳහා ඒ.ස.නි. පිටපත් කිරීමට ඔබන්න</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">නැවුම් කරන්න</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">ඒ.ස.නි. පිටපත් විය</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..5c4d020bb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sk/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Webová stránka</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Ovládacie prvky pre stránku v režime celej obrazovky</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Ťuknutím skopírujete webovú adresu pre túto aplikáciu</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Obnoviť</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Adresa bola skopírovaná.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..c22b7fc8f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-skr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ویب سائٹ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">فل سکرین سائٹ کنٹرول</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ایں ایپ کنوں یوآرایل دی نقل کرݨ کیتے انگل پھیرو</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">تازہ کرو</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">یوآرایل نقل تھی ڳیا۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..8e1c5cc9be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sl/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Spletno mesto</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrolniki celozaslonskega načina</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tapnite, da kopirate spletni naslov za to aplikacijo</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Osveži</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Spletni naslov kopiran.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..39c3df59b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sq/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sajt</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrolle sajti sa krejt ekrani</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Prekeni që të kopjohet URL-ja për këtë aplikacion</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Rifreskoje</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL-ja u kopjua.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..6a9bec3c3b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Веб страница</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Контроле странице на целом екрану</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Додирните да копирате адресу за ову апликацију</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Освежи</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Адреса је копирана.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..631735b8d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-su/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Raramatloka</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kontrol loka layar pinuh</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Toél pikeun niron URL jang ieu aplikasi</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Segerkeun</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ditiron.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..de149d7368
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Webbplats</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Helskärmskontroller på webbsidan</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tryck om du vill kopiera webbadressen till appen</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Uppdatera</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL kopierad</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..ff445ac441
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ta/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">வலைத்தளம்</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">முழு திரை தள கட்டுப்பாடுகள்</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">இச்செயலிக்கான உரலியை நகலெடுக்க தட்டவும்</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">புதுப்பி</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">தொடுப்பு நகலெடுக்கப்பட்டது.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..ad827968d3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-te/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">వెబ్‌సైటు</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">నిండు తెర సైటు నియంత్రణలు</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">ఈ అనువర్తనపు చిరునామాను కాపీచేసుకోడానికి తాకండి</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">రిఫ్రెష్ చేయి</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL కాపీ అయ్యింది.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..414344900a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tg/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Сомона</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Идоракунии намоиши сомона дар экрани пурра</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Барои нусха бардоштани нишонии ин барнома зарба занед</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Нав кардан</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Нишонӣ нусха бардошта шуд.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..31c77f5ac2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-th/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">เว็บไซต์</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">การควบคุมไซต์แบบเต็มหน้าจอ</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">แตะเพื่อคัดลอก URL สำหรับแอปนี้</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">เรียกใหม่</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">คัดลอก URL แล้ว</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..f354c17c1a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tl/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Mga full screen site control</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">i-Tap para makopya ang URL para sa app na ito</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">i-Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Nakopya na ang URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..cfdc11dd95
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tr/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Web sitesi</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Tam ekran site kontrolleri</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Bu uygulamanın adresini kopyalamak için dokunun</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Yenile</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Adres kopyalandı.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..6bb020883e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-trs/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sitio web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Si kontrô sîtio guendâ nahuin gachrà\’ riña aga\’a</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Gūru\’man ra\’a da\’ gūxūnt si URL app nan</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Nāgi\'iaj nākà</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Ngà naka URL</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..fb48394eff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tt/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Веб-сайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Тулы экрандагы сайтлар белән идарә итү</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Бу кушымтаның URL сылтамасын күчереп алу өчен басыгыз</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Яңарту</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL копияләнде.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..3985de99de
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Asmel Web</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..8b222cc65c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ug/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">تور بېكەت</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">بېكەت پۈتۈن ئېكران كونترولى</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">چېكىلسە ئەپنىڭ تور ئادرېسىنى كۆچۈرىدۇ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">يېڭىلا</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL كۆچۈرۈلدى.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..b73352ef4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-uk/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Вебсайт</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Елементи керування сайтом в повноекранному режимі</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Торкніться, щоб скопіювати URL-адресу цієї програми</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Оновити</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL скопійовано.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..688c60ee5f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-ur/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">ویب سائٹ</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">فل سکرین سائٹ کنٹرول</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">اس ایپ سے URL کی نقل کرنے کے لئے ٹیپ پڑیں</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">تازہ کریں</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL کی نقل کی گئی۔</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..6b50d12b0d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-uz/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Sayt</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Saytni toʻliq ekranda koʻrish boshqaruvlari</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Bu ilova uchun URLdan nusxa olish uchun bosing</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Yangilash</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URLdan nusxa olindi.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..1c69b01df5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-vi/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Trang web</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Kiểm soát trang web toàn màn hình</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Nhấn để sao chép URL cho ứng dụng này</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Làm mới</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">Đã sao chép URL.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..8f67cb1d1c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-yo/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Ojúlé wẹ́ẹ̀bù</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Sáíìtì ìṣàkóso ìwòran gbogbogbò</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tẹ̀ẹ́ to bá fẹ́ da URL fún áàpù yí kọ</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Sọdọ̀tun</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL ti wà ní àdàkọ.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..0830d7ca1e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">网站</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">网站全屏控件</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">点按即可复制此应用程序的网址</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">刷新</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">网址已复制。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..72649c8bc8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">網站</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">全螢幕網站控制元件</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">點擊即可複製此應用程式的網址</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">重新整理</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">已複製網址。</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..c53d2c2b16
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?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>
+ <!-- Default shortcut label used if website has no title -->
+ <string name="mozac_feature_pwa_default_shortcut_label">Website</string>
+
+ <!-- Name of the "notification channel" used for displaying site controls notification. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_feature_pwa_site_controls_notification_channel">Full screen site controls</string>
+ <!-- Text shown on the second row of the site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_notification_text">Tap to copy the URL for this app</string>
+ <!-- Refresh button in site controls notification. -->
+ <string name="mozac_feature_pwa_site_controls_refresh">Refresh</string>
+ <!-- Toast displayed when the website URL is copied. -->
+ <string name="mozac_feature_pwa_copy_success">URL copied.</string>
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/main/res/values/styles.xml b/mobile/android/android-components/components/feature/pwa/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..46b03ddac9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/main/res/values/styles.xml
@@ -0,0 +1,16 @@
+<?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>
+
+ <style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.NoActionBar">
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt
new file mode 100644
index 0000000000..078619b892
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt
@@ -0,0 +1,305 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.db.ManifestDao
+import mozilla.components.feature.pwa.db.ManifestEntity
+import mozilla.components.support.test.any
+import mozilla.components.support.test.capture
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class ManifestStorageTest {
+
+ private val firefoxManifest = WebAppManifest(
+ name = "Firefox",
+ startUrl = "https://firefox.com",
+ scope = "/",
+ )
+
+ private val googleMapsManifest = WebAppManifest(
+ name = "Google Maps",
+ startUrl = "https://google.com/maps",
+ scope = "https://google.com/maps/",
+ )
+
+ private val exampleWebAppManifest = WebAppManifest(
+ name = "Example Web App",
+ startUrl = "https://pwa.example.com/dashboard",
+ scope = "https://pwa.example.com/",
+ )
+
+ @Test
+ fun `load returns null if entry does not exist`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ mockDatabase(storage)
+ assertNull(storage.loadManifest("https://example.com"))
+ }
+
+ @Test
+ fun `load returns valid manifest`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org")
+ whenever(dao.getManifest("https://mozilla.org"))
+ .thenReturn(ManifestEntity(manifest))
+
+ assertEquals(manifest, storage.loadManifest("https://mozilla.org"))
+ }
+
+ @Test
+ fun `save saves the manifest as JSON`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ storage.saveManifest(firefoxManifest)
+ verify(dao).insertManifest(any())
+ Unit
+ }
+
+ @Test
+ fun `update replaces the manifest as JSON`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val existing = ManifestEntity(firefoxManifest, currentTime = 0)
+
+ `when`(dao.getManifest("https://firefox.com")).thenReturn(existing)
+
+ storage.updateManifest(firefoxManifest)
+ verify(dao).updateManifest(any())
+ Unit
+ }
+
+ @Test
+ fun `update does not replace non-existed manifest`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ `when`(dao.getManifest("https://firefox.com")).thenReturn(null)
+
+ storage.updateManifest(firefoxManifest)
+ verify(dao, never()).updateManifest(any())
+ Unit
+ }
+
+ @Test
+ fun `remove deletes saved manifests`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ storage.removeManifests(listOf("https://example.com", "https://proxx.app"))
+ verify(dao).deleteManifests(listOf("https://example.com", "https://proxx.app"))
+ Unit
+ }
+
+ @Test
+ fun `loading manifests by scope returns list of manifests`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val manifest1 = WebAppManifest(name = "Mozilla1", startUrl = "https://mozilla.org", scope = "https://mozilla.org/pwa/1/")
+ val manifest2 = WebAppManifest(name = "Mozilla2", startUrl = "https://mozilla.org", scope = "https://mozilla.org/pwa/1/")
+ val manifest3 = WebAppManifest(name = "Mozilla3", startUrl = "https://mozilla.org", scope = "https://mozilla.org/pwa/")
+
+ whenever(dao.getManifestsByScope("https://mozilla.org/index.html?key=value"))
+ .thenReturn(listOf(ManifestEntity(manifest1), ManifestEntity(manifest2), ManifestEntity(manifest3)))
+
+ assertEquals(
+ listOf(manifest1, manifest2, manifest3),
+ storage.loadManifestsByScope("https://mozilla.org/index.html?key=value"),
+ )
+ }
+
+ @Test
+ fun `loading manifests with share targets returns list of manifests`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val manifest1 = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ shareTarget = WebAppManifest.ShareTarget("https://mozilla.org/share"),
+ )
+ val manifest2 = WebAppManifest(
+ name = "Firefox",
+ startUrl = "https://firefox.com",
+ shareTarget = WebAppManifest.ShareTarget("https://firefox.com/share"),
+ )
+ val timeout = ManifestStorage.ACTIVE_THRESHOLD_MS
+ val currentTime = System.currentTimeMillis()
+ val deadline = currentTime - timeout
+
+ whenever(dao.getRecentShareableManifests(deadline))
+ .thenReturn(listOf(ManifestEntity(manifest1), ManifestEntity(manifest2)))
+
+ assertEquals(
+ listOf(manifest1, manifest2),
+ storage.loadShareableManifests(currentTime),
+ )
+ }
+
+ @Test
+ fun `updateManifestUsedAt updates usedAt to current timestamp`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org")
+ val entity = ManifestEntity(manifest, currentTime = 0)
+
+ val entityCaptor = ArgumentCaptor.forClass(ManifestEntity::class.java)
+
+ whenever(dao.getManifest(manifest.startUrl))
+ .thenReturn(entity)
+
+ assertEquals(0, entity.usedAt)
+
+ storage.updateManifestUsedAt(manifest)
+
+ verify(dao).updateManifest(capture<ManifestEntity>(entityCaptor))
+ assert(entityCaptor.value.usedAt > 0)
+ }
+
+ @Test
+ fun `has recent manifest returns false if no manifest is found`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val timeout = ManifestStorage.ACTIVE_THRESHOLD_MS
+ val currentTime = System.currentTimeMillis()
+ val deadline = currentTime - timeout
+
+ whenever(dao.hasRecentManifest("https://mozilla.org/", deadline))
+ .thenReturn(0)
+
+ assertFalse(storage.hasRecentManifest("https://mozilla.org/", currentTime))
+ }
+
+ @Test
+ fun `has recent manifest returns true if one or more manifests have been found`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+ val timeout = ManifestStorage.ACTIVE_THRESHOLD_MS
+ val currentTime = System.currentTimeMillis()
+ val deadline = currentTime - timeout
+
+ whenever(dao.hasRecentManifest("https://mozilla.org/", deadline))
+ .thenReturn(1)
+
+ assertTrue(storage.hasRecentManifest("https://mozilla.org/", currentTime))
+
+ whenever(dao.hasRecentManifest("https://mozilla.org/", deadline))
+ .thenReturn(5)
+
+ assertTrue(storage.hasRecentManifest("https://mozilla.org/", currentTime))
+ }
+
+ @Test
+ fun `recently used manifest count`() = runTest {
+ val testThreshold = 1000 * 60 * 24L
+ val storage = spy(ManifestStorage(testContext, activeThresholdMs = testThreshold))
+ val dao = mockDatabase(storage)
+ val currentTime = System.currentTimeMillis()
+ val deadline = currentTime - testThreshold
+
+ whenever(dao.recentManifestsCount(deadline))
+ .thenReturn(0)
+
+ assertEquals(0, storage.recentManifestsCount(currentTimeMs = currentTime))
+
+ whenever(dao.recentManifestsCount(deadline))
+ .thenReturn(5)
+
+ assertEquals(5, storage.recentManifestsCount(currentTimeMs = currentTime))
+
+ whenever(dao.recentManifestsCount(deadline - 10L))
+ .thenReturn(3)
+
+ assertEquals(
+ 3,
+ storage.recentManifestsCount(
+ activeThresholdMs = testThreshold + 10L,
+ currentTimeMs = currentTime,
+ ),
+ )
+ }
+
+ @Test
+ fun `warmUpScopes populates cache of already installed web app scopes`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ val manifest1 = ManifestEntity(manifest = firefoxManifest, currentTime = 0)
+ val manifest2 = ManifestEntity(manifest = googleMapsManifest, currentTime = 0)
+ val manifest3 = ManifestEntity(manifest = exampleWebAppManifest, currentTime = 0)
+
+ whenever(dao.getInstalledScopes(0)).thenReturn(listOf(manifest1, manifest2, manifest3))
+
+ storage.warmUpScopes(ManifestStorage.ACTIVE_THRESHOLD_MS)
+
+ assertEquals(
+ mapOf(
+ Pair("/", "https://firefox.com"),
+ Pair("https://google.com/maps/", "https://google.com/maps"),
+ Pair("https://pwa.example.com/", "https://pwa.example.com/dashboard"),
+ ),
+ storage.installedScopes,
+ )
+ }
+
+ @Test
+ fun `getInstalledScope returns cached scope for an url`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ val manifest1 = ManifestEntity(manifest = firefoxManifest, currentTime = 0)
+ val manifest2 = ManifestEntity(manifest = googleMapsManifest, currentTime = 0)
+ val manifest3 = ManifestEntity(manifest = exampleWebAppManifest, currentTime = 0)
+
+ whenever(dao.getInstalledScopes(0)).thenReturn(listOf(manifest1, manifest2, manifest3))
+
+ storage.warmUpScopes(ManifestStorage.ACTIVE_THRESHOLD_MS)
+
+ val result = storage.getInstalledScope("https://pwa.example.com/profile/me")
+
+ assertEquals("https://pwa.example.com/", result)
+ }
+
+ @Test
+ fun `getStartUrlForInstalledScope returns cached start url for a currently installed scope`() = runTest {
+ val storage = spy(ManifestStorage(testContext))
+ val dao = mockDatabase(storage)
+
+ val manifest1 = ManifestEntity(manifest = firefoxManifest, currentTime = 0)
+ val manifest2 = ManifestEntity(manifest = googleMapsManifest, currentTime = 0)
+ val manifest3 = ManifestEntity(manifest = exampleWebAppManifest, currentTime = 0)
+
+ whenever(dao.getInstalledScopes(0)).thenReturn(listOf(manifest1, manifest2, manifest3))
+
+ storage.warmUpScopes(ManifestStorage.ACTIVE_THRESHOLD_MS)
+
+ val result = storage.getStartUrlForInstalledScope("https://pwa.example.com/")
+
+ assertEquals("https://pwa.example.com/dashboard", result)
+ }
+
+ private fun mockDatabase(storage: ManifestStorage): ManifestDao = mock<ManifestDao>().also {
+ storage.manifestDao = lazy { it }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppInterceptorTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppInterceptorTest.kt
new file mode 100644
index 0000000000..9d0219c53d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppInterceptorTest.kt
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.request.RequestInterceptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WebAppInterceptorTest {
+ private lateinit var mockContext: Context
+ private lateinit var mockEngineSession: EngineSession
+ private lateinit var mockManifestStorage: ManifestStorage
+ private lateinit var webAppInterceptor: WebAppInterceptor
+
+ private val webUrl = "https://example.com"
+ private val webUrlWithWebApp = "https://google.com/maps/"
+ private val webUrlOutOfScope = "https://google.com/search/"
+
+ @Before
+ fun setup() {
+ mockContext = mock()
+ mockEngineSession = mock()
+ mockManifestStorage = mock()
+
+ webAppInterceptor = WebAppInterceptor(
+ context = mockContext,
+ manifestStorage = mockManifestStorage,
+ launchFromInterceptor = true,
+ )
+ }
+
+ @Test
+ fun `request is intercepted when navigating to an installed web app`() {
+ whenever(mockManifestStorage.getInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrlWithWebApp, null, true, false, false, false, false)
+
+ assert(response is RequestInterceptor.InterceptionResponse.Deny)
+ }
+
+ @Test
+ fun `request is not intercepted when url is out of scope`() {
+ whenever(mockManifestStorage.getInstalledScope(webUrlOutOfScope)).thenReturn(null)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrlOutOfScope)).thenReturn(null)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrlOutOfScope, null, true, false, false, false, false)
+
+ assertNull(response)
+ }
+
+ @Test
+ fun `request is not intercepted when url is not part of a web app`() {
+ whenever(mockManifestStorage.getInstalledScope(webUrl)).thenReturn(null)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrl)).thenReturn(null)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrl, null, true, false, false, false, false)
+
+ assertNull(response)
+ }
+
+ @Test
+ fun `request is intercepted with app intent if not launchFromInterceptor`() {
+ webAppInterceptor = WebAppInterceptor(
+ context = mockContext,
+ manifestStorage = mockManifestStorage,
+ launchFromInterceptor = false,
+ )
+
+ whenever(mockManifestStorage.getInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrlWithWebApp, null, true, false, false, false, false)
+
+ assert(response is RequestInterceptor.InterceptionResponse.AppIntent)
+ }
+
+ @Test
+ fun `launchFromInterceptor is enabled by default`() {
+ webAppInterceptor = WebAppInterceptor(
+ context = mockContext,
+ manifestStorage = mockManifestStorage,
+ )
+
+ whenever(mockManifestStorage.getInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+ whenever(mockManifestStorage.getStartUrlForInstalledScope(webUrlWithWebApp)).thenReturn(webUrlWithWebApp)
+
+ val response = webAppInterceptor.onLoadRequest(mockEngineSession, webUrlWithWebApp, null, true, false, false, false, false)
+
+ assert(response is RequestInterceptor.InterceptionResponse.Deny)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppLauncherActivityTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppLauncherActivityTest.kt
new file mode 100644
index 0000000000..3a3049b904
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppLauncherActivityTest.kt
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import android.content.Intent
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class WebAppLauncherActivityTest {
+
+ private val baseManifest = WebAppManifest(
+ name = "Test",
+ startUrl = "https://www.mozilla.org",
+ )
+
+ @Test
+ fun `DisplayMode-Browser launches browser`() {
+ val activity = spy(WebAppLauncherActivity())
+ doNothing().`when`(activity).launchBrowser(any())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.BROWSER)
+
+ activity.routeManifest(manifest.startUrl.toUri(), manifest)
+
+ verify(activity).launchBrowser(manifest.startUrl.toUri())
+ }
+
+ @Test
+ fun `DisplayMode-minimalui launches web app shell`() {
+ val activity = spy(WebAppLauncherActivity())
+ doNothing().`when`(activity).launchWebAppShell("https://www.mozilla.org".toUri())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.MINIMAL_UI)
+
+ activity.routeManifest(manifest.startUrl.toUri(), manifest)
+
+ verify(activity).launchWebAppShell(manifest.startUrl.toUri())
+ }
+
+ @Test
+ fun `DisplayMode-fullscreen launches web app shell`() {
+ val activity = spy(WebAppLauncherActivity())
+ doNothing().`when`(activity).launchWebAppShell("https://www.mozilla.org".toUri())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.FULLSCREEN)
+
+ activity.routeManifest(manifest.startUrl.toUri(), manifest)
+
+ verify(activity).launchWebAppShell(manifest.startUrl.toUri())
+ }
+
+ @Test
+ fun `DisplayMode-standalone launches web app shell`() {
+ val activity = spy(WebAppLauncherActivity())
+ doNothing().`when`(activity).launchWebAppShell("https://www.mozilla.org".toUri())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.STANDALONE)
+
+ activity.routeManifest(manifest.startUrl.toUri(), manifest)
+
+ verify(activity).launchWebAppShell(manifest.startUrl.toUri())
+ }
+
+ @Test
+ fun `launchBrowser starts activity with VIEW intent`() {
+ val activity = spy(WebAppLauncherActivity())
+ doReturn("test").`when`(activity).packageName
+ doNothing().`when`(activity).startActivity(any())
+
+ val manifest = baseManifest.copy(display = WebAppManifest.DisplayMode.BROWSER)
+
+ activity.launchBrowser(manifest.startUrl.toUri())
+
+ val captor = argumentCaptor<Intent>()
+ verify(activity).startActivity(captor.capture())
+
+ assertEquals(Intent.ACTION_VIEW, captor.value.action)
+ assertEquals("https://www.mozilla.org", captor.value.data!!.toString())
+ assertEquals("test", captor.value.`package`)
+ }
+
+ @Test
+ fun `launchWebAppShell starts activity with SHELL intent`() {
+ val activity = spy(WebAppLauncherActivity())
+ doReturn("test").`when`(activity).packageName
+ doNothing().`when`(activity).startActivity(any())
+
+ val url = "https://example.com".toUri()
+
+ activity.launchWebAppShell(url)
+
+ val captor = argumentCaptor<Intent>()
+ verify(activity).startActivity(captor.capture())
+
+ assertEquals(ACTION_VIEW_PWA, captor.value.action)
+ assertEquals(url, captor.value.data)
+ assertEquals("test", captor.value.`package`)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt
new file mode 100644
index 0000000000..48f556829c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt
@@ -0,0 +1,355 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.os.Build
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.graphics.drawable.IconCompat
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.feature.pwa.WebAppLauncherActivity.Companion.ACTION_PWA_LAUNCHER
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.util.ReflectionHelpers.setStaticField
+import kotlin.reflect.jvm.javaField
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class WebAppShortcutManagerTest {
+ private lateinit var context: Context
+
+ @Mock private lateinit var httpClient: Client
+
+ @Mock private lateinit var packageManager: PackageManager
+
+ @Mock private lateinit var shortcutManager: ShortcutManager
+
+ @Mock private lateinit var storage: ManifestStorage
+
+ @Mock private lateinit var icons: BrowserIcons
+ private lateinit var manager: WebAppShortcutManager
+ private val baseManifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://example.com",
+ )
+
+ @Before
+ fun setup() {
+ setSdkInt(0)
+ openMocks(this)
+ context = spy(testContext)
+
+ doReturn(packageManager).`when`(context).packageManager
+ doReturn(shortcutManager).`when`(context).getSystemService(ShortcutManager::class.java)
+ doReturn("").`when`(context).getString(R.string.mozac_feature_pwa_default_shortcut_label)
+
+ manager = spy(WebAppShortcutManager(context, httpClient, storage))
+ doReturn(icons).`when`(manager).icons
+ }
+
+ @After
+ fun teardown() = setSdkInt(0)
+
+ @Test
+ fun `requestPinShortcut no-op if pinning unsupported`() = runTest {
+ val manifest = baseManifest.copy(
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://example.com/icon.png",
+ sizes = listOf(Size(192, 192)),
+ ),
+ ),
+ )
+ val session = buildInstallableSession(manifest)
+ @Suppress("DEPRECATION")
+ `when`(packageManager.queryBroadcastReceivers(any(), anyInt())).thenReturn(emptyList())
+
+ manager.requestPinShortcut(context, session)
+ verify(manager, never()).buildWebAppShortcut(context, manifest)
+
+ setSdkInt(Build.VERSION_CODES.O)
+ `when`(shortcutManager.isRequestPinShortcutSupported).thenReturn(false)
+ clearInvocations(manager)
+
+ manager.requestPinShortcut(context, session)
+ verify(manager, never()).buildWebAppShortcut(context, manifest)
+ }
+
+ @Test
+ fun `requestPinShortcut won't make a PWA icon if the session is not installable`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+ val manifest = baseManifest.copy(
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = emptyList(), // no icons
+ )
+ val session = buildInstallableSession(manifest)
+ val shortcutCompat: ShortcutInfoCompat = mock()
+ `when`(shortcutManager.isRequestPinShortcutSupported).thenReturn(true)
+ doReturn(shortcutCompat).`when`(manager).buildBasicShortcut(context, session)
+
+ manager.requestPinShortcut(context, session)
+ verify(manager, never()).buildWebAppShortcut(context, manifest)
+ verify(manager).buildBasicShortcut(context, session)
+ }
+
+ @Test
+ fun `requestPinShortcut pins PWA shortcut`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val manifest = baseManifest.copy(
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://example.com/icon.png",
+ sizes = listOf(Size(192, 192)),
+ ),
+ ),
+ )
+
+ val session = buildInstallableSession(manifest)
+
+ val shortcutCompat: ShortcutInfoCompat = mock()
+ `when`(shortcutManager.isRequestPinShortcutSupported).thenReturn(true)
+ doReturn(shortcutCompat).`when`(manager).buildWebAppShortcut(context, manifest)
+
+ manager.requestPinShortcut(context, session)
+ verify(manager).buildWebAppShortcut(context, manifest)
+ verify(shortcutManager).requestPinShortcut(any(), any())
+ }
+
+ @Test
+ fun `requestPinShortcut pins basic shortcut`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val session = buildInstallableSession()
+
+ val shortcutCompat: ShortcutInfoCompat = mock()
+ `when`(shortcutManager.isRequestPinShortcutSupported).thenReturn(true)
+ doReturn(shortcutCompat).`when`(manager).buildBasicShortcut(context, session)
+
+ manager.requestPinShortcut(context, session)
+ verify(manager).buildBasicShortcut(context, session)
+ verify(shortcutManager).requestPinShortcut(any(), any())
+ }
+
+ @Test
+ fun `buildBasicShortcut uses manifest short name as label by default`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val session = createTab("https://www.mozilla.org", title = "Internet for people, not profit — Mozilla").let {
+ it.copy(
+ content = it.content.copy(
+ webAppManifest = WebAppManifest(
+ name = "Mozilla",
+ shortName = "Moz",
+ startUrl = "https://mozilla.org",
+ ),
+ ),
+ )
+ }
+
+ val shortcut = manager.buildBasicShortcut(context, session)
+
+ assertEquals("Moz", shortcut.shortLabel)
+ }
+
+ @Test
+ fun `buildBasicShortcut uses manifest name as label by default`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val session = createTab("https://www.mozilla.org", title = "Internet for people, not profit — Mozilla").let {
+ it.copy(
+ content = it.content.copy(
+ webAppManifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ ),
+ ),
+ )
+ }
+
+ val shortcut = manager.buildBasicShortcut(context, session)
+
+ assertEquals("Mozilla", shortcut.shortLabel)
+ }
+
+ @Test
+ fun `buildBasicShortcut uses session title as label if there is no manifest`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val expectedTitle = "Internet for people, not profit — Mozilla"
+
+ val session = createTab("https://mozilla.org", title = expectedTitle)
+
+ val shortcut = manager.buildBasicShortcut(context, session)
+
+ assertEquals(expectedTitle, shortcut.shortLabel)
+ }
+
+ @Test
+ fun `buildBasicShortcut can create a shortcut with a custom name`() = runTest {
+ setSdkInt(Build.VERSION_CODES.O)
+
+ val title = "Internet for people, not profit — Mozilla"
+ val expectedName = "Mozilla"
+
+ val session = createTab("https://mozilla.org", title = title)
+
+ val shortcut = manager.buildBasicShortcut(context, session, expectedName)
+
+ assertEquals(expectedName, shortcut.shortLabel)
+ }
+
+ @Test
+ fun `updateShortcuts no-op`() = runTest {
+ val manifests = listOf(baseManifest)
+ doReturn(null).`when`(manager).buildWebAppShortcut(context, manifests[0])
+
+ manager.updateShortcuts(context, manifests)
+ verify(manager, never()).buildWebAppShortcut(context, manifests[0])
+ verify(shortcutManager, never()).updateShortcuts(any())
+
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+ manager.updateShortcuts(context, manifests)
+ verify(shortcutManager).updateShortcuts(emptyList())
+ }
+
+ @Test
+ fun `updateShortcuts updates list of existing shortcuts`() = runTest {
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+ val manifests = listOf(baseManifest)
+ val shortcutCompat: ShortcutInfoCompat = mock()
+ val shortcut: ShortcutInfo = mock()
+ doReturn(shortcutCompat).`when`(manager).buildWebAppShortcut(context, manifests[0])
+ doReturn(shortcut).`when`(shortcutCompat).toShortcutInfo()
+
+ manager.updateShortcuts(context, manifests)
+ verify(shortcutManager).updateShortcuts(listOf(shortcut))
+ }
+
+ @Test
+ fun `buildWebAppShortcut builds shortcut and saves manifest`() = runTest {
+ doReturn(mock<IconCompat>()).`when`(manager).buildIconFromManifest(baseManifest)
+
+ val shortcut = manager.buildWebAppShortcut(context, baseManifest)!!
+ val intent = shortcut.intent
+
+ verify(storage).saveManifest(baseManifest)
+
+ assertEquals("https://example.com", shortcut.id)
+ assertEquals("Demo", shortcut.longLabel)
+ assertEquals("Demo", shortcut.shortLabel)
+ assertEquals(ACTION_PWA_LAUNCHER, intent.action)
+ assertEquals("https://example.com".toUri(), intent.data)
+ }
+
+ @Test
+ fun `buildWebAppShortcut builds shortcut with short name`() = runTest {
+ val manifest = WebAppManifest(name = "Demo Demo", shortName = "DD", startUrl = "https://example.com")
+ doReturn(mock<IconCompat>()).`when`(manager).buildIconFromManifest(manifest)
+
+ val shortcut = manager.buildWebAppShortcut(context, manifest)!!
+
+ assertEquals("https://example.com", shortcut.id)
+ assertEquals("Demo Demo", shortcut.longLabel)
+ assertEquals("DD", shortcut.shortLabel)
+ }
+
+ @Test
+ fun `findShortcut returns shortcut`() {
+ assertNull(manager.findShortcut(context, "https://mozilla.org"))
+
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+ val exampleShortcut = mock<ShortcutInfo>().apply {
+ `when`(id).thenReturn("https://example.com")
+ }
+ `when`(shortcutManager.pinnedShortcuts).thenReturn(listOf(exampleShortcut))
+
+ assertNull(manager.findShortcut(context, "https://mozilla.org"))
+
+ val mozShortcut = mock<ShortcutInfo>().apply {
+ `when`(id).thenReturn("https://mozilla.org")
+ }
+ `when`(shortcutManager.pinnedShortcuts).thenReturn(listOf(mozShortcut, exampleShortcut))
+
+ assertEquals(mozShortcut, manager.findShortcut(context, "https://mozilla.org"))
+ }
+
+ @Test
+ fun `checking unknown url returns uninstalled state`() = runTest {
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+
+ val url = "https://mozilla.org"
+ val currentTime = System.currentTimeMillis()
+
+ whenever(storage.hasRecentManifest(url, currentTime))
+ .thenReturn(false)
+
+ val installState = manager.getWebAppInstallState(url, currentTime)
+
+ assertEquals(WebAppShortcutManager.WebAppInstallState.NotInstalled, installState)
+ }
+
+ @Test
+ fun `checking a known url returns installed state`() = runTest {
+ setSdkInt(Build.VERSION_CODES.N_MR1)
+
+ val url = "https://mozilla.org/pwa/"
+ val currentTime = System.currentTimeMillis()
+
+ whenever(storage.hasRecentManifest(url, currentTime))
+ .thenReturn(true)
+
+ val installState = manager.getWebAppInstallState(url, currentTime)
+
+ assertEquals(WebAppShortcutManager.WebAppInstallState.Installed, installState)
+ }
+
+ private fun setSdkInt(sdkVersion: Int) {
+ setStaticField(Build.VERSION::SDK_INT.javaField, sdkVersion)
+ }
+
+ private fun buildInstallableSession(manifest: WebAppManifest? = null): SessionState {
+ val tab = createTab(manifest?.startUrl ?: "https://www.mozilla.org")
+
+ return tab.copy(
+ content = tab.content.copy(
+ webAppManifest = manifest,
+ securityInfo = SecurityInfoState(secure = true),
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt
new file mode 100644
index 0000000000..5bc24230f9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class WebAppUseCasesTest {
+ @Test
+ fun `isInstallable returns false if currentSession has no manifest`() {
+ val session = createTestSession(
+ secure = true,
+ manifest = null,
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(session),
+ selectedTabId = session.id,
+ ),
+ )
+
+ val webAppUseCases = WebAppUseCases(testContext, store, mock<WebAppShortcutManager>())
+ assertFalse(webAppUseCases.isInstallable())
+ }
+
+ @Test
+ fun `isInstallable returns true if currentSession has a manifest`() {
+ val manifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://example.com",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://example.com/icon.png",
+ sizes = listOf(Size(192, 192)),
+ ),
+ ),
+ )
+
+ val session = createTestSession(secure = true, manifest = manifest)
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(session),
+ selectedTabId = session.id,
+ ),
+ )
+
+ val shortcutManager: WebAppShortcutManager = mock()
+ `when`(shortcutManager.supportWebApps).thenReturn(true)
+
+ val webAppUseCases = WebAppUseCases(testContext, store, shortcutManager)
+ assertTrue(webAppUseCases.isInstallable())
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `isInstallable returns false if supportWebApps is false`() {
+ val manifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://example.com",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://example.com/icon.png",
+ sizes = listOf(Size(192, 192)),
+ ),
+ ),
+ )
+
+ val session = createTestSession(
+ secure = true,
+ manifest = manifest,
+ )
+
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(session),
+ selectedTabId = session.id,
+ ),
+ )
+
+ val shortcutManager: WebAppShortcutManager = mock()
+ `when`(shortcutManager.supportWebApps).thenReturn(false)
+
+ assertFalse(WebAppUseCases(testContext, store, shortcutManager).isInstallable())
+ }
+
+ @Test
+ fun `getInstallState returns Installed if manifest exists`() = runTest {
+ val httpClient: Client = mock()
+ val storage: ManifestStorage = mock()
+ val shortcutManager = WebAppShortcutManager(testContext, httpClient, storage)
+ val currentTime = System.currentTimeMillis()
+
+ val session = createTestSession(secure = true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(session),
+ selectedTabId = session.id,
+ ),
+ )
+
+ `when`(storage.hasRecentManifest("https://www.mozilla.org", currentTime)).thenReturn(true)
+
+ assertEquals(WebAppShortcutManager.WebAppInstallState.Installed, WebAppUseCases(testContext, store, shortcutManager).getInstallState(currentTime))
+ }
+}
+
+private fun createTestSession(
+ secure: Boolean,
+ manifest: WebAppManifest? = null,
+): TabSessionState {
+ val protocol = if (secure) {
+ "https"
+ } else {
+ "http"
+ }
+ val tab = createTab("$protocol://www.mozilla.org")
+
+ return tab.copy(
+ content = tab.content.copy(
+ securityInfo = SecurityInfoState(secure = secure),
+ webAppManifest = manifest,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/ActivityKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/ActivityKtTest.kt
new file mode 100644
index 0000000000..6721427f40
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/ActivityKtTest.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 mozilla.components.feature.pwa.ext
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.view.reportFullyDrawnSafe
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class ActivityKtTest {
+ private val baseManifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "/",
+ )
+
+ private lateinit var activity: Activity
+ private lateinit var logger: Logger
+
+ @Before
+ fun setUp() {
+ activity = mock()
+ logger = mock()
+ }
+
+ @Test
+ fun `applyOrientation calls setRequestedOrientation for every value`() {
+ WebAppManifest.Orientation.values().forEach { orientation ->
+ val activity: Activity = mock()
+ activity.applyOrientation(baseManifest.copy(orientation = orientation))
+ verify(activity).requestedOrientation = anyInt()
+ }
+ }
+
+ @Test
+ fun `applyOrientation applies common orientations`() {
+ run {
+ val activity: Activity = mock()
+ activity.applyOrientation(baseManifest.copy(orientation = WebAppManifest.Orientation.ANY))
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ }
+
+ run {
+ val activity: Activity = mock()
+ activity.applyOrientation(baseManifest.copy(orientation = WebAppManifest.Orientation.PORTRAIT))
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+ }
+
+ run {
+ val activity: Activity = mock()
+ activity.applyOrientation(baseManifest.copy(orientation = WebAppManifest.Orientation.LANDSCAPE))
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ }
+
+ run {
+ val activity: Activity = mock()
+ activity.applyOrientation(null)
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ }
+ }
+
+ @Test
+ fun `WHEN reportFullyDrawnSafe is called THEN reportFullyDrawn is called`() {
+ activity.reportFullyDrawnSafe(logger)
+ verify(activity).reportFullyDrawn()
+ }
+
+ @Test
+ fun `GIVEN reportFullyDrawn throws a SecurityException WHEN reportFullyDrawnSafe is called THEN the exception is caught and a log statement with fully drawn is logged`() {
+ val expectedSecurityException = SecurityException()
+ `when`(activity.reportFullyDrawn()).thenThrow(expectedSecurityException)
+ activity.reportFullyDrawnSafe(logger) // If an exception is thrown, this test will fail.
+
+ val msgArg = argumentCaptor<String>()
+ verify(logger).error(msgArg.capture(), eq(expectedSecurityException))
+ assertTrue(msgArg.value, msgArg.value.contains("Fully drawn"))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/CustomTabStateKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/CustomTabStateKtTest.kt
new file mode 100644
index 0000000000..03974a3942
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/CustomTabStateKtTest.kt
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.ext
+
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.OriginRelationPair
+import mozilla.components.feature.customtabs.store.VerificationStatus.FAILURE
+import mozilla.components.feature.customtabs.store.VerificationStatus.PENDING
+import mozilla.components.feature.customtabs.store.VerificationStatus.SUCCESS
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CustomTabStateKtTest {
+
+ @Test
+ fun `trustedOrigins is empty when there are no relationships`() {
+ val state = CustomTabState(relationships = emptyMap())
+ assertEquals(emptyList<Uri>(), state.trustedOrigins)
+ }
+
+ @Test
+ fun `trustedOrigins only includes the HANDLE_ALL_URLS relationship`() {
+ val state = CustomTabState(
+ relationships = mapOf(
+ OriginRelationPair("https://firefox.com".toUri(), RELATION_HANDLE_ALL_URLS) to SUCCESS,
+ OriginRelationPair("https://example.com".toUri(), RELATION_USE_AS_ORIGIN) to SUCCESS,
+ OriginRelationPair("https://mozilla.org".toUri(), RELATION_HANDLE_ALL_URLS) to PENDING,
+ ),
+ )
+ assertEquals(
+ listOf("https://firefox.com".toUri(), "https://mozilla.org".toUri()),
+ state.trustedOrigins,
+ )
+ }
+
+ @Test
+ fun `trustedOrigins only includes pending or success statuses`() {
+ val state = CustomTabState(
+ relationships = mapOf(
+ OriginRelationPair("https://firefox.com".toUri(), RELATION_HANDLE_ALL_URLS) to SUCCESS,
+ OriginRelationPair("https://example.com".toUri(), RELATION_USE_AS_ORIGIN) to FAILURE,
+ OriginRelationPair("https://mozilla.org".toUri(), RELATION_HANDLE_ALL_URLS) to PENDING,
+ ),
+ )
+ assertEquals(
+ listOf("https://firefox.com".toUri(), "https://mozilla.org".toUri()),
+ state.trustedOrigins,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/SessionStateKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/SessionStateKtTest.kt
new file mode 100644
index 0000000000..24009f5279
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/SessionStateKtTest.kt
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.ext
+
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class SessionStateKtTest {
+ private val demoManifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://mozilla.com",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ )
+ private val demoIcon = WebAppManifest.Icon(src = "https://mozilla.com/example.png")
+
+ @Test
+ fun `web app must be HTTPS to be installable`() {
+ val httpSession = createTestSession(secure = false)
+ assertNull(httpSession.installableManifest())
+ }
+
+ @Test
+ fun `web app must have manifest to be installable`() {
+ val noManifestSession = createTestSession(
+ secure = true,
+ manifest = null,
+ )
+ assertNull(noManifestSession.installableManifest())
+ }
+
+ @Test
+ fun `web app must have an icon to be installable`() {
+ val noIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest,
+ )
+ assertNull(noIconSession.installableManifest())
+
+ val noSizeIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(icons = listOf(demoIcon)),
+ )
+ assertNull(noSizeIconSession.installableManifest())
+
+ val onlyBadgeIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(
+ sizes = listOf(Size(512, 512)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ ),
+ )
+ assertNull(onlyBadgeIconSession.installableManifest())
+ }
+
+ @Test
+ fun `web app must have 192x192 icons to be installable`() {
+ val smallIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(32, 32))),
+ ),
+ ),
+ )
+ assertNull(smallIconSession.installableManifest())
+
+ val weirdSizeSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(50, 200))),
+ ),
+ ),
+ )
+ assertNull(weirdSizeSession.installableManifest())
+
+ val largeIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(192, 192))),
+ ),
+ ),
+ )
+ assertEquals(
+ demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(192, 192))),
+ ),
+ ),
+ largeIconSession.installableManifest(),
+ )
+
+ val multiSizeIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(16, 16), Size(512, 512))),
+ ),
+ ),
+ )
+ assertEquals(
+ demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(16, 16), Size(512, 512))),
+ ),
+ ),
+ multiSizeIconSession.installableManifest(),
+ )
+
+ val multiIconSession = createTestSession(
+ secure = true,
+ manifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(191, 193))),
+ demoIcon.copy(sizes = listOf(Size(512, 512))),
+ demoIcon.copy(
+ sizes = listOf(Size(192, 192)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ ),
+ )
+ assertEquals(
+ demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(191, 193))),
+ demoIcon.copy(sizes = listOf(Size(512, 512))),
+ demoIcon.copy(
+ sizes = listOf(Size(192, 192)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ ),
+ multiIconSession.installableManifest(),
+ )
+ }
+}
+
+private fun createTestSession(
+ secure: Boolean,
+ manifest: WebAppManifest? = null,
+): SessionState {
+ val protocol = if (secure) {
+ "https"
+ } else {
+ "http"
+ }
+ val tab = createTab("$protocol://www.mozilla.org")
+
+ return tab.copy(
+ content = tab.content.copy(
+ securityInfo = SecurityInfoState(secure = secure),
+ webAppManifest = manifest,
+ ),
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/UriKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/UriKtTest.kt
new file mode 100644
index 0000000000..52911f0ca3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/UriKtTest.kt
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.ext
+
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.android.net.sameHostWithoutMobileSubdomainAs
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UriKtTest {
+
+ @Test
+ fun `extracts scheme, host and port`() {
+ assertEquals("https://example.com", "https://example.com".toUri().toOrigin())
+ assertEquals("http://mozilla.org:80", "http://mozilla.org:80".toUri().toOrigin())
+ assertEquals("http://localhost:8080", "http://localhost:8080".toUri().toOrigin())
+ }
+
+ @Test
+ fun `removes user info`() {
+ assertEquals("https://example.com", "https://bob@example.com".toUri().toOrigin())
+ assertEquals("http://google.com", "HTTP://bob:pass@google.com".toUri().toOrigin())
+ }
+
+ @Test
+ fun `removes path`() {
+ assertEquals("https://example.com", "https://example.com/".toUri().toOrigin())
+ assertEquals("http://google.com", "http://google.com/search".toUri().toOrigin())
+ assertEquals("http://firefox.com", "http://firefox.com/en-US/foo".toUri().toOrigin())
+ }
+
+ @Test
+ fun `preserves missing scheme`() {
+ assertNull("example.com".toUri().toOrigin())
+ assertNull("/foo/bar".toUri().toOrigin())
+ }
+
+ @Test
+ fun `GIVEN Uris having the same host, one containing mobile subdomains WHEN compared THEN they have the same host without mobile subdomains`() {
+ assertTrue("https://m.youtube.com".toUri().sameHostWithoutMobileSubdomainAs("https://www.youtube.com".toUri()))
+ assertTrue("https://en.m.wikipedia.com".toUri().sameHostWithoutMobileSubdomainAs("https://en.wikipedia.com".toUri()))
+ assertFalse("https://m.en.youtube.com".toUri().sameHostWithoutMobileSubdomainAs("https://www.youtube.com".toUri()))
+ assertFalse("https://en.m.wikipedia.com".toUri().sameHostWithoutMobileSubdomainAs("https://it.wikipedia.com".toUri()))
+ }
+
+ private fun assertEquals(expected: String, actual: Uri?) = assertEquals(expected.toUri(), actual)
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/WebAppManifestKtTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/WebAppManifestKtTest.kt
new file mode 100644
index 0000000000..25240745f4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ext/WebAppManifestKtTest.kt
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.ext
+
+import android.graphics.Color
+import android.graphics.Color.rgb
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WebAppManifestKtTest {
+
+ private val demoManifest = WebAppManifest(name = "Demo", startUrl = "https://mozilla.com")
+ private val demoIcon = WebAppManifest.Icon(src = "https://mozilla.com/example.png")
+
+ @Test
+ fun `should use name as label`() {
+ val taskDescription = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://example.com",
+ ).toTaskDescription(null)
+ assertEquals("Demo", taskDescription.label)
+ assertEquals(0, taskDescription.primaryColor)
+ }
+
+ @Test
+ fun `should use themeColor as primaryColor`() {
+ val taskDescription = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com",
+ themeColor = rgb(255, 0, 255),
+ ).toTaskDescription(null)
+ assertEquals("My App", taskDescription.label)
+ assertEquals(rgb(255, 0, 255), taskDescription.primaryColor)
+ }
+
+ @Test
+ fun `should use themeColor as toolbarColor`() {
+ val config = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com",
+ themeColor = rgb(255, 0, 255),
+ backgroundColor = rgb(230, 230, 230),
+ ).toCustomTabConfig()
+ assertEquals(rgb(255, 0, 255), config.colorSchemes?.defaultColorSchemeParams?.toolbarColor)
+ assertEquals(Color.WHITE, config.colorSchemes?.defaultColorSchemeParams?.navigationBarColor)
+ assertNull(config.closeButtonIcon)
+ assertTrue(config.enableUrlbarHiding)
+ assertNull(config.actionButtonConfig)
+ assertTrue(config.showShareMenuItem)
+ assertEquals(0, config.menuItems.size)
+ }
+
+ @Test
+ fun `should return the scope as a uri`() {
+ val scope = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com/pwa",
+ scope = "https://example.com/",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ ).getTrustedScope()
+ assertEquals("https://example.com/".toUri(), scope)
+
+ val fallbackToStartUrl = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com/pwa",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ ).getTrustedScope()
+ assertEquals("https://example.com/pwa".toUri(), fallbackToStartUrl)
+ }
+
+ @Test
+ fun `should not return the scope if display mode is minimal-ui`() {
+ val scope = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com/pwa",
+ scope = "https://example.com/",
+ display = WebAppManifest.DisplayMode.MINIMAL_UI,
+ ).getTrustedScope()
+ assertNull(scope)
+
+ val fallbackToStartUrl = WebAppManifest(
+ name = "My App",
+ startUrl = "https://example.com/pwa",
+ display = WebAppManifest.DisplayMode.MINIMAL_UI,
+ ).getTrustedScope()
+ assertNull(fallbackToStartUrl)
+ }
+
+ @Test
+ fun `web app must have an icon to be installable`() {
+ val noIconManifest = demoManifest
+ assertFalse(noIconManifest.hasLargeIcons())
+
+ val noSizeIconManifest = demoManifest.copy(icons = listOf(demoIcon))
+ assertFalse(noSizeIconManifest.hasLargeIcons())
+
+ val onlyBadgeIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(
+ sizes = listOf(Size(512, 512)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ )
+ assertFalse(onlyBadgeIconManifest.hasLargeIcons())
+ }
+
+ @Test
+ fun `web app must have 192x192 icons to be installable`() {
+ val smallIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(32, 32))),
+ ),
+ )
+ assertFalse(smallIconManifest.hasLargeIcons())
+
+ val weirdSizeManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(50, 200))),
+ ),
+ )
+ assertFalse(weirdSizeManifest.hasLargeIcons())
+
+ val largeIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(192, 192))),
+ ),
+ )
+ assertTrue(largeIconManifest.hasLargeIcons())
+
+ val multiSizeIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(16, 16), Size(512, 512))),
+ ),
+ )
+ assertTrue(multiSizeIconManifest.hasLargeIcons())
+
+ val multiIconManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(191, 193))),
+ demoIcon.copy(sizes = listOf(Size(512, 512))),
+ demoIcon.copy(
+ sizes = listOf(Size(192, 192)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ )
+ assertTrue(multiIconManifest.hasLargeIcons())
+
+ val onlyBadgeManifest = demoManifest.copy(
+ icons = listOf(
+ demoIcon.copy(sizes = listOf(Size(191, 191))),
+ demoIcon.copy(
+ sizes = listOf(Size(192, 192)),
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ )
+ assertFalse(onlyBadgeManifest.hasLargeIcons())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt
new file mode 100644
index 0000000000..27b07cefb3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.feature
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.WebAppShortcutManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ManifestUpdateFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var shortcutManager: WebAppShortcutManager
+ private lateinit var storage: ManifestStorage
+ private lateinit var store: BrowserStore
+
+ private val sessionId = "external-app-session-id"
+ private val baseManifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ scope = "https://mozilla.org",
+ )
+
+ @Before
+ fun setUp() {
+ storage = mock()
+ shortcutManager = mock()
+
+ store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(
+ createCustomTab("https://mozilla.org", id = sessionId),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `start and stop handle null session`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ "not existing",
+ baseManifest,
+ )
+
+ feature.start()
+
+ store.waitUntilIdle()
+
+ feature.stop()
+
+ verify(storage).updateManifestUsedAt(baseManifest)
+ verify(storage, never()).updateManifest(any())
+ }
+
+ @Test
+ fun `Last usage is updated when feature is started`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ // Insert base manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ feature.updateUsageJob!!.joinBlocking()
+
+ verify(storage).updateManifestUsedAt(baseManifest)
+ }
+
+ @Test
+ fun `updateStoredManifest is called when the manifest changes`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ // Insert base manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ val newManifest = baseManifest.copy(shortName = "Moz")
+
+ // Update manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ newManifest,
+ ),
+ ).joinBlocking()
+
+ feature.updateJob!!.joinBlocking()
+
+ verify(storage).updateManifest(newManifest)
+ }
+
+ @Test
+ fun `updateStoredManifest is not called when the manifest is the same`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ feature.start()
+
+ // Update manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.updateJob?.joinBlocking()
+
+ verify(storage, never()).updateManifest(any())
+ }
+
+ @Test
+ fun `updateStoredManifest is not called when the manifest is removed`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ // Insert base manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ // Update manifest
+ store.dispatch(
+ ContentAction.RemoveWebAppManifestAction(
+ sessionId,
+ ),
+ ).joinBlocking()
+
+ feature.updateJob?.joinBlocking()
+
+ verify(storage, never()).updateManifest(any())
+ }
+
+ @Test
+ fun `updateStoredManifest is not called when the manifest has a different start URL`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(
+ testContext,
+ store,
+ shortcutManager,
+ storage,
+ sessionId,
+ baseManifest,
+ )
+
+ // Insert base manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ baseManifest,
+ ),
+ ).joinBlocking()
+
+ feature.start()
+
+ // Update manifest
+ store.dispatch(
+ ContentAction.UpdateWebAppManifestAction(
+ sessionId,
+ WebAppManifest(name = "Mozilla", startUrl = "https://netscape.com"),
+ ),
+ ).joinBlocking()
+
+ feature.updateJob?.joinBlocking()
+
+ verify(storage, never()).updateManifest(any())
+ }
+
+ @Test
+ fun `updateStoredManifest updates storage and shortcut`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(testContext, store, shortcutManager, storage, sessionId, baseManifest)
+
+ val manifest = baseManifest.copy(shortName = "Moz")
+ feature.updateStoredManifest(manifest)
+
+ verify(storage).updateManifest(manifest)
+ verify(shortcutManager).updateShortcuts(testContext, listOf(manifest))
+ }
+
+ @Test
+ fun `start updates last web app usage`() = runTestOnMain {
+ val feature = ManifestUpdateFeature(testContext, store, shortcutManager, storage, sessionId, baseManifest)
+
+ feature.start()
+
+ verify(storage).updateManifestUsedAt(baseManifest)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppActivityFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppActivityFeatureTest.kt
new file mode 100644
index 0000000000..9d3a844223
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppActivityFeatureTest.kt
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.feature
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import android.os.Looper.getMainLooper
+import android.view.View
+import android.view.Window
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CompletableDeferred
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.Icon
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class WebAppActivityFeatureTest {
+
+ @Mock private lateinit var activity: Activity
+
+ @Mock private lateinit var window: Window
+
+ @Mock private lateinit var decorView: View
+
+ @Mock private lateinit var layoutParams: WindowManager.LayoutParams
+
+ @Mock private lateinit var icons: BrowserIcons
+
+ @Before
+ fun setup() {
+ openMocks(this)
+
+ `when`(activity.window).thenReturn(window)
+ `when`(window.decorView).thenReturn(decorView)
+ `when`(window.attributes).thenReturn(layoutParams)
+ `when`(icons.loadIcon(any())).thenReturn(CompletableDeferred(mock<Icon>()))
+ }
+
+ @Test
+ fun `enters immersive mode only when display mode is fullscreen`() {
+ val basicManifest = WebAppManifest(
+ name = "Demo",
+ startUrl = "https://mozilla.com",
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ )
+ WebAppActivityFeature(activity, icons, basicManifest).onResume(mock())
+
+ val fullscreenManifest = basicManifest.copy(
+ display = WebAppManifest.DisplayMode.FULLSCREEN,
+ )
+ WebAppActivityFeature(activity, icons, fullscreenManifest).onResume(mock())
+ }
+
+ @Test
+ fun `applies orientation`() {
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "/",
+ orientation = WebAppManifest.Orientation.LANDSCAPE,
+ )
+
+ WebAppActivityFeature(activity, icons, manifest).onResume(mock())
+
+ verify(activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ fun `sets task description`() {
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "/",
+ )
+ val icon = Icon(mock(), source = Icon.Source.GENERATOR)
+ `when`(icons.loadIcon(any())).thenReturn(CompletableDeferred(icon))
+
+ WebAppActivityFeature(activity, icons, manifest).onResume(mock())
+ shadowOf(getMainLooper()).idle()
+
+ verify(activity).setTaskDescription(any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppContentFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppContentFeatureTest.kt
new file mode 100644
index 0000000000..ee2c1f323a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppContentFeatureTest.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 mozilla.components.feature.pwa.feature
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class WebAppContentFeatureTest {
+ private val customTabId = "custom-id"
+
+ @Test
+ fun `display mode is fullscreen based on PWA manifest`() {
+ val engineSession = mock<EngineSession>()
+ val engineState = EngineState(engineSession = engineSession)
+
+ val tab = CustomTabSessionState(
+ id = customTabId,
+ content = ContentState("https://mozilla.org"),
+ config = CustomTabConfig(),
+ engineState = engineState,
+ )
+
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val manifest = mockManifest(WebAppManifest.DisplayMode.FULLSCREEN)
+
+ val feature = WebAppContentFeature(
+ store,
+ tabId = tab.id,
+ manifest = manifest,
+ )
+ feature.onCreate(mock())
+
+ verify(engineSession).setDisplayMode(WebAppManifest.DisplayMode.FULLSCREEN)
+ }
+
+ private fun mockManifest(display: WebAppManifest.DisplayMode) = WebAppManifest(
+ name = "Mock",
+ startUrl = "https://mozilla.org",
+ scope = "https://mozilla.org",
+ display = display,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeatureTest.kt
new file mode 100644
index 0000000000..cd3323ba14
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppHideToolbarFeatureTest.kt
@@ -0,0 +1,343 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.feature
+
+import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS
+import androidx.browser.customtabs.CustomTabsSessionToken
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.customtabs.store.CustomTabState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceState
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.customtabs.store.OriginRelationPair
+import mozilla.components.feature.customtabs.store.ValidateRelationshipAction
+import mozilla.components.feature.customtabs.store.VerificationStatus
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class WebAppHideToolbarFeatureTest {
+
+ private val customTabId = "custom-id"
+ private var toolbarVisible = false
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ toolbarVisible = false
+ }
+
+ @Test
+ fun `hides toolbar immediately based on PWA manifest`() {
+ val tab = CustomTabSessionState(
+ id = customTabId,
+ content = ContentState("https://mozilla.org"),
+ config = CustomTabConfig(),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+
+ val feature = WebAppHideToolbarFeature(
+ store,
+ CustomTabsServiceStore(),
+ tabId = tab.id,
+ manifest = mockManifest("https://mozilla.org"),
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `hides toolbar immediately based on trusted origins`() {
+ val token = mock<CustomTabsSessionToken>()
+ val tab = CustomTabSessionState(
+ id = customTabId,
+ content = ContentState("https://mozilla.org"),
+ config = CustomTabConfig(sessionToken = token),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val customTabsStore = CustomTabsServiceStore(
+ CustomTabsServiceState(
+ tabs = mapOf(token to mockCustomTabState("https://firefox.com", "https://mozilla.org")),
+ ),
+ )
+
+ val feature = WebAppHideToolbarFeature(
+ store,
+ customTabsStore,
+ tabId = tab.id,
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `does not hide toolbar for a normal tab`() {
+ val tab = createTab("https://mozilla.org")
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+
+ val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertTrue(toolbarVisible)
+ }
+
+ @Test
+ fun `does not hide toolbar for an invalid tab`() {
+ val store = BrowserStore()
+
+ val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore()) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertTrue(toolbarVisible)
+ }
+
+ @Test
+ fun `does hide toolbar for a normal tab in fullscreen`() {
+ val tab = TabSessionState(
+ content = ContentState(
+ url = "https://mozilla.org",
+ fullScreen = true,
+ ),
+ )
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+
+ val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `does hide toolbar for a normal tab in PIP`() {
+ val tab = TabSessionState(
+ content = ContentState(
+ url = "https://mozilla.org",
+ pictureInPictureEnabled = true,
+ ),
+ )
+ val store = BrowserStore(BrowserState(tabs = listOf(tab)))
+
+ val feature = WebAppHideToolbarFeature(store, CustomTabsServiceStore(), tabId = tab.id) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `does not hide toolbar if origin is not trusted`() {
+ val token = mock<CustomTabsSessionToken>()
+ val tab = createCustomTab(
+ id = customTabId,
+ url = "https://firefox.com",
+ config = CustomTabConfig(sessionToken = token),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val customTabsStore = CustomTabsServiceStore(
+ CustomTabsServiceState(
+ tabs = mapOf(token to mockCustomTabState("https://mozilla.org")),
+ ),
+ )
+
+ val feature = WebAppHideToolbarFeature(
+ store,
+ customTabsStore,
+ tabId = tab.id,
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+ assertTrue(toolbarVisible)
+ }
+
+ @Test
+ fun `onUrlChanged hides toolbar if URL is in origin`() {
+ val token = mock<CustomTabsSessionToken>()
+ val tab = createCustomTab(
+ id = customTabId,
+ url = "https://mozilla.org",
+ config = CustomTabConfig(sessionToken = token),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val customTabsStore = CustomTabsServiceStore(
+ CustomTabsServiceState(
+ tabs = mapOf(token to mockCustomTabState("https://mozilla.com", "https://m.mozilla.com")),
+ ),
+ )
+ val feature = WebAppHideToolbarFeature(
+ store,
+ customTabsStore,
+ tabId = customTabId,
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.com/example-page"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://firefox.com/out-of-scope"),
+ ).joinBlocking()
+ assertTrue(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.com/back-in-scope"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://m.mozilla.com/second-origin"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `onUrlChanged hides toolbar if URL is in scope`() {
+ val tab = createCustomTab(id = customTabId, url = "https://mozilla.org")
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val feature = WebAppHideToolbarFeature(
+ store,
+ CustomTabsServiceStore(),
+ tabId = customTabId,
+ manifest = mockManifest("https://mozilla.github.io/my-app/"),
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app/"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://firefox.com/out-of-scope"),
+ ).joinBlocking()
+ assertTrue(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app-almost-in-scope"),
+ ).joinBlocking()
+ assertTrue(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/my-app/sub-page"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `onUrlChanged hides toolbar if URL is in ambiguous scope`() {
+ val tab = createCustomTab(id = customTabId, url = "https://mozilla.org")
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val feature = WebAppHideToolbarFeature(
+ store,
+ CustomTabsServiceStore(),
+ tabId = customTabId,
+ manifest = mockManifest("https://mozilla.github.io/prefix"),
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/prefix/"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+
+ store.dispatch(
+ ContentAction.UpdateUrlAction(customTabId, "https://mozilla.github.io/prefix-of/resource.html"),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+ }
+
+ @Test
+ fun `onTrustedScopesChange hides toolbar if URL is in origin`() {
+ val token = mock<CustomTabsSessionToken>()
+ val tab = createCustomTab(
+ id = customTabId,
+ url = "https://mozilla.com/example-page",
+ config = CustomTabConfig(sessionToken = token),
+ )
+ val store = BrowserStore(BrowserState(customTabs = listOf(tab)))
+ val customTabsStore = CustomTabsServiceStore(
+ CustomTabsServiceState(
+ tabs = mapOf(token to mockCustomTabState()),
+ ),
+ )
+ val feature = WebAppHideToolbarFeature(
+ store,
+ customTabsStore,
+ tabId = customTabId,
+ ) {
+ toolbarVisible = it
+ }
+ feature.start()
+
+ customTabsStore.dispatch(
+ ValidateRelationshipAction(
+ token,
+ RELATION_HANDLE_ALL_URLS,
+ "https://m.mozilla.com".toUri(),
+ VerificationStatus.PENDING,
+ ),
+ ).joinBlocking()
+ assertTrue(toolbarVisible)
+
+ customTabsStore.dispatch(
+ ValidateRelationshipAction(
+ token,
+ RELATION_HANDLE_ALL_URLS,
+ "https://mozilla.com".toUri(),
+ VerificationStatus.PENDING,
+ ),
+ ).joinBlocking()
+ assertFalse(toolbarVisible)
+ }
+
+ private fun mockCustomTabState(vararg origins: String) = CustomTabState(
+ relationships = origins.map { origin ->
+ OriginRelationPair(origin.toUri(), RELATION_HANDLE_ALL_URLS) to VerificationStatus.PENDING
+ }.toMap(),
+ )
+
+ private fun mockManifest(scope: String) = WebAppManifest(
+ name = "Mock",
+ startUrl = scope,
+ scope = scope,
+ display = WebAppManifest.DisplayMode.STANDALONE,
+ )
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeatureTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeatureTest.kt
new file mode 100644
index 0000000000..6ad7780d0b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/WebAppSiteControlsFeatureTest.kt
@@ -0,0 +1,147 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.feature
+
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.icons.BrowserIcons
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.Size
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class WebAppSiteControlsFeatureTest {
+
+ @Test
+ fun `register receiver on resume`() {
+ val controlsBuilder: SiteControlsBuilder = mock()
+ val filter: IntentFilter = mock()
+ whenever(controlsBuilder.getFilter()).thenReturn(filter)
+
+ val feature = spy(
+ WebAppSiteControlsFeature(
+ testContext,
+ mock(),
+ "session-id",
+ controlsBuilder = controlsBuilder,
+ notificationsDelegate = mock(),
+ ),
+ )
+
+ feature.onResume(mock())
+
+ verify(feature).registerReceiver(filter)
+ }
+
+ @Test
+ fun `unregister receiver on pause`() {
+ val context = spy(testContext)
+
+ doNothing().`when`(context).unregisterReceiver(any())
+
+ val feature = WebAppSiteControlsFeature(context, mock(), "session-id", mock(), notificationsDelegate = mock())
+ feature.onPause(mock())
+
+ verify(context).unregisterReceiver(feature)
+ }
+
+ @Test
+ fun `reload page when reload action is activated`() {
+ val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase = mock()
+
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(
+ createCustomTab("https://www.mozilla.org", id = "session-id"),
+ ),
+ ),
+ )
+
+ val feature = WebAppSiteControlsFeature(
+ testContext,
+ store,
+ reloadUrlUseCase,
+ "session-id",
+ mock(),
+ notificationsDelegate = mock(),
+ )
+ feature.onReceive(testContext, Intent("mozilla.components.feature.pwa.REFRESH"))
+
+ verify(reloadUrlUseCase).invoke("session-id")
+ }
+
+ @Test
+ fun `load monochrome icon if defined in manifest`() {
+ val icons: BrowserIcons = mock()
+ val manifest = WebAppManifest(
+ name = "Mozilla",
+ startUrl = "https://mozilla.org",
+ scope = "https://mozilla.org",
+ icons = listOf(
+ WebAppManifest.Icon(
+ src = "https://mozilla.org/logo_color.svg",
+ sizes = listOf(Size.ANY),
+ type = "image/svg+xml",
+ purpose = setOf(WebAppManifest.Icon.Purpose.ANY, WebAppManifest.Icon.Purpose.MASKABLE),
+ ),
+ WebAppManifest.Icon(
+ src = "https://mozilla.org/logo_black.svg",
+ sizes = listOf(Size.ANY),
+ type = "image/svg+xml",
+ purpose = setOf(WebAppManifest.Icon.Purpose.MONOCHROME),
+ ),
+ ),
+ )
+
+ val session = createCustomTab("https://www.mozilla.org", id = "session-id")
+ val store = BrowserStore(
+ BrowserState(
+ customTabs = listOf(session),
+ ),
+ )
+
+ val feature = WebAppSiteControlsFeature(
+ testContext,
+ store,
+ "session-id",
+ manifest,
+ icons = icons,
+ notificationsDelegate = mock(),
+ )
+ feature.onCreate(mock())
+
+ verify(icons).loadIcon(
+ IconRequest(
+ url = "https://mozilla.org",
+ size = IconRequest.Size.DEFAULT,
+ resources = listOf(
+ IconRequest.Resource(
+ url = "https://mozilla.org/logo_black.svg",
+ type = IconRequest.Resource.Type.MANIFEST_ICON,
+ sizes = listOf(Size.ANY),
+ mimeType = "image/svg+xml",
+ maskable = false,
+ ),
+ ),
+ color = Color.WHITE,
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt
new file mode 100644
index 0000000000..679c5c753e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.intent
+
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.os.Bundle
+import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION
+import androidx.browser.customtabs.TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY
+import androidx.core.net.toUri
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@Suppress("DEPRECATION")
+@Ignore("TrustedWebActivityIntentProcessorTest] is deprecated. See https://github.com/mozilla-mobile/android-components/issues/12024")
+class TrustedWebActivityIntentProcessorTest {
+
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setup() {
+ store = BrowserStore()
+ }
+
+ @Test
+ fun `process checks if intent action is not valid`() {
+ val processor = TrustedWebActivityIntentProcessor(mock(), mock(), mock(), mock())
+
+ assertFalse(processor.process(Intent(ACTION_VIEW_PWA)))
+ assertFalse(processor.process(Intent(ACTION_VIEW)))
+ assertFalse(
+ processor.process(
+ Intent(ACTION_VIEW).apply { putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true) },
+ ),
+ )
+ assertFalse(
+ processor.process(
+ Intent(ACTION_VIEW).apply {
+ putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false)
+ putExtra(EXTRA_SESSION, null as Bundle?)
+ },
+ ),
+ )
+ assertFalse(
+ processor.process(
+ Intent(ACTION_VIEW).apply {
+ putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
+ putExtra(EXTRA_SESSION, null as Bundle?)
+ },
+ ),
+ )
+ assertFalse(
+ processor.process(
+ Intent(ACTION_VIEW, null).apply {
+ putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
+ putExtra(EXTRA_SESSION, null as Bundle?)
+ },
+ ),
+ )
+ }
+
+ @Test
+ fun `process adds custom tab config`() {
+ val intent = Intent(ACTION_VIEW, "https://example.com".toUri()).apply {
+ putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true)
+ putExtra(EXTRA_SESSION, null as Bundle?)
+ }
+
+ val customTabsStore: CustomTabsServiceStore = mock()
+ val addTabUseCase: CustomTabsUseCases.AddCustomTabUseCase = mock()
+
+ val processor = TrustedWebActivityIntentProcessor(addTabUseCase, mock(), mock(), customTabsStore)
+ assertTrue(processor.process(intent))
+
+ verify(addTabUseCase).invoke(
+ "https://example.com",
+ source = SessionState.Source.Internal.HomeScreen,
+ customTabConfig = CustomTabConfig(externalAppType = ExternalAppType.TRUSTED_WEB_ACTIVITY),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt
new file mode 100644
index 0000000000..bd7da70474
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla.components.feature.pwa.intent
+
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.browser.state.state.CustomTabConfig
+import mozilla.components.browser.state.state.ExternalAppType
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.manifest.WebAppManifest
+import mozilla.components.feature.intent.ext.getSessionId
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.pwa.ext.getWebAppManifest
+import mozilla.components.feature.pwa.ext.putUrlOverride
+import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class WebAppIntentProcessorTest {
+ @Test
+ fun `process checks if intent action is not valid`() {
+ val store = BrowserStore()
+
+ val processor = WebAppIntentProcessor(store, mock(), mock(), mock())
+
+ assertFalse(processor.process(Intent(ACTION_VIEW)))
+ assertFalse(processor.process(Intent(ACTION_VIEW_PWA, null)))
+ assertFalse(processor.process(Intent(ACTION_VIEW_PWA, "".toUri())))
+ }
+
+ @Test
+ fun `process returns false if no manifest is in storage`() = runTest {
+ val storage: ManifestStorage = mock()
+ val processor = WebAppIntentProcessor(mock(), mock(), mock(), storage)
+
+ `when`(storage.loadManifest("https://mozilla.com")).thenReturn(null)
+
+ assertFalse(processor.process(Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())))
+ }
+
+ @Test
+ fun `process adds session ID and manifest to intent`() = runTest {
+ val store = BrowserStore()
+ val storage: ManifestStorage = mock()
+
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "https://mozilla.com",
+ )
+ `when`(storage.loadManifest("https://mozilla.com")).thenReturn(manifest)
+
+ val addTabUseCase: CustomTabsUseCases.AddWebAppTabUseCase = mock()
+ whenever(
+ addTabUseCase.invoke(
+ url = "https://mozilla.com",
+ source = SessionState.Source.Internal.HomeScreen,
+ customTabConfig = CustomTabConfig(
+ externalAppType = ExternalAppType.PROGRESSIVE_WEB_APP,
+ enableUrlbarHiding = true,
+ showCloseButton = false,
+ showShareMenuItem = true,
+
+ ),
+ webAppManifest = manifest,
+ ),
+ ).thenReturn("42")
+
+ val processor = WebAppIntentProcessor(store, addTabUseCase, mock(), storage)
+
+ val intent = Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())
+ assertTrue(processor.process(intent))
+
+ assertNotNull(intent.getSessionId())
+ assertEquals("42", intent.getSessionId())
+ assertEquals(manifest, intent.getWebAppManifest())
+ }
+
+ @Test
+ fun `process adds custom tab config`() = runTest {
+ val intent = Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())
+
+ val storage: ManifestStorage = mock()
+ val store = BrowserStore()
+
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "https://mozilla.com",
+ )
+ `when`(storage.loadManifest("https://mozilla.com")).thenReturn(manifest)
+
+ val addTabUseCase: CustomTabsUseCases.AddWebAppTabUseCase = mock()
+
+ val processor = WebAppIntentProcessor(store, addTabUseCase, mock(), storage)
+ assertTrue(processor.process(intent))
+
+ verify(addTabUseCase).invoke(
+ url = "https://mozilla.com",
+ source = SessionState.Source.Internal.HomeScreen,
+ customTabConfig = CustomTabConfig(
+ externalAppType = ExternalAppType.PROGRESSIVE_WEB_APP,
+ enableUrlbarHiding = true,
+ showCloseButton = false,
+ showShareMenuItem = true,
+
+ ),
+ webAppManifest = manifest,
+ )
+ }
+
+ @Test
+ fun `url override is applied to session if present`() = runTest {
+ val store = BrowserStore()
+
+ val storage: ManifestStorage = mock()
+ val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase = mock()
+ val processor = WebAppIntentProcessor(store, mock(), loadUrlUseCase, storage)
+ val urlOverride = "https://mozilla.com/deep/link/index.html"
+
+ val manifest = WebAppManifest(
+ name = "Test Manifest",
+ startUrl = "https://mozilla.com",
+ )
+
+ `when`(storage.loadManifest("https://mozilla.com")).thenReturn(manifest)
+
+ val intent = Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())
+
+ intent.putUrlOverride(urlOverride)
+
+ assertTrue(processor.process(intent))
+ verify(loadUrlUseCase).invoke(eq(urlOverride), any(), any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/pwa/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,2 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
diff --git a/mobile/android/android-components/components/feature/pwa/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/pwa/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/pwa/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28