diff options
Diffstat (limited to 'mobile/android/android-components/components/feature/pwa')
164 files changed, 6893 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/pwa/README.md b/mobile/android/android-components/components/feature/pwa/README.md new file mode 100644 index 0000000000..61da697817 --- /dev/null +++ b/mobile/android/android-components/components/feature/pwa/README.md @@ -0,0 +1,85 @@ +# [Android Components](../../../README.md) > Feature > Progressive Web Apps (PWA) + +Feature implementation for Progressive Web Apps (PWA). + +- https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps +- https://developer.mozilla.org/en-US/docs/Web/Manifest + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:feature-pwa:{latest-version}" +``` + +### Adding feature to application + +#### Creating basic homescreen shortcuts + +`WebAppUseCases` includes a use case to pin websites to the homescreen. When called, the method will show a prompt to the user to let them drag a shortcut with the currently selected session onto their homescreen. + +If you don't want to support full web apps and only want the shortcut functionality, set the `supportWebApps` parameter to `false` when creating `WebAppUseCases`. This causes all shortcuts to open a new tab in the browser. + +#### Opening web apps + +To open the pinned shortcut as a progressive web app, add `mozilla.components.feature.pwa.PWA_LAUNCHER` to your intent filter. + +```xml +<intent-filter> + <action android:name="mozilla.components.feature.pwa.VIEW_PWA" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:scheme="https" /> +</intent-filter> +``` + +You must also process the intent with the `WebAppIntentProcessor`. This processor will create a new session with the `webAppManifest` and `customTabConfig` fields set. + +The web app manifest will also be serialized onto the intent as a JSON string extra. It can be retrieved using the `Intent.getWebAppManifest` extension function. + +#### Hiding the toolbar + +`WebAppHideToolbarFeature` is used to hide the toolbar view when the user is visiting the website tied to the shortcut. Once they navigate away, the feature will show the toolbar again. + +This functionality pairs well with the `CustomTabsToolbarFeature`, which can be used to show a custom tab toolbar instead of a regular toolbar. + +#### Immersive mode, orientation settings, and recents entries. + +`WebAppActivityFeature` will set activity-level settings corresponding to the web app. + +The recents screen will show the icon and title of the web app. + +The activity orientation will be restricted to match `"orientation"` in the web app manifest. + +When `"display": "fullscreen"` is set in the web app manifest, the web app will be displayed in immersive mode. + +#### Displaying controls without a toolbar + +`WebAppSiteControlsFeature` will display a silent notification whenever a web app is open. This notification contains controls to interact with the web app, such as a refresh button and a shortcut to copy the URL. + +### Web content variables + +`WebAppContentFeature` will set web content variables to the web apps. Since the `"display"` value in the web app manifest file has to be applied in the CSS media query, the feature will apply it. + +## Facts + +This component emits the following [Facts](../../support/base/README.md#Facts): + +| Action | Item | Extras | Description | +|--------|---------|----------------|------------------------------------| +| CLICK | install_shortcut | | The user installs a PWA shortcut. | +| CLICK | homescreen_icon_tap | | The user tapped the PWA icon on the homescreen. | + +#### `itemExtras` + +| Key | Type | Value | +|--------------|---------|-----------------------------------| +| timingNs | Long | The current system time when a foreground or background action is taken. | + +## License + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/ diff --git a/mobile/android/android-components/components/feature/pwa/build.gradle b/mobile/android/android-components/components/feature/pwa/build.gradle new file mode 100644 index 0000000000..5d8b90b313 --- /dev/null +++ b/mobile/android/android-components/components/feature/pwa/build.gradle @@ -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/. */ + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'com.google.devtools.ksp' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + arg("room.generateKotlin", "true") + } + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.incremental": "true"] + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + + namespace 'mozilla.components.feature.pwa' +} + +tasks.withType(KotlinCompile).configureEach { + kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" +} + +dependencies { + implementation project(':browser-icons') + implementation project(':browser-state') + implementation project(':concept-engine') + implementation project(':concept-fetch') + implementation project(':feature-customtabs') + implementation project(':feature-tabs') + implementation project(':feature-intent') + implementation project(':feature-session') + implementation project(':service-digitalassetlinks') + implementation project(':support-base') + implementation project(':support-images') + implementation project(':support-ktx') + implementation project(':support-utils') + + implementation ComponentsDependencies.androidx_browser + implementation ComponentsDependencies.androidx_core_ktx + implementation ComponentsDependencies.androidx_lifecycle_runtime + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_room_runtime + ksp ComponentsDependencies.androidx_room_compiler + + testImplementation project(':support-test') + testImplementation project(':support-test-libstate') + + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.kotlin_reflect + testImplementation ComponentsDependencies.testing_coroutines + testImplementation ComponentsDependencies.testing_robolectric + + androidTestImplementation ComponentsDependencies.androidx_test_core + androidTestImplementation ComponentsDependencies.androidx_test_runner + androidTestImplementation ComponentsDependencies.androidx_test_rules + androidTestImplementation ComponentsDependencies.androidx_room_testing +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/feature/pwa/proguard-rules.pro b/mobile/android/android-components/components/feature/pwa/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/feature/pwa/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/0.json b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/0.json new file mode 100644 index 0000000000..3a1209f2e1 --- /dev/null +++ b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/0.json @@ -0,0 +1,51 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "dd9b306a96bcf751f6cfdc739f38f2b4", + "entities": [ + { + "tableName": "manifests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manifest` TEXT NOT NULL, `start_url` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`start_url`))", + "fields": [ + { + "fieldPath": "manifest", + "columnName": "manifest", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startUrl", + "columnName": "start_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "start_url" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"dd9b306a96bcf751f6cfdc739f38f2b4\")" + ] + } +} diff --git a/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/1.json b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/1.json new file mode 100644 index 0000000000..879029a799 --- /dev/null +++ b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/1.json @@ -0,0 +1,73 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "2dbe6c2ed8111e6d63f6bb78035424aa", + "entities": [ + { + "tableName": "manifests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manifest` TEXT NOT NULL, `start_url` TEXT NOT NULL, `scope` TEXT, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `used_at` INTEGER NOT NULL, PRIMARY KEY(`start_url`))", + "fields": [ + { + "fieldPath": "manifest", + "columnName": "manifest", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startUrl", + "columnName": "start_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedAt", + "columnName": "used_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "start_url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_manifests_scope", + "unique": false, + "columnNames": [ + "scope" + ], + "createSql": "CREATE INDEX `index_manifests_scope` ON `${TABLE_NAME}` (`scope`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2dbe6c2ed8111e6d63f6bb78035424aa')" + ] + } +} diff --git a/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/2.json b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/2.json new file mode 100644 index 0000000000..6de5b9ed0d --- /dev/null +++ b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/2.json @@ -0,0 +1,73 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "2dbe6c2ed8111e6d63f6bb78035424aa", + "entities": [ + { + "tableName": "manifests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manifest` TEXT NOT NULL, `start_url` TEXT NOT NULL, `scope` TEXT, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `used_at` INTEGER NOT NULL, PRIMARY KEY(`start_url`))", + "fields": [ + { + "fieldPath": "manifest", + "columnName": "manifest", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startUrl", + "columnName": "start_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedAt", + "columnName": "used_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "start_url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_manifests_scope", + "unique": false, + "columnNames": [ + "scope" + ], + "createSql": "CREATE INDEX `index_manifests_scope` ON `${TABLE_NAME}` (`scope`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2dbe6c2ed8111e6d63f6bb78035424aa')" + ] + } +} diff --git a/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/3.json b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/3.json new file mode 100644 index 0000000000..565165ec3b --- /dev/null +++ b/mobile/android/android-components/components/feature/pwa/schemas/mozilla.components.feature.pwa.db.ManifestDatabase/3.json @@ -0,0 +1,87 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "eb8c34cf7bcaf5f84bf0c3b407c8061a", + "entities": [ + { + "tableName": "manifests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manifest` TEXT NOT NULL, `start_url` TEXT NOT NULL, `scope` TEXT, `has_share_targets` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `used_at` INTEGER NOT NULL, PRIMARY KEY(`start_url`))", + "fields": [ + { + "fieldPath": "manifest", + "columnName": "manifest", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startUrl", + "columnName": "start_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hasShareTargets", + "columnName": "has_share_targets", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedAt", + "columnName": "used_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "start_url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_manifests_scope", + "unique": false, + "columnNames": [ + "scope" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_manifests_scope` ON `${TABLE_NAME}` (`scope`)" + }, + { + "name": "index_manifests_has_share_targets", + "unique": false, + "columnNames": [ + "has_share_targets" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_manifests_has_share_targets` ON `${TABLE_NAME}` (`has_share_targets`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb8c34cf7bcaf5f84bf0c3b407c8061a')" + ] + } +} 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 |