diff options
Diffstat (limited to 'mobile/android/android-components/components/feature/downloads')
184 files changed, 18339 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/downloads/README.md b/mobile/android/android-components/components/feature/downloads/README.md new file mode 100644 index 0000000000..a6e938389c --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/README.md @@ -0,0 +1,183 @@ +# [Android Components](../../../README.md) > Feature > Downloads + +Feature implementation for apps that want to use [Android downloads manager](https://developer.android.com/reference/android/app/DownloadManager). + +## 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-downloads:{latest-version}" +``` + +The `AbstractFetchDownloadService` also requires extra permissions needed to post notifications and to start downloads +- `android.permission.POST_NOTIFICATIONS` +- `android.permission.FOREGROUND_SERVICE` +- `android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK` + +The implementing service in the client app also needs to declare `dataSync` as `foregroundServiceType` in the manifest for +Android 14 compatibility. Adding the `FOREGROUND_SERVICE_DATA_SYNC` permission in the app manifest is not needed since it is declared in feature-downloads module. + +### DownloadsFeature +Feature implementation for proving download functionality for the selected session. + +```kotlin + + +//This will be called before starting each download in case you don't have the right permissions +// or if the user removed your permissions. +val onNeedToRequestPermissions = { + session: Session, download: Download -> + //request permission e.g (WRITE_EXTERNAL_STORAGE) + + // After you get granted the permissions, remember to call downloadsFeature.onPermissionsGranted() + // to start downloading the pending download. +} + +//This will be called after every download is completed. +val onDownloadCompleted = { + download: Download, downloadId: Long -> + //Show some UI to let user know the download was completed. +} + +val downloadsFeature = + DownloadsFeature(context, + onNeedToRequestPermissions /*Optional*/, + onDownloadCompleted /*Optional*/, + fragmentManager /*Optional, if it is provided, before every download a dialog will be shown*/, + dialog /*Optional, if it is not provided a simple dialog will be shown before every download, with a positive button and negative button.*/, + sessionManager = sessionManager) + +//Starts observing the selected session for new downloads and forward it to +// the download manager +downloadsFeature.start() + +//Stop observing the selected session +downloadsFeature.stop() + +``` + +### DownloadDialogFragment + This is general representation of a dialog meant to be used in collaboration with `DownloadsFeature` + to show a dialog before a download is triggered. If `SimpleDownloadDialogFragment` is not flexible enough for your use case you should inherit for this class. + +```kotlin +class FocusDialogDownloadFragment : DownloadDialogFragment() { + + /*Creating a customized the dialog*/ + override fun onCreateDialog(bundle: Bundle?): AlertDialog { + //DownloadsFeature will add these metadata before calling show() on the dialog. + val fileName = arguments?.getString(KEY_FILE_NAME) + //Not used, just for the sake you can use this metadata + val url = arguments?.getString(KEY_URL) + val contentLength = arguments?.getString(KEY_CONTENT_LENGTH) + + val builder = AlertDialog.Builder(requireContext()) + builder.setCancelable(true) + builder.setTitle(getString(R.string.download_dialog_title)) + + val inflater = activity!!.layoutInflater + val dialogView = inflater.inflate(R.layout.download_dialog, null) + builder.setView(dialogView) + + dialogView.download_dialog_icon.setImageResource(R.drawable.ic_download) + dialogView.download_dialog_file_name.text = fileName + dialogView.download_dialog_cancel.text = getString(R.string.download_dialog_action_cancel) + dialogView.download_dialog_download.text = + getString(R.string.download_dialog_action_download) + + dialogView.download_dialog_warning.text = getString(R.string.download_dialog_warning) + + setCancelButton(dialogView.download_dialog_cancel) + setDownloadButton(dialogView.download_dialog_download) + + return builder.create() + } + + private fun setDownloadButton(button: Button) { + button.setOnClickListener { + //Letting know DownloadFeature that can proceed with the download + onStartDownload() + TelemetryWrapper.downloadDialogDownloadEvent(true) + dismiss() + } + } + + private fun setCancelButton(button: Button) { + button.setOnClickListener { + TelemetryWrapper.downloadDialogDownloadEvent(false) + dismiss() + } + } +} + +//Adding our dialog to DownloadsFeature +val downloadsFeature = DownloadsFeature( + context(), + sessionManager = sessionManager, + fragmentManager = fragmentManager, + dialog = FocusDialogDownloadFragment() + ) + +downloadsFeature.start() +``` + +### SimpleDownloadDialogFragment + +A confirmation dialog to be called before a download is triggered. + +SimpleDownloadDialogFragment is the default dialog if you don't provide a value to DownloadsFeature. +It is composed by a title, a negative and a positive bottoms. When the positive button is clicked the download is triggered. + +```kotlin +//To use the default behavior, just provide a fragmentManager/childFragmentManager. + downloadsFeature = DownloadsFeature( + requireContext(), + sessionManager = components.sessionManager, + fragmentManager = fragmentManager /*If you're inside a Fragment use childFragmentManager '*/ + ) + + downloadsFeature.start() +``` +Customizing SimpleDownloadDialogFragment. + +```kotlin + val dialog = SimpleDownloadDialogFragment.newInstance( + dialogTitleText = R.string.dialog_title, + positiveButtonText = R.string.download, + negativeButtonText = R.string.cancel, + cancelable = true, + themeResId = R.style.your_theme + ) + + downloadsFeature = DownloadsFeature( + requireContext(), + sessionManager = components.sessionManager, + fragmentManager = fragmentManager, + dialog = dialog + ) + + downloadsFeature.start() + ``` + +## Facts + +This component emits the following [Facts](../../support/base/README.md#Facts): + +| Action | Item | Description | +|-----------|--------------|---------------------------------------------------| +| RESUME | notification | The user resumes a download. | +| PAUSE | notification | The user pauses a download. | +| CANCEL | notification | The user cancels a download. | +| TRY_AGAIN | notification | The user taps on try again when a download fails. | +| OPEN | notification | The user opens a downloaded file. | +| DISPLAY | prompt | A download prompt was shown. | +| CANCEL | prompt | A download prompt was canceled. | + +## 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/downloads/build.gradle b/mobile/android/android-components/components/feature/downloads/build.gradle new file mode 100644 index 0000000000..1072c051b1 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/build.gradle @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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: 'kotlin-parcelize' + +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") + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + + buildFeatures { + viewBinding true + } + + namespace 'mozilla.components.feature.downloads' +} + +tasks.withType(KotlinCompile).configureEach { + kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" +} + +dependencies { + implementation project(':browser-state') + implementation project(':concept-fetch') + implementation project(':support-ktx') + implementation project(':support-base') + implementation project(':support-utils') + implementation project(':ui-icons') + implementation project(':ui-widgets') + + implementation ComponentsDependencies.androidx_constraintlayout + implementation ComponentsDependencies.androidx_core_ktx + implementation ComponentsDependencies.androidx_lifecycle_livedata + implementation ComponentsDependencies.androidx_localbroadcastmanager + implementation ComponentsDependencies.androidx_paging + implementation ComponentsDependencies.androidx_recyclerview + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_room_runtime + ksp ComponentsDependencies.androidx_room_compiler + + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_coroutines + testImplementation ComponentsDependencies.testing_robolectric + testImplementation project(':concept-engine') + testImplementation project(':support-test') + testImplementation project(':support-test-libstate') + + androidTestImplementation project(':support-android-test') + + androidTestImplementation ComponentsDependencies.androidx_room_testing + androidTestImplementation ComponentsDependencies.androidx_arch_core_testing + androidTestImplementation ComponentsDependencies.androidx_test_core + androidTestImplementation ComponentsDependencies.androidx_test_runner + androidTestImplementation ComponentsDependencies.androidx_test_rules + androidTestImplementation ComponentsDependencies.testing_coroutines + +} + +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/downloads/proguard-rules.pro b/mobile/android/android-components/components/feature/downloads/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/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/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/1.json b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/1.json new file mode 100644 index 0000000000..dce503c5d4 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/1.json @@ -0,0 +1,76 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "342d0e5d0a0fcde72b88ac4585caf842", + "entities": [ + { + "tableName": "downloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "destinationDirectory", + "columnName": "destination_directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "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, '342d0e5d0a0fcde72b88ac4585caf842')" + ] + } +} diff --git a/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/2.json b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/2.json new file mode 100644 index 0000000000..43c2d3dda9 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/2.json @@ -0,0 +1,82 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "1c1abe6e744766f8b4f8e4b402b2f099", + "entities": [ + { + "tableName": "downloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `is_private` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "destinationDirectory", + "columnName": "destination_directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrivate", + "columnName": "is_private", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "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, '1c1abe6e744766f8b4f8e4b402b2f099')" + ] + } +} diff --git a/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/3.json b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/3.json new file mode 100644 index 0000000000..3cc682cc1b --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/3.json @@ -0,0 +1,76 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "342d0e5d0a0fcde72b88ac4585caf842", + "entities": [ + { + "tableName": "downloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "destinationDirectory", + "columnName": "destination_directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "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, '342d0e5d0a0fcde72b88ac4585caf842')" + ] + } +} diff --git a/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/4.json b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/4.json new file mode 100644 index 0000000000..8a66ffc3a9 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/schemas/mozilla.components.feature.downloads.db.DownloadsDatabase/4.json @@ -0,0 +1,76 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "342d0e5d0a0fcde72b88ac4585caf842", + "entities": [ + { + "tableName": "downloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "destinationDirectory", + "columnName": "destination_directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "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, '342d0e5d0a0fcde72b88ac4585caf842')" + ] + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt b/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt new file mode 100644 index 0000000000..e7c689b4f0 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt @@ -0,0 +1,281 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.content.Context +import androidx.room.Room +import androidx.room.testing.MigrationTestHelper +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.feature.downloads.db.DownloadsDatabase +import mozilla.components.feature.downloads.db.Migrations +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +private const val MIGRATION_TEST_DB = "migration-test" + +@ExperimentalCoroutinesApi +class OnDeviceDownloadStorageTest { + private lateinit var context: Context + private lateinit var storage: DownloadStorage + private lateinit var database: DownloadsDatabase + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + DownloadsDatabase::class.java, + ) + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + database = Room.inMemoryDatabaseBuilder(context, DownloadsDatabase::class.java).build() + + storage = DownloadStorage(context) + storage.database = lazy { database } + } + + @After + fun tearDown() { + database.close() + } + + @Test + fun migrate1to2() { + helper.createDatabase(MIGRATION_TEST_DB, 1).apply { + query("SELECT * FROM downloads").use { cursor -> + assertEquals(-1, cursor.columnNames.indexOf("is_private")) + } + execSQL( + "INSERT INTO " + + "downloads " + + "(id, url, file_name, content_type,content_length,status,destination_directory,created_at) " + + "VALUES " + + "(1,'url','file_name','content_type',1,1,'destination_directory',1)", + ) + } + + val dbVersion2 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 2, true, Migrations.migration_1_2) + + dbVersion2.query("SELECT * FROM downloads").use { cursor -> + assertTrue(cursor.columnNames.contains("is_private")) + + cursor.moveToFirst() + assertEquals(0, cursor.getInt(cursor.getColumnIndexOrThrow("is_private"))) + } + } + + @Test + fun migrate2to3() { + helper.createDatabase(MIGRATION_TEST_DB, 2).apply { + query("SELECT * FROM downloads").use { cursor -> + assertTrue(cursor.columnNames.contains("is_private")) + } + // A private download + execSQL( + "INSERT INTO " + + "downloads " + + "(id, url, file_name, content_type,content_length,status,destination_directory,created_at,is_private) " + + "VALUES " + + "(1,'url','file_name','content_type',1,1,'destination_directory',1,1)", + ) + + // A normal download + execSQL( + "INSERT INTO " + + "downloads " + + "(id, url, file_name, content_type,content_length,status,destination_directory,created_at,is_private) " + + "VALUES " + + "(2,'url','file_name','content_type',1,1,'destination_directory',1,0)", + ) + } + + val dbVersion2 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 3, true, Migrations.migration_2_3) + + dbVersion2.query("SELECT * FROM downloads").use { cursor -> + assertFalse(cursor.columnNames.contains("is_private")) + assertEquals(1, cursor.count) + + cursor.moveToFirst() + // Only non private downloads should be in the db. + assertEquals(2, cursor.getInt(cursor.getColumnIndexOrThrow("id"))) + } + } + + @Test + fun migrate3to4() { + helper.createDatabase(MIGRATION_TEST_DB, 3).apply { + // A data url download + execSQL( + "INSERT INTO " + + "downloads " + + "(id, url, file_name, content_type,content_length,status,destination_directory,created_at) " + + "VALUES " + + "(1,'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==','file_name','content_type',1,1,'destination_directory',1)", + ) + // A normal url download + execSQL( + "INSERT INTO " + + "downloads " + + "(id, url, file_name, content_type,content_length,status,destination_directory,created_at) " + + "VALUES " + + "(2,'url','file_name','content_type',1,1,'destination_directory',1)", + ) + } + + val dbVersion4 = helper.runMigrationsAndValidate(MIGRATION_TEST_DB, 4, true, Migrations.migration_3_4) + + dbVersion4.query("SELECT * FROM downloads").use { cursor -> + assertEquals(2, cursor.count) + + cursor.moveToFirst() + // Data url must be removed from download 1. + assertEquals("", cursor.getString(cursor.getColumnIndexOrThrow("url"))) + + cursor.moveToNext() + + // The download 1 must keep its url. + assertEquals("url", cursor.getString(cursor.getColumnIndexOrThrow("url"))) + } + } + + @Test + fun testAddingDownload() = runTest { + val download1 = createMockDownload("1", "url1") + val download2 = createMockDownload("2", "url2") + val download3 = createMockDownload("3", "url3") + + storage.add(download1) + storage.add(download2) + storage.add(download3) + + val downloads = getDownloadsPagedList() + + assertEquals(3, downloads.size) + + assertTrue(DownloadStorage.isSameDownload(download1, downloads.first())) + assertTrue(DownloadStorage.isSameDownload(download2, downloads[1])) + assertTrue(DownloadStorage.isSameDownload(download3, downloads[2])) + } + + @Test + fun testAddingDataURLDownload() = runTest { + val download1 = createMockDownload("1", "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==") + val download2 = createMockDownload("2", "url2") + + storage.add(download1) + storage.add(download2) + + val downloads = getDownloadsPagedList() + + assertEquals(2, downloads.size) + + assertTrue(DownloadStorage.isSameDownload(download1.copy(url = ""), downloads.first())) + assertTrue(DownloadStorage.isSameDownload(download2, downloads[1])) + } + + @Test + fun testUpdatingDataURLDownload() = runTest { + val download1 = createMockDownload("1", "url1") + val download2 = createMockDownload("2", "url2") + + storage.add(download1) + storage.add(download2) + + var downloads = getDownloadsPagedList() + + assertEquals(2, downloads.size) + + assertTrue(DownloadStorage.isSameDownload(download1, downloads.first())) + assertTrue(DownloadStorage.isSameDownload(download2, downloads[1])) + + val updatedDownload1 = createMockDownload("1", "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==") + val updatedDownload2 = createMockDownload("2", "updated_url2") + + storage.update(updatedDownload1) + storage.update(updatedDownload2) + + downloads = getDownloadsPagedList() + + assertTrue(DownloadStorage.isSameDownload(updatedDownload1.copy(url = ""), downloads.first())) + assertTrue(DownloadStorage.isSameDownload(updatedDownload2, downloads[1])) + } + + @Test + fun testRemovingDownload() = runTest { + val download1 = createMockDownload("1", "url1") + val download2 = createMockDownload("2", "url2") + + storage.add(download1) + storage.add(download2) + + assertEquals(2, getDownloadsPagedList().size) + + storage.remove(download1) + + val downloads = getDownloadsPagedList() + val downloadFromDB = downloads.first() + + assertEquals(1, downloads.size) + assertTrue(DownloadStorage.isSameDownload(download2, downloadFromDB)) + } + + @Test + fun testGettingDownloads() = runTest { + val download1 = createMockDownload("1", "url1") + val download2 = createMockDownload("2", "url2") + + storage.add(download1) + storage.add(download2) + + val downloads = getDownloadsPagedList() + + assertEquals(2, downloads.size) + + assertTrue(DownloadStorage.isSameDownload(download1, downloads.first())) + assertTrue(DownloadStorage.isSameDownload(download2, downloads[1])) + } + + @Test + fun testRemovingDownloads() = runTest { + for (index in 1..2) { + storage.add(createMockDownload(index.toString(), "url1")) + } + + var pagedList = getDownloadsPagedList() + + assertEquals(2, pagedList.size) + + pagedList.forEach { download -> + storage.remove(download) + } + + pagedList = getDownloadsPagedList() + + assertTrue(pagedList.isEmpty()) + } + + private fun createMockDownload(id: String, url: String): DownloadState { + return DownloadState( + id = id, + url = url, + contentType = "application/zip", + contentLength = 5242880, + userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36", + ) + } + + private suspend fun getDownloadsPagedList(): List<DownloadState> { + return storage.getDownloadsList() + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt b/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt new file mode 100644 index 0000000000..ac7e4da2fb --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.downloads.db + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.test.runTest +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.feature.downloads.DownloadStorage +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class DownloadDaoTest { + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + private lateinit var database: DownloadsDatabase + private lateinit var dao: DownloadDao + + @Before + fun setUp() { + database = Room.inMemoryDatabaseBuilder(context, DownloadsDatabase::class.java).build() + dao = database.downloadDao() + } + + @After + fun tearDown() { + database.close() + } + + @Test + fun testInsertingAndReadingDownloads() = runTest { + val download = insertMockDownload("1", "https://www.mozilla.org/file1.txt") + val pagedList = getDownloadsPagedList() + + assertEquals(1, pagedList.size) + assertTrue(DownloadStorage.isSameDownload(download, pagedList[0].toDownloadState())) + } + + @Test + fun testRemoveAllDownloads() = runTest { + for (index in 1..4) { + insertMockDownload(index.toString(), "https://www.mozilla.org/file1.txt") + } + + var pagedList = getDownloadsPagedList() + + assertEquals(4, pagedList.size) + dao.deleteAllDownloads() + + pagedList = getDownloadsPagedList() + + assertTrue(pagedList.isEmpty()) + } + + @Test + fun testRemovingDownloads() = runTest { + for (index in 1..2) { + insertMockDownload(index.toString(), "https://www.mozilla.org/file1.txt") + } + + var pagedList = getDownloadsPagedList() + + assertEquals(2, pagedList.size) + + pagedList.forEach { + dao.delete(it) + } + + pagedList = getDownloadsPagedList() + + assertTrue(pagedList.isEmpty()) + } + + @Test + fun testUpdateDownload() = runTest { + insertMockDownload("1", "https://www.mozilla.org/file1.txt") + + var pagedList = getDownloadsPagedList() + + assertEquals(1, pagedList.size) + + val download = pagedList.first() + + val updatedDownload = download.toDownloadState().copy("new_url") + + dao.update(updatedDownload.toDownloadEntity()) + pagedList = getDownloadsPagedList() + + assertEquals("new_url", pagedList.first().url) + } + + private suspend fun getDownloadsPagedList(): List<DownloadEntity> { + return dao.getDownloadsList() + } + + private suspend fun insertMockDownload(id: String, url: String): DownloadState { + val download = DownloadState( + id = id, + url = url, + contentType = "application/zip", + contentLength = 5242880, + userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36", + ) + dao.insert(download.toDownloadEntity()) + return download + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/downloads/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0b82a27a0a --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> + + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" + tools:ignore="ScopedStorage" + android:maxSdkVersion="28"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> + + <!-- Needed to prompt the user to give permission to install a downloaded apk --> + <uses-permission-sdk-23 android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> + + <!-- Needed to receive broadcasts from AC code--> + <uses-permission android:name="${applicationId}.permission.RECEIVE_DOWNLOAD_BROADCAST" /> + + <permission + android:name="${applicationId}.permission.RECEIVE_DOWNLOAD_BROADCAST" + android:protectionLevel="signature" /> + + <application android:supportsRtl="true"> + <provider + android:name="mozilla.components.feature.downloads.provider.FileProvider" + android:authorities="${applicationId}.feature.downloads.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/feature_downloads_file_paths" /> + </provider> + </application> +</manifest> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt new file mode 100644 index 0000000000..6b03d9a059 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt @@ -0,0 +1,1120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE +import android.app.DownloadManager.EXTRA_DOWNLOAD_ID +import android.app.Notification +import android.app.Service +import android.content.ActivityNotFoundException +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_VIEW +import android.content.IntentFilter +import android.net.Uri +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.Bundle +import android.os.Environment +import android.os.IBinder +import android.os.ParcelFileDescriptor +import android.provider.MediaStore +import android.provider.MediaStore.setIncludePending +import android.webkit.MimeTypeMap +import android.widget.Toast +import androidx.annotation.ColorRes +import androidx.annotation.GuardedBy +import androidx.annotation.VisibleForTesting +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status +import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED +import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED +import mozilla.components.browser.state.state.content.DownloadState.Status.DOWNLOADING +import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED +import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED +import mozilla.components.browser.state.state.content.DownloadState.Status.PAUSED +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Headers.Names.CONTENT_RANGE +import mozilla.components.concept.fetch.Headers.Names.RANGE +import mozilla.components.concept.fetch.MutableHeaders +import mozilla.components.concept.fetch.Request +import mozilla.components.feature.downloads.DownloadNotification.NOTIFICATION_DOWNLOAD_GROUP_ID +import mozilla.components.feature.downloads.ext.addCompletedDownload +import mozilla.components.feature.downloads.ext.isScheme +import mozilla.components.feature.downloads.ext.withResponse +import mozilla.components.feature.downloads.facts.emitNotificationCancelFact +import mozilla.components.feature.downloads.facts.emitNotificationOpenFact +import mozilla.components.feature.downloads.facts.emitNotificationPauseFact +import mozilla.components.feature.downloads.facts.emitNotificationResumeFact +import mozilla.components.feature.downloads.facts.emitNotificationTryAgainFact +import mozilla.components.support.base.android.NotificationsDelegate +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.kotlin.ifNullOrEmpty +import mozilla.components.support.ktx.kotlin.sanitizeURL +import mozilla.components.support.ktx.kotlinx.coroutines.throttleLatest +import mozilla.components.support.utils.DownloadUtils +import mozilla.components.support.utils.ext.registerReceiverCompat +import mozilla.components.support.utils.ext.stopForegroundCompat +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import kotlin.random.Random + +/** + * Service that performs downloads through a fetch [Client] rather than through the native + * Android download manager. + * + * To use this service, you must create a subclass in your application and add it to the manifest. + */ +@Suppress("TooManyFunctions", "LargeClass", "ComplexMethod") +abstract class AbstractFetchDownloadService : Service() { + protected abstract val store: BrowserStore + protected abstract val notificationsDelegate: NotificationsDelegate + + private val notificationUpdateScope = MainScope() + + protected abstract val httpClient: Client + + protected open val style: Style = Style() + + @VisibleForTesting + internal val context: Context get() = this + + @VisibleForTesting + internal var compatForegroundNotificationId: Int = COMPAT_DEFAULT_FOREGROUND_ID + private val logger = Logger("AbstractFetchDownloadService") + + internal var downloadJobs = mutableMapOf<String, DownloadJobState>() + + // TODO Move this to browser store and make immutable: + // https://github.com/mozilla-mobile/android-components/issues/7050 + internal data class DownloadJobState( + var job: Job? = null, + @Volatile var state: DownloadState, + var currentBytesCopied: Long = 0, + @GuardedBy("context") var status: Status, + var foregroundServiceId: Int = 0, + var downloadDeleted: Boolean = false, + var notifiedStopped: Boolean = false, + var lastNotificationUpdate: Long = 0L, + var createdTime: Long = System.currentTimeMillis(), + ) { + internal fun canUpdateNotification(): Boolean { + return isUnderNotificationUpdateLimit() && !notifiedStopped + } + + /** + * Android imposes a limit on of how often we can send updates for a notification. + * The limit is one second per update. + * See https://developer.android.com/training/notify-user/build-notification.html#Updating + * This function indicates if we are under that limit. + */ + internal fun isUnderNotificationUpdateLimit(): Boolean { + return getSecondsSinceTheLastNotificationUpdate() >= 1 + } + + @Suppress("MagicNumber") + internal fun getSecondsSinceTheLastNotificationUpdate(): Long { + return (System.currentTimeMillis() - lastNotificationUpdate) / 1000 + } + } + + internal fun setDownloadJobStatus(downloadJobState: DownloadJobState, status: Status) { + synchronized(context) { + if (status == DOWNLOADING) { + downloadJobState.notifiedStopped = false + } + downloadJobState.status = status + updateDownloadState(downloadJobState.state.copy(status = status)) + } + } + + internal fun getDownloadJobStatus(downloadJobState: DownloadJobState): Status { + synchronized(context) { + return downloadJobState.status + } + } + + internal val broadcastReceiver by lazy { + object : BroadcastReceiver() { + @Suppress("LongMethod") + override fun onReceive(context: Context, intent: Intent?) { + val downloadId = + intent?.extras?.getString(DownloadNotification.EXTRA_DOWNLOAD_ID) ?: return + val currentDownloadJobState = downloadJobs[downloadId] ?: return + + when (intent.action) { + ACTION_PAUSE -> { + setDownloadJobStatus(currentDownloadJobState, PAUSED) + currentDownloadJobState.job?.cancel() + emitNotificationPauseFact() + logger.debug("ACTION_PAUSE for ${currentDownloadJobState.state.id}") + } + + ACTION_RESUME -> { + setDownloadJobStatus(currentDownloadJobState, DOWNLOADING) + + currentDownloadJobState.job = CoroutineScope(IO).launch { + startDownloadJob(currentDownloadJobState) + } + + emitNotificationResumeFact() + logger.debug("ACTION_RESUME for ${currentDownloadJobState.state.id}") + } + + ACTION_CANCEL -> { + cancelDownloadJob(currentDownloadJobState) + removeDownloadJob(currentDownloadJobState) + emitNotificationCancelFact() + logger.debug("ACTION_CANCEL for ${currentDownloadJobState.state.id}") + } + + ACTION_TRY_AGAIN -> { + removeNotification(context, currentDownloadJobState) + currentDownloadJobState.lastNotificationUpdate = System.currentTimeMillis() + setDownloadJobStatus(currentDownloadJobState, DOWNLOADING) + + currentDownloadJobState.job = CoroutineScope(IO).launch { + startDownloadJob(currentDownloadJobState) + } + + emitNotificationTryAgainFact() + logger.debug("ACTION_TRY_AGAIN for ${currentDownloadJobState.state.id}") + } + + ACTION_DISMISS -> { + removeDownloadJob(currentDownloadJobState) + logger.debug("ACTION_DISMISS for ${currentDownloadJobState.state.id}") + } + + ACTION_OPEN -> { + if (!openFile(context, currentDownloadJobState.state)) { + val fileExt = MimeTypeMap.getFileExtensionFromUrl( + currentDownloadJobState.state.filePath.toString(), + ) + val errorMessage = applicationContext.getString( + R.string.mozac_feature_downloads_open_not_supported1, + fileExt, + ) + + Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show() + logger.debug("ACTION_OPEN errorMessage for ${currentDownloadJobState.state.id} ") + } + + emitNotificationOpenFact() + logger.debug("ACTION_OPEN for ${currentDownloadJobState.state.id}") + } + } + } + } + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onCreate() { + super.onCreate() + registerNotificationActionsReceiver() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val download = intent?.getStringExtra(EXTRA_DOWNLOAD_ID)?.let { + store.state.downloads[it] + } ?: return START_REDELIVER_INTENT + + when (intent.action) { + ACTION_REMOVE_PRIVATE_DOWNLOAD -> { + handleRemovePrivateDownloadIntent(download) + } + ACTION_TRY_AGAIN -> { + val newDownloadState = download.copy(status = DOWNLOADING) + store.dispatch( + DownloadAction.UpdateDownloadAction( + newDownloadState, + ), + ) + handleDownloadIntent(newDownloadState) + } + + else -> { + handleDownloadIntent(download) + } + } + + return super.onStartCommand(intent, flags, startId) + } + + @VisibleForTesting + internal fun handleRemovePrivateDownloadIntent(download: DownloadState) { + if (download.private) { + downloadJobs[download.id]?.let { + // Do not cancel already completed downloads. + if (it.status != COMPLETED) { + cancelDownloadJob(it) + } + removeDownloadJob(it) + } + store.dispatch(DownloadAction.RemoveDownloadAction(download.id)) + } + } + + @VisibleForTesting + internal fun handleDownloadIntent(download: DownloadState) { + // If the job already exists, then don't create a new ID. This can happen when calling tryAgain + val foregroundServiceId = downloadJobs[download.id]?.foregroundServiceId ?: Random.nextInt() + + val actualStatus = if (download.status == INITIATED) DOWNLOADING else download.status + + // Create a new job and add it, with its downloadState to the map + val downloadJobState = DownloadJobState( + state = download.copy(status = actualStatus, notificationId = foregroundServiceId), + foregroundServiceId = foregroundServiceId, + status = actualStatus, + ) + + store.dispatch(DownloadAction.UpdateDownloadAction(downloadJobState.state)) + + if (actualStatus == DOWNLOADING) { + downloadJobState.job = CoroutineScope(IO).launch { + startDownloadJob(downloadJobState) + } + } + + downloadJobs[download.id] = downloadJobState + + setForegroundNotification(downloadJobState) + + notificationUpdateScope.launch { + while (isActive) { + delay(PROGRESS_UPDATE_INTERVAL) + updateDownloadNotification() + if (downloadJobs.isEmpty()) cancel() + } + } + } + + @VisibleForTesting + internal fun cancelDownloadJob( + currentDownloadJobState: DownloadJobState, + ) { + currentDownloadJobState.lastNotificationUpdate = System.currentTimeMillis() + setDownloadJobStatus( + currentDownloadJobState, + CANCELLED, + ) + currentDownloadJobState.job?.cancel() + currentDownloadJobState.job = CoroutineScope(IO).launch { + deleteDownloadingFile(currentDownloadJobState.state) + currentDownloadJobState.downloadDeleted = + true + } + } + + /** + * Android rate limits notifications being sent, so we must send them on a delay so that + * notifications are not dropped + */ + @Suppress("ComplexMethod") + private fun updateDownloadNotification() { + for (download in downloadJobs.values) { + if (!download.canUpdateNotification()) { continue } + /* + * We want to keep a consistent state in the UI, download.status can be changed from + * another thread while we are posting updates to the UI, causing inconsistent UIs. + * For this reason, we ONLY use the latest status during an UI update, new changes + * will be posted in subsequent updates. + */ + val uiStatus = getDownloadJobStatus(download) + + updateForegroundNotificationIfNeeded(download) + + // Dispatch the corresponding notification based on the current status + updateDownloadNotification(uiStatus, download) + + if (uiStatus != DOWNLOADING) { + sendDownloadStopped(download) + } + } + } + + /** + * Data class for styling download notifications. + * @param notificationAccentColor accent color for all download notifications. + */ + data class Style( + @ColorRes + val notificationAccentColor: Int = R.color.mozac_feature_downloads_notification, + ) + + /** + * Updates the notification state with the passed [download] data. + * Be aware that you need to pass [latestUIStatus] as [DownloadJobState.status] can be modified + * from another thread, causing inconsistencies in the ui. + */ + @VisibleForTesting + internal fun updateDownloadNotification( + latestUIStatus: Status, + download: DownloadJobState, + scope: CoroutineScope = CoroutineScope(IO), + ) { + val notification = when (latestUIStatus) { + DOWNLOADING -> DownloadNotification.createOngoingDownloadNotification( + context, + download, + style.notificationAccentColor, + ) + PAUSED -> DownloadNotification.createPausedDownloadNotification( + context, + download, + style.notificationAccentColor, + ) + FAILED -> DownloadNotification.createDownloadFailedNotification( + context, + download, + style.notificationAccentColor, + ) + COMPLETED -> { + addToDownloadSystemDatabaseCompat(download.state, scope) + DownloadNotification.createDownloadCompletedNotification( + context, + download, + style.notificationAccentColor, + ) + } + CANCELLED -> { + removeNotification(context, download) + download.lastNotificationUpdate = System.currentTimeMillis() + null + } + INITIATED -> null + } + + notification?.let { + notificationsDelegate.notify( + notificationId = download.foregroundServiceId, + notification = it, + ) + download.lastNotificationUpdate = System.currentTimeMillis() + } + } + + override fun onTaskRemoved(rootIntent: Intent?) { + stopSelf() + } + + override fun onDestroy() { + super.onDestroy() + + clearAllDownloadsNotificationsAndJobs() + unregisterNotificationActionsReceiver() + } + + // Cancels all running jobs and remove all notifications. + // Also cleans any resources that we were holding like broadcastReceivers + internal fun clearAllDownloadsNotificationsAndJobs() { + val notificationManager = NotificationManagerCompat.from(context) + + stopForegroundCompat(true) + compatForegroundNotificationId = COMPAT_DEFAULT_FOREGROUND_ID + + // Before doing any cleaning, we have to stop the notification updater scope. + // To ensure we are not recreating the notifications. + notificationUpdateScope.cancel() + + downloadJobs.values.forEach { state -> + notificationManager.cancel(state.foregroundServiceId) + state.job?.cancel() + } + if (SDK_INT >= Build.VERSION_CODES.N) { + notificationManager.cancel(NOTIFICATION_DOWNLOAD_GROUP_ID) + } + } + + @Suppress("TooGenericExceptionCaught") + internal fun startDownloadJob(currentDownloadJobState: DownloadJobState) { + logger.debug("Starting download for ${currentDownloadJobState.state.id} ") + try { + performDownload(currentDownloadJobState) + } catch (e: Exception) { + logger.error("Unable to complete download for ${currentDownloadJobState.state.id} marked as FAILED", e) + setDownloadJobStatus(currentDownloadJobState, FAILED) + } + } + + internal fun deleteDownloadingFile(downloadState: DownloadState) { + val downloadedFile = File(downloadState.filePath) + downloadedFile.delete() + } + + /** + * Adds a file to the downloads database system, so it could appear in Downloads App + * (and thus become eligible for management by the Downloads App) only for compatible devices + * otherwise nothing will happen. + */ + @VisibleForTesting + internal fun addToDownloadSystemDatabaseCompat( + download: DownloadState, + scope: CoroutineScope = CoroutineScope(IO), + ) { + if (!shouldUseScopedStorage()) { + val fileName = download.fileName + ?: throw IllegalStateException("A fileName for a download is required") + val file = File(download.filePath) + // addCompletedDownload can't handle any non http(s) urls + scope.launch { + addCompletedDownload( + title = fileName, + description = fileName, + isMediaScannerScannable = true, + mimeType = getSafeContentType(context, download.filePath, download.contentType), + path = file.absolutePath, + length = download.contentLength ?: file.length(), + // Only show notifications if our channel is blocked + showNotification = !DownloadNotification.isChannelEnabled(context), + download, + ) + } + } + } + + @VisibleForTesting + @Suppress("LongParameterList") + internal fun addCompletedDownload( + title: String, + description: String, + isMediaScannerScannable: Boolean, + mimeType: String, + path: String, + length: Long, + showNotification: Boolean, + download: DownloadState, + ) { + try { + val url = if (!download.isScheme(listOf("http", "https"))) null else download.url.toUri() + context.addCompletedDownload( + title = title, + description = description, + isMediaScannerScannable = isMediaScannerScannable, + mimeType = mimeType, + path = path, + length = length, + // Only show notifications if our channel is blocked + showNotification = showNotification, + uri = url, + referer = download.referrerUrl?.toUri(), + ) + } catch (e: IllegalArgumentException) { + logger.error("Unable add the download to the system database", e) + } + } + + @VisibleForTesting + internal fun registerNotificationActionsReceiver() { + val filter = IntentFilter().apply { + addAction(ACTION_PAUSE) + addAction(ACTION_RESUME) + addAction(ACTION_CANCEL) + addAction(ACTION_DISMISS) + addAction(ACTION_TRY_AGAIN) + addAction(ACTION_OPEN) + } + + context.registerReceiverCompat( + broadcastReceiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + } + + @VisibleForTesting + internal fun unregisterNotificationActionsReceiver() { + context.unregisterReceiver(broadcastReceiver) + } + + @VisibleForTesting + internal fun removeDownloadJob(downloadJobState: DownloadJobState) { + downloadJobs.remove(downloadJobState.state.id) + if (downloadJobs.isEmpty()) { + stopSelf() + } else { + updateForegroundNotificationIfNeeded(downloadJobState) + removeNotification(context, downloadJobState) + } + } + + @VisibleForTesting + internal fun removeNotification(context: Context, currentDownloadJobState: DownloadJobState) { + NotificationManagerCompat.from(context).cancel(currentDownloadJobState.foregroundServiceId) + } + + /** + * Refresh the notification group content only for devices that support it, + * otherwise nothing will happen. + */ + @VisibleForTesting + internal fun updateNotificationGroup(): Notification? { + return if (SDK_INT >= Build.VERSION_CODES.N) { + val downloadList = downloadJobs.values.toList() + val notificationGroup = + DownloadNotification.createDownloadGroupNotification( + context, + downloadList, + style.notificationAccentColor, + ) + + notificationsDelegate.notify( + notificationId = NOTIFICATION_DOWNLOAD_GROUP_ID, + notification = notificationGroup, + ) + notificationGroup + } else { + null + } + } + + internal fun createCompactForegroundNotification(downloadJobState: DownloadJobState): Notification { + val notification = + DownloadNotification.createOngoingDownloadNotification( + context, + downloadJobState, + style.notificationAccentColor, + ) + compatForegroundNotificationId = downloadJobState.foregroundServiceId + + notificationsDelegate.notify( + notificationId = compatForegroundNotificationId, + notification = notification, + ) + + downloadJobState.lastNotificationUpdate = System.currentTimeMillis() + + return notification + } + + @VisibleForTesting + internal fun getForegroundId(): Int { + return if (SDK_INT >= Build.VERSION_CODES.N) { + NOTIFICATION_DOWNLOAD_GROUP_ID + } else { + compatForegroundNotificationId + } + } + + /** + * We have two different behaviours as notification groups are not supported in all devices. + * For devices that support it, we create a separate notification which will be the foreground + * notification, it will be always present until we don't have more active downloads. + * For devices that doesn't support notification groups, we set the latest active notification as + * the foreground notification and we keep changing it to the latest active download. + */ + @VisibleForTesting + internal fun setForegroundNotification(downloadJobState: DownloadJobState) { + var previousDownload: DownloadJobState? = null + + val (notificationId, notification) = if (SDK_INT >= Build.VERSION_CODES.N) { + NOTIFICATION_DOWNLOAD_GROUP_ID to updateNotificationGroup() + } else { + previousDownload = downloadJobs.values.firstOrNull { + it.foregroundServiceId == compatForegroundNotificationId + } + downloadJobState.foregroundServiceId to createCompactForegroundNotification( + downloadJobState, + ) + } + + startForeground(notificationId, notification) + /** + * In devices that doesn't use notification groups, every new download becomes the new foreground one, + * unfortunately, when we call startForeground it removes the previous foreground notification + * when it's not an ongoing one, for this reason, we have to recreate the deleted notification. + * By the way ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH) + * doesn't work neither calling stopForeground(false) and then calling startForeground, + * it always deletes the previous notification :( + */ + previousDownload?.let { + updateDownloadNotification(previousDownload.status, it) + } + } + + /** + * Indicates the status of a download has changed and maybe the foreground notification needs, + * to be updated. For devices that support group notifications, we update the overview + * notification. For devices that don't support group notifications, we try to find a new + * active download and selected it as the new foreground notification. + */ + internal fun updateForegroundNotificationIfNeeded(download: DownloadJobState) { + if (SDK_INT < Build.VERSION_CODES.N) { + /** + * For devices that don't support notification groups, we have to keep updating + * the foreground notification id, when the previous one gets a state that + * is likely to be dismissed. + */ + val status = download.status + val foregroundId = download.foregroundServiceId + val isSelectedForegroundId = compatForegroundNotificationId == foregroundId + val needNewForegroundNotification = when (status) { + COMPLETED, FAILED, CANCELLED -> true + else -> false + } + + if (isSelectedForegroundId && needNewForegroundNotification) { + // We need to deselect the actual foreground notification, because while it is + // selected the user will not be able to dismiss it. + stopForegroundCompat(false) + + // Now we need to find a new foreground notification, if needed. + val newSelectedForegroundDownload = downloadJobs.values.firstOrNull { it.status == DOWNLOADING } + newSelectedForegroundDownload?.let { + setForegroundNotification(it) + } + } + } else { + // This device supports notification groups, we just need to update the summary notification + updateNotificationGroup() + } + // If all downloads have been completed we don't need the status of + // foreground service anymore, we can call stopForeground and let the user + // swipe the foreground notification. + val finishedDownloading = downloadJobs.values.toList().all { it.status == COMPLETED } + if (finishedDownloading) { + stopForegroundCompat(false) + } + } + + @Suppress("ComplexCondition", "ComplexMethod") + internal fun performDownload(currentDownloadJobState: DownloadJobState, useHttpClient: Boolean = false) { + val download = currentDownloadJobState.state + val isResumingDownload = currentDownloadJobState.currentBytesCopied > 0L + val headers = MutableHeaders() + + if (isResumingDownload) { + headers.append(RANGE, "bytes=${currentDownloadJobState.currentBytesCopied}-") + } + + var isUsingHttpClient = false + val request = Request( + download.url.sanitizeURL(), + headers = headers, + private = download.private, + referrerUrl = download.referrerUrl, + ) + // When resuming a download we need to use the httpClient as + // download.response doesn't support adding headers. + val response = if (isResumingDownload || useHttpClient || download.response == null) { + isUsingHttpClient = true + httpClient.fetch(request) + } else { + requireNotNull(download.response) + } + logger.debug("Fetching download for ${currentDownloadJobState.state.id} ") + + // If we are resuming a download and the response does not contain a CONTENT_RANGE + // we cannot be sure that the request will properly be handled + if (response.status != PARTIAL_CONTENT_STATUS && response.status != OK_STATUS || + (isResumingDownload && !response.headers.contains(CONTENT_RANGE)) + ) { + response.close() + // We experienced a problem trying to fetch the file, send a failure notification + currentDownloadJobState.currentBytesCopied = 0 + currentDownloadJobState.state = currentDownloadJobState.state.copy(currentBytesCopied = 0) + setDownloadJobStatus(currentDownloadJobState, FAILED) + logger.debug("Unable to fetching Download for ${currentDownloadJobState.state.id} status FAILED") + return + } + + response.body.useStream { inStream -> + var copyInChuckStatus: CopyInChuckStatus? = null + val newDownloadState = download.withResponse(response.headers, inStream) + currentDownloadJobState.state = newDownloadState + + useFileStream(newDownloadState, isResumingDownload) { outStream -> + copyInChuckStatus = copyInChunks(currentDownloadJobState, inStream, outStream, isUsingHttpClient) + } + + if (copyInChuckStatus != CopyInChuckStatus.ERROR_IN_STREAM_CLOSED) { + verifyDownload(currentDownloadJobState) + } + } + } + + /** + * Updates the status of an ACTIVE download to completed or failed based on bytes copied + */ + internal fun verifyDownload(download: DownloadJobState) { + if (getDownloadJobStatus(download) == DOWNLOADING && + download.currentBytesCopied < download.state.contentLength ?: 0 + ) { + setDownloadJobStatus(download, FAILED) + logger.error("verifyDownload for ${download.state.id} FAILED") + } else if (getDownloadJobStatus(download) == DOWNLOADING) { + setDownloadJobStatus(download, COMPLETED) + /** + * In cases when we don't get the file size provided initially, we have to + * use downloadState.currentBytesCopied as a fallback. + */ + val fileSizeNotFound = download.state.contentLength == null || download.state.contentLength == 0L + if (fileSizeNotFound) { + val newState = download.state.copy(contentLength = download.currentBytesCopied) + updateDownloadState(newState) + } + logger.debug("verifyDownload for ${download.state.id} ${download.status}") + } + } + + @VisibleForTesting + internal enum class CopyInChuckStatus { + COMPLETED, ERROR_IN_STREAM_CLOSED + } + + @VisibleForTesting + internal fun copyInChunks( + downloadJobState: DownloadJobState, + inStream: InputStream, + outStream: OutputStream, + downloadWithHttpClient: Boolean = false, + ): CopyInChuckStatus { + val data = ByteArray(CHUNK_SIZE) + logger.debug( + "starting copyInChunks ${downloadJobState.state.id}" + + " currentBytesCopied ${downloadJobState.state.currentBytesCopied}", + ) + + val throttleUpdateDownload = throttleLatest<Long>( + PROGRESS_UPDATE_INTERVAL, + coroutineScope = CoroutineScope(IO), + ) { copiedBytes -> + val newState = downloadJobState.state.copy(currentBytesCopied = copiedBytes) + updateDownloadState(newState) + } + + var isInStreamClosed = false + // To ensure that we copy all files (even ones that don't have fileSize, we must NOT check < fileSize + while (getDownloadJobStatus(downloadJobState) == DOWNLOADING) { + var bytesRead: Int + try { + bytesRead = inStream.read(data) + } catch (e: IOException) { + if (downloadWithHttpClient) { + throw e + } + isInStreamClosed = true + break + } + // If bytesRead is -1, there's no data left to read from the stream + if (bytesRead == -1) { break } + downloadJobState.currentBytesCopied += bytesRead + + throttleUpdateDownload(downloadJobState.currentBytesCopied) + + outStream.write(data, 0, bytesRead) + } + if (isInStreamClosed) { + // In cases where [download.response] is available and users with slow + // networks start a download but quickly press pause and then resume + // [isResumingDownload] will be false as there will be not enough time + // for bytes to be copied, but the stream in [download.response] will be closed, + // we have to fallback to [httpClient] + performDownload(downloadJobState, useHttpClient = true) + return CopyInChuckStatus.ERROR_IN_STREAM_CLOSED + } + logger.debug( + "Finishing copyInChunks ${downloadJobState.state.id} " + + "currentBytesCopied ${downloadJobState.currentBytesCopied}", + ) + return CopyInChuckStatus.COMPLETED + } + + /** + * Informs [mozilla.components.feature.downloads.manager.FetchDownloadManager] that a download + * is no longer in progress due to being paused, completed, or failed + */ + private fun sendDownloadStopped(downloadState: DownloadJobState) { + downloadState.notifiedStopped = true + + val intent = Intent(ACTION_DOWNLOAD_COMPLETE) + intent.putExtra(EXTRA_DOWNLOAD_STATUS, getDownloadJobStatus(downloadState)) + intent.putExtra(EXTRA_DOWNLOAD_ID, downloadState.state.id) + intent.setPackage(context.packageName) + + context.sendBroadcast(intent, "${context.packageName}.permission.RECEIVE_DOWNLOAD_BROADCAST") + } + + /** + * Creates an output stream on the local filesystem, then informs the system that a download + * is complete after [block] is run. + * + * Encapsulates different behaviour depending on the SDK version. + */ + @SuppressLint("NewApi") + internal fun useFileStream( + download: DownloadState, + append: Boolean, + block: (OutputStream) -> Unit, + ) { + val downloadWithUniqueFileName = makeUniqueFileNameIfNecessary(download, append) + updateDownloadState(downloadWithUniqueFileName) + + if (shouldUseScopedStorage()) { + useFileStreamScopedStorage(downloadWithUniqueFileName, block) + } else { + useFileStreamLegacy(downloadWithUniqueFileName, append, block) + } + } + + @VisibleForTesting + internal fun shouldUseScopedStorage() = getSdkVersion() >= Build.VERSION_CODES.Q + + /** + * Gets the SDK version from the system. + * Used for testing since current robolectric version doesn't allow mocking API 29, remove after + * update + */ + @VisibleForTesting + internal fun getSdkVersion(): Int = SDK_INT + + /** + * Updates the given [updatedDownload] in the store and in the [downloadJobs]. + */ + @VisibleForTesting + internal fun updateDownloadState(updatedDownload: DownloadState) { + downloadJobs[updatedDownload.id]?.state = updatedDownload + store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload)) + } + + /** + * Returns an updated [DownloadState] with a unique fileName if the file is not being appended + */ + @Suppress("Deprecation") + internal fun makeUniqueFileNameIfNecessary( + download: DownloadState, + append: Boolean, + ): DownloadState { + if (append) { + return download + } + + return download.fileName?.let { + download.copy( + fileName = DownloadUtils.uniqueFileName( + Environment.getExternalStoragePublicDirectory(download.destinationDirectory), + it, + ), + ) + } ?: download + } + + @TargetApi(Build.VERSION_CODES.Q) + @VisibleForTesting + internal fun useFileStreamScopedStorage(download: DownloadState, block: (OutputStream) -> Unit) { + val values = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, download.fileName) + put( + MediaStore.Downloads.MIME_TYPE, + getSafeContentType(context, download.filePath, download.contentType), + ) + put(MediaStore.Downloads.SIZE, download.contentLength) + put(MediaStore.Downloads.IS_PENDING, 1) + } + + val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val resolver = applicationContext.contentResolver + var downloadUri = queryDownloadMediaStore(applicationContext, download) + + if (downloadUri == null) { + downloadUri = resolver.insert(collection, values) + } + + downloadUri?.let { + val pfd = resolver.openFileDescriptor(it, "w") + ParcelFileDescriptor.AutoCloseOutputStream(pfd).use(block) + + values.clear() + values.put(MediaStore.Downloads.IS_PENDING, 0) + resolver.update(it, values, null, null) + } ?: throw IOException("Failed to register download with content resolver") + } + + @TargetApi(Build.VERSION_CODES.P) + @Suppress("Deprecation") + @VisibleForTesting + internal fun useFileStreamLegacy(download: DownloadState, append: Boolean, block: (OutputStream) -> Unit) { + createDirectoryIfNeeded(download) + FileOutputStream(File(download.filePath), append).use(block) + } + + @VisibleForTesting + internal fun createDirectoryIfNeeded(download: DownloadState) { + val directory = File(download.directoryPath) + if (!directory.exists()) { + directory.mkdir() + } + } + + companion object { + /** + * Launches an intent to open the given file, returns whether or not the file could be opened + */ + fun openFile( + applicationContext: Context, + download: DownloadState, + ): Boolean { + val newIntent = createOpenFileIntent(applicationContext, download) + + return try { + applicationContext.startActivity(newIntent) + true + } catch (error: ActivityNotFoundException) { + false + } + } + + /** + * Creates an Intent which can then be used to open the file specified. + * @param context the current Android *Context* + * @param download contains the details of the downloaded file to be opened. + */ + fun createOpenFileIntent( + context: Context, + download: DownloadState, + ): Intent { + val filePath = download.filePath + val contentType = download.contentType + + // For devices that support the scoped storage we can query the directly the download + // media store otherwise we have to construct the uri based on the file path. + val fileUri: Uri = + if (SDK_INT >= Build.VERSION_CODES.Q) { + queryDownloadMediaStore(context, download) + ?: getFilePathUri(context, filePath) + } else { + // Create a new file with the location of the saved file to extract the correct path + // `file` has the wrong path, so we must construct it based on the `fileName` and `dir.path`s + getFilePathUri(context, filePath) + } + + val newIntent = + Intent(ACTION_VIEW).apply { + setDataAndType(fileUri, getSafeContentType(context, fileUri, contentType)) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + } + + return newIntent + } + + @TargetApi(Build.VERSION_CODES.Q) + @VisibleForTesting + internal fun queryDownloadMediaStore(applicationContext: Context, download: DownloadState): Uri? { + val resolver = applicationContext.contentResolver + val queryProjection = arrayOf(MediaStore.Downloads._ID) + val querySelection = "${MediaStore.Downloads.DISPLAY_NAME} = ?" + val querySelectionArgs = arrayOf("${download.fileName}") + + val queryBundle = Bundle().apply { + putString(ContentResolver.QUERY_ARG_SQL_SELECTION, querySelection) + putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, querySelectionArgs) + } + + // Query if we have a pending download with the same name. This can happen + // if a download was interrupted, failed or cancelled before the file was + // written to disk. Our logic above will have generated a unique file name + // based on existing files on the device, but we might already have a row + // for the download in the content resolver. + + val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val queryCollection = + if (SDK_INT >= Build.VERSION_CODES.R) { + queryBundle.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE) + collection + } else { + @Suppress("DEPRECATION") + setIncludePending(collection) + } + + var downloadUri: Uri? = null + resolver.query( + queryCollection, + queryProjection, + queryBundle, + null, + )?.use { + if (it.count > 0) { + val idColumnIndex = it.getColumnIndex(MediaStore.Downloads._ID) + it.moveToFirst() + downloadUri = ContentUris.withAppendedId(collection, it.getLong(idColumnIndex)) + } + } + + return downloadUri + } + + @VisibleForTesting + internal fun getSafeContentType(context: Context, constructedFilePath: Uri, contentType: String?): String { + val contentTypeFromFile = context.contentResolver.getType(constructedFilePath) + val resultContentType = if (!contentTypeFromFile.isNullOrEmpty()) { + contentTypeFromFile + } else { + contentType.ifNullOrEmpty { "*/*" } + } + return DownloadUtils.sanitizeMimeType(resultContentType).ifNullOrEmpty { "*/*" } + } + + @VisibleForTesting + internal fun getSafeContentType(context: Context, filePath: String, contentType: String?): String { + return getSafeContentType(context, getFilePathUri(context, filePath), contentType) + } + + @VisibleForTesting + internal fun getFilePathUri(context: Context, filePath: String): Uri { + return FileProvider.getUriForFile( + context, + context.packageName + FILE_PROVIDER_EXTENSION, + File(filePath), + ) + } + + private const val FILE_PROVIDER_EXTENSION = ".feature.downloads.fileprovider" + private const val CHUNK_SIZE = 32 * 1024 + private const val PARTIAL_CONTENT_STATUS = 206 + private const val OK_STATUS = 200 + + /** + * This interval was decided on by balancing the limit of the system (200ms) and allowing + * users to press buttons on the notification. If a new notification is presented while a + * user is tapping a button, their press will be cancelled. + */ + internal const val PROGRESS_UPDATE_INTERVAL = 750L + + const val EXTRA_DOWNLOAD_STATUS = "mozilla.components.feature.downloads.extras.DOWNLOAD_STATUS" + const val ACTION_OPEN = "mozilla.components.feature.downloads.OPEN" + const val ACTION_PAUSE = "mozilla.components.feature.downloads.PAUSE" + const val ACTION_RESUME = "mozilla.components.feature.downloads.RESUME" + const val ACTION_CANCEL = "mozilla.components.feature.downloads.CANCEL" + const val ACTION_DISMISS = "mozilla.components.feature.downloads.DISMISS" + const val ACTION_REMOVE_PRIVATE_DOWNLOAD = "mozilla.components.feature.downloads.ACTION_REMOVE_PRIVATE_DOWNLOAD" + const val ACTION_TRY_AGAIN = "mozilla.components.feature.downloads.TRY_AGAIN" + const val COMPAT_DEFAULT_FOREGROUND_ID = -1 + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt new file mode 100644 index 0000000000..8450318929 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.os.Bundle +import androidx.appcompat.app.AppCompatDialogFragment +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.BYTES_TO_MB_LIMIT +import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KILOBYTE +import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE +import mozilla.components.feature.downloads.ext.realFilenameOrGuessed + +/** + * This is a general representation of a dialog meant to be used in collaboration with [DownloadsFeature] + * to show a dialog before a download is triggered. + * If [SimpleDownloadDialogFragment] is not flexible enough for your use case you should inherit for this class. + * Be mindful to call [onStartDownload] when you want to start the download. + */ +abstract class DownloadDialogFragment : AppCompatDialogFragment() { + + /** + * A callback to trigger a download, call it when you are ready to start a download. For instance, + * a valid use case can be in confirmation dialog, after the positive button is clicked, + * this callback must be called. + */ + var onStartDownload: () -> Unit = {} + + var onCancelDownload: () -> Unit = {} + + /** + * Add the metadata of this download object to the arguments of this fragment. + */ + fun setDownload(download: DownloadState) { + val args = arguments ?: Bundle() + args.putString(KEY_FILE_NAME, download.realFilenameOrGuessed) + args.putString(KEY_URL, download.url) + args.putLong(KEY_CONTENT_LENGTH, download.contentLength ?: 0) + arguments = args + } + + companion object { + /** + * Key for finding the file name in the arguments. + */ + const val KEY_FILE_NAME = "KEY_FILE_NAME" + + /** + * Key for finding the content length in the arguments. + */ + const val KEY_CONTENT_LENGTH = "KEY_CONTENT_LENGTH" + + /** + * Key for finding the url in the arguments. + */ + const val KEY_URL = "KEY_URL" + + const val FRAGMENT_TAG = "SHOULD_DOWNLOAD_PROMPT_DIALOG" + + const val MEGABYTE = 1024.0 * 1024.0 + + const val KILOBYTE = 1024.0 + + const val BYTES_TO_MB_LIMIT = 0.01 + } +} + +/** + * Converts the bytes to megabytes with two decimal places and returns a formatted string + */ +fun Long.toMegabyteString(): String { + return String.format("%.2f MB", this / MEGABYTE) +} + +/** + * Converts the bytes to kilobytes with two decimal places and returns a formatted string + */ +fun Long.toKilobyteString(): String { + return String.format("%.2f KB", this / KILOBYTE) +} + +/** + * Converts the bytes to megabytes or kilobytes( if size smaller than 0.01 MB) + * with two decimal places and returns a formatted string + */ +fun Long.toMegabyteOrKilobyteString(): String { + return if (this / MEGABYTE < BYTES_TO_MB_LIMIT) { + this.toKilobyteString() + } else { + this.toMegabyteString() + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadMiddleware.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadMiddleware.kt new file mode 100644 index 0000000000..c7e480e038 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadMiddleware.kt @@ -0,0 +1,186 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.downloads + +import android.app.DownloadManager +import android.content.Context +import android.content.Intent +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.selector.getNormalOrPrivateTabs +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED +import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED +import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_REMOVE_PRIVATE_DOWNLOAD +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.lib.state.Store +import mozilla.components.support.base.log.logger.Logger +import kotlin.coroutines.CoroutineContext + +/** + * [Middleware] implementation for managing downloads via the provided download service. Its + * purpose is to react to global download state changes (e.g. of [BrowserState.downloads]) + * and notify the download service, as needed. + */ +@Suppress("ComplexMethod") +class DownloadMiddleware( + private val applicationContext: Context, + private val downloadServiceClass: Class<*>, + coroutineContext: CoroutineContext = Dispatchers.IO, + @get:VisibleForTesting + internal val downloadStorage: DownloadStorage = DownloadStorage(applicationContext), +) : Middleware<BrowserState, BrowserAction> { + private val logger = Logger("DownloadMiddleware") + + private var scope = CoroutineScope(coroutineContext) + + override fun invoke( + context: MiddlewareContext<BrowserState, BrowserAction>, + next: (BrowserAction) -> Unit, + action: BrowserAction, + ) { + when (action) { + is DownloadAction.RemoveDownloadAction -> removeDownload(action.downloadId, context.store) + is DownloadAction.RemoveAllDownloadsAction -> removeDownloads() + is DownloadAction.UpdateDownloadAction -> updateDownload(action.download, context) + is DownloadAction.RestoreDownloadsStateAction -> restoreDownloads(context.store) + is ContentAction.CancelDownloadAction -> closeDownloadResponse(context.store, action.sessionId) + is DownloadAction.AddDownloadAction -> { + if (!action.download.private && !saveDownload(context.store, action.download)) { + // The download was already added before, so we are ignoring this request. + logger.debug( + "Ignored add action for ${action.download.id} " + + "download already in store.downloads", + ) + return + } + } + else -> { + // no-op + } + } + + next(action) + + when (action) { + is TabListAction.RemoveAllTabsAction, + is TabListAction.RemoveAllPrivateTabsAction, + -> removePrivateNotifications(context.store) + is TabListAction.RemoveTabsAction, + is TabListAction.RemoveTabAction, + -> { + val privateTabs = context.store.state.getNormalOrPrivateTabs(private = true) + if (privateTabs.isEmpty()) { + removePrivateNotifications(context.store) + } + } + is DownloadAction.AddDownloadAction -> sendDownloadIntent(action.download) + is DownloadAction.RestoreDownloadStateAction -> sendDownloadIntent(action.download) + else -> { + // no-op + } + } + } + + private fun removeDownload( + downloadId: String, + store: Store<BrowserState, BrowserAction>, + ) = scope.launch { + store.state.downloads[downloadId]?.let { + downloadStorage.remove(it) + logger.debug("Removed download ${it.fileName} from the storage") + } + } + + private fun removeDownloads() = scope.launch { + downloadStorage.removeAllDownloads() + } + + private fun updateDownload(updated: DownloadState, context: MiddlewareContext<BrowserState, BrowserAction>) { + if (updated.private) return + context.state.downloads[updated.id]?.let { old -> + // To not overwhelm the storage, we only send updates that are relevant, + // we only care about properties, that we are stored on the storage. + if (!DownloadStorage.isSameDownload(old, updated)) { + scope.launch { + downloadStorage.update(updated) + } + logger.debug("Updated download ${updated.fileName} on the storage") + } + } + } + + private fun restoreDownloads(store: Store<BrowserState, BrowserAction>) = scope.launch { + downloadStorage.getDownloadsList().forEach { download -> + if (!store.state.downloads.containsKey(download.id) && !download.private) { + store.dispatch(DownloadAction.RestoreDownloadStateAction(download)) + logger.debug("Download restored from the storage ${download.fileName}") + } + } + } + + @VisibleForTesting + internal fun saveDownload(store: Store<BrowserState, BrowserAction>, download: DownloadState): Boolean { + return if (!store.state.downloads.containsKey(download.id) && !download.private) { + scope.launch { + downloadStorage.add(download) + logger.debug("Added download ${download.fileName} to the storage") + } + true + } else { + false + } + } + + @VisibleForTesting + internal fun closeDownloadResponse(store: Store<BrowserState, BrowserAction>, tabId: String) { + store.state.findTabOrCustomTab(tabId)?.let { + it.content.download?.response?.close() + } + } + + @VisibleForTesting + internal fun sendDownloadIntent(download: DownloadState) { + if (download.status !in arrayOf(COMPLETED, CANCELLED, FAILED)) { + val intent = Intent(applicationContext, downloadServiceClass) + intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, download.id) + startForegroundService(intent) + logger.debug("Sending download intent ${download.fileName}") + } + } + + @VisibleForTesting + internal fun startForegroundService(intent: Intent) { + ContextCompat.startForegroundService(applicationContext, intent) + } + + @VisibleForTesting + internal fun removeStatusBarNotification(store: Store<BrowserState, BrowserAction>, download: DownloadState) { + download.notificationId?.let { + val intent = Intent(applicationContext, downloadServiceClass) + intent.action = ACTION_REMOVE_PRIVATE_DOWNLOAD + intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, download.id) + applicationContext.startService(intent) + store.dispatch(DownloadAction.DismissDownloadNotificationAction(download.id)) + } + } + + @VisibleForTesting + internal fun removePrivateNotifications(store: Store<BrowserState, BrowserAction>) { + val privateDownloads = store.state.downloads.filterValues { it.private } + privateDownloads.forEach { removeStatusBarNotification(store, it.value) } + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt new file mode 100644 index 0000000000..9b9c9aa34c --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import androidx.annotation.VisibleForTesting +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.NotificationManagerCompat.IMPORTANCE_NONE +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED +import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED +import mozilla.components.browser.state.state.content.DownloadState.Status.DOWNLOADING +import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED +import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED +import mozilla.components.browser.state.state.content.DownloadState.Status.PAUSED +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_CANCEL +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_DISMISS +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_PAUSE +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_RESUME +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_TRY_AGAIN +import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState +import mozilla.components.support.utils.PendingIntentUtils +import kotlin.random.Random + +@Suppress("LargeClass") +internal object DownloadNotification { + + private const val NOTIFICATION_CHANNEL_ID = "mozac.feature.downloads.generic" + private const val NOTIFICATION_GROUP_KEY = "mozac.feature.downloads.group" + internal const val NOTIFICATION_DOWNLOAD_GROUP_ID = 100 + private const val LEGACY_NOTIFICATION_CHANNEL_ID = "Downloads" + internal const val PERCENTAGE_MULTIPLIER = 100 + + internal const val EXTRA_DOWNLOAD_ID = "downloadId" + + @VisibleForTesting + internal fun createDownloadGroupNotification( + context: Context, + notifications: List<DownloadJobState>, + notificationAccentColor: Int, + ): Notification { + val allDownloadsHaveFinished = notifications.all { it.status != DOWNLOADING } + val icon = if (allDownloadsHaveFinished) { + R.drawable.mozac_feature_download_ic_download_complete + } else { + R.drawable.mozac_feature_download_ic_ongoing_download + } + val summaryList = getSummaryList(context, notifications) + val summaryLine1 = summaryList.first() + val summaryLine2 = if (summaryList.size == 2) summaryList[1] else "" + + return NotificationCompat.Builder(context, ensureChannelExists(context)) + .setSmallIcon(icon) + .setColor(ContextCompat.getColor(context, notificationAccentColor)) + .setContentTitle( + context.applicationContext.getString(R.string.mozac_feature_downloads_notification_channel), + ) + .setContentText(summaryList.joinToString("\n")) + .setStyle(NotificationCompat.InboxStyle().addLine(summaryLine1).addLine(summaryLine2)) + .setGroup(NOTIFICATION_GROUP_KEY) + .setGroupSummary(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .build() + } + + /** + * Build the notification to be displayed while the download service is active. + */ + fun createOngoingDownloadNotification( + context: Context, + downloadJobState: DownloadJobState, + notificationAccentColor: Int, + ): Notification { + val downloadState = downloadJobState.state + val channelId = ensureChannelExists(context) + val isIndeterminate = downloadJobState.isIndeterminate() + val percentCopied = downloadJobState.getPercent() ?: -1 + + return NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.mozac_feature_download_ic_ongoing_download) + .setContentTitle(downloadState.fileName) + .setContentText(downloadJobState.getProgress()) + .setColor(ContextCompat.getColor(context, notificationAccentColor)) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setProgress(DownloadNotification.PERCENTAGE_MULTIPLIER, percentCopied, isIndeterminate) + .setOngoing(true) + .setWhen(downloadJobState.createdTime) + .setOnlyAlertOnce(true) + .addAction(getPauseAction(context, downloadState.id)) + .addAction(getCancelAction(context, downloadState.id)) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCompatGroup(NOTIFICATION_GROUP_KEY) + .build() + } + + /** + * Build the notification to be displayed while the download service is paused. + */ + fun createPausedDownloadNotification( + context: Context, + downloadJobState: DownloadJobState, + notificationAccentColor: Int, + ): Notification { + val channelId = ensureChannelExists(context) + + val downloadState = downloadJobState.state + return NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.mozac_feature_download_ic_download) + .setContentTitle(downloadState.fileName) + .setContentText( + context.applicationContext.getString(R.string.mozac_feature_downloads_paused_notification_text), + ) + .setColor(ContextCompat.getColor(context, notificationAccentColor)) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setOngoing(true) + .setWhen(downloadJobState.createdTime) + .setOnlyAlertOnce(true) + .addAction(getResumeAction(context, downloadState.id)) + .addAction(getCancelAction(context, downloadState.id)) + .setDeleteIntent(createDismissPendingIntent(context, downloadState.id)) + .setCompatGroup(NOTIFICATION_GROUP_KEY) + .build() + } + + /** + * Build the notification to be displayed when a download finishes. + */ + fun createDownloadCompletedNotification( + context: Context, + downloadJobState: DownloadJobState, + notificationAccentColor: Int, + contentIntent: PendingIntent = createOpenFilePendingIntent(context, downloadJobState.state), + ): Notification { + val channelId = ensureChannelExists(context) + val downloadState = downloadJobState.state + + return NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.mozac_feature_download_ic_download_complete) + .setContentTitle(downloadState.fileName) + .setWhen(downloadJobState.createdTime) + .setOnlyAlertOnce(true) + .setContentText( + context.applicationContext.getString(R.string.mozac_feature_downloads_completed_notification_text2), + ) + .setColor(ContextCompat.getColor(context, notificationAccentColor)) + .setContentIntent(contentIntent) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setDeleteIntent(createDismissPendingIntent(context, downloadState.id)) + .setCompatGroup(NOTIFICATION_GROUP_KEY) + .build() + } + + /** + * Build the notification to be displayed when a download fails to finish. + */ + fun createDownloadFailedNotification( + context: Context, + downloadJobState: DownloadJobState, + notificationAccentColor: Int, + ): Notification { + val channelId = ensureChannelExists(context) + val downloadState = downloadJobState.state + + return NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.mozac_feature_download_ic_download_failed) + .setContentTitle(downloadState.fileName) + .setContentText( + context.applicationContext.getString(R.string.mozac_feature_downloads_failed_notification_text2), + ) + .setColor(ContextCompat.getColor(context, notificationAccentColor)) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .addAction(getTryAgainAction(context, downloadState.id)) + .addAction(getCancelAction(context, downloadState.id)) + .setWhen(downloadJobState.createdTime) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setDeleteIntent(createDismissPendingIntent(context, downloadState.id)) + .setCompatGroup(NOTIFICATION_GROUP_KEY) + .build() + } + + @VisibleForTesting + internal fun getSummaryList(context: Context, notifications: List<DownloadJobState>): List<String> { + return notifications.take(2).map { downloadState -> + "${downloadState.state.fileName} ${downloadState.getStatusDescription(context)}" + } + } + + /** + * Check if notifications from the download channel are enabled. + * Verifies that app notifications, channel notifications, and group notifications are enabled. + */ + fun isChannelEnabled(context: Context): Boolean { + return if (SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager: NotificationManager = context.getSystemService()!! + if (!notificationManager.areNotificationsEnabled()) return false + + val channelId = ensureChannelExists(context) + val channel = notificationManager.getNotificationChannel(channelId) + if (channel.importance == IMPORTANCE_NONE) return false + + true + } else { + NotificationManagerCompat.from(context).areNotificationsEnabled() + } + } + + /** + * Make sure a notification channel for download notification exists. + * + * Returns the channel id to be used for download notifications. + */ + private fun ensureChannelExists(context: Context): String { + if (SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager: NotificationManager = context.getSystemService()!! + + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + context.applicationContext.getString(R.string.mozac_feature_downloads_notification_channel), + NotificationManager.IMPORTANCE_LOW, + ) + + notificationManager.createNotificationChannel(channel) + + notificationManager.deleteNotificationChannel(LEGACY_NOTIFICATION_CHANNEL_ID) + } + + return NOTIFICATION_CHANNEL_ID + } + + private fun createOpenFilePendingIntent(context: Context, downloadState: DownloadState) = + PendingIntent.getActivity( + context, + 0, + AbstractFetchDownloadService.createOpenFileIntent(context, downloadState), + PendingIntentUtils.defaultFlags, + ) + + private fun getPauseAction(context: Context, downloadStateId: String): NotificationCompat.Action { + val pauseIntent = createPendingIntent(context, ACTION_PAUSE, downloadStateId) + + return NotificationCompat.Action.Builder( + 0, + context.applicationContext.getString(R.string.mozac_feature_downloads_button_pause), + pauseIntent, + ).build() + } + + private fun getResumeAction(context: Context, downloadStateId: String): NotificationCompat.Action { + val resumeIntent = createPendingIntent(context, ACTION_RESUME, downloadStateId) + + return NotificationCompat.Action.Builder( + 0, + context.applicationContext.getString(R.string.mozac_feature_downloads_button_resume), + resumeIntent, + ).build() + } + + private fun getCancelAction(context: Context, downloadStateId: String): NotificationCompat.Action { + val cancelIntent = createPendingIntent(context, ACTION_CANCEL, downloadStateId) + + return NotificationCompat.Action.Builder( + 0, + context.applicationContext.getString(R.string.mozac_feature_downloads_button_cancel), + cancelIntent, + ).build() + } + + private fun getTryAgainAction(context: Context, downloadStateId: String): NotificationCompat.Action { + val tryAgainIntent = createPendingIntent(context, ACTION_TRY_AGAIN, downloadStateId) + + return NotificationCompat.Action.Builder( + 0, + context.applicationContext.getString(R.string.mozac_feature_downloads_button_try_again), + tryAgainIntent, + ).build() + } + + private fun createDismissPendingIntent(context: Context, downloadStateId: String): PendingIntent { + return createPendingIntent(context, ACTION_DISMISS, downloadStateId) + } + + private fun createPendingIntent(context: Context, action: String, downloadStateId: String): PendingIntent { + val intent = Intent(action) + intent.setPackage(context.applicationContext.packageName) + intent.putExtra(EXTRA_DOWNLOAD_ID, downloadStateId) + + // We generate a random requestCode in order to generate a distinct PendingIntent: + // https://developer.android.com/reference/android/app/PendingIntent.html + return PendingIntent.getBroadcast( + context.applicationContext, + Random.nextInt(), + intent, + PendingIntentUtils.defaultFlags, + ) + } +} + +@VisibleForTesting +internal fun NotificationCompat.Builder.setCompatGroup(groupKey: String): NotificationCompat.Builder { + return if (SDK_INT >= Build.VERSION_CODES.N) { + setGroup(groupKey) + } else { + this + } +} + +private fun DownloadJobState.getPercent(): Int? { + val bytesCopied = currentBytesCopied + val contentLength = state.contentLength + return if (contentLength == null || contentLength == 0L) { + null + } else { + (DownloadNotification.PERCENTAGE_MULTIPLIER * bytesCopied / contentLength).toInt() + } +} + +@VisibleForTesting +internal fun DownloadJobState.getProgress(): String { + val bytesCopied = currentBytesCopied + return if (isIndeterminate()) { + "" + } else { + "${DownloadNotification.PERCENTAGE_MULTIPLIER * bytesCopied / state.contentLength!!}%" + } +} + +private fun DownloadJobState.isIndeterminate(): Boolean { + val bytesCopied = currentBytesCopied + return state.contentLength == null || bytesCopied == 0L || state.contentLength == 0L +} + +@VisibleForTesting +internal fun DownloadJobState.getStatusDescription(context: Context): String { + return when (this.status) { + DOWNLOADING -> { + getProgress() + } + + PAUSED -> { + context.applicationContext.getString(R.string.mozac_feature_downloads_paused_notification_text) + } + + COMPLETED -> { + context.applicationContext.getString(R.string.mozac_feature_downloads_completed_notification_text2) + } + + FAILED -> { + context.applicationContext.getString(R.string.mozac_feature_downloads_failed_notification_text2) + } + + CANCELLED, INITIATED -> "" + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadStorage.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadStorage.kt new file mode 100644 index 0000000000..f1ee939204 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadStorage.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.downloads + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.paging.DataSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.feature.downloads.db.DownloadsDatabase +import mozilla.components.feature.downloads.db.toDownloadEntity + +/** + * A storage implementation for organizing download. + */ +class DownloadStorage(context: Context) { + + @VisibleForTesting + internal var database: Lazy<DownloadsDatabase> = lazy { DownloadsDatabase.get(context) } + + private val downloadDao by lazy { database.value.downloadDao() } + + /** + * Adds a new [download]. + */ + suspend fun add(download: DownloadState) { + downloadDao.insert(download.toDownloadEntity()) + } + + /** + * Returns a [Flow] list of all the [DownloadState] instances. + */ + fun getDownloads(): Flow<List<DownloadState>> { + return downloadDao.getDownloads().map { list -> + list.map { entity -> entity.toDownloadState() } + } + } + + /** + * Returns a [List] of all the [DownloadState] instances. + */ + suspend fun getDownloadsList(): List<DownloadState> { + return downloadDao.getDownloadsList().map { entity -> + entity.toDownloadState() + } + } + + /** + * Returns all saved [DownloadState] instances as a [DataSource.Factory]. + */ + fun getDownloadsPaged(): DataSource.Factory<Int, DownloadState> = downloadDao + .getDownloadsPaged() + .map { entity -> + entity.toDownloadState() + } + + /** + * Removes the given [download]. + */ + suspend fun remove(download: DownloadState) { + downloadDao.delete(download.toDownloadEntity()) + } + + /** + * Update the given [download]. + */ + suspend fun update(download: DownloadState) { + downloadDao.update(download.toDownloadEntity()) + } + + /** + * Removes all the downloads. + */ + suspend fun removeAllDownloads() { + downloadDao.deleteAllDownloads() + } + + companion object { + /** + * Takes two [DownloadState] objects and the determine if they are the same, be aware this + * only takes into considerations fields that are being stored, + * not all the field on [DownloadState] are stored. + */ + fun isSameDownload(first: DownloadState, second: DownloadState): Boolean { + return first.id == second.id && + first.fileName == second.fileName && + first.url == second.url && + first.contentType == second.contentType && + first.contentLength == second.contentLength && + first.status == second.status && + first.destinationDirectory == second.destinationDirectory && + first.createdTime == second.createdTime + } + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt new file mode 100644 index 0000000000..a5b94c0545 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt @@ -0,0 +1,499 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ResolveInfo +import android.widget.Toast +import androidx.annotation.ColorRes +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.Companion.PRIVATE +import androidx.core.net.toUri +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.mapNotNull +import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.FRAGMENT_TAG +import mozilla.components.feature.downloads.dialog.DeniedPermissionDialogFragment +import mozilla.components.feature.downloads.ext.realFilenameOrGuessed +import mozilla.components.feature.downloads.facts.emitPromptDismissedFact +import mozilla.components.feature.downloads.facts.emitPromptDisplayedFact +import mozilla.components.feature.downloads.manager.AndroidDownloadManager +import mozilla.components.feature.downloads.manager.DownloadManager +import mozilla.components.feature.downloads.manager.noop +import mozilla.components.feature.downloads.manager.onDownloadStopped +import mozilla.components.feature.downloads.ui.DownloadAppChooserDialog +import mozilla.components.feature.downloads.ui.DownloaderApp +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.feature.OnNeedToRequestPermissions +import mozilla.components.support.base.feature.PermissionsFeature +import mozilla.components.support.ktx.android.content.appName +import mozilla.components.support.ktx.android.content.isPermissionGranted +import mozilla.components.support.ktx.kotlin.isSameOriginAs +import mozilla.components.support.utils.Browsers + +/** + * The name of the file to be downloaded. + */ +@JvmInline +value class Filename(val value: String) + +/** + * The size of the file to be downloaded expressed as the number of `bytes`. + * The value will be `0` if the size is unknown. + */ +@JvmInline +value class ContentSize(val value: Long) + +/** + * The list of all applications that can perform a download, including this application. + */ +@JvmInline +value class ThirdPartyDownloaderApps(val value: List<DownloaderApp>) + +/** + * Callback for when the user picked a certain application with which to download the current file. + */ +@JvmInline +value class ThirdPartyDownloaderAppChosenCallback(val value: (DownloaderApp) -> Unit) + +/** + * Callback for when the positive button of a download dialog was tapped. + */ +@JvmInline +value class PositiveActionCallback(val value: () -> Unit) + +/** + * Callback for when the negative button of a download dialog was tapped. + */ +@JvmInline +value class NegativeActionCallback(val value: () -> Unit) + +/** + * Feature implementation to provide download functionality for the selected + * session. The feature will subscribe to the selected session and listen + * for downloads. + * + * @property applicationContext a reference to the application context. + * @property onNeedToRequestPermissions a callback invoked when permissions + * need to be requested before a download can be performed. Once the request + * is completed, [onPermissionsResult] needs to be invoked. + * @property onDownloadStopped a callback invoked when a download is paused or completed. + * @property downloadManager a reference to the [DownloadManager] which is + * responsible for performing the downloads. + * @property store a reference to the application's [BrowserStore]. + * @property useCases [DownloadsUseCases] instance for consuming processed downloads. + * @property fragmentManager a reference to a [FragmentManager]. If a fragment + * manager is provided, a dialog will be shown before every download. + * @property promptsStyling styling properties for the dialog. + * @property shouldForwardToThirdParties Indicates if downloads should be forward to third party apps, + * if there are multiple apps a chooser dialog will shown. + * @property customFirstPartyDownloadDialog An optional delegate for showing a dialog for a download + * that will be processed by the current application. + * @property customThirdPartyDownloadDialog An optional delegate for showing a dialog for a download + * that can be processed by multiple installed applications including the current one. + */ +@Suppress("LargeClass") +class DownloadsFeature( + private val applicationContext: Context, + private val store: BrowserStore, + @get:VisibleForTesting(otherwise = PRIVATE) + internal val useCases: DownloadsUseCases, + override var onNeedToRequestPermissions: OnNeedToRequestPermissions = { }, + onDownloadStopped: onDownloadStopped = noop, + private val downloadManager: DownloadManager = AndroidDownloadManager(applicationContext, store), + private val tabId: String? = null, + private val fragmentManager: FragmentManager? = null, + private val promptsStyling: PromptsStyling? = null, + private val shouldForwardToThirdParties: () -> Boolean = { false }, + private val customFirstPartyDownloadDialog: + ((Filename, ContentSize, PositiveActionCallback, NegativeActionCallback) -> Unit)? = null, + private val customThirdPartyDownloadDialog: + ((ThirdPartyDownloaderApps, ThirdPartyDownloaderAppChosenCallback, NegativeActionCallback) -> Unit)? = null, +) : LifecycleAwareFeature, PermissionsFeature { + + var onDownloadStopped: onDownloadStopped + get() = downloadManager.onDownloadStopped + set(value) { downloadManager.onDownloadStopped = value } + + init { + this.onDownloadStopped = onDownloadStopped + } + + private var scope: CoroutineScope? = null + + @VisibleForTesting + internal var dismissPromptScope: CoroutineScope? = null + + @VisibleForTesting + internal var previousTab: SessionState? = null + + /** + * Starts observing downloads on the selected session and sends them to the [DownloadManager] + * to be processed. + */ + @Suppress("Deprecation") + override fun start() { + // Dismiss the previous prompts when the user navigates to another site. + // This prevents prompts from the previous page from covering content. + dismissPromptScope = store.flowScoped { flow -> + flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(tabId) } + .distinctUntilChangedBy { it.content.url } + .collect { + val currentHost = previousTab?.content?.url + val newHost = it.content.url + + // The user is navigating to another site + if (currentHost?.isSameOriginAs(newHost) == false) { + previousTab?.let { tab -> + // We have an old download request. + tab.content.download?.let { download -> + useCases.cancelDownloadRequest.invoke(tab.id, download.id) + dismissAllDownloadDialogs() + previousTab = null + } + } + } + } + } + + scope = store.flowScoped { flow -> + flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(tabId) } + .distinctUntilChangedBy { it.content.download } + .collect { state -> + state.content.download?.let { downloadState -> + previousTab = state + processDownload(state, downloadState) + } + } + } + } + + /** + * Calls the tryAgain function of the corresponding [DownloadManager] + */ + @Suppress("Unused") + fun tryAgain(id: String) { + downloadManager.tryAgain(id) + } + + /** + * Stops observing downloads on the selected session. + */ + override fun stop() { + scope?.cancel() + dismissPromptScope?.cancel() + downloadManager.unregisterListeners() + } + + /** + * Notifies the [DownloadManager] that a new download must be processed. + */ + @VisibleForTesting + internal fun processDownload(tab: SessionState, download: DownloadState): Boolean { + val apps = getDownloaderApps(applicationContext, download) + // We only show the dialog If we have multiple apps that can handle the download. + val shouldShowAppDownloaderDialog = shouldForwardToThirdParties() && apps.size > 1 + + return if (shouldShowAppDownloaderDialog) { + when (customThirdPartyDownloadDialog) { + null -> showAppDownloaderDialog(tab, download, apps) + else -> customThirdPartyDownloadDialog.invoke( + ThirdPartyDownloaderApps(apps), + ThirdPartyDownloaderAppChosenCallback { + onDownloaderAppSelected(it, tab, download) + }, + NegativeActionCallback { + useCases.cancelDownloadRequest.invoke(tab.id, download.id) + }, + ) + } + + false + } else { + if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) { + when { + customFirstPartyDownloadDialog != null && !download.skipConfirmation -> { + customFirstPartyDownloadDialog.invoke( + Filename(download.realFilenameOrGuessed), + ContentSize(download.contentLength ?: 0), + PositiveActionCallback { + startDownload(download) + useCases.consumeDownload.invoke(tab.id, download.id) + }, + NegativeActionCallback { + useCases.cancelDownloadRequest.invoke(tab.id, download.id) + }, + ) + false + } + + fragmentManager != null && !download.skipConfirmation -> { + showDownloadDialog(tab, download) + false + } + + else -> { + useCases.consumeDownload(tab.id, download.id) + startDownload(download) + } + } + } else { + onNeedToRequestPermissions(downloadManager.permissions) + false + } + } + } + + @VisibleForTesting + internal fun startDownload(download: DownloadState): Boolean { + val id = downloadManager.download(download) + return if (id != null) { + true + } else { + showDownloadNotSupportedError() + false + } + } + + /** + * Notifies the feature that the permissions request was completed. It will then + * either trigger or clear the pending download. + */ + override fun onPermissionsResult(permissions: Array<String>, grantResults: IntArray) { + if (permissions.isEmpty()) { + // If we are requesting permissions while a permission prompt is already being displayed + // then Android seems to call `onPermissionsResult` immediately with an empty permissions + // list. In this case just ignore it. + return + } + + withActiveDownload { (tab, download) -> + if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) { + if (shouldForwardToThirdParties()) { + startDownload(download) + useCases.consumeDownload(tab.id, download.id) + } else { + processDownload(tab, download) + } + } else { + useCases.cancelDownloadRequest.invoke(tab.id, download.id) + showPermissionDeniedDialog() + } + } + } + + @VisibleForTesting(otherwise = PRIVATE) + internal fun showDownloadNotSupportedError() { + Toast.makeText( + applicationContext, + applicationContext.getString( + R.string.mozac_feature_downloads_file_not_supported2, + applicationContext.appName, + ), + Toast.LENGTH_LONG, + ).show() + } + + @VisibleForTesting(otherwise = PRIVATE) + internal fun showDownloadDialog( + tab: SessionState, + download: DownloadState, + dialog: DownloadDialogFragment = getDownloadDialog(), + ) { + dialog.setDownload(download) + + dialog.onStartDownload = { + startDownload(download) + useCases.consumeDownload.invoke(tab.id, download.id) + } + + dialog.onCancelDownload = { + useCases.cancelDownloadRequest.invoke(tab.id, download.id) + } + + if (!isAlreadyADownloadDialog() && fragmentManager != null && !fragmentManager.isDestroyed) { + emitPromptDisplayedFact() + dialog.showNow(fragmentManager, FRAGMENT_TAG) + } + } + + private fun getDownloadDialog(): DownloadDialogFragment { + return findPreviousDownloadDialogFragment() ?: SimpleDownloadDialogFragment.newInstance( + promptsStyling = promptsStyling, + ) + } + + @VisibleForTesting + internal fun showAppDownloaderDialog( + tab: SessionState, + download: DownloadState, + apps: List<DownloaderApp>, + appChooserDialog: DownloadAppChooserDialog = getAppDownloaderDialog(), + ) { + appChooserDialog.setApps(apps) + appChooserDialog.onAppSelected = { app -> + onDownloaderAppSelected(app, tab, download) + } + + appChooserDialog.onDismiss = { + emitPromptDismissedFact() + useCases.cancelDownloadRequest.invoke(tab.id, download.id) + } + + if (!isAlreadyAppDownloaderDialog() && fragmentManager != null && !fragmentManager.isDestroyed) { + emitPromptDisplayedFact() + appChooserDialog.showNow(fragmentManager, DownloadAppChooserDialog.FRAGMENT_TAG) + } + } + + @VisibleForTesting + internal fun onDownloaderAppSelected(app: DownloaderApp, tab: SessionState, download: DownloadState) { + if (app.packageName == applicationContext.packageName) { + if (applicationContext.isPermissionGranted(downloadManager.permissions.asIterable())) { + startDownload(download) + useCases.consumeDownload(tab.id, download.id) + } else { + onNeedToRequestPermissions(downloadManager.permissions) + } + } else { + try { + applicationContext.startActivity(app.toIntent()) + } catch (error: ActivityNotFoundException) { + val errorMessage = applicationContext.getString( + R.string.mozac_feature_downloads_unable_to_open_third_party_app, + app.name, + ) + Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show() + } + useCases.consumeDownload(tab.id, download.id) + } + } + + private fun getAppDownloaderDialog() = findPreviousAppDownloaderDialogFragment() + ?: DownloadAppChooserDialog.newInstance( + promptsStyling?.gravity, + promptsStyling?.shouldWidthMatchParent, + ) + + @VisibleForTesting + internal fun isAlreadyAppDownloaderDialog(): Boolean { + return findPreviousAppDownloaderDialogFragment() != null + } + + private fun findPreviousAppDownloaderDialogFragment(): DownloadAppChooserDialog? { + return fragmentManager?.findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG) as? DownloadAppChooserDialog + } + + @VisibleForTesting(otherwise = PRIVATE) + internal fun isAlreadyADownloadDialog(): Boolean { + return findPreviousDownloadDialogFragment() != null + } + + private fun findPreviousDownloadDialogFragment(): DownloadDialogFragment? { + return fragmentManager?.findFragmentByTag(FRAGMENT_TAG) as? DownloadDialogFragment + } + + private fun withActiveDownload(block: (Pair<SessionState, DownloadState>) -> Unit) { + val state = store.state.findTabOrCustomTabOrSelectedTab(tabId) ?: return + val download = state.content.download ?: return + block(Pair(state, download)) + } + + /** + * Find all apps that can perform a download, including this app. + */ + @VisibleForTesting + internal fun getDownloaderApps(context: Context, download: DownloadState): List<DownloaderApp> { + val packageManager = context.packageManager + + val browsers = Browsers.findResolvers(context, packageManager, includeThisApp = true) + .associateBy { it.activityInfo.identifier } + + val thisApp = browsers.values + .firstOrNull { it.activityInfo.packageName == context.packageName } + ?.toDownloaderApp(context, download) + + // Check for data URL that can cause a TransactionTooLargeException when querying for apps + // See https://github.com/mozilla-mobile/android-components/issues/9665 + if (download.url.startsWith("data:")) { + return listOfNotNull(thisApp) + } + + val apps = Browsers.findResolvers( + context, + packageManager, + includeThisApp = false, + url = download.url, + contentType = download.contentType, + ) + // Remove browsers and returns only the apps that can perform a download plus this app. + return apps.filter { !browsers.contains(it.activityInfo.identifier) } + .map { it.toDownloaderApp(context, download) } + listOfNotNull(thisApp) + } + + @VisibleForTesting + internal fun dismissAllDownloadDialogs() { + findPreviousDownloadDialogFragment()?.dismiss() + findPreviousAppDownloaderDialogFragment()?.dismiss() + } + + private val ActivityInfo.identifier: String get() = packageName + name + + @VisibleForTesting + internal fun DownloaderApp.toIntent(): Intent { + return Intent(Intent.ACTION_VIEW).apply { + setDataAndTypeAndNormalize(url.toUri(), contentType) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + setClassName(packageName, activityName) + addCategory(Intent.CATEGORY_BROWSABLE) + } + } + + /** + * Styling for the download dialog prompt + */ + data class PromptsStyling( + val gravity: Int, + val shouldWidthMatchParent: Boolean = false, + @ColorRes + val positiveButtonBackgroundColor: Int? = null, + @ColorRes + val positiveButtonTextColor: Int? = null, + val positiveButtonRadius: Float? = null, + val fileNameEndMargin: Int? = null, + ) + + @VisibleForTesting + internal fun showPermissionDeniedDialog() { + fragmentManager?.let { + val dialog = DeniedPermissionDialogFragment.newInstance( + R.string.mozac_feature_downloads_write_external_storage_permissions_needed_message, + ) + dialog.showNow(fragmentManager, DeniedPermissionDialogFragment.FRAGMENT_TAG) + } + } +} + +@VisibleForTesting +internal fun ResolveInfo.toDownloaderApp(context: Context, download: DownloadState): DownloaderApp { + return DownloaderApp( + loadLabel(context.packageManager).toString(), + this, + activityInfo.packageName, + activityInfo.name, + download.url, + download.contentType, + ) +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsUseCases.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsUseCases.kt new file mode 100644 index 0000000000..343fdfd65f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsUseCases.kt @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.store.BrowserStore + +/** + * Contains use cases related to the downloads feature. + * + * @param store the application's [BrowserStore]. + */ +class DownloadsUseCases( + store: BrowserStore, +) { + + /** + * Use case that cancels the download request from a tab. + */ + class CancelDownloadRequestUseCase( + private val store: BrowserStore, + ) { + /** + * Cancels the download request the session with the given [tabId]. + */ + operator fun invoke(tabId: String, downloadId: String) { + store.dispatch(ContentAction.CancelDownloadAction(tabId, downloadId)) + } + } + + class ConsumeDownloadUseCase( + private val store: BrowserStore, + ) { + /** + * Consumes the download with the given [downloadId] from the session with the given + * [tabId]. + */ + operator fun invoke(tabId: String, downloadId: String) { + store.dispatch( + ContentAction.ConsumeDownloadAction( + tabId, + downloadId, + ), + ) + } + } + + /** + * Use case that allows to restore downloads from the storage. + */ + class RestoreDownloadsUseCase(private val store: BrowserStore) { + /** + * Restores downloads from the storage. + */ + operator fun invoke() { + store.dispatch(DownloadAction.RestoreDownloadsStateAction) + } + } + + /** + * Use case that allows to remove a download. + */ + class RemoveDownloadUseCase(private val store: BrowserStore) { + /** + * Removes the download with the given [downloadId]. + */ + operator fun invoke(downloadId: String) { + store.dispatch(DownloadAction.RemoveDownloadAction(downloadId)) + } + } + + /** + * Use case that allows to remove all downloads. + */ + class RemoveAllDownloadsUseCase(private val store: BrowserStore) { + /** + * Removes all downloads. + */ + operator fun invoke() { + store.dispatch(DownloadAction.RemoveAllDownloadsAction) + } + } + + val cancelDownloadRequest = CancelDownloadRequestUseCase(store) + val consumeDownload = ConsumeDownloadUseCase(store) + val restoreDownloads = RestoreDownloadsUseCase(store) + val removeDownload = RemoveDownloadUseCase(store) + val removeAllDownloads = RemoveAllDownloadsUseCase(store) +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragment.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragment.kt new file mode 100644 index 0000000000..66695d4a5d --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragment.kt @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package mozilla.components.feature.downloads + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.GradientDrawable +import android.os.Bundle +import android.text.method.ScrollingMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.LinearLayout +import android.widget.RelativeLayout +import androidx.annotation.StringRes +import androidx.annotation.StyleRes +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.core.view.marginBottom +import androidx.core.view.marginStart +import androidx.core.view.marginTop +import mozilla.components.feature.downloads.databinding.MozacDownloadsPromptBinding + +/** + * A confirmation dialog to be called before a download is triggered. + * Meant to be used in collaboration with [DownloadsFeature] + * + * [SimpleDownloadDialogFragment] is the default dialog used by DownloadsFeature if you don't provide a value. + * It is composed by a title, a negative and a positive bottoms. When the positive button is clicked + * the download is triggered. + * + */ +class SimpleDownloadDialogFragment : DownloadDialogFragment() { + + private val safeArguments get() = requireNotNull(arguments) + + @VisibleForTesting + internal var testingContext: Context? = null + + internal val dialogGravity: Int get() = + safeArguments.getInt(KEY_DIALOG_GRAVITY, DEFAULT_VALUE) + internal val dialogShouldWidthMatchParent: Boolean get() = + safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT) + + internal val positiveButtonBackgroundColor get() = + safeArguments.getInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, DEFAULT_VALUE) + internal val positiveButtonTextColor get() = + safeArguments.getInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, DEFAULT_VALUE) + internal val positiveButtonRadius get() = + safeArguments.getFloat(KEY_POSITIVE_BUTTON_RADIUS, DEFAULT_VALUE.toFloat()) + internal val fileNameEndMargin get() = + safeArguments.getInt(KEY_FILE_NAME_END_MARGIN, DEFAULT_VALUE) + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val sheetDialog = Dialog(requireContext()) + sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + sheetDialog.setCanceledOnTouchOutside(false) + + val rootView = createContainer() + sheetDialog.setContainerView(rootView) + sheetDialog.window?.apply { + if (dialogGravity != DEFAULT_VALUE) { + setGravity(dialogGravity) + } + + if (dialogShouldWidthMatchParent) { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + // This must be called after addContentView, or it won't fully fill to the edge. + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + return sheetDialog + } + + @SuppressLint("InflateParams") + private fun createContainer(): View { + val rootView = LayoutInflater.from(requireContext()).inflate( + R.layout.mozac_downloads_prompt, + null, + false, + ) + + val binding = MozacDownloadsPromptBinding.bind(rootView) + + with(requireBundle()) { + binding.title.text = if (getLong(KEY_CONTENT_LENGTH) <= 0L) { + getString(R.string.mozac_feature_downloads_dialog_download) + } else { + val contentSize = getLong(KEY_CONTENT_LENGTH).toMegabyteOrKilobyteString() + getString(getInt(KEY_TITLE_TEXT, R.string.mozac_feature_downloads_dialog_title2), contentSize) + } + + if (positiveButtonBackgroundColor != DEFAULT_VALUE) { + val backgroundTintList = ContextCompat.getColorStateList( + requireContext(), + positiveButtonBackgroundColor, + ) + binding.downloadButton.backgroundTintList = backgroundTintList + } + + if (positiveButtonTextColor != DEFAULT_VALUE) { + val color = ContextCompat.getColor(requireContext(), positiveButtonTextColor) + binding.downloadButton.setTextColor(color) + } + + if (positiveButtonRadius != DEFAULT_VALUE.toFloat()) { + val shape = GradientDrawable() + shape.shape = GradientDrawable.RECTANGLE + shape.setColor( + ContextCompat.getColor( + requireContext(), + positiveButtonBackgroundColor, + ), + ) + shape.cornerRadius = positiveButtonRadius + binding.downloadButton.background = shape + } + + if (fileNameEndMargin != DEFAULT_VALUE) { + binding.filename.layoutParams = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT, + ).apply { + marginEnd = fileNameEndMargin + marginStart = binding.filename.marginStart + topMargin = binding.filename.marginTop + bottomMargin = binding.filename.marginBottom + addRule(RelativeLayout.BELOW, R.id.title) + addRule(RelativeLayout.END_OF, R.id.icon) + addRule(RelativeLayout.ALIGN_BASELINE, R.id.icon) + } + } + + binding.filename.text = getString(KEY_FILE_NAME, "") + binding.filename.movementMethod = ScrollingMovementMethod() + + binding.downloadButton.text = getString( + getInt(KEY_DOWNLOAD_TEXT, R.string.mozac_feature_downloads_dialog_download), + ) + + binding.closeButton.setOnClickListener { + onCancelDownload() + dismiss() + } + + binding.downloadButton.setOnClickListener { + onStartDownload() + dismiss() + } + } + + return rootView + } + + private fun Dialog.setContainerView(rootView: View) { + if (dialogShouldWidthMatchParent) { + setContentView(rootView) + } else { + addContentView( + rootView, + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, + ), + ) + } + } + + companion object { + /** + * A builder method for creating a [SimpleDownloadDialogFragment] + */ + fun newInstance( + @StringRes dialogTitleText: Int = R.string.mozac_feature_downloads_dialog_title2, + @StringRes downloadButtonText: Int = R.string.mozac_feature_downloads_dialog_download, + @StyleRes themeResId: Int = 0, + promptsStyling: DownloadsFeature.PromptsStyling? = null, + ): SimpleDownloadDialogFragment { + val fragment = SimpleDownloadDialogFragment() + val arguments = fragment.arguments ?: Bundle() + + with(arguments) { + putInt(KEY_DOWNLOAD_TEXT, downloadButtonText) + putInt(KEY_THEME_ID, themeResId) + putInt(KEY_TITLE_TEXT, dialogTitleText) + + promptsStyling?.apply { + putInt(KEY_DIALOG_GRAVITY, gravity) + putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, shouldWidthMatchParent) + + positiveButtonBackgroundColor?.let { + putInt(KEY_POSITIVE_BUTTON_BACKGROUND_COLOR, it) + } + + positiveButtonTextColor?.let { + putInt(KEY_POSITIVE_BUTTON_TEXT_COLOR, it) + } + + positiveButtonRadius?.let { + putFloat(KEY_POSITIVE_BUTTON_RADIUS, it) + } + + fileNameEndMargin?.let { + putInt(KEY_FILE_NAME_END_MARGIN, it) + } + } + } + + fragment.arguments = arguments + + return fragment + } + + const val KEY_DOWNLOAD_TEXT = "KEY_DOWNLOAD_TEXT" + + // WARNING: If KEY_CONTENT_LENGTH is <= 0, this will be overriden with the default string "Download" + const val KEY_TITLE_TEXT = "KEY_TITLE_TEXT" + const val KEY_THEME_ID = "KEY_THEME_ID" + + private const val KEY_POSITIVE_BUTTON_BACKGROUND_COLOR = "KEY_POSITIVE_BUTTON_BACKGROUND_COLOR" + private const val KEY_POSITIVE_BUTTON_TEXT_COLOR = "KEY_POSITIVE_BUTTON_TEXT_COLOR" + private const val KEY_POSITIVE_BUTTON_RADIUS = "KEY_POSITIVE_BUTTON_RADIUS" + private const val KEY_FILE_NAME_END_MARGIN = "KEY_FILE_NAME_END_MARGIN" + private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY" + private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT" + private const val DEFAULT_VALUE = Int.MAX_VALUE + } + + private fun requireBundle(): Bundle { + return arguments ?: throw IllegalStateException("Fragment $this arguments is not set.") + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadDao.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadDao.kt new file mode 100644 index 0000000000..edd31debd2 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadDao.kt @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.db + +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +/** + * Internal dao for accessing and modifying Downloads in the database. + */ +@Dao +internal interface DownloadDao { + + @Insert + suspend fun insert(entity: DownloadEntity): Long + + @Update + suspend fun update(entity: DownloadEntity) + + @Query("SELECT * FROM downloads ORDER BY created_at DESC") + fun getDownloads(): Flow<List<DownloadEntity>> + + @Query("SELECT * FROM downloads ORDER BY created_at DESC") + suspend fun getDownloadsList(): List<DownloadEntity> + + @Delete + suspend fun delete(entity: DownloadEntity) + + @Query("DELETE FROM downloads") + suspend fun deleteAllDownloads() + + @Query("SELECT * FROM downloads ORDER BY created_at DESC") + fun getDownloadsPaged(): DataSource.Factory<Int, DownloadEntity> +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadEntity.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadEntity.kt new file mode 100644 index 0000000000..be913e51eb --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadEntity.kt @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.downloads.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import mozilla.components.browser.state.state.content.DownloadState + +/** + * Internal entity representing a download as it gets saved to the database. + */ +@Entity(tableName = "downloads") +internal data class DownloadEntity( + @PrimaryKey + @ColumnInfo(name = "id") + var id: String, + + @ColumnInfo(name = "url") + var url: String, + + @ColumnInfo(name = "file_name") + var fileName: String?, + + @ColumnInfo(name = "content_type") + var contentType: String?, + + @ColumnInfo(name = "content_length") + var contentLength: Long?, + + @ColumnInfo(name = "status") + var status: DownloadState.Status, + + @ColumnInfo(name = "destination_directory") + var destinationDirectory: String, + + @ColumnInfo(name = "created_at") + var createdAt: Long, + +) { + + internal fun toDownloadState(): DownloadState { + return DownloadState( + url, + fileName, + contentType, + contentLength, + currentBytesCopied = 0, + status = status, + userAgent = null, + destinationDirectory = destinationDirectory, + referrerUrl = null, + skipConfirmation = false, + id = id, + sessionId = null, + createdTime = createdAt, + ) + } +} + +internal fun DownloadState.toDownloadEntity(): DownloadEntity { + /** + * Data URLs cause problems when restoring the values from the db, + * as the string could be so long that it could break the maximum allowed size for a cursor, + * causing SQLiteBIobTooBigException when restoring downloads from the DB. + */ + val isDataURL = url.startsWith("data:") + val sanitizedURL = if (isDataURL) { + "" + } else { + url + } + + return DownloadEntity( + id, + sanitizedURL, + fileName, + contentType, + contentLength, + status = status, + destinationDirectory = destinationDirectory, + createdAt = createdTime, + ) +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadsDatabase.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadsDatabase.kt new file mode 100644 index 0000000000..9adabfe9cd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/db/DownloadsDatabase.kt @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverter +import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import mozilla.components.browser.state.state.content.DownloadState + +/** + * Internal database for saving downloads. + */ +@Database(entities = [DownloadEntity::class], version = 4) +@TypeConverters(StatusConverter::class) +internal abstract class DownloadsDatabase : RoomDatabase() { + abstract fun downloadDao(): DownloadDao + + companion object { + @Volatile + private var instance: DownloadsDatabase? = null + + @Synchronized + fun get(context: Context): DownloadsDatabase { + instance?.let { return it } + + return Room.databaseBuilder( + context, + DownloadsDatabase::class.java, + "mozac_downloads_database", + ).addMigrations( + Migrations.migration_1_2, + Migrations.migration_2_3, + Migrations.migration_3_4, + ).build().also { + instance = it + } + } + } +} + +@Suppress("MaxLineLength", "MagicNumber") +internal object Migrations { + val migration_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE downloads ADD COLUMN is_private INTEGER NOT NULL DEFAULT 0", + ) + } + } + val migration_2_3 = object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + // Create a temporal table + db.execSQL("CREATE TABLE temp_downloads (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))") + // Copy the data + db.execSQL("INSERT INTO temp_downloads (id,url,file_name,content_type,content_length,status,destination_directory,created_at) SELECT id,url,file_name,content_type,content_length,status,destination_directory,created_at FROM downloads where is_private = 0") + // Remove the old table + db.execSQL("DROP TABLE downloads") + // Rename the table name to the correct one + db.execSQL("ALTER TABLE temp_downloads RENAME TO downloads") + } + } + + val migration_3_4 = object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { + // Clear any data urls. + db.execSQL("UPDATE downloads SET url='' WHERE url LIKE 'data:%' ") + } + } +} + +@Suppress("unused") +internal class StatusConverter { + private val statusArray = DownloadState.Status.values() + + @TypeConverter + fun toInt(status: DownloadState.Status): Int { + return status.id + } + + @TypeConverter + fun toStatus(index: Int): DownloadState.Status? { + return statusArray.find { it.id == index } + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragment.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragment.kt new file mode 100644 index 0000000000..1278bd37fa --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragment.kt @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.dialog + +import android.app.Dialog +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import mozilla.components.support.base.R +import mozilla.components.ui.widgets.withCenterAlignedButtons + +internal const val KEY_MESSAGE = "KEY_MESSAGE" + +/** + * A dialog to be displayed when the Android permission is denied, + * users should be notified and offered a way activate it on the app settings. + * The dialog will have two buttons: One "Go to settings" and another for "Dismissing". + */ +class DeniedPermissionDialogFragment : DialogFragment() { + internal val message: Int by lazy { safeArguments.getInt(KEY_MESSAGE) } + val safeArguments get() = requireNotNull(arguments) + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder = AlertDialog.Builder(requireContext()) + .setMessage(message) + .setCancelable(true) + .setNegativeButton(R.string.mozac_support_base_permissions_needed_negative_button) { _, _ -> + dismiss() + } + .setPositiveButton(R.string.mozac_support_base_permissions_needed_positive_button) { _, _ -> + openSettingsPage() + } + return builder.create().withCenterAlignedButtons() + } + + @VisibleForTesting + internal fun openSettingsPage() { + dismiss() + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", requireContext().packageName, null) + intent.data = uri + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + requireContext().startActivity(intent) + } + + companion object { + /** + * A builder method for creating a [DeniedPermissionDialogFragment] + * @param message the message of the dialog. + **/ + fun newInstance( + @StringRes message: Int, + ): DeniedPermissionDialogFragment { + val fragment = DeniedPermissionDialogFragment() + val arguments = fragment.arguments ?: Bundle() + + with(arguments) { + putInt(KEY_MESSAGE, message) + } + + fragment.arguments = arguments + return fragment + } + + const val FRAGMENT_TAG = "DENIED_DOWNLOAD_PERMISSION_PROMPT_DIALOG" + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/Context.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/Context.kt new file mode 100644 index 0000000000..7e7b28c68c --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/Context.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.downloads.ext + +import android.annotation.TargetApi +import android.app.DownloadManager +import android.content.Context +import android.net.Uri +import android.os.Build +import androidx.core.content.getSystemService + +/** + * Wraps around [DownloadManager.addCompletedDownload] and calls the correct + * method depending on the SDK version. + * + * Deprecated in Android Q, use MediaStore on that version. + */ +@TargetApi(Build.VERSION_CODES.P) +@Suppress("Deprecation", "LongParameterList") +internal fun Context.addCompletedDownload( + title: String, + description: String, + isMediaScannerScannable: Boolean, + mimeType: String, + path: String, + length: Long, + showNotification: Boolean, + uri: Uri?, + referer: Uri?, +) = getSystemService<DownloadManager>()!!.run { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + addCompletedDownload( + title, + description, + isMediaScannerScannable, + mimeType, + path, + length, + showNotification, + uri, + referer, + ) + } else { + addCompletedDownload( + title, + description, + isMediaScannerScannable, + mimeType, + path, + length, + showNotification, + ) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt new file mode 100644 index 0000000000..460b834dce --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ext/DownloadState.kt @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.ext + +import androidx.core.net.toUri +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.concept.fetch.Headers +import mozilla.components.concept.fetch.Headers.Names.CONTENT_DISPOSITION +import mozilla.components.concept.fetch.Headers.Names.CONTENT_LENGTH +import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE +import mozilla.components.support.ktx.kotlin.sanitizeFileName +import mozilla.components.support.utils.DownloadUtils +import java.io.InputStream +import java.net.URLConnection + +internal fun DownloadState.isScheme(protocols: Iterable<String>): Boolean { + val scheme = url.trim().toUri().scheme ?: return false + return protocols.contains(scheme) +} + +/** + * Returns a copy of the download with some fields filled in based on values from a response. + * + * @param headers Headers from the response. + * @param stream Stream of the response body. + */ +internal fun DownloadState.withResponse(headers: Headers, stream: InputStream?): DownloadState { + val contentDisposition = headers[CONTENT_DISPOSITION] + var contentType = this.contentType + if (contentType == null && stream != null) { + contentType = URLConnection.guessContentTypeFromStream(stream) + } + if (contentType == null) { + contentType = headers[CONTENT_TYPE] + } + + val newFileName = if (fileName.isNullOrBlank()) { + DownloadUtils.guessFileName(contentDisposition, destinationDirectory, url, contentType) + } else { + fileName + } + return copy( + fileName = newFileName?.sanitizeFileName(), + contentType = contentType, + contentLength = contentLength ?: headers[CONTENT_LENGTH]?.toLongOrNull(), + ) +} + +internal val DownloadState.realFilenameOrGuessed + get() = fileName ?: DownloadUtils.guessFileName(null, destinationDirectory, url, contentType) diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/facts/DownloadsFacts.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/facts/DownloadsFacts.kt new file mode 100644 index 0000000000..cdbf443a8d --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/facts/DownloadsFacts.kt @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.downloads.facts + +import mozilla.components.feature.downloads.facts.DownloadsFacts.Items.PROMPT +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 [DownloadsFeature] + */ +class DownloadsFacts { + /** + * Items that specify which portion of the [DownloadsFeature] was interacted with + */ + object Items { + const val NOTIFICATION = "notification" + const val PROMPT = "prompt" + } +} + +internal fun emitNotificationResumeFact() = emitFact(Action.RESUME) +internal fun emitNotificationPauseFact() = emitFact(Action.PAUSE) +internal fun emitNotificationCancelFact() = emitFact(Action.CANCEL) +internal fun emitNotificationTryAgainFact() = emitFact(Action.TRY_AGAIN) +internal fun emitNotificationOpenFact() = emitFact(Action.OPEN) +internal fun emitPromptDisplayedFact() = emitFact(Action.DISPLAY, item = PROMPT) +internal fun emitPromptDismissedFact() = emitFact(Action.CANCEL, item = PROMPT) + +private fun emitFact( + action: Action, + item: String = DownloadsFacts.Items.NOTIFICATION, +) { + Fact( + Component.FEATURE_DOWNLOADS, + action, + item, + ).collect() +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/AndroidDownloadManager.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/AndroidDownloadManager.kt new file mode 100644 index 0000000000..173409553d --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/AndroidDownloadManager.kt @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.manager + +import android.Manifest.permission.INTERNET +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE +import android.app.DownloadManager.EXTRA_DOWNLOAD_ID +import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.util.LongSparseArray +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.core.net.toUri +import androidx.core.util.set +import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Headers.Names.COOKIE +import mozilla.components.concept.fetch.Headers.Names.REFERRER +import mozilla.components.concept.fetch.Headers.Names.USER_AGENT +import mozilla.components.feature.downloads.AbstractFetchDownloadService +import mozilla.components.feature.downloads.ext.isScheme +import mozilla.components.support.utils.DownloadUtils +import mozilla.components.support.utils.ext.getSerializableExtraCompat +import mozilla.components.support.utils.ext.registerReceiverCompat + +typealias SystemDownloadManager = android.app.DownloadManager +typealias SystemRequest = android.app.DownloadManager.Request + +/** + * Handles the interactions with the [AndroidDownloadManager]. + * + * @property applicationContext a reference to [Context] applicationContext. + */ +class AndroidDownloadManager( + private val applicationContext: Context, + private val store: BrowserStore, + override var onDownloadStopped: onDownloadStopped = noop, +) : BroadcastReceiver(), DownloadManager { + + private val downloadRequests = LongSparseArray<SystemRequest>() + private var isSubscribedReceiver = false + + // Do not require WRITE_EXTERNAL_STORAGE permission on API 29 and above (using scoped storage) + override val permissions + get() = if (getSDKVersion() >= Build.VERSION_CODES.Q) { + arrayOf(INTERNET) + } else { + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE) + } + + @VisibleForTesting + internal fun getSDKVersion() = SDK_INT + + /** + * Schedules a download through the [AndroidDownloadManager]. + * @param download metadata related to the download. + * @param cookie any additional cookie to add as part of the download request. + * @return the id reference of the scheduled download. + */ + override fun download(download: DownloadState, cookie: String): String? { + val androidDownloadManager: SystemDownloadManager = applicationContext.getSystemService()!! + + if (!download.isScheme(listOf("http", "https"))) { + // We are ignoring everything that is not http or https. This is a limitation of + // Android's download manager. There's no reason to show a download dialog for + // something we can't download anyways. + return null + } + + validatePermissionGranted(applicationContext) + + val request = download.toAndroidRequest(cookie) + val downloadID = androidDownloadManager.enqueue(request) + store.dispatch(DownloadAction.AddDownloadAction(download.copy(id = downloadID.toString()))) + downloadRequests[downloadID] = request + registerBroadcastReceiver() + return downloadID.toString() + } + + override fun tryAgain(downloadId: String) { + val androidDownloadManager: SystemDownloadManager = applicationContext.getSystemService()!! + androidDownloadManager.enqueue(downloadRequests[downloadId.toLong()]) + } + + /** + * Remove all the listeners. + */ + override fun unregisterListeners() { + if (isSubscribedReceiver) { + applicationContext.unregisterReceiver(this) + isSubscribedReceiver = false + downloadRequests.clear() + } + } + + private fun registerBroadcastReceiver() { + if (!isSubscribedReceiver) { + val filter = IntentFilter(ACTION_DOWNLOAD_COMPLETE) + + applicationContext.registerReceiverCompat( + this, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + + isSubscribedReceiver = true + } + } + + /** + * Invoked when a download is complete. Notifies [onDownloadStopped] and removes the queued + * download if it's complete. + */ + override fun onReceive(context: Context, intent: Intent) { + val downloadID = intent.getStringExtra(EXTRA_DOWNLOAD_ID) ?: "" + val download = store.state.downloads[downloadID] + val downloadStatus = + intent.getSerializableExtraCompat(AbstractFetchDownloadService.EXTRA_DOWNLOAD_STATUS, Status::class.java) + as Status + + if (download != null) { + onDownloadStopped(download, downloadID, downloadStatus) + } + } +} + +private fun DownloadState.toAndroidRequest(cookie: String): SystemRequest { + val request = SystemRequest(url.toUri()) + .setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + + if (!contentType.isNullOrEmpty()) { + request.setMimeType(contentType) + } + + with(request) { + addRequestHeaderSafely(USER_AGENT, userAgent) + addRequestHeaderSafely(COOKIE, cookie) + addRequestHeaderSafely(REFERRER, referrerUrl) + } + + val fileName = if (fileName.isNullOrBlank()) { + DownloadUtils.guessFileName(null, destinationDirectory, url, contentType) + } else { + fileName + } + request.setDestinationInExternalPublicDir(destinationDirectory, fileName) + + return request +} + +internal fun SystemRequest.addRequestHeaderSafely(name: String, value: String?) { + if (value.isNullOrEmpty()) return + addRequestHeader(name, value) +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/DownloadManager.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/DownloadManager.kt new file mode 100644 index 0000000000..0bd71e11c6 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/DownloadManager.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.downloads.manager + +import android.content.Context +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status +import mozilla.components.support.ktx.android.content.isPermissionGranted + +typealias onDownloadStopped = (DownloadState, String, Status) -> Unit + +interface DownloadManager { + + val permissions: Array<String> + + var onDownloadStopped: onDownloadStopped + + /** + * Schedules a download through the [DownloadManager]. + * @param download metadata related to the download. + * @param cookie any additional cookie to add as part of the download request. + * @return the id reference of the scheduled download. + */ + fun download( + download: DownloadState, + cookie: String = "", + ): String? + + /** + * Schedules another attempt at downloading the given download. + * @param downloadId the id of the previously attempted download + */ + fun tryAgain( + downloadId: String, + ) + + fun unregisterListeners() = Unit +} + +fun DownloadManager.validatePermissionGranted(context: Context) { + if (!context.isPermissionGranted(permissions.asIterable())) { + throw SecurityException("You must be granted ${permissions.joinToString()}") + } +} + +internal val noop: onDownloadStopped = { _, _, _ -> } diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/FetchDownloadManager.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/FetchDownloadManager.kt new file mode 100644 index 0000000000..dc8266c003 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/FetchDownloadManager.kt @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.downloads.manager + +import android.Manifest.permission.FOREGROUND_SERVICE +import android.Manifest.permission.INTERNET +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.annotation.SuppressLint +import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE +import android.app.DownloadManager.EXTRA_DOWNLOAD_ID +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.P +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.downloads.AbstractFetchDownloadService +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.EXTRA_DOWNLOAD_STATUS +import mozilla.components.feature.downloads.ext.isScheme +import mozilla.components.support.base.android.NotificationsDelegate +import mozilla.components.support.utils.ext.getSerializableExtraCompat +import mozilla.components.support.utils.ext.registerReceiverCompat +import kotlin.reflect.KClass + +/** + * Handles the interactions with [AbstractFetchDownloadService]. + * + * @property applicationContext a reference to [Context] applicationContext. + * @property service The subclass of [AbstractFetchDownloadService] to use. + */ +class FetchDownloadManager<T : AbstractFetchDownloadService>( + private val applicationContext: Context, + private val store: BrowserStore, + private val service: KClass<T>, + override var onDownloadStopped: onDownloadStopped = noop, + private val notificationsDelegate: NotificationsDelegate, +) : BroadcastReceiver(), DownloadManager { + + private var isSubscribedReceiver = false + + // Do not require WRITE_EXTERNAL_STORAGE permission on API 29 and above (using scoped storage) + override val permissions + @SuppressLint("InlinedApi") + get() = if (getSDKVersion() >= Build.VERSION_CODES.Q) { + arrayOf(INTERNET, FOREGROUND_SERVICE) + } else if (getSDKVersion() >= P) { + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE, FOREGROUND_SERVICE) + } else { + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE) + } + + @VisibleForTesting + internal fun getSDKVersion() = SDK_INT + + /** + * Schedules a download through the [AbstractFetchDownloadService]. + * @param download metadata related to the download. + * @param cookie any additional cookie to add as part of the download request. + * @return the id reference of the scheduled download. + */ + override fun download(download: DownloadState, cookie: String): String? { + if (!download.isScheme(listOf("http", "https", "data", "blob", "moz-extension"))) { + return null + } + validatePermissionGranted(applicationContext) + + if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationsDelegate.requestNotificationPermission() + } + + // The middleware will notify the service to start the download + // once this action is processed. + store.dispatch(DownloadAction.AddDownloadAction(download)) + + registerBroadcastReceiver() + return download.id + } + + override fun tryAgain(downloadId: String) { + val download = store.state.downloads[downloadId] ?: return + + val intent = Intent(applicationContext, service.java) + intent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + intent.action = AbstractFetchDownloadService.ACTION_TRY_AGAIN + applicationContext.startService(intent) + + registerBroadcastReceiver() + } + + /** + * Remove all the listeners. + */ + override fun unregisterListeners() { + if (isSubscribedReceiver) { + applicationContext.unregisterReceiver(this) + isSubscribedReceiver = false + } + } + + private fun registerBroadcastReceiver() { + if (!isSubscribedReceiver) { + val filter = IntentFilter(ACTION_DOWNLOAD_COMPLETE) + + applicationContext.registerReceiverCompat( + this, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + + isSubscribedReceiver = true + } + } + + /** + * Invoked when a download is complete. Notifies [onDownloadStopped] and removes the queued + * download if it's complete. + */ + override fun onReceive(context: Context, intent: Intent) { + val downloadID = intent.getStringExtra(EXTRA_DOWNLOAD_ID) ?: "" + val download = store.state.downloads[downloadID] + val downloadStatus = intent.getSerializableExtraCompat(EXTRA_DOWNLOAD_STATUS, Status::class.java) + as Status? + + if (download != null && downloadStatus != null) { + onDownloadStopped(download, downloadID, downloadStatus) + } + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/provider/FileProvider.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/provider/FileProvider.kt new file mode 100644 index 0000000000..9aa89519a6 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/provider/FileProvider.kt @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package mozilla.components.feature.downloads.provider + +/** + * A file provider to provide functionality for the feature downloads component. + * + * We need this class to create a fully qualified class name that doesn't clash with other + * file providers in other components see https://stackoverflow.com/a/43444164/5533820. + * + * Be aware, when creating new file resources avoid using common names like "@xml/file_paths", + * as other file providers could be using the same names and this could case unexpected behaviors. + * As a convention try to use unique names like using the name of the component as a prefix of the + * name of the file, like component_xxx_file_paths.xml. + */ +/** @suppress */ +class FileProvider : androidx.core.content.FileProvider() diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeature.kt new file mode 100644 index 0000000000..d02e36c307 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeature.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.downloads.temporary + +import android.content.Context +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.CopyInternetResourceAction +import mozilla.components.browser.state.action.ShareInternetResourceAction +import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab +import mozilla.components.browser.state.state.content.ShareInternetResourceState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Client +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.ktx.android.content.copyImage +import java.util.concurrent.TimeUnit + +/** + * [LifecycleAwareFeature] implementation for copying online resources. + * + * This will intercept only [ShareInternetResourceAction] [BrowserAction]s. + * + * Following which it will transparently + * - download internet resources while respecting the private mode related to cookies handling + * - temporarily cache the downloaded resources + * - copy the resource to the device clipboard. + * + * with a 1 second timeout to ensure a smooth UX. + * + * To finish the process in this small timeframe the feature is recommended to be used only for images. + * + * @property context Android context used for various platform interactions. + * @property store a reference to the application's [BrowserStore]. + * @property tabId ID of the tab session, or null if the selected session should be used. + * @property onCopyConfirmation The confirmation action of copying an image. + * @param httpClient Client used for downloading internet resources. + * @param cleanupCacheCoroutineDispatcher Coroutine dispatcher used for the cleanup of old + * cached files. Defaults to IO. + */ +class CopyDownloadFeature( + private val context: Context, + private val store: BrowserStore, + private val tabId: String?, + private val onCopyConfirmation: () -> Unit, + httpClient: Client, + cleanupCacheCoroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, +) : TemporaryDownloadFeature( + context, + httpClient, + cleanupCacheCoroutineDispatcher, +) { + + /** + * At most time to allow for the file to be downloaded. + */ + private val operationTimeoutMs by lazy { TimeUnit.MINUTES.toMinutes(1) } + + override fun start() { + scope = store.flowScoped { flow -> + flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(tabId) } + .distinctUntilChangedBy { it.content.copy } + .collect { state -> + state.content.copy?.let { copyState -> + logger.debug("Starting the copying process") + startCopy(copyState) + + // This is a fire and forget action, not something that we want lingering the tab state. + store.dispatch(CopyInternetResourceAction.ConsumeCopyAction(state.id)) + } + } + } + } + + @VisibleForTesting + internal fun startCopy(internetResource: ShareInternetResourceState) { + val coroutineExceptionHandler = coroutineExceptionHandler("Copy") + + scope?.launch(coroutineExceptionHandler) { + withTimeout(operationTimeoutMs) { + val download = download(internetResource) + copy(download.canonicalPath, onCopyConfirmation) + } + } + } + + @VisibleForTesting + internal fun copy(filePath: String, onCopyConfirmation: () -> Unit) = + context.copyImage(filePath, onCopyConfirmation) +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeature.kt new file mode 100644 index 0000000000..ac6b0023b9 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeature.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.downloads.temporary + +import android.content.Context +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.ShareInternetResourceAction +import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab +import mozilla.components.browser.state.state.content.ShareInternetResourceState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Client +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.ktx.android.content.shareMedia + +/** + * At most time to allow for the file to be downloaded and action to be performed. + */ +private const val OPERATION_TIMEOUT_MS: Long = 1000L + +/** + * [LifecycleAwareFeature] implementation for sharing online resources. + * + * This will intercept only [ShareInternetResourceAction] [BrowserAction]s. + * + * Following which it will transparently + * - download internet resources while respecting the private mode related to cookies handling + * - temporarily cache the downloaded resources + * - automatically open the platform app chooser to share the cached files with other installed Android apps. + * + * with a 1 second timeout to ensure a smooth UX. + * + * To finish the process in this small timeframe the feature is recommended to be used only for images. + * + * @property context Android context used for various platform interactions + * @property store a reference to the application's [BrowserStore] + * @property tabId ID of the tab session, or null if the selected session should be used. + * @param httpClient Client used for downloading internet resources + * @param cleanupCacheCoroutineDispatcher Coroutine dispatcher used for the cleanup of old + * cached files. Defaults to IO. + */ +class ShareDownloadFeature( + private val context: Context, + private val store: BrowserStore, + private val tabId: String?, + httpClient: Client, + cleanupCacheCoroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, +) : TemporaryDownloadFeature(context, httpClient, cleanupCacheCoroutineDispatcher) { + + override fun start() { + scope = store.flowScoped { flow -> + flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(tabId) } + .distinctUntilChangedBy { it.content.share } + .collect { state -> + state.content.share?.let { shareState -> + logger.debug("Starting the sharing process") + startSharing(shareState) + + // This is a fire and forget action, not something that we want lingering the tab state. + store.dispatch(ShareInternetResourceAction.ConsumeShareAction(state.id)) + } + } + } + } + + @VisibleForTesting + internal fun startSharing(internetResource: ShareInternetResourceState) { + val coroutineExceptionHandler = coroutineExceptionHandler("Share") + + scope?.launch(coroutineExceptionHandler) { + withTimeout(OPERATION_TIMEOUT_MS) { + val download = download(internetResource) + share( + contentType = internetResource.contentType, + filePath = download.canonicalPath, + ) + } + } + } + + @VisibleForTesting + internal fun share( + filePath: String, + contentType: String?, + subject: String? = null, + message: String? = null, + ) = context.shareMedia(filePath, contentType, subject, message) +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/TemporaryDownloadFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/TemporaryDownloadFeature.kt new file mode 100644 index 0000000000..3a4119315c --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/temporary/TemporaryDownloadFeature.kt @@ -0,0 +1,156 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.temporary + +import android.content.Context +import android.webkit.MimeTypeMap +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import mozilla.components.browser.state.state.content.ShareInternetResourceState +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Headers +import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.kotlin.ifNullOrEmpty +import mozilla.components.support.ktx.kotlin.sanitizeURL +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.net.URLConnection +import kotlin.math.absoluteValue +import kotlin.random.Random + +/** + * Default mime type for base64 images data URLs not containing the media type. + */ +@VisibleForTesting +internal const val DEFAULT_IMAGE_EXTENSION = "jpg" + +/** + * Subdirectory of Context.getCacheDir() where the resources to be shared are stored. + * + * Location must be kept in sync with the paths our FileProvider can share from. + */ +@VisibleForTesting +internal var cacheDirName = "mozac_share_cache" + +/** + * Base class for downloading resources from the internet and storing them in a temporary cache. + * + * @property context Android context used for various platform interactions. + * @property httpClient Client used for downloading internet resources. + * @param cleanupCacheCoroutineDispatcher Coroutine dispatcher used for the cleanup of old + * cached files. Defaults to IO. + */ +abstract class TemporaryDownloadFeature( + private val context: Context, + private val httpClient: Client, + cleanupCacheCoroutineDispatcher: CoroutineDispatcher = IO, +) : LifecycleAwareFeature { + + val logger = Logger("TemporaryDownloadFeature") + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + var scope: CoroutineScope? = null + + override fun stop() { + scope?.cancel() + } + + init { + CoroutineScope(cleanupCacheCoroutineDispatcher).launch { + cleanupCache() + } + } + + @WorkerThread + @VisibleForTesting + internal fun download(internetResource: ShareInternetResourceState): File { + val request = Request( + internetResource.url.sanitizeURL(), + private = internetResource.private, + referrerUrl = internetResource.referrerUrl, + ) + val response = if (internetResource.response == null) { + httpClient.fetch(request) + } else { + requireNotNull(internetResource.response) + } + + if (response.status != Response.SUCCESS) { + response.close() + // We experienced a problem trying to fetch the file, nothing more we can do. + throw (RuntimeException("Resource is not available to download")) + } + + var tempFile: File? = null + response.body.useStream { input -> + val fileExtension = '.' + getFileExtension(response.headers, input) + tempFile = getTempFile(fileExtension) + FileOutputStream(tempFile).use { output -> input.copyTo(output) } + } + + return tempFile!! + } + + @VisibleForTesting + internal fun getFilename(fileExtension: String) = + Random.nextInt().absoluteValue.toString() + fileExtension + + @VisibleForTesting + internal fun getTempFile(fileExtension: String) = + File(getMediaShareCacheDirectory(), getFilename(fileExtension)) + + @VisibleForTesting + internal fun getCacheDirectory() = File(context.cacheDir, cacheDirName) + + @VisibleForTesting + internal fun getFileExtension(responseHeaders: Headers, responseStream: InputStream): String { + val mimeType = URLConnection.guessContentTypeFromStream(responseStream) ?: responseHeaders[CONTENT_TYPE] + + return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType).ifNullOrEmpty { DEFAULT_IMAGE_EXTENSION } + } + + @VisibleForTesting + internal fun getMediaShareCacheDirectory(): File { + val mediaShareCacheDir = getCacheDirectory() + if (!mediaShareCacheDir.exists()) { + mediaShareCacheDir.mkdirs() + } + return mediaShareCacheDir + } + + @VisibleForTesting + internal fun cleanupCache() { + logger.debug("Deleting previous cache of shared files") + getCacheDirectory().listFiles()?.forEach { it.delete() } + } + + protected fun coroutineExceptionHandler(action: String) = + CoroutineExceptionHandler { _, throwable -> + when (throwable) { + is InterruptedException -> { + logger.warn("$action failed: operation timeout reached") + } + + is IOException, + is RuntimeException, + is NullPointerException, + -> { + logger.warn("$action failed: $throwable") + } + } + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialog.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialog.kt new file mode 100644 index 0000000000..73cad1e2e2 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialog.kt @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.ui + +import android.annotation.SuppressLint +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.appcompat.widget.AppCompatImageButton +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.feature.downloads.R +import mozilla.components.support.utils.ext.getParcelableArrayListCompat +import java.util.ArrayList + +/** + * A dialog where an user can select with which app a download must be performed. + */ +internal class DownloadAppChooserDialog : AppCompatDialogFragment() { + private val safeArguments get() = requireNotNull(arguments) + internal val appsList: ArrayList<DownloaderApp> + get() = + safeArguments.getParcelableArrayListCompat(KEY_APP_LIST, DownloaderApp::class.java) + ?: arrayListOf() + + internal val dialogGravity: Int get() = + safeArguments.getInt(KEY_DIALOG_GRAVITY, DEFAULT_VALUE) + internal val dialogShouldWidthMatchParent: Boolean get() = + safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT) + + /** + * Indicates the user has selected an application to perform the download + */ + internal var onAppSelected: ((DownloaderApp) -> Unit) = {} + + internal var onDismiss: () -> Unit = {} + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val sheetDialog = Dialog(requireContext()) + sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + sheetDialog.setCanceledOnTouchOutside(false) + + val rootView = createContainer() + sheetDialog.setContainerView(rootView) + sheetDialog.window?.apply { + if (dialogGravity != DEFAULT_VALUE) { + setGravity(dialogGravity) + } + + if (dialogShouldWidthMatchParent) { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + // This must be called after addContentView, or it won't fully fill to the edge. + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + return sheetDialog + } + + @SuppressLint("InflateParams") + private fun createContainer(): View { + val rootView = LayoutInflater.from(requireContext()).inflate( + R.layout.mozac_downloader_chooser_prompt, + null, + false, + ) + + val recyclerView = rootView.findViewById<RecyclerView>(R.id.apps_list) + recyclerView.adapter = DownloaderAppAdapter(rootView.context, appsList) { app -> + onAppSelected(app) + dismiss() + } + + rootView.findViewById<AppCompatImageButton>(R.id.close_button).setOnClickListener { + dismiss() + onDismiss() + } + + return rootView + } + + fun setApps(apps: List<DownloaderApp>) { + val args = arguments ?: Bundle() + args.putParcelableArrayList(KEY_APP_LIST, ArrayList(apps)) + arguments = args + } + + private fun Dialog.setContainerView(rootView: View) { + if (dialogShouldWidthMatchParent) { + setContentView(rootView) + } else { + addContentView( + rootView, + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, + ), + ) + } + } + + companion object { + /** + * A builder method for creating a [DownloadAppChooserDialog] + */ + fun newInstance( + gravity: Int? = DEFAULT_VALUE, + dialogShouldWidthMatchParent: Boolean? = false, + ): DownloadAppChooserDialog { + val fragment = DownloadAppChooserDialog() + val arguments = fragment.arguments ?: Bundle() + + with(arguments) { + gravity?.let { putInt(KEY_DIALOG_GRAVITY, it) } + dialogShouldWidthMatchParent?.let { putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, it) } + } + + fragment.arguments = arguments + + return fragment + } + + private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY" + private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT" + private const val DEFAULT_VALUE = Int.MAX_VALUE + + private const val KEY_APP_LIST = "KEY_APP_LIST" + internal const val FRAGMENT_TAG = "SHOULD_APP_DOWNLOAD_PROMPT_DIALOG" + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt new file mode 100644 index 0000000000..75abeef01f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt @@ -0,0 +1,219 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.ui + +import android.app.Dialog +import android.content.DialogInterface +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.GradientDrawable +import android.os.Bundle +import android.os.Parcelable +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.LinearLayout +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.content.ContextCompat +import kotlinx.parcelize.Parcelize +import mozilla.components.feature.downloads.R +import mozilla.components.feature.downloads.databinding.MozacDownloadCancelBinding +import mozilla.components.support.utils.ext.getParcelableCompat + +/** + * The dialog warns the user that closing last private tab leads to cancellation of active private + * downloads. + */ +class DownloadCancelDialogFragment : AppCompatDialogFragment() { + + var onAcceptClicked: ((tabId: String?, source: String?) -> Unit)? = null + var onDenyClicked: (() -> Unit)? = null + + private val safeArguments get() = requireNotNull(arguments) + private val downloadCount by lazy { safeArguments.getInt(KEY_DOWNLOAD_COUNT) } + private val tabId by lazy { safeArguments.getString(KEY_TAB_ID) } + private val source by lazy { safeArguments.getString(KEY_SOURCE) } + private val promptStyling by lazy { + safeArguments.getParcelableCompat(KEY_STYLE, PromptStyling::class.java) ?: PromptStyling() + } + private val promptText by lazy { + safeArguments.getParcelableCompat(KEY_TEXT, PromptText::class.java) ?: PromptText() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return Dialog(requireContext()).apply { + requestWindowFeature(Window.FEATURE_NO_TITLE) + setCanceledOnTouchOutside(true) + + setContainerView(promptStyling.shouldWidthMatchParent, createContainer()) + + window?.apply { + setGravity(promptStyling.gravity) + + if (promptStyling.shouldWidthMatchParent) { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + // This must be called after addContentView, or it won't fully fill to the edge. + setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + } + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDenyClicked?.invoke() + } + + @Suppress("NestedBlockDepth") + private fun Dialog.setContainerView(dialogShouldWidthMatchParent: Boolean, rootView: View) { + if (dialogShouldWidthMatchParent) { + setContentView(rootView) + } else { + addContentView( + rootView, + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, + ), + ) + } + } + + @Suppress("InflateParams", "NestedBlockDepth") + private fun createContainer() = LayoutInflater.from(requireContext()).inflate( + R.layout.mozac_download_cancel, + null, + false, + ).apply { + with(MozacDownloadCancelBinding.bind(this)) { + acceptButton.setOnClickListener { + onAcceptClicked?.invoke(tabId, source) + dismiss() + } + + denyButton.setOnClickListener { + onDenyClicked?.invoke() + dismiss() + } + + with(promptText) { + title.text = getString(titleText) + body.text = buildWarningText(downloadCount, bodyText) + acceptButton.text = getString(acceptText) + denyButton.text = getString(denyText) + } + + with(promptStyling) { + positiveButtonBackgroundColor?.let { + val backgroundTintList = ContextCompat.getColorStateList(requireContext(), it) + acceptButton.backgroundTintList = backgroundTintList + + // It appears there is not guaranteed way to get background color of a button, + // there are always nullable types, hence the code changing the positiveButtonRadius + // executes only if positiveButtonBackgroundColor is provided + positiveButtonRadius?.let { + val shape = GradientDrawable() + shape.shape = GradientDrawable.RECTANGLE + shape.setColor( + ContextCompat.getColor( + requireContext(), + positiveButtonBackgroundColor, + ), + ) + shape.cornerRadius = positiveButtonRadius + acceptButton.background = shape + } + } + + positiveButtonTextColor?.let { + val color = ContextCompat.getColor(requireContext(), it) + acceptButton.setTextColor(color) + } + } + } + } + + @VisibleForTesting + internal fun buildWarningText(downloadCount: Int, @StringRes stringId: Int) = String.format( + getString(stringId), + downloadCount, + ) + + companion object { + private const val KEY_DOWNLOAD_COUNT = "KEY_DOWNLOAD_COUNT" + private const val KEY_TAB_ID = "KEY_TAB_ID" + private const val KEY_SOURCE = "KEY_SOURCE" + private const val KEY_STYLE = "KEY_STYLE" + private const val KEY_TEXT = "KEY_TEXT" + + /** + * Returns a new instance of [DownloadCancelDialogFragment]. + * @param downloadCount The number of currently active downloads. + * @param promptStyling Styling properties for the dialog. + * @param onPositiveButtonClicked A lambda called when the allow button is clicked. + * @param onNegativeButtonClicked A lambda called when the deny button is clicked. + */ + fun newInstance( + downloadCount: Int, + tabId: String? = null, + source: String? = null, + promptText: PromptText? = null, + promptStyling: PromptStyling? = null, + onPositiveButtonClicked: ((tabId: String?, source: String?) -> Unit)? = null, + onNegativeButtonClicked: (() -> Unit)? = null, + ): DownloadCancelDialogFragment { + return DownloadCancelDialogFragment().apply { + this.arguments = Bundle().apply { + putInt(KEY_DOWNLOAD_COUNT, downloadCount) + tabId?.let { putString(KEY_TAB_ID, it) } + source?.let { putString(KEY_SOURCE, it) } + promptText?.let { putParcelable(KEY_TEXT, it) } + promptStyling?.let { putParcelable(KEY_STYLE, it) } + } + this.onAcceptClicked = onPositiveButtonClicked + this.onDenyClicked = onNegativeButtonClicked + } + } + } + + /** + * Styling for the downloads cancellation dialog. + * Note that for [positiveButtonRadius] to be applied, + * specifying [positiveButtonBackgroundColor] is necessary. + */ + @Parcelize + data class PromptStyling( + val gravity: Int = Gravity.BOTTOM, + val shouldWidthMatchParent: Boolean = true, + @ColorRes + val positiveButtonBackgroundColor: Int? = null, + @ColorRes + val positiveButtonTextColor: Int? = null, + val positiveButtonRadius: Float? = null, + ) : Parcelable + + /** + * The class gives an option to override string resources used by [DownloadCancelDialogFragment]. + */ + @Parcelize + data class PromptText( + @StringRes + val titleText: Int = R.string.mozac_feature_downloads_cancel_active_downloads_warning_content_title, + @StringRes + val bodyText: Int = R.string.mozac_feature_downloads_cancel_active_private_downloads_warning_content_body, + @StringRes + val acceptText: Int = R.string.mozac_feature_downloads_cancel_active_downloads_accept, + @StringRes + val denyText: Int = R.string.mozac_feature_downloads_cancel_active_private_downloads_deny, + ) : Parcelable +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderApp.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderApp.kt new file mode 100644 index 0000000000..2871429ee4 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderApp.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.downloads.ui + +import android.annotation.SuppressLint +import android.content.pm.ResolveInfo +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Represents an app that can perform downloads. + * + * @property name Name of the app. + * @property resolver The [ResolveInfo] for this app. + * @property packageName Package of the app. + * @property activityName Activity that will be shared to. + * @property url The full url to the content that should be downloaded. + * @property contentType Content type (MIME type) to indicate the media type of the download. + */ +@SuppressLint("ParcelCreator") +@Parcelize +data class DownloaderApp( + val name: String, + val resolver: ResolveInfo, + val packageName: String, + val activityName: String, + val url: String, + val contentType: String?, +) : Parcelable diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt new file mode 100644 index 0000000000..5be32f71b3 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapter.kt @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.ui + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.feature.downloads.R + +/** + * An adapter for displaying the applications that can perform downloads. + */ +class DownloaderAppAdapter( + context: Context, + private val apps: List<DownloaderApp>, + val onAppSelected: ((DownloaderApp) -> Unit), +) : RecyclerView.Adapter<DownloaderAppViewHolder>() { + + private val inflater = LayoutInflater.from(context) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloaderAppViewHolder { + val view = inflater.inflate(R.layout.mozac_download_app_list_item, parent, false) + + val nameLabel = view.findViewById<TextView>(R.id.app_name) + val iconImage = view.findViewById<ImageView>(R.id.app_icon) + + return DownloaderAppViewHolder(view, nameLabel, iconImage) + } + + override fun getItemCount(): Int = apps.size + + override fun onBindViewHolder(holder: DownloaderAppViewHolder, position: Int) { + val app = apps[position] + val context = holder.itemView.context + with(app) { + holder.nameLabel.text = name + holder.iconImage.setImageDrawable(app.resolver.loadIcon(context.packageManager)) + holder.bind(app, onAppSelected) + } + } +} + +/** + * View holder for a [DownloaderApp] item. + */ +class DownloaderAppViewHolder( + itemView: View, + val nameLabel: TextView, + val iconImage: ImageView, +) : RecyclerView.ViewHolder(itemView) { + /** + * Show a certain downloader application in the current View. + */ + fun bind(app: DownloaderApp, onAppSelected: ((DownloaderApp) -> Unit)) { + itemView.app = app + itemView.setOnClickListener { + onAppSelected(it.app) + } + } + + internal var View.app: DownloaderApp + get() = tag as DownloaderApp + set(value) { + tag = value + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download.xml new file mode 100644 index 0000000000..f85368e457 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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" + android:viewportHeight="24"> + <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/> + <path + android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" + android:fillColor="#FFF"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim0.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim0.xml new file mode 100644 index 0000000000..fe77c7f45a --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim0.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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" + android:viewportHeight="24"> + <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/> + <path + android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" + android:fillColor="#3FFF"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim1.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim1.xml new file mode 100644 index 0000000000..b14f73537b --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim1.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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" + android:viewportHeight="24"> + <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/> + <path + android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" + android:fillColor="#3FFF"/> + <path + android:name="animated_fill" + android:pathData="M 0 0 L 24 0 L 24 4 L 0 4 Z" + android:fillColor="#FFF"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim2.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim2.xml new file mode 100644 index 0000000000..77353bc0b1 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim2.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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" + android:viewportHeight="24"> + <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/> + <path + android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" + android:fillColor="#3FFF"/> + <path + android:name="animated_fill" + android:pathData="M 0 0 L 24 0 L 24 6 L 0 6 Z" + android:fillColor="#FFF"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim3.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim3.xml new file mode 100644 index 0000000000..f326e0d24a --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim3.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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" + android:viewportHeight="24"> + <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/> + <path + android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" + android:fillColor="#3FFF"/> + <path + android:name="animated_fill" + android:pathData="M 0 0 L 24 0 L 24 8 L 0 8 Z" + android:fillColor="#FFF"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim4.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim4.xml new file mode 100644 index 0000000000..0f9feaf19f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim4.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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" + android:viewportHeight="24"> + <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/> + <path + android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" + android:fillColor="#3FFF"/> + <path + android:name="animated_fill" + android:pathData="M 0 0 L 24 0 L 24 10 L 0 10 Z" + android:fillColor="#FFF"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim5.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim5.xml new file mode 100644 index 0000000000..8102a93b58 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_anim5.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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" + android:viewportHeight="24"> + <clip-path android:pathData="M13,3a1,1 0,1 0,-2 0v11.6l-4.3,-4.3a1,1 0,0 0,-1.4 1.4l6,6a1,1 0,0 0,1.4 0l6,-6a1,1 0,0 0,-1.4 -1.4L13,14.6V3zM5,21a1,1 0,0 1,1 -1h12a1,1 0,1 1,0 2H6a1,1 0,0 1,-1 -1z"/> + <path + android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" + android:fillColor="#3FFF"/> + <path + android:name="animated_fill" + android:pathData="M 0 0 L 24 0 L 24 20 L 0 20 Z" + android:fillColor="#FFF"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_complete.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_complete.xml new file mode 100644 index 0000000000..83225e9e42 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_complete.xml @@ -0,0 +1,14 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="m6,20h12c0.5523,0 1,0.4477 1,1 0,0.5128 -0.386,0.9355 -0.8834,0.9933l-0.1166,0.0067h-12c-0.5523,0 -1,-0.4477 -1,-1 0,-0.5128 0.386,-0.9355 0.8834,-0.9933l0.1166,-0.0067h12zM18.7041,7.2899c0.3922,0.3889 0.3948,1.022 0.0059,1.4142l-8,8.0675c-0.3923,0.3956 -1.0324,0.3943 -1.4231,-0.003l-4,-4.0675c-0.3872,-0.3938 -0.382,-1.0269 0.0118,-1.4142 0.3938,-0.3872 1.0269,-0.382 1.4142,0.0118l3.29,3.3455 7.287,-7.3484c0.3889,-0.3922 1.022,-0.3948 1.4142,-0.0059z" + android:fillColor="#FFF" + android:fillType="evenOdd"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_failed.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_failed.xml new file mode 100644 index 0000000000..5d52532588 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_download_failed.xml @@ -0,0 +1,8 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<vector android:height="24dp" android:viewportHeight="16" + android:viewportWidth="16" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FFF" android:pathData="M14.742,12.106L9.789,2.2a2,2 0,0 0,-3.578 0l-4.953,9.91A2,2 0,0 0,3.047 15h9.905a2,2 0,0 0,1.79 -2.894zM7,5a1,1 0,0 1,2 0v4a1,1 0,0 1,-2 0zM8,13.25A1.25,1.25 0,1 1,9.25 12,1.25 1.25,0 0,1 8,13.25z"/> +</vector> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_ongoing_download.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_ongoing_download.xml new file mode 100644 index 0000000000..298fcac943 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/drawable/mozac_feature_download_ic_ongoing_download.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> + <item android:drawable="@drawable/mozac_feature_download_ic_download_anim0" android:duration="200" /> + <item android:drawable="@drawable/mozac_feature_download_ic_download_anim1" android:duration="200" /> + <item android:drawable="@drawable/mozac_feature_download_ic_download_anim2" android:duration="200" /> + <item android:drawable="@drawable/mozac_feature_download_ic_download_anim3" android:duration="200" /> + <item android:drawable="@drawable/mozac_feature_download_ic_download_anim4" android:duration="200" /> + <item android:drawable="@drawable/mozac_feature_download_ic_download_anim5" android:duration="200" /> + <item android:drawable="@drawable/mozac_feature_download_ic_download" android:duration="200" /> +</animation-list> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_app_list_item.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_app_list_item.xml new file mode 100644 index 0000000000..5233427097 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_app_list_item.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="76dp" + android:layout_height="80dp" + android:background="?selectableItemBackground" + tools:ignore="Overdraw"> + + <ImageView + android:id="@+id/app_icon" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginTop="8dp" + android:importantForAccessibility="no" + app:layout_constraintBottom_toTopOf="@id/app_name" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:srcCompat="@tools:sample/avatars" /> + + <TextView + android:id="@+id/app_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="5dp" + android:ellipsize="end" + android:gravity="center|top" + android:lines="2" + android:textAlignment="gravity" + android:textSize="12sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/app_icon" + tools:text="Copy to clipboard" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml new file mode 100644 index 0000000000..4f034e5a70 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml @@ -0,0 +1,78 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:windowBackground" + tools:ignore="Overdraw"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_alignParentTop="true" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:importantForAccessibility="no" + android:scaleType="center" + app:srcCompat="@drawable/mozac_ic_information_24" + app:tint="?android:attr/textColorPrimary" /> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/icon" + android:layout_alignBottom="@+id/icon" + android:layout_toEndOf="@id/icon" + android:gravity="center_vertical" + android:paddingStart="4dp" + android:paddingEnd="8dp" + android:textColor="?android:attr/textColorPrimary" + android:textSize="16sp" + tools:text="@string/mozac_feature_downloads_cancel_active_downloads_warning_content_title" + tools:textColor="#000000" /> + + <TextView + android:id="@+id/body" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/title" + android:layout_alignStart="@id/title" + android:layout_marginTop="8dp" + android:paddingStart="4dp" + android:paddingEnd="8dp" + android:textColor="?android:attr/textColorPrimary" + tools:text="@string/mozac_feature_downloads_cancel_active_private_downloads_warning_content_body" /> + + <Button + android:id="@+id/deny_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/body" + android:layout_marginTop="16dp" + android:layout_toStartOf="@id/accept_button" + android:textAlignment="center" + android:textAllCaps="false" + tools:text="@string/mozac_feature_downloads_cancel_active_private_downloads_deny" /> + + <Button + android:id="@+id/accept_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/body" + android:layout_alignParentEnd="true" + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="16dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:textAlignment="center" + android:textAllCaps="false" + tools:text="@string/mozac_feature_downloads_cancel_active_downloads_accept" /> +</RelativeLayout> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloader_chooser_prompt.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloader_chooser_prompt.xml new file mode 100644 index 0000000000..2509ab8a6b --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloader_chooser_prompt.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/relativeLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:windowBackground" + android:orientation="vertical" + tools:ignore="Overdraw"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:importantForAccessibility="no" + android:scaleType="center" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/mozac_feature_download_ic_download" + app:tint="?android:attr/textColorPrimary" /> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:paddingStart="5dp" + android:paddingTop="4dp" + android:paddingEnd="5dp" + android:text="@string/mozac_feature_downloads_third_party_app_chooser_dialog_title" + android:textColor="?android:attr/textColorPrimary" + app:layout_constraintEnd_toStartOf="@+id/close_button" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintTop_toTopOf="parent" + tools:textColor="#000000" /> + + <androidx.appcompat.widget.AppCompatImageButton + android:id="@+id/close_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="3dp" + android:layout_marginEnd="12dp" + android:layout_marginTop="16dp" + android:background="@null" + android:contentDescription="@string/mozac_feature_downloads_button_close" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/mozac_ic_cross_24" + app:tint="?android:attr/textColorPrimary" + tools:textColor="#000000" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/apps_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:paddingBottom="16dp" + android:orientation="horizontal" + android:clipToPadding="false" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/icon" + tools:listitem="@layout/mozac_download_app_list_item" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloads_prompt.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloads_prompt.xml new file mode 100644 index 0000000000..485533ce1b --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/layout/mozac_downloads_prompt.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:background="?android:windowBackground" + android:orientation="vertical" + tools:ignore="Overdraw"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_alignParentTop="true" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:importantForAccessibility="no" + android:scaleType="center" + app:srcCompat="@drawable/mozac_feature_download_ic_download" + app:tint="?android:attr/textColorPrimary" /> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBaseline="@id/icon" + android:layout_alignParentTop="true" + android:layout_marginStart="3dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="11dp" + android:layout_toStartOf="@id/close_button" + android:layout_toEndOf="@id/icon" + android:paddingStart="5dp" + android:paddingTop="4dp" + android:paddingEnd="5dp" + android:textColor="?android:attr/textColorPrimary" + tools:text="Download (85.7 MB)" + tools:textColor="#000000" /> + + <androidx.appcompat.widget.AppCompatImageButton + android:id="@+id/close_button" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignBaseline="@id/icon" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" + android:layout_marginStart="3dp" + android:scaleType="centerInside" + android:background="@null" + android:contentDescription="@string/mozac_feature_downloads_button_close" + app:srcCompat="@drawable/mozac_ic_cross_24" + app:tint="?android:attr/textColorPrimary" + tools:textColor="#000000" /> + + <TextView + android:id="@+id/filename" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:maxHeight="160dp" + android:layout_below="@id/title" + android:layout_alignBaseline="@id/icon" + android:layout_marginStart="3dp" + android:layout_marginTop="16dp" + android:layout_toEndOf="@id/icon" + android:paddingStart="5dp" + android:paddingTop="4dp" + android:paddingEnd="5dp" + android:scrollbars="vertical" + android:textColor="?android:attr/textColorPrimary" + tools:text="@tools:sample/lorem/random" + tools:textColor="#000000" /> + + <Button + android:id="@+id/download_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/filename" + android:layout_alignParentEnd="true" + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="16dp" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:text="@string/mozac_feature_downloads_dialog_download" + android:textAlignment="center" + android:textAllCaps="false" /> +</RelativeLayout> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-am/strings.xml new file mode 100644 index 0000000000..3fb72a4502 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-am/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">የወረዱ</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ማውረድ ባለበት ቆሟል</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ማውረድ ተጠናቅቋል</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ማውረድ አልተሳካም</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">(%1$s)ን አውርድ </string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">አውርድ</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ተወው</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ይህን የፋይል አይነት ማውረድ አይችልም</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ፋይል መክፈት አልተቻለም</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ፋይሎችን የሚከፍት ምንም መተግበሪያ አልተገኘም</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">ባለበት አቁም</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ካቆመበት ቀጥል</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ተወው</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ክፈት</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">እንደገና ሞክር</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">ዝጋ</string> + + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">በመጠቀም አከናውን</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$sን መክፈት አልተቻለም</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ፋይሎችን ለማውረድ የፋይሎች እና የሚዲያ መዳረሻ ፍቃድ ያስፈልጋል። ወደ አንድሮይድ ቅንብሮች ይሂዱ፣ ፈቃዶችን ይንኩ እና ፍቀድን ይንኩ።</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">የግል ውርዶች ይሰረዙ?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ሁሉንም የግል ትሮችን አሁን ከዘጉ፣ %1$sን ማውረድ ይሰረዛል። እርግጠኛ ነዎት ከግል አሰሳ መውጣት ይፈልጋሉ?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ማውረዶችን አቋርጥ </string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">በግል አሰሳ ውስጥ ይቆዩ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-an/strings.xml new file mode 100644 index 0000000000..2d6bfdc42a --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-an/strings.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descargas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">S’ha pausau la descarga</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Ha rematau la descarga</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Ha fallau la descarga</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Descargar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar esta mena de fichero</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No s’ha puesto ubrir lo fichero</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No s’ha trobau garra aplicación pa ubrir los fichers %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausar</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Continar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ubrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Tornar-lo a prebar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zarrar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar l\'acción con</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se puede ubrir %1$s</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000..76de9b97a6 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ar/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">التنزيلات</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">أُلبث التنزيل</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">اكتمل التنزيل</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">فشل التنزيل</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">نزّل (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">نزّل</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ألغِ</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">لا يقدر %1$s على تنزيل هذا النوع من الملفات</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">لا يمكن فتح الملف</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">لم يوجد أي تطبيق يفتح ملفات %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">ألبِث</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">استأنف</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ألغِ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">افتح</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">أعِد المحاولة</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">أغلِق</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">أكمل الإجراء باستخدام</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">تعذر فتح %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">تصريح الوصول إلى الملفات والوسائط مطلوب لتنزيل الملفات. انتقل إلى إعدادات أندرويد، وانقر ”الأذونات“ ثم انقر ”سماح“.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">أتريد إلغاء التنزيلات الخاصة؟</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">إن أغلقت كل الألسنة الخاصة الآن، فسيُلغى تنزيل %1$s. هل أنت متأكد أنك تريد مغادرة التصفح الخاص؟</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ألغِ التنزيلات</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">ابقَ في التصفح الخاص</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ast/strings.xml new file mode 100644 index 0000000000..5f9d21334a --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ast/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descargues</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descarga en posa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Completóse la descarga</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">La descarga falló</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Baxar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Baxar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Encaboxar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s nun pue baxar esti tipu de ficheru</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Nun se pudo abrir el ficheru</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nun s\'atopó nenguna aplicación p\'abrir ficheros «%1$s»</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Posar</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Siguir</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Encaboxar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Retentar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zarrar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar l\'aición con:</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nun ye posible abrir «%1$s»</string> + + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tienes de permitir l\'accesu a los ficheros ya al conteníu multimedia pa baxar ficheros. Vete a la configuración d\'Android, dempués a los permisos ya toca Permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Quies encaboxar les descargues privaes?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si zarres agora toles llingüetes privaes, va encaboxase la descarga de «%1$s». ¿De xuru que quies colar del mou de restolar en privao?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Encaboxales</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Quedar</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000..fd2a8b0a86 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-az/strings.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Endirmələr</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Endirməyə fasilə verildi</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Endirmə bitdi</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Endirmə uğursuz oldu</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Endir (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Endir</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Ləğv et</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s bu fayl növünü endirə bilmir</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Faylı açmaq mümkün olmadı</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s fayllarını açmaq üçün tətbiq tapılmadı</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Fasilə ver</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Davam etdir</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Ləğv et</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Aç</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Təkrar Yoxla</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Qapat</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Əməliyyatı bununla tamamla</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ilə açıla bilmir</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-azb/strings.xml new file mode 100644 index 0000000000..8f630f592b --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-azb/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">یئندیریلنلر</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">یئندیرمه دایاندیریلدی</string> + + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">یئندیرمه کامیل اولدو</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">یئندیرمه باشاریسیز</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">یئندیر (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">یئندیر</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">لغو</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">فایل آچیلانمادی.</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s فایلی آچماق اوچون هئچ بیر اپ تاپیلمادی.</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">دایاندیر</string> + + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">سوردور</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">لغو</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">آچ</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">یئنیدن چالیش</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">باغلا</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ایشه آلاراق حرکتی تماملایین</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s آچیلمیر</string> + + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">فایللاری یئندیرمک اوچون فایللار و مدیا ایجازهسی ایستهنیلیر. اندروید تنظیملرینه گئچین، ایجازهلره توخونون و ایجازه وئرین.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">گیزلی یئندیرمهلر لغو ائدیلسین؟</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ایندی بوتون گیزلی تاغلاری باغلاساز %1$s یئندیرمهسی لغو ائدیلهجک. گیزلی موروچودان آیریلماق ایستهدیگیزه اطمینانیز وار؟</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">یئندیرمهلری لغو ائله</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">گیزلی موروچودا قالین</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-be/strings.xml new file mode 100644 index 0000000000..a3dda8db06 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-be/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Сцягванні</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Сцягванне прыпынена</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Сцягванне скончана</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Няўдача сцягвання</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Сцягванне (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Сцягнуць</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Адмяніць</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s не можа сцягнуць гэты тып файла</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Немагчыма адкрыць файл</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Не знойдзена праграма, каб адкрыць файлы %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Прыпыніць</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Працягнуць</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Адмяніць</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Адкрыць</string> + + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Паспрабаваць зноў</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Закрыць</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Завяршыць дзеянне з дапамогаю</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Не ўдалося адкрыць %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Для сцягвання файлаў патрэбен доступ да файлаў і мультымедыя. Перайдзіце ў налады Android, націсніце «Дазволы», затым «Дазволіць».</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Скасаваць прыватныя сцягванні?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Калі вы зараз закрыеце ўсе прыватныя карткі, сцягванне %1$s будзе скасавана. Вы сапраўды жадаеце выйсці з прыватнага аглядання?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Скасаваць сцягванні</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Застацца ў прыватным агляданні</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000..d49e0557ac --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bg/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Изтегляния</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Изтеглянето е поставено на пауза</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Изтеглянето е завършено</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Изтеглянето е неуспешно</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Изтегляне (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Изтегляне</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Отказ</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s не може да изтегли този вид файл</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Отварянето на файла е невъзможно</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Не е открито приложение, което да отваря файлове от вида %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Пауза</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Продължаване</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Отказ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Отваряне</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Повторен опит</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Затваряне</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Завършване на действието използвайки</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Не успя да се отвори %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">За сваляне на файлове е необходимо за достъп до медия и файлове. За да разрешите, влезте в настройки на Android, след което докоснете разрешения и позволяване.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Отмяна на поверителните изтегляния?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ако затворите всички поверителни раздели, %1$s изтегляния ще бъдат прекъснати. Сигурни ли сте, че искате да напуснете поверителното разглеждане?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Прекъсване</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Оставане в поверително разглеждане</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000000..9a6fc87ea9 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bn/strings.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ডাউনলোডগুলো</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ডাউনলোডে বিরতি দেওয়া হয়েছে</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ডাউনলোড সম্পন্ন হয়েছে</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ডাউনলোড ব্যর্থ হয়েছে</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ডাউনলোড (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ডাউনলোড</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">বাতিল</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s এই ধরণের ফাইল ডাউনলোড করতে পারবেন না</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ফাইল খোলা যায়নি</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ফাইলটি খুলতে কোনো অ্যাপ পাওয়া যায়নি</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">বিরতি দিন</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">পুনরায় শুরু করুন</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">বাতিল</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">খুলুন</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">পুনরায় চেষ্টা করুন</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">বন্ধ</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s খুলতে ব্যর্থ</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-br/strings.xml new file mode 100644 index 0000000000..4521b68b21 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-br/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Pellgargadurioù</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Pellgargañ ehanet</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Pellgargadur echu</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Cʼhwitadenn war ar pellgargadur</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Pellgargañ (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Pellgargañ</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Nullañ</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ne cʼhall ket pellgargañ ar rizh restr-mañ</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Ne cʼhaller ket digeriñ ar restr</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Arload ebet evit digeriñ restroù %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Ehanañ</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Kendercʼhel</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Nullañ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Digeriñ</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Klask en-dro</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Serriñ</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Kenderc’hel gant</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">N’haller ket digeriñ %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Aotreoù haeziñ ar restroù ha mediaoù a zo ret kaout a-benn pellgargañ restroù. Mont da arventennoù Android, pouezit war an aotreoù ha pouezit war aotren.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Nullañ ar pellgargadennoù prevez?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ma serrit ho holl ivinelloù prevez bremañ e vo paouezet gant pellgargadenn %1$s.Sur hoc’h e gell deoc’h kuitaat ar merdeiñ prevez?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Nullañ ar bellgargañ</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Chom er Merdeiñ Prevez</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000..28ef89bf9a --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-bs/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Preuzimanja</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Preuzimanje pauzirano</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Preuzimanje završeno</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Neuspjelo preuzimanje</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Preuzimanje (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Preuzmi</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Otkaži</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ne može preuzeti ovaj tip fajla</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Ne mogu otvoriti fajl.</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nije pronađena aplikacija za otvaranje %1$s fajlova</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pauziraj</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Nastavi</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Otkaži</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Otvori</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Pokušaj ponovo</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zatvori</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Završi radnju pomoću</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ne mogu otvoriti %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Pristup datotekama i medijima je potreban za preuzimanje datoteka. Idite na postavke Androida, dodirnite dozvole i dodirnite dozvoli.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Otkazati privatna preuzimanja?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ako sada zatvorite sve privatne tabove, %1$s preuzimanja će biti otkazano. Jeste li sigurni da želite napustiti privatno pretraživanje?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Otkaži preuzimanja</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ostani u privatnom pretraživanju</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000..fdfe1510ed --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ca/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Baixades</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Baixada en pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Ha acabat la baixada</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Ha fallat la baixada</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Baixada (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Baixa</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancel·la</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">El %1$s no pot baixar aquest tipus de fitxer</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No s’ha pogut obrir el fitxer</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No s’ha trobat cap aplicació per a obrir els fitxers %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Reprèn</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancel·la</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Obre</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Torna-ho a provar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Tanca</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completa l’acció mitjançant</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">No s’ha pogut obrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Cal permís d’accés als fitxers i contingut multimèdia per baixar fitxers. Aneu als paràmetres de l’Android, aneu als permisos i trieu «Permet».</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Voleu cancel·lar les baixades privades?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si tanqueu totes les pestanyes privades ara, la baixada %1$s es cancel·larà. Esteu segur que voleu deixar la navegació privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancel·la les baixades</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Continua la navegació privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cak/strings.xml new file mode 100644 index 0000000000..a2fb97d855 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cak/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Taq qasanïk</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Xq\'at qasanïk</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Xtz\'aqät qasanïk</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Xsach qasanïk</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Qasanïk (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Tiqasäx</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Tiq\'at</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Man nitikïr ta nuqasaj re ruwäch yakb\'äl re\' ri %1$s</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Man nijaqatäj ta ri yakb\'äl</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Majun chokoy xilitäj richin nijaq %1$s taq yakb\'äl</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Tuxlan</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Titikïr chik el</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Tiq\'at</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Tijaq</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Titojtob\'ëx chik</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Titz\'apïx</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Titz\'aqatisäïx rub\'anik rik\'in</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Man tikirel ta nijaq %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">K\'o chi niya\' q\'ij richin ye\'ok chi kipam ri taq yakb\'äl chuqa\' taq k\'ïy k\'oxom richin yeqasäx taq yakb\'äl. Tib\'an b\'enam pa runuk\'ulem Android, tapitz\'a\' ya\'oj q\'ij chuqa\' tipitz\' ri tiya\' q\'ij.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿La ye\'aq\'ät ri ichinan taq qasanïk?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">¿We xke\'atz\'apij wakami ronojel ri ichinan taq ruwi\', xkeq\'at %1$s qasanïk. ¿La at jikïl chi nawajo\' yatel pa ri ichinan okem pa k\'amaya\'l?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Keq\'at qasanïk</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Tik\'oje\' pa ichinan okem pa k\'amaya\'l</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ceb/strings.xml new file mode 100644 index 0000000000..20b253dcaa --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ceb/strings.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Mga Download</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Download na-pause</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Download human na</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Download na-pakyas</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Download</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancel</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s dili kadownload ani nga file type</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Dili maka-abli sa file</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Wala\'y app nakita nga maka-abli sa %1$s mga file</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Resume</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancel</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Open</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Try Again</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Close</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Kumpletoha ang action gamit ang</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Dili ka-abli sa %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Permission access sa files ug media kinahanglan para madownload ang mga file. Adto sa Android settings, tap ang permissions, ug tap allow.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">I-cancel ang mga private downloads?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">I-cancel ang mga downloads</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Magpabilin sa private browsing</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ckb/strings.xml new file mode 100644 index 0000000000..9b8597cfb3 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ckb/strings.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">داگرتنەکان</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">داگرتن وەستێنرا</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">داوگرتن تەواو بوو</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">داگرتن سەرکەوتوو نەبوو</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">داگرتن (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">داگرتن</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">پاشگەزبوونەوە</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ناتوانێت ئەم جۆری پەڕگەیە دابگرێت</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ناتوانرێت پەڕگە بکرێتەوە</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">هیچ بەرنامەیەک نەدۆزرایەوە بتوانێت پەڕگەی %1$s بکاتەوە</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">وچان</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">بەردەوامبوونەوە</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">پاشگەزبوونەوە</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">کردنەوە</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">دووبارە هەوڵ بدەرەوە</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">داخستن</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">کردار تەواوبکە بەبەکارهێنانی</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">نەتوانرا %1$s بکرێتەوە</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-co/strings.xml new file mode 100644 index 0000000000..1114f1fe53 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-co/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Scaricamenti</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Scaricamentu messu in pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Scaricamentu compiu</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Fiascu di u scaricamentu</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Scaricamentu (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Scaricà</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Abbandunà</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ùn pò micca scaricà stu tipu di schedariu</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Impussibule d’apre u schedariu</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Alcuna appiecazione trova per apre i schedarii %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Cuntinuà</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Abbandunà</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Apre</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Pruvà torna</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Chjode</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Cumplettà l’azzione cù</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impussibule d’apre %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">I diritti d’accessu à i schedarii è i medià sò richiesti per scaricà schedarii. Accidite à e preferenze d’Android, picchichjate Permessi, è dopu Permette.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Abbandunà i scaricamenti in a navigazione privata ?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">A chjusura di tutte l’unghjette private subitu pianterà u scaricamentu di %1$s. Vulete veramente piantà a navigazione privata ?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Abbandunà i scaricamenti</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stà in navigazione privata</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000..826fa7ab25 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cs/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Stahování</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Stahování pozastaveno</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Stahování dokončeno</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Stahování selhalo</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Stáhnout soubor %1$s</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Stáhnout</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Zrušit</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s nemůže stáhnout tento typ souboru</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Soubor se nepodařilo otevřít</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Pro otevření souborů typu %1$s nebyla nalezena žádná aplikace</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pozastavit</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Pokračovat</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Zrušit</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Otevřít</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Zkusit znovu</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zavřít</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dokončit akci pomocí</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Aplikaci %1$s se nepodařilo otevřít</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Pro stahování souborů je potřeba oprávnění pro přístup k souborům a médiím. Otevřete nastavení systému Android, klepněte na nastavení oprávnění a vyberte Povolit.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Zrušit stahování z anonymního prohlížení?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Pokud zavřete všechny anonymní panely, bude také zrušeno stahování souboru %1$s. Opravdu chcete ukončit anonymní prohlížení?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Zrušit stahování</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Zůstat v anonymním prohlížení</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cy/strings.xml new file mode 100644 index 0000000000..ba05a7e824 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-cy/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Llwythi</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Oedi’r llwythi</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Llwythi cyflawn</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Methodd y llwytho</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Llwytho (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Llwytho i Lawr</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Diddymu</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Nid yw %1$s yn gallu llwytho’r math hwn o ffeil</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Methu agor y ffeil</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Heb ganfod unrhyw ap sy’n agor ffeiliau %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Oedi</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Ailgychwyn</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Diddymu</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Agor</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Ceisiwch Eto</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Cau</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Cwblhau’r weithred gyda</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Methu agor %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Mae angen caniatâd mynedid i ffeiliau a chyfryngau i lwytho ffeiliau i lawr. Ewch i osodiadau Android, tapiwch caniatâd, a thapio caniatáu.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Diddymu llwythi preifat?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Os ydych yn cau pob ffenestr Pori Preifat nawr, bydd %1$s llwyth yn cael ei ddiddymu. Ydych chi’n siŵr eich bod am adael y modd Pori Preifat?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Diddymu pob llwyth</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Aros yn y modd pori preifat</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-da/strings.xml new file mode 100644 index 0000000000..bad7aae935 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-da/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Filhentninger</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Hentning sat på pause</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Hentning fuldført</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Hentning mislykkedes</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Hent (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Hent</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Annuller</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s kan ikke hente denne filtype</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Kunne ikke åbne filen</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ingen app fundet til at åbne %1$s-filer</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Genoptag</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Annuller</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Åbn</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Prøv igen</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Luk</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Udfør handlingen med</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kunne ikke åbne %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tilladelse til at få adgang til filer og medier er nødvendig for at hente filer. Gå til Indstillinger i Android, tryk på Tilladelser, og tryk så på Tillad.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Annuller private filhentninger?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Hvis du lukker alle private faneblade nu, vil hentning af %1$s blive annulleret. Er du sikker på, at du vil forlade privat browsing-tilstand?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Annuller filhentninger</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Forbliv i privat browsing-tilstand</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000..4f1478259e --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-de/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Download pausiert</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Download abgeschlossen</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Download fehlgeschlagen</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Herunterladen (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Herunterladen</string> + + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Abbrechen</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s kann diesen Dateityp nicht herunterladen</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Datei konnte nicht geöffnet werden</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Keine App zum Öffnen von %1$s-Dateien gefunden</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausieren</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Fortsetzen</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Abbrechen</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Öffnen</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Erneut versuchen</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Schließen</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Aktion abschließen mittels</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s kann nicht geöffnet werden</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Berechtigung für Datei- und Medienzugriff erforderlich, um Dateien herunterzuladen. Öffnen Sie die Android-Einstellungen, tippen Sie auf Berechtigungen und tippen Sie auf Erlauben.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Private Downloads abbrechen?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Wenn Sie jetzt alle privaten Tabs schließen, wird der Download von %1$s abgebrochen. Soll der Private Modus wirklich verlassen werden?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Downloads abbrechen</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Im Privaten Modus bleiben</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-dsb/strings.xml new file mode 100644 index 0000000000..6efee27722 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-dsb/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Ześěgnjenja</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Ześěgnjenje jo se zastajiło</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Ześěgnjenje jo se dokóńcyło</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Ześěgnjenje njejo se raźiło</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Ześěgnuś (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Ześěgnuś</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Pśetergnuś</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s njamóžo toś ten datajowy typ ześěgnuś</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Dataja njedajo se wócyniś</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Žedno nałoženje za wócynjanje datajow typa %1$s namakane</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Zastajiś</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Pókšacowaś</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Pśetergnuś</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Wócyniś</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Hyšći raz wopytaś</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zacyniś</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Akciju skóńcyś z pomocu</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s njedajo se wócyniś</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Pśistup k datajam a medijam jo trjebny, aby dataje ześěgnuł. Pśejźćo k nastajenjam Android, pótusniśo pšawa a pón Dowóliś.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Priwatne ześěgnjenja pśetergnuś?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jolic zacynijośo něnto wšykne priwatne rejtariki, se %1$s ześěgnjenje pśetergnjo. Cośo priwatny modus napšawdu spušćiś?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Ześěgnjenja pśetergnuś</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">W priwatnem modusu wóstaś</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-el/strings.xml new file mode 100644 index 0000000000..8b27d69322 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-el/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Λήψεις</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Λήψη σε παύση</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Η λήψη ολοκληρώθηκε</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Αποτυχία λήψης</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Λήψη (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Λήψη</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Ακύρωση</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Το %1$s δεν μπορεί να κάνει λήψη αυτού του τύπου αρχείου</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Αδυναμία ανοίγματος αρχείου</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Δεν βρέθηκε εφαρμογή για άνοιγμα αρχείων %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Παύση</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Συνέχιση</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Ακύρωση</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Άνοιγμα</string> + + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Δοκιμή ξανά</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Κλείσιμο</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Ολοκλήρωση ενέργειας με</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Αδυναμία ανοίγματος του %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Απαιτείται άδεια για πρόσβαση σε αρχεία και πολυμέσα για τη λήψη αρχείων. Μεταβείτε στις Ρυθμίσεις Android, πατήστε «Δικαιώματα» και επιλέξτε «Να επιτρέπεται».</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Ακύρωση ιδιωτικών λήψεων;</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Αν κλείσετε τώρα όλες τις ιδιωτικές καρτέλες, θα ακυρωθεί %1$s λήψη. Θέλετε σίγουρα να αποχωρήσετε από την ιδιωτική περιήγηση;</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Ακύρωση λήψεων</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Παραμονή σε ιδιωτική περιήγηση</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rCA/strings.xml new file mode 100644 index 0000000000..fb702f10cf --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rCA/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Download paused</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Download completed</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Download failed</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Download</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancel</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s can’t download this file type</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Could not open file</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No app found to open %1$s files</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Resume</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancel</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Open</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Try Again</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Close</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Complete action using</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Unable to open %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Files and media permission access needed to download files. Go to Android settings, tap permissions, and tap allow.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancel private downloads?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">If you close all Private tabs now, %1$s download will be cancelled. Are you sure you want to leave Private Browsing?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancel downloads</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stay in private browsing</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..fb702f10cf --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Download paused</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Download completed</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Download failed</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Download</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancel</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s can’t download this file type</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Could not open file</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No app found to open %1$s files</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Resume</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancel</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Open</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Try Again</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Close</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Complete action using</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Unable to open %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Files and media permission access needed to download files. Go to Android settings, tap permissions, and tap allow.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancel private downloads?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">If you close all Private tabs now, %1$s download will be cancelled. Are you sure you want to leave Private Browsing?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancel downloads</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stay in private browsing</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000000..dc04266501 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-eo/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Elŝutoj</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Elŝuto paŭzinta</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Elŝuto kompleta</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Elŝuto malsukcesa</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Elŝuto (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Elŝuti</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Nuligi</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ne povas elŝuti tiun ĉi tipon de dosiero</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Ne eblis malfermi la dosieron</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Neniu programo trovita por malfermi dosierojn %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Paŭzigi</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Daŭrigi</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Nuligi</string> + + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Malfermi</string> + + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Klopodi denove</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Fermi</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Kompletigi agon per</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ne eblis malfermi %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">La permeso aliri la konservejon de dosieroj kaj aŭdvidaĵojn estas bezonata por elŝuti dosierojn. Iru al la agordoj de Android, tuŝetu Permesoj kaj poste Permesi.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Ĉu nuligi privatajn elŝutojn?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se vi fermas ĉiujn viajn langetojn de privata retumo nun, %1$s elŝutoj estos nuligitaj. Ĉu vi certe volas forlasi la privatan retumon?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Nuligi elŝutojn</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Resti en privata retumo</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rAR/strings.xml new file mode 100644 index 0000000000..a35f9710bd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rAR/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descargas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descarga pausada</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Descarga completa</string> + + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Falló la descarga</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Descarga (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Descargar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No se pudo abrir el archivo</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No se encontró ninguna aplicación para abrir archivos %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausar</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Reanudar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Volver a intentar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Cerrar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se puede abrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ir a la configuración de Android, tocar permisos y tocar permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cerrás todas las pestañas privadas ahora, se cancelará(n) %1$s descarga(s). ¿Estás seguro de querer dejar la navegación privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descargas</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Seguir en la navegación privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rCL/strings.xml new file mode 100644 index 0000000000..9103fb0e84 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rCL/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descargas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descarga pausada</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Descarga completada</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Descarga fallida</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Bajar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Bajar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No se pudo abrir el archivo</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No se encontró ninguna aplicación para abrir archivos %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausar</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Continuar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Volver a intentarlo</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Cerrar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se pudo abrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ve a la configuración de Android, toca permisos y toca permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cierra todas las pestañas privadas ahora, %1$s descargas serán canceladas. ¿De verdad quieres dejar la navegación privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descarga</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Permanecer en navegación privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rES/strings.xml new file mode 100644 index 0000000000..759558d337 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descargas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descarga en pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Descarga completa</string> + + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Descarga fallida</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Descargar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No se ha podido abrir el archivo</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No se ha encontrado ninguna aplicación para abrir archivos %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Continuar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Reintentar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Cerrar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se puede abrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ve a los ajustes de Android, toca permisos y toca permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cierra todas las pestañas privadas ahora, la descarga de %1$s se cancelará. ¿Estás seguro de que quieres abandonar la navegación privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descargas</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Seguir en navegación privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rMX/strings.xml new file mode 100644 index 0000000000..6b02c90772 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es-rMX/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descargas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descarga pausada</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Descarga completa</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Descarga fallada</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Descargar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No se pudo abrir el archivo</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No se encontró ninguna aplicación para abrir archivos %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausar</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Reanudar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Reintentar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Cerrar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se pudo abrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ve a la configuración de Android, toca permisos y toca permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cierras todas las pestañas privadas ahora, se cancelará la descarga de %1$s. ¿Estás seguro de que deseas salir de la navegación privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descargas</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Permanecer en la navegación privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..759558d337 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-es/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descargas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descarga en pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Descarga completa</string> + + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Descarga fallida</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Descargar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s no puede descargar este tipo de archivo</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No se ha podido abrir el archivo</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No se ha encontrado ninguna aplicación para abrir archivos %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Continuar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Reintentar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Cerrar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">No se puede abrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Se necesita permiso de acceso a archivos y medios para descargar archivos. Ve a los ajustes de Android, toca permisos y toca permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Cancelar descargas privadas?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si cierra todas las pestañas privadas ahora, la descarga de %1$s se cancelará. ¿Estás seguro de que quieres abandonar la navegación privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar descargas</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Seguir en navegación privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..be4a1a152c --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-et/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Allalaadimised</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Allalaadimine on pausil</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Allalaadimine lõpetati</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Allalaadimine ebaõnnestus</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Kas soovid faili %1$s alla laadida?</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Laadi alla</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Loobu</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$sil pole võimalik seda tüüpi faile alla laadida</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Faili avamine ebaõnnestus.</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s failide avamiseks puudub äpp</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Paus</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Jätka</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Katkesta</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ava</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Proovi uuesti</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Sulge</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Lõpeta tegevus kasutades äppi</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Pole võimalik avada äppi %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Failide allalaadimiseks on vajalik failide ja meedia ligipääsu õigus. Mine Androidi sätetesse, vali õigused ning puuduta lubamise valikut.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Kas katkestada privaatsed allalaadimised?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Kui sa sulged praegu kõik privaatsed kaardid, siis faili %1$s allalaadimine katkestatakse. Kas oled kindel, et soovid privaatsest veebilehitsemisest väljuda?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Katkesta allalaadimised</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Jää privaatse veebilehitsemise režiimi</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..362cef474e --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-eu/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Deskargak</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Deskarga pausatuta</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Deskarga burututa</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Deskargak huts egin du</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Deskargatu (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Deskargatu</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Utzi</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s(e)k ezin du fitxategi mota hau deskargatu</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Ezin da fitxategia ireki</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ez da aplikaziorik aurkitu %1$s motako fitxategiak irekitzeko</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausatu</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Berrekin</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Utzi</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ireki</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Saiatu berriro</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Itxi</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Burutu ekintza honekin</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ezin da %1$s ireki</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Fitxategiak deskargatzeko, fitxategi eta multimediaren sarbide-baimenak behar dira. Zoaz Android-en ezarpenetara, sakatu baimenak eta sakatu baimendu.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Utzi deskarga pribatuak?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Fitxa pribatu guztiak orain itxiz gero, %1$s fitxategiaren deskarga bertan behera utziko da. Ziur zaude nabigatze pribatua utzi nahi duzula?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Utzi deskargak</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Mantendu nabigatze pribatuan</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000000..6d11b9cb08 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fa/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">بارگیریها</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">بارگیری مکث شد</string> + + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">بارگیری کامل شد</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">بارگیری شکست خورد</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">بارگیری (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">بارگیری</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">لغو</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s نمیتواند این نوع پرونده را بارگیری کند</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">پرونده باز نشد</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">هیچ کارهای برای گشودن پروندههای %1$s یافت نشد</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">مکث</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ازسرگیری</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">لغو</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">باز کردن</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">تلاش مجدد</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">بستن</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">تکمیل عملکرد با استفاده از</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">نتوانست %1$s را باز کند</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">برای بارگیری پروندهها، اجازهٔ دسترسی به پروندهها و رسانه لازم است. به تنطیمات اندروید بروید، روی اجازهها بزنید و سپس اجازه دادن را بزنید.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">بارگیریهای خصوصی لغو شوند؟</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">اگر اکنون همهٔ زبانههای خصوصی را ببندید، بارگیری %1$s لغو میشود. آیا مطمئنید که میخواهید از مرور خصوصی خارج شوید؟</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">لغو بارگیریها</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">باقی ماندن در مرور خصوصی</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ff/strings.xml new file mode 100644 index 0000000000..3da43915d6 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ff/strings.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Gaawte</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Aawtogol dartinaama</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Aawtagol gasii</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Aawtagol woorii</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Aawtogol (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Aawto</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Haaytu</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s waawaa aawtaade sifaa ndee fiilde</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Horiima udditde fiilde</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Alaa jaaɓngal yiytaa ngam udditde piille %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Sabbo</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Fuɗɗito</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Haaytu</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Uddit</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Eto goɗngol</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Uddu</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Horiima uddide %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Yamiroore keɓgol piille e mejaaje ena coklaa ngam aawtaade piille. Yah to teelte Android, tappu e jamirooje, tappaa yamir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Haaytin gaawte cuuriiɗe?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Haaytin gaawte</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Heddo e banngogol suturo</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000..216806c75a --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fi/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Lataukset</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Lataus keskeytetty</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Lataus valmis</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Lataus epäonnistui</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Lataa (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Lataa</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Peruuta</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ei voi ladata tätä tiedostotyyppiä</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Tiedostoa ei voitu avata</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">"%1$s"-tiedostojen avaamiseen ei löytynyt sovellusta</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Keskeytä</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Jatka</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Peruuta</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Avaa</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Yritä uudelleen</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Sulje</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Suorita toiminto sovelluksella</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ei voitu avata %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tiedostojen lataamiseksi tarvitaan tiedostojen ja median käyttöoikeudet. Siirry Androidin asetuksiin, napauta käyttöoikeudet ja napauta salli.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Perutetaanko yksityiset lataukset?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jos suljet kaikki yksityiset välilehdet nyt, tiedoston %1$s lataus perutaan. Haluatko varmasti poistua yksityisestä selaamisesta?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Peruuta lataukset</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Pysy yksityisessä selaamisessa</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..886d0d1e85 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fr/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Téléchargements</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Téléchargement mis en pause</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Téléchargement terminé</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Le téléchargement a échoué</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Télécharger (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Télécharger</string> + + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Annuler</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ne peut pas télécharger ce type de fichier</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Impossible d’ouvrir le fichier</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Aucune application trouvée pour ouvrir les fichiers %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Reprendre</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Annuler</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ouvrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Réessayer</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Fermer</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Continuer avec</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impossible d’ouvrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Les autorisations d’accès au stockage de fichiers et de médias sont nécessaires pour télécharger des fichiers. Rendez-vous dans les paramètres d’Android, sélectionnez Autorisations puis Autoriser.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Annuler les téléchargements privés ?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si vous fermez tous les onglets de navigation privée maintenant, le téléchargement de %1$s sera annulé. Voulez-vous vraiment quitter la navigation privée ?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Annuler les téléchargements</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Rester en mode de navigation privée</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fur/strings.xml new file mode 100644 index 0000000000..609b7ca086 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fur/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Discjamâts</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Discjariament in pause</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Discjariament completât</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Discjariament falît</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Discjame (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Discjame</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Anule</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s nol pues discjamâ chest gjenar di file</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Impussibil vierzi il file</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nissune aplicazion cjatade par vierzi i files %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Ripie</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Anule</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Vierç</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Torne prove</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Siere</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Complete azion doprant</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impussibil vierzi %1$s</string> + + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Par discjamâ i files al è necessari il permès di acedi a files e contignûts multimediâi. Va su lis impostazions di Android, tocje Autorizazions e dopo permet.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Anulâ i discjariaments in modalitât privade?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se tu sieris dutis lis schedis privadis cumò, al vignarà anulât il discjariament di %1$s. Lassâ pardabon la navigazion privade?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Anule i discjariaments</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Reste te navigazion privade</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fy-rNL/strings.xml new file mode 100644 index 0000000000..c4a8844052 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-fy-rNL/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Downloaden pauzearre</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Downloaden foltôge</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Downloaden mislearre</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Downloade (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Downloade</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Annulearje</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s kin dit bestânstype net downloade</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Kin bestân net iepenje</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Gjin app fûn om %1$s-bestannen mei te iepenjen</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pauzearje</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Ferfetsje</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Annulearje</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Iepenje</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Opnij probearje</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Slute</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Hanneling foltôgje mei</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kin %1$s net iepenje</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tagong ta bestannen en media fereaske om bestannen te downloaden. Gean nei Android-ynstellingen, tik op machtigingen en tik op tastean.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Priveedownloads annulearje?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">As jo no alle priveeljepblêden slute, sil %1$s download annulearre wurde. Binne jo wis dat jo Priveenavigaasje ferlitte wolle?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Downloads annulearje</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Yn priveenavigaasje bliuwe</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ga-rIE/strings.xml new file mode 100644 index 0000000000..d39b15c1a8 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ga-rIE/strings.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Íoslódálacha</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Cuireadh an íoslódáil ar shos</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Íoslódáil críochnaithe</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Theip ar íoslódáil</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Íoslódáil (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Íoslódáil</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cealaigh</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Ní féidir le %1$s comhad den chineál seo a íoslódáil</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Níorbh fhéidir an comhad a oscailt</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Sos</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Lean</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cealaigh</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Oscail</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Bain Triail Eile As</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Dún</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gd/strings.xml new file mode 100644 index 0000000000..e38a01f7e1 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gd/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Luchdaidhean a-nuas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Tha an luchdadh a-nuas na stad</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Chaidh a luchdadh a-nuas</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Dh’fhàillig an luchdadh a-nuas</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Luchdadh a-nuas (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Luchdaich a-nuas</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Sguir dheth</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Chan urrainn dha %1$s a leithid seo a dh’fhaidhle a luchdadh a-nuas</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Cha b’ urrainn dhuinn am faidhle fhosgladh</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Cha deach aplacaid a lorg a dh‘fhosgladh faidhlichean %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Cuir na stad</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Lean air</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Sguir dheth</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Fosgail</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Feuch ris a-rithist</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Dùin</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Coilean seo le</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Cha ghabh %1$s fhosgladh</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tha feum air cead inntrigidh do dh’fhaidhlichean is meadhanan mus luchdaich thu a-nuas faidhle. Tadhail air roghainnean Android is thoir gnogag air a’ chead.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">A bheil thu airson sgur de gach luchdadh a-nuas prìobhaideach?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ma dhùineas tu gach taba brabhsaidh prìobhaideach an-dràsta, thèid crìoch a chur air luchdadh a-nuas an fhaidhle “%1$s”. A bheil thu cinnteach gu bheil thu airson am brabhsadh prìobhaideach fhàgail?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Sguir de gach luchdadh a-nuas</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Fuirich sa bhrabhsadh phrìobhaideach</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000..f5bf3cac41 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gl/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descargas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descarga en pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Rematou a descarga</string> + + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Produciuse un fallo ao descargar</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Descargar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Descargar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s non pode descargar este tipo de ficheiro</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Non foi posíbel abrir o ficheiro</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Non se atopou ningunha aplicación para abrir ficheiros %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Retomar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Tentar de novo</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Pechar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar acción usando</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Non foi posíbel abrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Precísase permiso de acceso a ficheiros e recursos multimedia para descargar ficheiros. Vaia á configuración de Android, toque nos permisos e toque en permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancelar as descargas privadas?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se pecha agora todos os separadores privados, cancelarase a descarga de %1$s. Seguro que quere saír da navegación privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar as descargas</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ficar na navegación privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gn/strings.xml new file mode 100644 index 0000000000..4353487f58 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gn/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Ñemboguejy</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Ñemboguejy opyta</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Oguejypáma</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Ñemboguejy ojavy</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Emboguejy (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Mboguejy</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Heja</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ndaikatúi omboguejy koichagua marandurenda</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Ndaikatúi eike marandurendápe</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ndojejuhúi tembiporu’i embojuruja hag̃ua %1$s marandurenda</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Mombyta</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Ku’ejey</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Heja</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Mbojuruja</string> + + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Eha’ãjey</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Mboty</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Emoĩmba tembiapo rupi</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ndaikatúi ijuruja %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Oñeikotevẽ marandurendápe jeike ha ñemoneĩ emboguejy hag̃ua marandurenda. Eho Android ñembohekópe, eikutu ñemoneĩ ha eikutu moneĩ.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">¿Ehejase ñemboguejy ñemigua?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Embotýramo umi kundahára ñemigua rovetã ko’ág̃a, ojehejáta ñemboguejy %1$s. ¿Añetéhápe rehejase ñeikundaha ñemigua?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Eheja ñemboguejy</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Epyta kundaha ñemiguápe</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gu-rIN/strings.xml new file mode 100644 index 0000000000..d3b6f95bfd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-gu-rIN/strings.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ડાઉનલોડ્સ</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ડાઉનલોડ અટકાવ્યુ</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ડાઉનલોડ પૂરુ થયું</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ડાઉનલોડ નિષ્ફળ થયું</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">(%1$s) ડાઉનલોડ કરો</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ડાઉનલોડ કરો</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">રદ કરો</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s આ પ્રકારની ફાઇલને ડાઉનલોડ કરી શકતા નથી</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ફાઇલ ખોલી શકાતી નથી</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ફાઇલો ખોલવા માટે કોઈ એપ્લિકેશન મળી નથી</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">અટકાવો</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ફરી શરૂ કરો</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">રદ કરો</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ખોલો</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ફરીથી પ્રયત્ન કરો</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">બંધ કરો</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">આની મદદથી ક્રિયા પૂર્ણ કરો</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ખોલવામાં અસમર્થ</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hi-rIN/strings.xml new file mode 100644 index 0000000000..2ce36bf6ee --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hi-rIN/strings.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">डाउनलोड</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">डाउनलोड रोका गया</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">डाउनलोड संपन्न</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">डाउनलोड विफल</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">(%1$s) डाउनलोड करें</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">डाउनलोड करें</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">रद्द करें</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s इस प्रकार के फाइल को डाउनलोड नहीं कर सकता</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">फ़ाइल खोला नहीं जा सका</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s फाइलें खोलने के लिए कोई ऐप नहीं मिला</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">रोकें</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">पुनः चलाएं</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">रद्द करें</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">खोलें</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">पुनः प्रयास करें</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">बंद करें</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">इसके उपयोग से कार्य पूरा करें</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s को खोलने में असमर्थ रहा</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hil/strings.xml new file mode 100644 index 0000000000..39955ba66e --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hil/strings.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string> + + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Padayun</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Pagabuksan</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Sirado</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000000..15da0f5495 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hr/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Preuzimanja</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Preuzimanje je pauzirano</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Preuzimanje završeno</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Preuzimanje neuspjelo</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Preuzmi (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Preuzmi</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Odustani</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ne može preuzeti ovu vrstu datoteka</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Nije bilo moguće otvoriti datoteku</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nije pronađena aplikacija za otvaranje %1$s datoteka</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pauziraj</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Nastavi</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Odustani</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Otvori</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Pokušaj ponovo</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zatvori</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Završi radnju koristeći</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nije moguće otvoriti %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Za preuzimanje datoteka potrebna je dozvola za pristup datotekama i medijima. Idi u postavke sustava Android, odaberi dozvole i odaberi "Dopusti".</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Prekinuti privatna preuzimanja?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ukoliko zatvoriš sve prozore privatnog pretraživanja, prekinut će se preuzimanje datoteke %1$s. Stvarno želiš prekinuti privatno pretraživanje?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Prekini preuzimanja</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ostani u privatnom pretraživanju</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hsb/strings.xml new file mode 100644 index 0000000000..6224c59482 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hsb/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Sćehnjenja</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Sćehnjenje je zastajene</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Sćehnjenje dokónčene</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Sćehnjenje njeje so poradźiło</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Sćahnyć (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Sćahnyć</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Přetorhnyć</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s njemóže tutón datajowy typ sćahnyć</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Dataja njeda so wočinić</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Žane nałoženje za wočinjenje datajow typa %1$s namakane</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Zastajić</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Pokročować</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Přetorhnyć</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Wočinić</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Hišće raz spytać</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Začinić</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Akciju skónčić z pomocu</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s njeda so wočinić</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Přistup k datajam a medijam je trěbny, zo byšće dataje sćahnył. Přeńdźće k nastajenjam Android, podótkńće so prawow a potom Dowolić.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Priwatne sćehnjenja přetorhnyć?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jeli nětko wšě priwatne rajtarki začiniće, so sćehnjenje dataje %1$s přetorhnje. Chceće priwatny modus woprawdźe wopušćić?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Sćehnjenja přetorhnyć</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">W priwatnym modusu wostać</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..5bd9b4214d --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hu/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Letöltések</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">A letöltés szüneteltetve</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">A letöltés befejeződött</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">A letöltés sikertelen</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Letöltés (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Letöltés</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Mégse</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">A %1$s nem tudja letölteni ezt a fájltípust</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">A fájl megnyitása sikertelen</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nem található alkalmazás, amely megnyitná a(z) %1$s fájlokat</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Szünet</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Folytatás</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Mégse</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Megnyitás</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Próbálja újra</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Bezárás</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Művelet befejezése ezzel:</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">A(z) %1$s nem nyitható meg</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">A fájlok letöltéséhez fájl és média engedély hozzáférés szükséges. Nyissa meg az Android beállításokat, koppintson az engedélyekre, és koppintson az engedélyezésre.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Megszakítja a privát letöltéseket?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ha most bezárja az összes privát lapot, akkor %1$s letöltés megszakad. Biztos, hogy ki akar lépni a privát böngészésből?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Letöltések megszakítása</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Maradok privát böngészésben</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000000..5bdc00e926 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-hy-rAM/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Ներբեռնումներ</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Ներբեռնումը դադարեցված է</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Ներբեռնվեց</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Ներբեռնումը ձախողվեց</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Ներբեռնել (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Ներբեռնել</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Չեղարկել</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s-ը չի կարող ներբեռնել այս տեսակի ֆայլը</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Հնարավոր չէ բացել ֆայլը</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ֆայլերը բացելու համար որևէ հավելված չի գտնվել</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Դադարեցնել</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Վերսկսել</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Չեղարկել</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Բացել</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Կրկին փորձել</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Փակել</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Ավարտել գործողությունը հետևյալով՝</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Անհնար է բացել %1$s-ը</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Ֆայլեր ներբեռնելու համար անհրաժեշտ է ֆայլերի և մեդիայի թույլտվություն: Անցեք Android-ի կարգավորումներին, հպեք թույլտվություններին և հպեք թույլատրել:</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Չեղարկե՞լ անձնական ներբեռնումները:</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Եթե հիմա փակեք բոլոր Մասնավոր ներդիրները, %1$s ներբեռնում կչեղարկվի: Փակե՞լ:</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Չեղարկել ներբեռնումները</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Շարունակել Մասնավոր դիտարկումը</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ia/strings.xml new file mode 100644 index 0000000000..2f69b7435d --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ia/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Discargamentos</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Discargamento pausate</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Discargamento completate</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Discargamento fallite</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Discargar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Discargar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancellar</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s non pote discargar iste typo de file</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Impossibile aperir file.</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nulle app trovate pro aperir %1$s files</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausar</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Reprender</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancellar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Aperir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Retentar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Clauder</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar le action per</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impossibile aperir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Le permission de accesso a files e multimedia es necessari pro discargar files. Vade al parametros de Android, tocca Permissiones e tocca Permitter.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancellar le discargamentos private?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si tu claude tote le schedas private ora, %1$s discargamento essera cancellate. Desira tu vermente abandonar le Navigation private?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancellar le discargamentos</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Restar in navigation private</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-in/strings.xml new file mode 100644 index 0000000000..442a1b812b --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-in/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Unduhan</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Unduhan ditunda</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Unduhan selesai</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Unduhan gagal</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Unduh (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Unduh</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Batal</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s tidak dapat mengunduh jenis berkas ini</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Tidak dapat membuka berkas</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Tidak ditemukan aplikasi untuk membuka berkas %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Tunda</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Lanjutkan</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Batalkan</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Buka</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Coba Lagi</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Tutup</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Selesaikan aksi menggunakan</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Tidak dapat membuka %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Akses izin berkas dan media diperlukan untuk mengunduh berkas. Buka pengaturan Android, ketuk perizinan, dan ketuk izinkan.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Batalkan unduhan pribadi?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jika Anda menutup semua tab pada Penjelajahan Pribadi sekarang, %1$s unduhan akan dibatalkan. Yakin akan meninggalkan Penjelajahan Pribadi?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Batalkan unduhan</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Tetap dalam penjelajahan pribadi</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000..2ce32b7cfe --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-is/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Sóttar skrár</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Hlé gert á niðurhali</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Niðurhali lokið</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Niðurhal mistókst</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Niðurhal (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Sækja</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Hætta við</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s getur ekki sótt þessa skráartegund</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Ekki var hægt að opna skrá</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ekkert forrit fannst sem getur opnað %1$s skrár</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Í bið</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Halda áfram</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Hætta við</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Opna</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Reyna aftur</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Loka</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Ljúka aðgerð með því að nota</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ekki tókst að opna %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Heimildir fyrir aðgang að skrám og miðlum þarf til að sækja skrár. Farðu í Android-stillingar, ýttu á heimildir og ýttu á að leyfa.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Hætta við einkaniðurhal?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ef þú lokar öllum huliðsgluggum, þá verður hætt við %1$s niðurhal. Ertu viss um að þú viljir hætta í huliðsvafri?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Hætta við niðurhal</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Vera áfram í huliðsvafri</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..e73298c2ec --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-it/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Download</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Download in pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Download completato</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Download non riuscito</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Scarica</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Annulla</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Non è possibile scaricare questo tipo di file in %1$s</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Impossibile aprire il file</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nessuna app trovata per aprire file di tipo %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Riprendi</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Annulla</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Apri</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Riprova</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Chiudi</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completa azione con</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impossibile aprire %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Per scaricare i file è richiesto il permesso di accedere a file e contenuti multimediali. Vai alle impostazioni di Android, tocca Autorizzazioni e successivamente Consenti.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Annullare i download in modalità Navigazione anonima?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Chiudendo tutte le schede in modalità anonima verrà annullato il download di %1$s. Abbandonare la modalità Navigazione anonima?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Annulla i download</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Rimani in modalità Navigazione anonima</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-iw/strings.xml new file mode 100644 index 0000000000..6b0ef451d2 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-iw/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">הורדות</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ההורדה הושהתה</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ההורדה הושלמה</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ההורדה נכשלה</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">הורדה (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">הורדה</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ביטול</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">אין באפשרות %1$s להוריד את סוג הקובץ הזה</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">אין אפשרות לפתוח את הקובץ</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">לא נמצא יישומון לפתיחת קובצי %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">השהייה</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">המשך</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ביטול</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">פתיחה</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ניסיון חוזר</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">סגירה</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">השלמת הפעולה באמצעות</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">לא ניתן לפתוח את %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">יש צורך בגישה להרשאה לקבצים ומדיה על מנת להוריד קבצים. יש לעבור אל ההגדרות של Android, להקיש על הרשאות ולהקיש על ״לאפשר״.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">לבטל את ההורדות הפרטיות?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">אם כל הלשוניות הפרטיות ייסגרו כעת, ההורדה של %1$s תבוטל. האם ברצונך לצאת ממצב גלישה פרטית?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ביטול ההורדות</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">להישאר במצב גלישה פרטית</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..59844154cd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ja/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ダウンロード一覧</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ダウンロードを一時停止しました</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ダウンロードが完了しました</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ダウンロードに失敗しました</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ダウンロード (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ダウンロード</string> + + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">キャンセル</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s はこのファイルの種類をダウンロードできません</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ファイルを開けませんでした</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ファイルを開けるアプリが見つかりません</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">一時停止</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">再開</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">キャンセル</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">開く</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">再試行</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">閉じる</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">使用するアプリを選択</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s を開けません</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ファイルをダウンロードするにはファイルとメディアへのアクセス許可が必要です。Android のアプリの設定を開き、[権限] をタップし、[許可] をタップしてください。</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">プライベートダウンロードをキャンセルしますか?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">すべてのプライベートウィンドウを今すぐ閉じると、%1$s ファイルのダウンロードがキャンセルされます。プライベートブラウジングモードを終了してもよろしいですか?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ダウンロードをキャンセル</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">プライベートブラウジングを継続</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ka/strings.xml new file mode 100644 index 0000000000..ec729a5e36 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ka/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ჩამოტვირთვები</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ჩამოტვირთვა შეჩერებულია</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ჩამოტვირთვა დასრულდა</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ჩამოტვირთვა ვერ მოხერხდა</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ჩამოტვირთვა (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ჩამოტვირთვა</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">გაუქმება</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ვერ ჩამოტვირთავს ამ სახის ფაილს</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ფაილის გახსნა ვერ ხერხდება</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">არ მოიძებნა აპი, რომლითაც გაიხსნება %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">შეჩერება</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">გაგრძელება</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">გაუქმება</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">გახსნა</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ხელახლა</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">დახურვა</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">მოქმედების შესასრულებლად გამოიყენება</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">ვერ გაიხსნა %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ფაილების ნებართვებთან წვდომაა საჭირო, ჩამოსატვირთად. გადადით Android-ის პარამეტრებში, შეეხეთ ნებართვებს და შემდეგ დაშვებას.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">გაუქმდეს ყველა პირადი ჩამოტვირთვა?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">თუ პირადი დათვალიერების ყველა ფანჯარას დახურავთ, %1$s ჩამოტვირთვა გაუქმდება. ნამდვილად გსურთ პირადი დათვალიერების დატოვება?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ჩამოტვირთვების გაუქმება</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">პირად ფანჯარაში დარჩენა</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kaa/strings.xml new file mode 100644 index 0000000000..0be25ed38f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kaa/strings.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Júklengenler</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Júklew pauzalandı</string> + + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Júklew ámelge asırıldı</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Júklew sátsiz boldı</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Júklew (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Júklew</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Biykarlaw</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s bul fayl túrin júkley almadı</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Fayldı ashıw múmkin bolmadı</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s faylların ashatuǵın baǵdarlama tabılmadı</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pauza</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Qayta baslaw</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Biykarlaw</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ashıw</string> + + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Qayta urınıp kóriw</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Jabıw</string> + + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Háreketti tómendegiden paydalanıp juwmaqlaw</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ashıw múmkin emes</string> + + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Fayllardı júklep alıw ushın fayllar hám medialarǵa kiriwge ruqsat kerek. Android sazlawlarına ótiń, ruqsatlar bólimin tańlań hám ruqsat beriw túymesin basıń.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Jeke júklemelerdi biykarlayıq pa?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Eger házir barlıq jeke betlerdi jawsańız, %1$sdı júklew biykarlanadı. Jeke kóriwden shıǵıwǵa isenimińiz kámil me?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Júklemelerdi biykarlaw</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Jeke kóriwde qalıw</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kab/strings.xml new file mode 100644 index 0000000000..6d7c286752 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kab/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Isidar</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Asader yesteɛfa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Asader yemmed</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Ifuyla ittwazedmen</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Asader (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Sader</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Sefsex</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ur izmir ara ad d-isader anaw-a n yifuyla</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Ur yezmir ara ad yeldi afaylu</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ulac asnas yettwafen i twaledyawt n yifuyla %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Seṛǧu</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Kemmel</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Sefsex</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ldi</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Ɛreḍ tikelt nniḍen</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Mdel</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Fak tigawt s uqeqdec</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ur yizmir ara ad yeldi %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tisirag n unekcum ɣer yifuyla d yimidyaten ttusrant i usader n yufuyla. Rzu ɣer yiɣewwaren n Android, fren tisirag syen sit ɣef sireg.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Sefsex isidar usligen?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ma tmedleḍ akk accaren usligen tura, %1$s n uzdam ad iţwasefsex. Tebɣiḍ aţefɣeḍ si tunigin tusligt?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Sefsex isadaren</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Qqim deg tunigin tusligt</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kk/strings.xml new file mode 100644 index 0000000000..a7557a17cd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kk/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Жүктемелер</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Жүктеп алу аялдатылды</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Жүктеп алу аяқталды</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Жүктеп алу сәтсіз аяқталды</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Жүктеп алу (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Жүктеп алу</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Бас тарту</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s бұл файл түрін жүктей алмайды</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Файлды ашу мүмкін емес</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s файлдарын ашатын қолданба табылмады</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Аялдату</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Жалғастыру</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Бас тарту</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ашу</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Қайтадан көру</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Жабу</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Әрекетті келесіні қолданып аяқтау</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ашу мүмкін емес</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Файлдарды жүктеп алу үшін файлдар мен медиа рұқсаты қажет. Android баптауларына өтіп, рұқсаттарды шертіңіз және рұқсат етуді таңдаңыз.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Жекелік жүктемелерден бас тарту керек пе?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Егер қазір барлық жекелік шолу беттерін жапсаңыз, %1$s жүктемеден бас тартылады. Жекелік шолу режимінен шығуды шынымен қалайсыз ба?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Жүктемелерден бас тарту</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Жекелік шолу режимінде қалу</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kmr/strings.xml new file mode 100644 index 0000000000..26bcf428f1 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kmr/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Daxistinên te</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Daxistin hate sekinandin</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Daxistin qediya</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Daxistin bi ser neket</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Daxîne (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Daxîne</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Betal bike</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s nikare vê cureya dosyeyê daxîne</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Dosye nehate vekirin</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Sepana ku karibe dosyeyên %1$s’ê veke, nehate dîtin</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Bisekinîne</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Bidomîne</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Betal bike</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Veke</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Dîsa biceribîne</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Bigire</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Çalakiyê bi vê temam bike</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s nayê vekirin</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Ji bo jêbarkirina pelan pêdivî bi destûra pelan û ya medyayê heye. Here beşa sazkariyên Androîdê, li ser destûran bitikîne û destûrdanê bitikîne.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Daxistinên taybet betal bikî?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Heke tu hemû hilpekînên taybet niha bigirî, %1$s jêbarkirin dê bên betalkirin. Tu ji dil dixwazî ji gera taybet derkevî?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Jêbarkirinan betal bike</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Di gera taybet de bimîne</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kn/strings.xml new file mode 100644 index 0000000000..03ae4d90ec --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-kn/strings.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ಡೌನ್ಲೋಡ್ಗಳು</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ಡೌನ್ಲೋಡ್ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ಡೌನ್ಲೋಡ್ ಪೂರ್ಣಗೊಂಡಿದೆ</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ಡೌನ್ಲೋಡ್ ವಿಫಲವಾಗಿದೆ</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ಡೌನ್ಲೋಡ್ ಮಾಡಿ (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ಡೌನ್ಲೋಡ್</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ರದ್ದು ಮಾಡು</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ಈ ಫೈಲ್ ಪ್ರಕಾರವನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ಫೈಲ್ ತೆರೆಯಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ಫೈಲ್ಗಳನ್ನು ತೆರೆಯಲು ಯಾವುದೇ ಅಪ್ಲಿಕೇಶನ್ ಕಂಡುಬಂದಿಲ್ಲ</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">ವಿರಾಮ</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ಪುನರಾರಂಭಿಸು</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ರದ್ದು ಮಾಡು</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ತೆರೆ</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ಇನ್ನೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸಿ</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">ಮುಚ್ಚು</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000000..84b62db1fd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ko/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">다운로드</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">다운로드 일시 중지됨</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">다운로드 완료</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">다운로드 실패</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">다운로드 (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">다운로드</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">취소</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s는 이 파일 형식은 다운로드 할 수 없습니다</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">파일을 열 수 없습니다</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s 파일을 열 앱이 없습니다</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">일시 중지</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">계속</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">취소</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">열기</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">다시 시도</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">닫기</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">다음 앱을 사용하여 작업 완료</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s 앱을 열 수 없음</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">파일을 다운로드하려면 파일 및 미디어 권한 액세스가 필요합니다. Android 설정으로 이동하여 권한을 누르고 허용을 누르세요.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">사생활 보호 다운로드를 취소하시겠습니까?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">모든 사생활 보호 탭을 닫으면, %1$s개의 다운로드가 취소됩니다. 정말 사생활 보호 모드에서 나가시겠습니까?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">다운로드 취소</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">사생활 보호 모드 계속하기</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lij/strings.xml new file mode 100644 index 0000000000..17882b46bc --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lij/strings.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descaregamenti</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descaregamento in pösa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Descaregamento finio</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Descaregamento falio</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Descaregamento (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Descarega</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Anulla</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">No l\'é poscibile scaregâ sto tipo de schedaio in %1$s</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No pòsso arvî o schedaio</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nisciunn-a app pe arvî schedai do tipo %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pösa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Repiggia</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Anulla</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Arvi</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Preuva torna</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Særa</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..7cf44e4f42 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lo/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ດາວໂຫລດ</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ການດາວໂຫລດໄດ້ຢຸດຊົ່ວຄາວແລ້ວ</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ການດາວໂຫລດສຳເລັດແລ້ວ</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ການດາວໂຫລດລົ້ມເຫລວ</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ດາວໂຫລດ (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ດາວໂຫລດ</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ຍົກເລີກ</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ບໍ່ສາມາດດາວໂຫລດເອກະສານປະເພດນີ້ໄດ້</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ບໍ່ສາມາດເປີດໄຟລໄດ້</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">ບໍ່ພົບແອັບທີ່ຈະໃຊ້ເປີດໄຟລ %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">ຢຸດ</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ດຳເນີນການຕໍ່</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ຍົກເລີກ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ເປີດ</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ຫລອງໃຫມ່ອີກຄັ້ງ</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">ປິດ</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ດຳເນີນການໂດຍໃຊ້</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">ບໍ່ສາມາດເປີດ %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ຈຳເປັນຕ້ອງມີສິດການເຂົ້າໄປຫາໄຟລ ແລະ ມີເດຍ ເພື່ອດາວໂຫລດໄຟລເຫລົ້ານັ້ນ. ໄປທີ່ການຕັ້ງຄ່າ Android, ແຕະໃສ່ສິດອະນຸຍາດ, ແລະ ແຕະໃສ່ອະນຸຍາດ.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">ຍົກເລີກການດາວໂຫຼດສ່ວນຕົວບໍ?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ຖ້າທ່ານປິດແທັບສ່ວນຕົວທັງໝົດດຽວນີ້, ການດາວໂຫຼດ %1$s ຈະຖືກຍົກເລີກ. ທ່ານແນ່ໃຈແລ້ວບໍ່ວ່າຕ້ອງການອອກຈາກການທ່ອງເວັບແບບສ່ວນຕົວ?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ຍົກເລີກການດາວໂຫຼດ</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">ຢູ່ໃນໂຫມດການທອງເວັບແບບສ່ວນຕົວ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000000..b564aff148 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-lt/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Atsiuntimai</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Atsiuntimas pristabdytas</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Atsiuntimas baigtas</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Atsiųsti nepavyko</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Atsiuntimas („%1$s“)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Atsiųsti</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Atsisakyti</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">„%1$s“ negali atsiųsti šio tipo failų</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Nepavyko atverti failo</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nerasta programų, galinčių atverti %1$s tipo failus</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pristabdyti</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Tęsti</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Atsisakyti</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Atverti</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Bandyti dar kartą</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Užverti</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Užbaigti veiksmą naudojant</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nepavyko atverti „%1$s“</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Norint parsiųsti failus, reikia leidimo prie failų ir medijos. Eikite į „Android“ nustatymus, bakstelėkite leidimus, ir bakstelėkite leisti.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Atsisakyti privačių atsiuntimų?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Jei dabar užversite visas privačiojo naršymo korteles, bus nutrauktas %1$s atsiuntimas. Ar tikrai norite išeiti iš privačiojo naršymo seanso?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Atsisakyti atsiuntimų</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Tęsti privatųjį naršymą</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-mix/strings.xml new file mode 100644 index 0000000000..ccf2f57281 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-mix/strings.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Snuù</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Snuù (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Snuù</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Kunchatu</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Kuna</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Kasi</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ml/strings.xml new file mode 100644 index 0000000000..6f5347ef00 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ml/strings.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ഡൗണ്ലോഡുകള്</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ഡൗൺലോഡ് താൽക്കാലികമായി നിർത്തി</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ഡൗണ്ലോഡ് പൂര്ത്തിയായിരിക്കുന്നു</string> + + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ഡൗൺലോഡ് പരാജയപ്പെട്ടു</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ഡൗൺലോഡ് ചെയ്യുക (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ഡൗണ്ലോഡ്</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">റദ്ദാക്കുക</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s - ഈ ഫയൽ തരം ഡൗൺലോഡ് ചെയ്യാൻ സാധ്യമല്ല</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ഫയൽ തുറക്കാൻ സാധിച്ചില്ല</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ഫയലുകൾ തുറക്കുന്നതിന് ആപ്പുകളൊന്നും കണ്ടെത്തിയില്ല</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">തൽക്കാലം നിര്ത്തുക</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">തുടരുക</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">റദ്ദാക്കുക</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">തുറക്കുക</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">വീണ്ടും ശ്രമിയ്ക്കുക</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">അടയ്ക്കുക</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-mr/strings.xml new file mode 100644 index 0000000000..6baf608c96 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-mr/strings.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">डाउनलोड</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">डाउनलोड स्थगित</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">डाउनलोड पूर्ण</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">डाउनलोड अयशस्वी</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">डाउनलोड (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">डाउनलोड</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">रद्द</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ह्या प्रकारच्या फाईल डाउनलोड करू शकत नाही</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">दस्तावेज उघडू शकले नाही</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s फाईल उघडण्यासाठी कुठलेही अॅप सापडले नाही</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">विराम</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">पुन्हा सुरू करा</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">रद्द करा</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">उघडा</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">पुन्हा प्रयत्न करा</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">बंद करा</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">वापरून कृती पूर्ण करा</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s उघडण्यात अक्षम</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-my/strings.xml new file mode 100644 index 0000000000..88db33e437 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-my/strings.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ဆွဲချချက်များ</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ဒေါင်းလုပ်ခေတ္တရပ်နားသည်</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ဆွဲချချက်များ ပြီးသွားပြီ</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ဆွဲယူမှု မအောင်မြင်ပါ</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ဆွဲယူပါ (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ဆွဲယူပါ</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ပယ်ဖျက်ပါ</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s သည်ဒီဖိုင်အမျိုးအစားကို မဆွဲယူနိုင်ပါ။</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ဖိုင်ဖွင့်၍ မရပါ</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1"> %1$s ဖိုင် ဖွင့်ရန် အက်ပ် မရှိပါ။</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">ခေတ္တရပ်တန့်ပါ</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ဆက်လက်ဆောင်ရွက်ပါ</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ပယ်ဖျက်ပါ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ဖွင့်ပါ</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ထပ်ကြိုးစားပါ</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">ပိတ်ပါ</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ယခုတာဝန်အား</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s အားဖွင့်လို့မရပါ</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ဖိုင်များကို ဆွဲချက်ချရန် ဖိုင်များနှင့် မီဒီယာ တို့၏ ခွင့်ပြုချက် လိုအပ်သည်။ Android ဆက်တင်များ သို့သွားပါ။ ခွင့်ပြုချက်များကို နှိပ်ပါ။ ပြီးနောက် ခွင့်ပြုပါ ကိုနှိပ်ပါ။</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 0000000000..ae08db228c --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Nedlastinger</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Nedlasting satt på pause</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Nedlasting fullført</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Nedlasting feilet</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Last ned (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Last ned</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Avbryt</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s kan ikke laste ned denne filtypen</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Klarte ikke åpne filen</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ingen app funnet for å åpne %1$s-filer</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Fortsett</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Avbryt</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Åpne</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Prøv på nytt</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Lukk</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Fullfør handling med</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kan ikke å åpne %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tilgang til filer og medier er nødvendig for å laste ned filer. Gå til Android-innstillinger, trykk på tillatelser og trykk på tillat.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Avbryte private nedlastinger?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Hvis du lukker alle privat nettlesing-vinduene nå, vil %1$s-nedlastingen bli avbrutt. Er du sikker på at du vil gå ut av privat nettlesing?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Avbryt nedlastinger</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Fortsett med privat nettlesing</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ne-rNP/strings.xml new file mode 100644 index 0000000000..08361e125f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ne-rNP/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">डाउनलोडहरु</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">डाउनलोड रोकियो</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">डाउनलोड पूरा भयो</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">डाउनलोड असफल भयो</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">डाउनलोड (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">डाउनलोड</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">रद्द गर्नुहोस्</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ले यो फाइलको प्रकार डाउनलोड गर्न सक्दैन</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">यो फाइल खोल्न सकिएन</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s फाइलहरू खोल्न कुनै एप फेला परेनन्</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">रोक्नुहोस्</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">पुनः सुचारु गर्नुहोस्</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">रद्द गर्नुहोस्</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">खोल्नुहोस्</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">पुनः प्रयास गर्नुहोस्</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">बन्द गर्नुहोस्</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">प्रयोग गरेर कार्य सम्पन्न गर्नुहोस्</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s खोल्न सकिएन</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">फाइलहरू डाउनलोड गर्न फाइलहरू र मिडिया अनुमति पहुँच आवश्यक छ। एण्ड्रोइड सेटिङ्गहरूमा जानुहोस्, अनुमति ट्याप गर्नुहोस्, र अनुमति ट्याप गर्नुहोस्।</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">निजी डाउनलोडहरू रद्द गर्ने हो?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">यदि तपाईंले अहिले सबै निजी ट्याबहरू बन्द गर्नुभयो भने, %1$s डाउनलोड रद्द हुनेछ। के तपाइँ निजी ब्राउजिङ छोड्न निश्चित हुनुहुन्छ?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">डाउनलोडहरू रद्द गर्नुहोस्</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">निजी ब्राउजिङ्गमै बस्नुहोस्</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000..48fe33f965 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nl/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Downloaden gepauzeerd</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Downloaden voltooid</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Downloaden mislukt</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Downloaden (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Downloaden</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Annuleren</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s kan dit bestandstype niet downloaden</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Kan bestand niet openen</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Geen app gevonden om %1$s-bestanden mee te openen</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pauzeren</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Hervatten</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Annuleren</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Openen</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Opnieuw proberen</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Sluiten</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Handeling voltooien met</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kan %1$s niet openen</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Toegang tot bestanden en media vereist om bestanden te downloaden. Ga naar Android-instellingen, tik op machtigingen en tik op toestaan.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Privédownloads annuleren?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Als u nu alle privétabbladen sluit, zal %1$s download worden geannuleerd. Weet u zeker dat u Privénavigatie wilt verlaten?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Downloads annuleren</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">In privénavigatie blijven</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nn-rNO/strings.xml new file mode 100644 index 0000000000..2083b1535c --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-nn-rNO/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Nedlastingar</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Nedlasting sett på pause</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Nedlasting fullført</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Nedlasting feila</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Last ned (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Last ned</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Avbryt</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s kan ikkje laste ned denne filtypen</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Klarte ikkje å opne fila</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ingen app funnen for å opne %1$s-filer</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Fortset</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Avbryt</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Opne</string> + + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Prøv på nytt</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Lat att</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Fullfør handlinga med</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Klarte ikkje å opne %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Tilgang til filer og medium er nødvendig for å laste ned filer. Gå til Android-innstillingar, trykk på løyve (tillatelser) og trykk på tillat.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Avbryte private nedlastingar?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Om du lèt att alle private faner no, blir %1$s-nedlastinga avbroten. Er du sikker på at du vil gå ut av privat nettlesing?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Avbryt nedlastingar</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Fortset med privat nettlesing</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-oc/strings.xml new file mode 100644 index 0000000000..07aa0b35b6 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-oc/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Telecargaments</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Lo telecargament es en pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Telecargament acabat</string> + + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Lo telecargament a fracassat</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Telecargar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Telecargar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Anullar</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s pòt pas telecargar aqueste tipe de fichièr</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Dubertura impossibla del fichièr</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Cap d’aplicacions per dobrir los fichièrs %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Reprendre</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Anullar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Dobrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Tornar ensajar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Tampar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Contunhar l‘accion amb</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Dobertura impossibla %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Las autorizacion d’accès a l’emmagazinatge de fichièrs e dels mèdias son necessàrias per telecargar de fichièrs. Anatz als paramètres d’Android, seleccionatz Autorizacions puèi Autorizar.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Anullar los telecargaments privats ?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se sortissètz ara del mòde de navegacion privada, anullarà %1$s telecargaments. Volètz vertadièrament sortir del mòde de navegacion privada ?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Anullar los telecargaments</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Demorar dins lo mòde de navegacion privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-or/strings.xml new file mode 100644 index 0000000000..58b4eedd5b --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-or/strings.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ଡାଉନଲୋଡ଼ସମୂହ</string> + + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ଆହରଣ ସମ୍ପୂର୍ଣ୍ଣ ହୋଇଛି</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ଆହରଣ ବିଫଳ ହେଲା</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ଡାଉନଲୋଡ଼(%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ଡାଉନଲୋଡ଼</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ବାତିଲ କରନ୍ତୁ</string> + + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ବାତିଲ କରନ୍ତୁ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ଖୋଲନ୍ତୁ</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">ବନ୍ଦ କରନ୍ତୁ</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rIN/strings.xml new file mode 100644 index 0000000000..3ca780189f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rIN/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ਡਾਊਨਲੋਡ</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ਡਾਉਨਲੋਡ ਰੁਕ ਗਿਆ</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ਡਾਊਨਲੋਡ ਪੂਰਾ ਹੋਇਆ</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ਡਾਊਨਲੋਡ ਅਸਫ਼ਲ ਹੈ</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">(%1$s) ਡਾਉਨਲੋਡ</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ਡਾਊਨਲੋਡ ਕਰੋ</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ਰੱਦ ਕਰੋ</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ਇਹ ਫਾਈਲ ਕਿਸਮ ਡਾਊਨਲੋਡ ਨਹੀਂ ਕਰ ਸਕਦਾ ਹੈ</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ਫ਼ਾਈਲ ਨੂੰ ਖੋਲ੍ਹਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ਫਾਇਲਾਂ ਖੋਲ੍ਹਣ ਲਈ ਕੋਈ ਐਪ ਨਹੀਂ ਲੱਭੀ</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">ਥੰਮੋ</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ਮੁੜ-ਪ੍ਰਾਪਤ</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ਰੱਦ ਕਰੋ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ਖੋਲ੍ਹੋ</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">ਬੰਦ ਕਰੋ</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ਇਸ ਨਾਲ ਕਾਰਵਾਈ ਪੂਰੀ ਕਰੋ</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ਨੂੰ ਖੋਲ੍ਹਣ ਵਿੱਚ ਅਸਮਰੱਥ</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ਫਾਇਲਾਂ ਡਾਊਨਲੋਡ ਕਰਨ ਲਈ ਫਾਇਲਾਂ ਤੇ ਮੀਡੀਆ ਇਜਾਜ਼ਤ ਪਹੁੰਚ ਦੀ ਲੋੜ ਹੈ। Android ਸੈਟਿੰਗਾਂ ਉੱਤੇ ਜਾਓ, ਇਜਾਜ਼ਤਾਂ ਨੂੰ ਛੂਹੋ ਅਤੇ ਮਨਜ਼ੂਰੀ ਨੂੰ ਛੂਹੋ।</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">ਨਿੱਜੀ ਡਾਊਨਲੋਡਾਂ ਨੂੰ ਰੱਦ ਕਰਨਾ ਹੈ?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ਜੇ ਤੁਸੀਂ ਹੁਣੇ ਸਾਰੀਆਂ ਨਿੱਜੀ ਟੈਬਾਂ ਨੂੰ ਬੰਦ ਕੀਤਾ ਤਾਂ %1$s ਡਾਊਨਲੋਡ ਨੂੰ ਰੱਦ ਕੀਤਾ ਜਾਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਨਿੱਜੀ ਬਰਾਊਜ਼ਿੰਗ ਨੂੰ ਛੱਡਣਾ ਚਾਹੁੰਦੇ ਹੋ?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ਡਾਊਨਲੋਡਾਂ ਨੂੰ ਰੱਦ ਕਰੋ</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">ਨਿੱਜੀ ਬਰਾਊਜ਼ਿੰਗ ਵਿੱਚ ਰਹੋ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rPK/strings.xml new file mode 100644 index 0000000000..ef11d19c67 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pa-rPK/strings.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ڈاؤںلوڈ</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ڈاؤںلوڈ رک گیا</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ڈاؤںلوڈ پورا ہویا</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ڈاؤںلوڈ نال غلطی ہو گئی اے</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ڈاؤںلوڈ کرن (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">آہو، ڈاؤںلوڈ کرو</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">رد کرو</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">ایہہ فائل دی قسم %1$s ڈاؤںلوڈ کر نہیں سکدی</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">فائل کھولھ نہیں سکدی</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s فائلاں کھولھݨ لئی کوئی ایپ نہیں لبھی</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">روکو</string> + + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">پھر جاری کرو</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">رد کرو</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">کھولھو</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">فیر کرو</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">بند کرو</string> + + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">کیہنوں نال کم کرو</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ایپ کھولھ نہیں سکدی</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ڈاؤںلوڈ کرن لئی فائلاں دی اجازت لوڑدی اے۔ فون دیاں سیٹنگاں نوں جاؤ تے اجازت دیݨ دی چوݨ لاؤ۔</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">نجی ڈاؤںلوڈاں نوں رد کرو؟</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">جے ساریاں ٹیباں کیتیاں %1$s فائل دا ڈاؤںلوڈ رد کر جاؤگے۔ تسیں پکے او؟</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">رد کرو</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">نجی ورتوں چ رہو</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000000..663b43a376 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pl/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Pobieranie plików</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Wstrzymano pobieranie</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Ukończono pobieranie</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Pobranie się nie powiodło</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Pobieranie (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Pobierz</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Anuluj</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s nie może pobrać pliku tego typu</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Nie można otworzyć pliku</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nie odnaleziono aplikacji zdolnej do otwierania plików %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Wstrzymaj</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Wznów</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Anuluj</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Otwórz</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Spróbuj ponownie</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zamknij</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dokończ za pomocą aplikacji</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nie można otworzyć aplikacji %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Do pobierania plików wymagany jest dostęp do plików i multimediów. Przejdź do ustawień Androida, stuknij „Uprawnienia” i stuknij „Zezwól”.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Czy anulować pobieranie plików?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Zamknięcie wszystkich prywatnych kart teraz spowoduje przerwanie pobierania pliku „%1$s”. Czy na pewno opuścić tryb prywatny?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Anuluj pobieranie</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Pozostań w trybie prywatnym</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..bf941eb32f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Download pausado</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Download concluído</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Download falhou</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Baixar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Baixar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">O %1$s não pode baixar este tipo de arquivo</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Não foi possível abrir o arquivo</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nenhum aplicativo encontrado para abrir arquivos %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausar</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Continuar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Tentar novamente</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Fechar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Completar ação usando</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Não foi possível abrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">É necessário ter permissão de acesso a arquivos e mídia para baixar arquivos. Abra a configuração do Android, toque em permissões e toque em permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancelar downloads privativos?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se fechar agora todas as abas privativas, %1$s download será cancelado. Tem certeza que quer sair do modo de navegação privativa?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar downloads</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Permanecer na navegação privativa</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000..0dc5869bb6 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Transferências</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Transferência em pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Transferência concluída</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Transferência falhada</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Transferência (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Transferir</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancelar</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">O %1$s não pode transferir este tipo de ficheiro</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Não foi possível abrir o ficheiro</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Não foi encontrada nenhuma aplicação para abrir ficheiros %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Retomar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancelar</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Abrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Tentar novamente</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Fechar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Concluir ação utilizando</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Não foi possível abrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">A permissão de acesso a ficheiros e media é necessária para transferir ficheiros. Aceda às configurações do Android, toque em permissões e toque em permitir.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancelar transferências privadas?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Se fechar todos os separadores Privados agora, %1$s transferências serão canceladas. Tem a certeza de que pretende sair da navegação privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancelar transferências</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Manter a navegação privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-rm/strings.xml new file mode 100644 index 0000000000..1990f9a0f5 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-rm/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Telechargiadas</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Telechargia ruaussa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Telechargiada cumplettada</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Telechargiada betg reussida</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Telechargiar (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Telechargiar</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Interrumper</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s na po betg telechargiar quest tip da datoteca</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Impussibel dad avrir la datoteca</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Chattà nagina app per avrir datotecas %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Cuntinuar</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Interrumper</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Avrir</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Reempruvar</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Serrar</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Cumplettar l\'acziun cun</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impussibel d\'avrir %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Per pudair telechargiar datotecas è la permissiun da pudair acceder a datotecas e medias necessaria. Va als parameters dad Android, smatga sin permissiuns e lura sin permetter.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Interrumper las telechargiadas privatas?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Sche ti serras ussa tut ils tabs privats vegn la telechargiada da %1$s interrutta. Vuls ti propi bandunar il modus privat?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Interrumper las telechargiadas</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Restar en il modus privat</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000000..c6a98c2471 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ro/strings.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Descărcări</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Descărcare trecută în pauză</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Descărcare finalizată</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Descărcare eșuată</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Descărcare (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Descarcă</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Renunță</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s nu poate descărca acest tip de fișier</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Fișierul nu poate fi deschis</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nu s-a găsit nicio aplicație care să deschidă fișierele %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pauză</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Continuă</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Renunță</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Deschide</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Încearcă din nou</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Închide</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Finalizează acțiunea folosind</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s nu poate fi deschis</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000..378f1328a0 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ru/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Загрузки</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Загрузка приостановлена</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Загрузка завершена</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Загрузка не удалась</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Загрузить (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Загрузить</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Отмена</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s не может загрузить этот тип файла</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Не удалось открыть файл</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Не найдено приложений, умеющих открывать файлы %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Приостановить</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Возобновить</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Отменить</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Открыть</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Попробовать снова</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Закрыть</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Завершить действие используя</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Не удалось открыть %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Для загрузки файлов требуется разрешение на доступ к файлам и мультимедиа. Перейдите в системные настройки Android, выберите «Разрешения», затем выберите «Разрешить».</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Отменить приватные загрузки?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Если вы сейчас закроете все приватные вкладки, загрузка %1$s будет отменена. Вы уверены, что хотите выйти из Приватного просмотра?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Отменить загрузки</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Остаться в приватном просмотре</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sat/strings.xml new file mode 100644 index 0000000000..88bca32b6e --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sat/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ᱰᱟᱣᱱᱞᱚᱰ ᱠᱚ</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ᱰᱟᱣᱱᱞᱚᱰ ᱛᱷᱩᱠᱩᱢ ᱟᱠᱟᱱᱟ</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ᱰᱟᱣᱱᱞᱚᱰ ᱯᱩᱨᱟᱹᱣᱮᱱᱟ</string> + + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ᱰᱟᱣᱱᱞᱚᱰ ᱰᱤᱜᱟᱹᱣᱮᱱᱟ</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ᱰᱟᱣᱱᱞᱚᱰ (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ᱰᱟᱣᱱᱞᱚᱰ</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ᱵᱟᱹᱰᱨᱟᱹ</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ᱱᱚᱶᱟ ᱞᱮᱠᱟᱱᱟ ᱨᱮᱫ ᱵᱟᱭ ᱰᱟᱣᱱᱞᱚᱰ ᱫᱟᱲᱮᱜ ᱠᱟᱱᱟᱭ</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ᱨᱮᱫ ᱵᱟᱝ ᱠᱷᱩᱞᱟᱹ ᱫᱟᱲᱮᱞᱟᱱᱟ</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ᱨᱮᱫ ᱠᱚ ᱠᱷᱩᱞᱟᱹ ᱞᱟᱹᱜᱤᱫ ᱪᱮᱫ ᱦᱚᱸ ᱮᱯ ᱵᱟᱝ ᱧᱟᱢ ᱞᱟᱱᱟ</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">ᱛᱷᱩᱠᱩᱢ ᱢᱮ</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ᱫᱩᱦᱲᱟᱹ ᱮᱦᱚᱵᱽ</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ᱵᱟᱹᱰᱨᱟᱹ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ᱡᱷᱚᱡᱽ ᱢᱮ</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ᱫᱚᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">ᱵᱚᱸᱫᱚᱭ ᱢᱮ</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ᱠᱟᱹᱢᱤ ᱠᱚ ᱪᱟᱵᱟᱭ ᱢᱮ</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ᱵᱟᱝ ᱠᱷᱩᱞᱟᱹ ᱫᱟᱲᱮᱞᱟᱱᱟ</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ᱨᱮᱫ ᱰᱟᱣᱱᱞᱚᱰ ᱞᱟᱹᱜᱤᱫ ᱨᱮᱫ ᱟᱨ ᱢᱮᱰᱤᱭᱟ ᱯᱟᱹᱨᱢᱤᱥᱚᱱ ᱟᱫᱮᱨ ᱫᱚᱨᱠᱟᱨ ᱾ ᱮᱱᱰᱨᱚᱭᱮᱰ ᱥᱟᱡᱟᱣ ᱛᱮ ᱪᱟᱞᱟᱜ ᱢᱮ, ᱯᱟᱹᱨᱢᱤᱥᱚᱱ ᱨᱮ ᱴᱤᱯᱟᱹᱣ ᱢᱮ, ᱟᱨ ᱮᱢ ᱪᱷᱚ ᱨᱮ ᱴᱤᱯᱟᱹᱣ ᱢᱮ ᱾</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">ᱱᱤᱡᱚᱨᱟᱜ ᱰᱟᱣᱱᱞᱚᱰ ᱠᱚ ᱵᱟᱹᱰᱨᱟᱹᱭᱟᱢ ᱥᱮ?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ᱡᱤᱫᱤ ᱟᱢ ᱱᱤᱛ ᱨᱮ ᱡᱷᱚᱛᱚ ᱴᱮᱵᱽ ᱠᱚ ᱵᱚᱸᱫᱚᱭᱟᱢ, %1$s ᱰᱟᱣᱱᱞᱚᱰ ᱠᱚ ᱵᱟᱹᱛᱤᱞᱚᱜᱼᱟ ᱾ ᱟᱢ ᱪᱮᱫ ᱡᱷᱚᱛᱚ ᱞᱮᱠᱷᱟᱛᱮ ᱯᱨᱟᱭᱣᱮᱴ ᱵᱽᱨᱟᱣᱡᱤᱝ ᱟᱲᱟᱜ ᱥᱮᱱᱟᱢ ᱠᱟᱱᱟ ᱥᱮ?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ᱰᱟᱣᱱᱞᱚᱰ ᱠᱚ ᱵᱟᱹᱰᱨᱟᱹᱭ ᱢᱮ</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">ᱯᱨᱟᱭᱣᱮᱴ ᱵᱽᱨᱟᱣᱡᱤᱝ ᱨᱮ ᱛᱟᱦᱮᱸᱱ ᱢᱮ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sc/strings.xml new file mode 100644 index 0000000000..e4f9e4f188 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sc/strings.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Iscarrigamentos</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Pàusa in s’iscarrigamentu</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Iscarrigamentu cumpletu</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Faddina in s’iscarrigamentu</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Iscarrigamentu (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Iscàrriga</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Annulla</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s non podet iscarrigare custa genia de archìviu</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Impossìbile abèrrere s’archìviu</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nissuna aplicatzione agatada pro abèrrere is archìvios %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pàusa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Avia</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Annulla</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Aberi</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Torra a proare</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Serra</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Cumpleta s’atzione cun</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Impossìbile abèrrere %1$s</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Boles annullare totu is iscarrigamentos privados?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Si serras immoe totu is ischedas de navigatzione privada, %1$s iscarrigamentu at a èssere annulladu. Seguru chi boles lassare sa navigatzione privada?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Annulla s’iscarrigamentu</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Abarra in sa modalidade de navigatzione privada</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-si/strings.xml new file mode 100644 index 0000000000..2a786c68fd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-si/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">බාගැනීම්</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">බාගැනීමේ විරාමයකි</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">බාගැනීම සම්පූර්ණයි</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">බාගැනීමට අසමත්!</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">(%1$s) බාගන්න</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">බාගන්න</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">අවලංගු</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s සඳහා මෙම ගොනු වර්ගය බාගැනීමට නොහැකිය</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ගොනුව ඇරීමට නොහැකිය</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ගොනු විවෘත කිරීමට යෙදුමක් හමු නොවිණි</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">විරාමයක්</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">නැවතත්</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">අවලංගු</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">අරින්න</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">නැවත</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">වසන්න</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">මෙය භාවිතයෙන් සිදු කරන්න</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s විවෘත කළ නොහැකිය</string> + + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ගොනු බාගැනීම සඳහා ගොනු සහ මාධ්ය වෙත ප්රවේශ අවසරය වුවමනාය. ඇන්ඩ්රොයිඩ් සැකසුම් වෙත ගොස්, අවසර -> ඉඩ දෙන්න තට්ටු කරන්න.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">පෞද්. බාගැනීම් අවලංගු කරන්නද?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ඔබ දැන් සියළුම පෞද්. පටිති වසා දැමුවහොත්, %1$s බාගැනීම අවලංගු වනු ඇත. ඔබට මෙය හැරයාමට අවශ්ය බව විශ්වාසද?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">බාගැනීම අවලංගු</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">පෞද්. පිරික්සීමෙහි රැඳෙන්න</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000..ea55916a75 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sk/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Stiahnuté súbory</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Sťahovanie je pozastavené</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Sťahovanie je dokončené</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Sťahovanie zlyhalo</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Stiahnuť (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Stiahnuť</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Zrušiť</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Aplikácia %1$s nedokáže stiahnuť tento typ súboru</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Nepodarilo sa otvoriť súbor</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nenašla sa žiadna aplikácia, ktorá by dokázala otvoriť súbor %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pozastaviť</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Pokračovať</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Zrušiť</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Otvoriť</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Skúsiť znova</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zavrieť</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dokončiť akciu použitím</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nepodarilo sa otvoriť %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Na stiahnutie súborov je potrebné povolenie prístupu k súborom a médiám. Ak ho chcete povoliť, prejdite do sekcie Povolenia v nastaveniach systému Android.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Zrušiť súkromné sťahovanie?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ak teraz zatvoríte všetky súkromné karty, sťahovanie súboru %1$s sa zruší. Naozaj chcete opustiť súkromné prehliadanie?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Zrušiť sťahovanie</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Zostať v režime Súkromné prehliadanie</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-skr/strings.xml new file mode 100644 index 0000000000..ff8515d020 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-skr/strings.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ڈاؤن لوڈاں</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ڈاؤن لوڈ رک ڳیا</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ڈاؤن لوڈ مکمل تھی ڳیا</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ڈاؤن لوڈ ناکام تھیا</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">ڈاؤن لوڈ(%1$s) </string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ڈاؤن لوڈ</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">منسوخ</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ایہ فائل قسم ڈاؤن لوڈ کائنی کر سڳدا</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">فائل کائنی کھول سڳا</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s فائلاں کھولݨ کیتے کوئی ایپ کائنی لبھی</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">ذرا روکو</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ولدا جاری کرو</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">منسوخ</string> + + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">کھولو</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ولدا کوشش کرو</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">بند کرو</string> + + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ایں کوں ورتݨ نال عمل پورا کرو</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s کھولݨ وچ ناکام ریہا</string> + + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">فائلاں کوں ڈاؤن لوڈ کرݨ کیتے فائلاں تے میڈیا دی اجازت تائیں رسائی دی لوڑ ہے۔ اینڈرائیڈ ترتیباں تے ون٘ڄو، اجازتاں تے انگل پھیرو تے اجازت ݙیوو تے انگل پھیرو۔</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">نجی ڈاؤن لوڈاں منسوخ کروں؟</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">جے تساں ہݨ ساریاں نجی ٹیباں بند کریندے ہو، %1$s ڈوان لوڈ منسوخ کر ݙتی ویسی۔ بھل ا تہاکوں پک ہے جو تساں نجی براؤزنگ چھوڑݨ چاہندے ہو؟</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ڈاؤن لوڈ منسوخ کرو</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">نجی براؤزنگ وچ راہوو</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000000..2477f39b91 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sl/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Prenosi</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Prenos zaustavljen</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Prenos dokončan</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Prenos neuspešen</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Prenesi (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Prenesi</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Prekliči</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ne more prenesti te vrste datoteke</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Datoteke ni bilo mogoče odpreti</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ni aplikacije za odpiranje datotek %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Premor</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Nadaljuj</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Prekliči</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Odpri</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Poskusi znova</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Zapri</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dokončaj dejanje z</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ni mogoče odpreti %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Za prenašanje datotek so potrebna dovoljenja za dostop do datotek in predstavnosti. V nastavitvah sistema Android tapnite Dovoljenja in Dovoli.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Prekliči zasebne prenose?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Če zdaj zaprete vse zasebne zavihke, bo preklican prenos %1$s. Ste prepričani, da želite zapustiti zasebno brskanje?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Prekliči prenose</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ostani v zasebnem brskanju</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000..fc96e4b812 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sq/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Shkarkime</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Shkarkim i ndalur</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Shkarkim i plotësuar</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Shkarkim i dështuar</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Shkarkim (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Shkarkoje</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Anuloje</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s s’mund ta shkarkojë këtë lloj kartelash</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">S’hapet dot kartelë</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">S’u gjet aplikacion për hapje kartelash %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Ndale</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Vazhdoje</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Anuloje</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Hape</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Riprovo</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Mbylle</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Plotësoje veprimin duke përdorur</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">S’arrihet të hapet %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Leje mbi kartela dhe media të nevojshme për të shkarkuar kartela. Kaloni te rregullimet e Android-it, prekni Leje dhe prekni Lejoje.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Të anulohen shkarkimet private?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Nëse i mbyllni tani krejt skedat Private, do të anulohet shkarkimi %1$s. Jeni i sigurt se doni të dilni nga mënyra e Shfletimit Privat?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Anuloji shkarkimet</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Qëndro në shfletim privat</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000000..7cd3c3ee6a --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sr/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Преузимања</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Преузимање паузирано</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Преузимање завршено</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Преузимање неуспешно</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Преузми (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Преузми</string> + + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Откажи</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s не може преузети ову врсту датотеке</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Није могуће отворити датотеку</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Није пронађена апликација за отварање %1$s датотека</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Паузирај</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Настави</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Откажи</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Отвори</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Покушај поново</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Затвори</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Доврши акцију користећи</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Није могуће отворити %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">За преузимање датотека потребан је приступ датотекама и медијима. Идите на Android подешавања, изаберите Дозволе, а затим Дозволи.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Отказати приватна преузимања?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Ако затворите све приватне језичке сада, преузимања (укупно %1$s) биће отказана. Да ли сигурно желите напустити приватно прегледање?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Откажи преузимања</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Остани у приватном прегледању</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-su/strings.xml new file mode 100644 index 0000000000..73528dccc5 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-su/strings.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Undeuran</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Ngundeur direureuhkeun</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Ngundeur anggeus</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Ngundeur gagal</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Undeur (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Undeur</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Bolay</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s teu bisa ngundeur tipe berkas kieu</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Teu bisa muka berkas</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Teu manggih aplikasi pikeun muka berkas %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Reureuh</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Teruskeun</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Bolay</string> + + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Buka</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Pecakan Deui</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Tutup</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Anggeuskeun peta maké</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Teu bisa muka %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Butuh aksés idin berkas jeung média pikeun ngundeur berkas. Buka setélan Android, toél idin, laju pilih idinan.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Bedokeun ngundeur nyamuni?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Upama anjeun nutup sakabéh tab Nyamuni ayeuna, %1$s undeuran bakal bedo. Yakin rék ninggalkeun Pamaluruhan Nyamuni?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Bolaykeun ngundeur</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Angger dina langlangan nyamuni</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sv-rSE/strings.xml new file mode 100644 index 0000000000..44bb37134c --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-sv-rSE/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Hämtningar</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Hämtning pausad</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Hämtning slutförd</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Hämtningen misslyckades</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Hämta (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Hämta</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Avbryt</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s kan inte ladda ner den här filtypen</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Det gick inte att öppna filen</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ingen app hittades för att öppna %1$s-filer</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pausa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Återuppta</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Avbryt</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Öppna</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Försök igen</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Stäng</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Slutför åtgärden med</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kunde inte öppna %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Åtkomst till filer och media krävs för att ladda ner filer. Gå till Android-inställningar, tryck på behörigheter och tryck på tillåt.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Vill du avbryta privata nedladdningar?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Om du stänger alla privata flikar nu avbryts nedladdningen av %1$s. Är du säker på att du vill lämna privat surfning?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Avbryt nedladdningar</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stanna i privat surfning</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ta/strings.xml new file mode 100644 index 0000000000..76a5bfbe6e --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ta/strings.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">பதிவிறக்கங்கள்</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">பதிவிறக்கம் இடைநிறுத்தப்பட்டது</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">பதிவிறக்கம் முடிந்தது</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">பதிவிறக்கம் தோல்வியடைந்தது</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">பதிவிறக்கவா (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">பதிவிறக்கு</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">இரத்து செய்</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s இக்கோப்பு வகையைப் பதிவிறக்க இயலவில்லை</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">கோப்பைத் திறக்க முடியவில்லை</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s கோப்புகளைத் திறப்பதற்கான செயலிகள் இல்லை</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">இடைநிறுத்து</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">தொடர்க</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ரத்துசெய்</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">திற</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">மீண்டும் முயற்சிக்கவும்</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">மூடு</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">பயன்படுத்தி செயற்பாட்டை முடி</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s திறக்க முடியவில்லை</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000..f335a2e3e8 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-te/strings.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">దింపుకోళ్ళు</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">దింపుకోలు నిలిపివేయబడింది</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">దింపుకోలు పూర్తయ్యింది</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">దింపుకోలు విఫలమైంది</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">దింపుకోలు (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">దించుకో</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">రద్దుచేయి</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">ఈ రకపు ఫైలును %1$s దించుకోలేదు</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ఫైలును తెరవలేకపోయాం</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ఫైళ్ళను తెరవడానికి తగ్గ అనువర్తనం కనబడలేదు</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">నిలిపివేయి</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">కొనసాగించు</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">రద్దుచేయి</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">తెరువు</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">మళ్ళీ ప్రయత్నించు</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">మూసివేయి</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">దీన్ని వాడి చర్యను పూర్తిచేయి</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$sను తెరవలేకున్నాం</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">అంతరంగిక దింపుకోళ్ళను రద్దుచేయాలా?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">దింపుకోళ్ళను రద్దుచేయి</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">అంతరంగిక విహారణలోనే ఉండు</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tg/strings.xml new file mode 100644 index 0000000000..0ba8cc1054 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tg/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Боргириҳо</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Боргирӣ таваққуф карда шуд</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Боргирӣ анҷом ёфт</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Боргирӣ иҷро нашуд</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Боргирӣ (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Боргирӣ кардан</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Бекор кардан</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ин навъи файлро боргирӣ карда наметавонад</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Файлро кушода натавонист</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Ягон барномае барои кушодани файлҳои %1$s ёфт нашуд</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Таваққуф кардан</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Давом додан</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Бекор кардан</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Кушодан</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Аз нав кӯшиш кардан</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Пӯшидан</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Иҷро кардани амал ба воситаи</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s кушода намешавад</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Барои боргирӣ кардани файлҳо иҷозати дастрасӣ ба файлҳо ва расонаҳо лозим аст. Ба танзимоти Android гузаред, иҷозатҳоро ламс кунед ва иҷозат диҳед.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Боргириҳои хусусиро бекор мекунед?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Агар шумо ҳозир ҳамаи равзанаҳои хусусиро пӯшед, боргирии %1$s бекор карда мешавад. Шумо мутмаин ҳастед, ки мехоҳед тамошокунии хусусиро тарк кунед?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Бекор кардани боргириҳо</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Истодан дар тамошокунии хусусӣ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..70f626e69f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-th/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">การดาวน์โหลด</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">การดาวน์โหลดถูกหยุดชั่วคราว</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">การดาวน์โหลดเสร็จสมบูรณ์</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">การดาวน์โหลดล้มเหลว</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">การดาวน์โหลด (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ดาวน์โหลด</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">ยกเลิก</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s ไม่สามารถดาวน์โหลดไฟล์ชนิดนี้</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ไม่สามารถเปิดไฟล์</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">ไม่พบแอปที่เปิดไฟล์ %1$s ได้</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">หยุดชั่วคราว</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">ดาวน์โหลดต่อ</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">ยกเลิก</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">เปิด</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">ลองอีกครั้ง</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">ปิด</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">ดำเนินการโดยใช้</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">ไม่สามารถเปิด %1$s ได้</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">จำเป็นต้องมีสิทธิ์เข้าถึงไฟล์และสื่อเพื่อดาวน์โหลดไฟล์ ไปยังการตั้งค่า Android แตะสิทธิอนุญาต แล้วแตะอนุญาต</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">ยกเลิกการดาวน์โหลดส่วนตัว?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">หากคุณปิดแท็บท่องเว็บแบบส่วนตัวทั้งหมดตอนนี้ %1$s การดาวน์โหลดจะถูกยกเลิก คุณแน่ใจหรือไม่ว่าต้องการออกจากการท่องเว็บแบบส่วนตัว?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">ยกเลิกการดาวน์โหลด</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">คงอยู่ในการท่องเว็บแบบส่วนตัว</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tl/strings.xml new file mode 100644 index 0000000000..b08a9a75ce --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tl/strings.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Mga Download</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Naka-pause ang pag-download</string> + + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Kumpleto na ang download</string> + + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Nabigo ang pag-download</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Mag-download (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">i-Download</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Kanselahin</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Hindi ma-download ng %1$s ang uri ng file na ito</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Hindi mabuksan ang file</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Walang nahanap na app upang buksan ang %1$s na mga file</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">i-Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Ipagpatuloy</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Kanselahin</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Buksan</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Subukan Muli</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Isara</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Kumpletuhin ang pagkilos gamit ang</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Hindi mabuksan ang %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Kailangan ng pahintulot sa pag-access ng mga file at media upang ma-download ang mga file. Pumunta sa Android settings, pindutin ang permissions, at pindutin ang allow.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">I kansela ang pag download sa Private?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">I kansela and download</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Manatili sa Pribadong pag Browse</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000000..98e4da0d6f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tr/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">İndirilenler</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">İndirme duraklatıldı</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">İndirme tamamlandı</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">İndirme başarısız oldu</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">İndir (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">İndir</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">İptal</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s bu dosya türünü indiremiyor</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Dosya açılamadı</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s dosyalarını açacak uygulama bulunamadı</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Duraklat</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Sürdür</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">İptal</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Aç</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Yeniden dene</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Kapat</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Eylemi şununla tamamla</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s açılamıyor</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Dosya indirmek için dosyalar ve medya erişimine izin vermeniz gerekiyor. Android ayarlarından izinlere girerek izin verin.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Gizli indirmeler iptal edilsin mi?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Gizli sekmeleri kapatırsanız %1$s indirme işlemi iptal edilecek. Gizli gezintiden çıkmak istediğinize emin misiniz?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">İndirmeleri iptal et</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Gizli gezintiyi sürdür</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-trs/strings.xml new file mode 100644 index 0000000000..ec98213be7 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-trs/strings.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Nej sa nadunïnjt</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Giyi\'chin\' riña sa nadunïnjt</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Ngà gisîj nahuij nadunin</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Nu ga\'ue nādunin</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Nādūnïnj (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Nādunïnj</string> + + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Dūyichin\'</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s na\'uēj nādūnïnj archibô huā dānanj</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Nu ga\'ue nāyī\'nin archîbo</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Nitāj aplikasiûn nikājt da\' nā\'nïn riña nej archibô %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Dūnikïn\' akuan\'</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Nāyì\'ì ñû</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Dūyichin\'</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Nā\'nīn</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Ginùn huin ñû</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Nārán</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Dūnahuij sa \'iát ngà sa gu\'nàj</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Nu ga\’ue nāyi’nïn riña %1$s</string> + + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Achín nī’hia da’ gātū riña nej ārchîbo nī gā’hue nādunïn nej man. Guīj riña gā’hue nāgi’hiát sa màn riña Android, guru’man ra’a riña tāj sa achín nì’hiaj nī gūru’man ra’a gā’huēj.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Dūyichîn’t nej sa nadunïnj hùi raj.</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Sisī nāránt riña daran’ nej rakïj ñanj riña aché nun huît hìaj nī, nārè sa nadunïnjt riña %1$s. Ruhuâ yāngà’ gāhuīt riña aché nu huìt nan anj.</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Dūyichin’ nej sa nadunï̄njt</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Ginu ngè riña aché nun huìt</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tt/strings.xml new file mode 100644 index 0000000000..c48ffd0286 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tt/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Йөкләп алулар</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Йөкләп алу туктатылды</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Йөкләп алу тәмамланды</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Йөкләп алу уңышсыз тәмамланды</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Йөкләп алу (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Йөкләп алу</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Баш тарту</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s бу файл төрен йөкли алмый</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Файлны ачып булмады</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s файлларын ачу өчен кушымта табылмады</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Туктату</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Дәвам итү</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Баш тарту</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ачу</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Янәдән тырышып карау</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Ябу</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Түбәндәгене кулланып гамәлне тәмамлау</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s кушымтасын ачу мөмкин түгел</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Файлларны йөкләп алу өчен файллардан һәм медиадан файдалану рөхсәте кирәк. Android көйләүләренә кереп, "рөхсәтләр" дигән битне ачып, "рөхсәт итү" дигәнгә басыгыз.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Шәхси иңдерүләрдән баш тартырга телисезме?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Йөкләүдән баш тарту</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Хосусый гизү режимында калу</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tzm/strings.xml new file mode 100644 index 0000000000..d690959857 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-tzm/strings.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Agamen</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Agam ibedden</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Agem (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Agem</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">ur izmir %1$s ad d-yagem anaw n ufaylu-a</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Ur izmir ad irẓem afaylu</string> + + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Rẓem</string> + + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Arm daɣ</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Mḍel</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Ur izmir ad irẓem %1$s</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ug/strings.xml new file mode 100644 index 0000000000..62a2546c82 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ug/strings.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">چۈشۈرۈلمىلەر</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">چۈشۈرۈش توختىتىلدى</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">چۈشۈرۈش تاماملاندى</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">چۈشۈرۈش مەغلۇپ بولدى</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">چۈشۈرۈش(%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">چۈشۈرۈش</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">بىكار قىلىش</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s بۇ تىپتىكى ھۆججەتنى چۈشۈرەلمەيدۇ</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">ھۆججەتنى ئاچالمىدى</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$sھۆججىتىنى ئاچىدىغان ئەپ تېپىلمىدى</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">توختىتىش</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">داۋاملاشتۇرۇش</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">بىكار قىلىش</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">ئېچىش</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">قايتا سىناڭ</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">تاقاش</string> + + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">تۆۋەندىكى ئەپتە مەشغۇلاتنى تاماملايدۇ</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app"> %1$sنى ئاچالمىدى</string> + + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">ھۆججەت چۈشۈرۈشتە ھۆججەت ۋە ۋاسىتە زىيارەت قىلىش ئىجازىتىگە ئېھتىياجلىق. ئاندىرويىد تەڭشەككە يۆتكىلىپ، ھوقۇقنى چېكىپ، يول قوينى چېكىڭ.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">شەخسىي چۈشۈرۈشتىن ۋاز كېچەمدۇ؟</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">ئەگەر ھازىر ھەممە شەخسىيەت بەتكۈچنى تاقىسىڭىز، %1$s چۈشۈرۈشتىن ۋاز كېچىدۇ. راستىنلا شەخسىي زىيارەت ھالىتىدىن چېكىنەمسىز؟</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">چۈشۈرۈشنى بىكار قىلىش</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">شەخسىي زىيارەت ھالىتىدە قالدۇر</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..7424f8a852 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-uk/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Завантаження</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Завантаження призупинено</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Завантаження завершено</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Завантаження невдале</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Завантажити (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Завантажити</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Скасувати</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s не може завантажити цей тип файлу</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Не вдається відкрити файл</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Не знайдено програми, щоб відкрити файли %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Призупинити</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Продовжити</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Скасувати</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Відкрити</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Спробувати знову</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Закрити</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Завершити дію за допомогою</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Не вдалося відкрити %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Доступ до файлів та медіафайлів необхідний для завантаження файлів. Перейдіть до налаштувань Android, торкніться дозволів і торкніться дозволити.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Скасувати приватні завантаження?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Якщо ви закриєте всі приватні вкладки, завантаження %1$s буде скасовано. Ви дійсно хочете вийти з режиму приватного перегляду?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Скасувати завантаження</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Залишитись в режимі приватного перегляду</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ur/strings.xml new file mode 100644 index 0000000000..2c9abb21b0 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-ur/strings.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">ڈاؤن لوڈز</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">ڈاؤن لوڈ موقوف ہوا</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">ڈاؤن لوڈ مکمل</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">ڈاؤن لوڈ ناکام</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">(%1$s) ڈاؤن لوڈ کریں</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">ڈاؤن لوڈ</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">منسوخ کریں</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s اس قسم کی فائل کو ڈاؤن لوڈ نہیں کرسکتے ہیں</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">فائل نہیں کھول سکتے</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s فائلوں کو کھولنے کے لئے کوئی ایپ نہیں ملا</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">توقف کریں</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">پھر جاری کریں</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">منسوخ کریں</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">کھولیں</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">دوبارہ کوشش کریں</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">بند کریں</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">عمل مکمل کریں بمع</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s کھولنے میں ناکام رہا</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">نجی ڈاؤن لوڈز کو منسوخ کریں؟</string> + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-uz/strings.xml new file mode 100644 index 0000000000..97274f3ac5 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-uz/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Yuklanmalar</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Yuklanma pauza qilindi</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Yuklab olindi</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Yuklab olinmadi</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Yuklab olish (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Yuklab olish</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Bekor qilish</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Bu turdagi faylni %1$s yuklab olmaydi</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Fayl ochilmadi</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">%1$s ta faylni ochish uchun hech qanday ilova topilmadi</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pauza</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Davom etish</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Bekor qilish</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ochish</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Yana urinish</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Yopish</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Harakatni quyidagidan foydalanib tugatish</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">%1$s ochib boʻlmadi</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Fayllarni yuklab olish uchun fayllar va mediaga kirish ruxsati kerak. Android sozlamalariga oʻting, ruxsatnomalar va ruxsat berish tugmasini bosing.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Maxfiy yuklanmalar bekor qilinsinmi?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Barcha maxfiy varaqlarni yopsangiz, %1$s ta yuklanma bekor qilinadi. Maxfiy rejimdan chiqishni istaysizmi?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Yuklanmalarni bekor qilish</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Maxfiy rejimda qolaman</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-vec/strings.xml new file mode 100644 index 0000000000..7dc177ba61 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-vec/strings.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Download en pausa</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Download conpletà</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Download falìo</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Download</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Anuƚa</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">Non xe posibiƚe scargare sto tipo de file en %1$s</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">No xe posibiƚe vèrxere el file</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pauxa</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Ricomisìa</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Anuƚa</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Vèrxi</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Prova ancora</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Sara su</string> + + </resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..ca208ff304 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-vi/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Tải xuống</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Đã tạm dừng tải xuống</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Đã hoàn tất tải xuống</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Có lỗi khi tải xuống</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Tải xuống (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Tải xuống</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Hủy bỏ</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s không thể tải xuống loại tập tin này</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Không thể mở tập tin</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Không tìm thấy ứng dụng nào để mở %1$s</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Tạm dừng</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Tiếp tục</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Hủy bỏ</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Mở</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Thử lại</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Đóng</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Hoàn thành hành động bằng</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Không thể mở %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Cần có quyền truy cập vào tập tin và phương tiện để tải tập tin xuống. Đi tới cài đặt Android, nhấn vào quyền và nhấn cho phép.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Hủy các tải xuống riêng tư?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Nếu bạn đóng tất cả các thẻ riêng tư ngay bây giờ, quá trình tải xuống %1$s sẽ bị hủy. Bạn có chắc chắn muốn thoát khỏi duyệt web riêng tư không?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Hủy tải xuống</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Vẫn ở lại chế độ duyệt web riêng tư</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-yo/strings.xml new file mode 100644 index 0000000000..dc2831d64f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-yo/strings.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Àwọn ìgbàsílẹ̀</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Ìdádúró ráńpẹ́ fún ìgbàsílẹ̀</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Ìgbàsílẹ̀ ti parí</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Ìgbàsílẹ̀ kùnà</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Ìgbàsílẹ̀ (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Ṣe ìgbàsílẹ̀</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Parẹ́</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s kò le ṣe ìgbàsílẹ̀ fún irúfẹ́́ fáìlì yí</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Kò le ṣí fáìlì</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">Kò ṣàwárí áàpù tí a lè fi ṣí %1$s fáìlì</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Ìdádúró ráńpẹ́</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Tún bẹ̀rẹ̀</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Parẹ́</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Ṣí</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Gbìyànjú Si </string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Padé</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Parí ìgbésẹ̀ pẹ̀lú lílo</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Kò ṣe é sí %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Ó nílò ìgbàláàyè fáìlì àti ìkànnì láti ṣe ìgbàsílẹ̀ àwọn fáìlì. Lọ sí orí àwọn ètò Áńdíróìdì, tẹ àwọn ìgbàláàyè, ko sì tẹ gbà láàyè.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Pa àwọn ìgbàsílẹ̀ ìkọ̀kọ̀ rẹ́?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">Tí o bá pa gbogbo àwọn táàbù ìkọ̀kọ̀ báyìí, %1$s ìgbàsílẹ̀ yóó paarẹ́. Ṣe ó dá ọ lójú pé o fẹ́ fi àyẹ̀wò ìkọ̀kọ̀ re kalẹ̀?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Pa àwọn ìgbàsílẹ̀ rẹ́</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Dúró sí àyẹ̀wò ìkọ̀kọ̀</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..fd5ce5ba85 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">下载</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">下载已暂停</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">下载完毕</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">下载失败</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">下载(%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">下载</string> + + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">取消</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s 无法下载此种文件类型</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">无法打开文件</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">找不到可用于打开 %1$s 文件的应用程序</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">暂停</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">继续</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">取消</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">打开</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">重试</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">关闭</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">使用下列应用完成操作</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">无法打开 %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">下载文件需要文件和媒体访问权限。请前往 Android 设置,点按“权限”,并选择“允许”。</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">要取消隐私下载吗?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">若您现在关闭所有隐私浏览标签页,%1$s 个下载将被取消。确定要离开隐私浏览模式吗?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">取消下载</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">留在隐私浏览模式</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..ce88358dfd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">下載項目</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">已暫停下載</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">下載完成</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">下載失敗</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">下載(%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">下載</string> + + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">取消</string> + + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s 無法下載此類檔案</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">無法開啟檔案</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">找不到可用來開啟 %1$s 檔案的應用程式</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">暫停</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">繼續</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">取消</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">開啟</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">再試一次</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">關閉</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">使用下列軟體完成操作</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">無法開啟 %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">需要有檔案與媒體存取權限才能下載檔案。請到 Android 設定當中的「權限」點擊「允許」。</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">要取消隱私下載項目嗎?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">如果您現在關閉所有隱私瀏覽分頁,將會取消 %1$s 項下載工作,確定要離開隱私瀏覽模式嗎?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">取消下載</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">留在隱私瀏覽模式</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values/colors.xml new file mode 100644 index 0000000000..2c92281f2f --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + <color name="mozac_feature_downloads_notification">#607D8B</color> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/values/strings.xml new file mode 100644 index 0000000000..8018b4abee --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/values/strings.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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> + <!-- Name of the "notification channel" used for displaying download notification. See https://developer.android.com/training/notify-user/channels --> + <string name="mozac_feature_downloads_notification_channel">Downloads</string> + + <!-- Text shown on the second row of a paused download notification. --> + <string name="mozac_feature_downloads_paused_notification_text">Download paused</string> + <!-- Text shown on the second row of an completed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_completed_notification_text2">Download completed</string> + <!-- Text shown on the second row of an failed download notification. The filename is shown on the first row. --> + <string name="mozac_feature_downloads_failed_notification_text2">Download failed</string> + + <!-- Alert dialog confirmation before download a file, this is the title. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_dialog_title2">Download (%1$s)</string> + <!-- Alert dialog confirmation before download a file, this is the positive action. --> + <string name="mozac_feature_downloads_dialog_download">Download</string> + <!-- Alert dialog confirmation before download a file, this is the negative action. --> + <string name="mozac_feature_downloads_dialog_cancel">Cancel</string> + <!-- Error shown when the user is trying to download a invalid file. %1$s will be replaced with the name of the app. --> + <string name="mozac_feature_downloads_file_not_supported2">%1$s can’t download this file type</string> + + <!-- Message that appears when the downloaded file could not be opened--> + <string name="mozac_feature_downloads_could_not_open_file">Could not open file</string> + + <!-- Message that appears when the downloaded file is a specific type that Android doesn't support opening--> + <string name="mozac_feature_downloads_open_not_supported1">No app found to open %1$s files</string> + + <!-- Button that pauses the download when pressed --> + <string name="mozac_feature_downloads_button_pause">Pause</string> + <!-- Button that resumes the download when pressed --> + <string name="mozac_feature_downloads_button_resume">Resume</string> + <!-- Button that cancels the download when pressed --> + <string name="mozac_feature_downloads_button_cancel">Cancel</string> + <!-- Button that opens the downloaded file when pressed --> + <string name="mozac_feature_downloads_button_open">Open</string> + <!-- Button that restarts the download after a failed attempt --> + <string name="mozac_feature_downloads_button_try_again">Try Again</string> + + <!-- Content description for close button --> + <string name="mozac_feature_downloads_button_close">Close</string> + <!-- Title for the third party download app chooser dialog --> + <string name="mozac_feature_downloads_third_party_app_chooser_dialog_title">Complete action using</string> + <!-- Message that appears when trying to download with an external app fails. %1$s will be replaced with the name the external app. -->--> + <string name="mozac_feature_downloads_unable_to_open_third_party_app">Unable to open %1$s</string> + <!-- Text for the info dialog when write to storage permissions have been denied but user tries to download a file. --> + <string name="mozac_feature_downloads_write_external_storage_permissions_needed_message">Files and media permission access needed to download files. Go to Android settings, tap permissions, and tap allow.</string> + + <!-- Alert dialog confirmation before cancelling downloads, this is the title --> + <string name="mozac_feature_downloads_cancel_active_downloads_warning_content_title">Cancel private downloads?</string> + <!-- Alert dialog confirmation before cancelling private downloads, this is the body. %1$s will be replaced with the name of the file. --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_warning_content_body">If you close all Private tabs now, %1$s download will be canceled. Are you sure you want to leave Private Browsing?</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the positive action. --> + <string name="mozac_feature_downloads_cancel_active_downloads_accept">Cancel downloads</string> + <!-- Alert dialog confirmation before cancelling downloads, this is the negative action. Leaves user in Private browsing --> + <string name="mozac_feature_downloads_cancel_active_private_downloads_deny">Stay in private browsing</string> +</resources> diff --git a/mobile/android/android-components/components/feature/downloads/src/main/res/xml/feature_downloads_file_paths.xml b/mobile/android/android-components/components/feature/downloads/src/main/res/xml/feature_downloads_file_paths.xml new file mode 100644 index 0000000000..85507b5f6d --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/main/res/xml/feature_downloads_file_paths.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> +<paths> + <external-path name="Download" path="." /> + + <!-- Offer access only to files under Context.getCacheDir()/media_share_cache/ --> + <cache-path name="mediaShareCache" path="mozac_share_cache"/> +</paths> diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt new file mode 100644 index 0000000000..f95c4438ad --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt @@ -0,0 +1,2155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.app.DownloadManager +import android.app.DownloadManager.EXTRA_DOWNLOAD_ID +import android.app.Notification +import android.app.NotificationManager +import android.app.Service +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.FileProvider +import androidx.core.content.getSystemService +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED +import mozilla.components.browser.state.state.content.DownloadState.Status.DOWNLOADING +import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED +import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.MutableHeaders +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_CANCEL +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_PAUSE +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_REMOVE_PRIVATE_DOWNLOAD +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_RESUME +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_TRY_AGAIN +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.PROGRESS_UPDATE_INTERVAL +import mozilla.components.feature.downloads.AbstractFetchDownloadService.CopyInChuckStatus.ERROR_IN_STREAM_CLOSED +import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState +import mozilla.components.feature.downloads.DownloadNotification.NOTIFICATION_DOWNLOAD_GROUP_ID +import mozilla.components.feature.downloads.facts.DownloadsFacts.Items.NOTIFICATION +import mozilla.components.support.base.android.NotificationsDelegate +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.processor.CollectionProcessor +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.eq +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.utils.ext.stopForegroundCompat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.isNull +import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doCallRealMethod +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.doThrow +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoInteractions +import org.mockito.MockitoAnnotations.openMocks +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements +import org.robolectric.shadows.ShadowNotificationManager +import java.io.File +import java.io.IOException +import java.io.InputStream +import kotlin.random.Random + +@RunWith(AndroidJUnit4::class) +@Config(shadows = [ShadowFileProvider::class]) +class AbstractFetchDownloadServiceTest { + + @Rule @JvmField + val folder = TemporaryFolder() + + // We need different scopes and schedulers because: + // - the service will continuously try to update the download notification using MainScope() + // - if using the same scope in tests the test won't end + // - need a way to advance main dispatcher used by the service. + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val mainDispatcher = coroutinesTestRule.testDispatcher + private val testsDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler()) + + @Mock private lateinit var client: Client + private lateinit var browserStore: BrowserStore + private lateinit var notificationManagerCompat: NotificationManagerCompat + + private lateinit var notificationsDelegate: NotificationsDelegate + + private lateinit var service: AbstractFetchDownloadService + + private lateinit var shadowNotificationService: ShadowNotificationManager + + @Before + fun setup() { + openMocks(this) + browserStore = BrowserStore() + + notificationManagerCompat = spy(NotificationManagerCompat.from(testContext)) + notificationsDelegate = NotificationsDelegate(notificationManagerCompat) + service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + doReturn(testContext).`when`(service).context + doNothing().`when`(service).useFileStream(any(), anyBoolean(), any()) + doReturn(true).`when`(notificationManagerCompat).areNotificationsEnabled() + + shadowNotificationService = + shadowOf(testContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) + } + + @Test + fun `begins download when started`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + assertEquals(download.url, providedDownload.value.state.url) + assertEquals(download.fileName, providedDownload.value.state.fileName) + + // Ensure the job is properly added to the map + assertEquals(1, service.downloadJobs.count()) + assertNotNull(service.downloadJobs[providedDownload.value.state.id]) + } + + @Test + fun `WHEN a download intent is received THEN handleDownloadIntent must be called`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val downloadIntent = Intent("ACTION_DOWNLOAD") + + doNothing().`when`(service).handleRemovePrivateDownloadIntent(any()) + doNothing().`when`(service).handleDownloadIntent(any()) + + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + service.onStartCommand(downloadIntent, 0, 0) + + verify(service).handleDownloadIntent(download) + verify(service, never()).handleRemovePrivateDownloadIntent(download) + } + + @Test + fun `WHEN an intent does not provide an action THEN handleDownloadIntent must be called`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val downloadIntent = Intent() + + doNothing().`when`(service).handleRemovePrivateDownloadIntent(any()) + doNothing().`when`(service).handleDownloadIntent(any()) + + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + service.onStartCommand(downloadIntent, 0, 0) + + verify(service).handleDownloadIntent(download) + verify(service, never()).handleRemovePrivateDownloadIntent(download) + } + + @Test + fun `WHEN a try again intent is received THEN handleDownloadIntent must be called`() = + runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val downloadIntent = Intent(ACTION_TRY_AGAIN) + + doNothing().`when`(service).handleRemovePrivateDownloadIntent(any()) + doNothing().`when`(service).handleDownloadIntent(any()) + + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + val newDownloadState = download.copy(status = DOWNLOADING) + browserStore.dispatch(DownloadAction.AddDownloadAction(newDownloadState)).joinBlocking() + + service.onStartCommand(downloadIntent, 0, 0) + + verify(service).handleDownloadIntent(newDownloadState) + assertEquals(newDownloadState.status, DOWNLOADING) + verify(service, never()).handleRemovePrivateDownloadIntent(newDownloadState) + } + + @Test + fun `WHEN a remove download intent is received THEN handleRemoveDownloadIntent must be called`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val downloadIntent = Intent(ACTION_REMOVE_PRIVATE_DOWNLOAD) + + doNothing().`when`(service).handleRemovePrivateDownloadIntent(any()) + doNothing().`when`(service).handleDownloadIntent(any()) + + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + service.onStartCommand(downloadIntent, 0, 0) + + verify(service).handleRemovePrivateDownloadIntent(download) + verify(service, never()).handleDownloadIntent(download) + } + + @Test + fun `WHEN handleRemovePrivateDownloadIntent with a private download is called THEN removeDownloadJob must be called`() { + val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = true) + val downloadJobState = DownloadJobState(state = downloadState, status = COMPLETED) + val browserStore = mock<BrowserStore>() + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + doAnswer { }.`when`(service).removeDownloadJob(any()) + + service.downloadJobs[downloadState.id] = downloadJobState + + service.handleRemovePrivateDownloadIntent(downloadState) + + verify(service, times(0)).cancelDownloadJob(downloadJobState) + verify(service).removeDownloadJob(downloadJobState) + verify(browserStore).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id)) + } + + @Test + fun `WHEN handleRemovePrivateDownloadIntent is called with a private download AND not COMPLETED status THEN removeDownloadJob and cancelDownloadJob must be called`() { + val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = true) + val downloadJobState = DownloadJobState(state = downloadState, status = DOWNLOADING) + val browserStore = mock<BrowserStore>() + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + doAnswer { }.`when`(service).removeDownloadJob(any()) + + service.downloadJobs[downloadState.id] = downloadJobState + + service.handleRemovePrivateDownloadIntent(downloadState) + + verify(service).cancelDownloadJob(downloadJobState) + verify(service).removeDownloadJob(downloadJobState) + verify(browserStore).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id)) + } + + @Test + fun `WHEN handleRemovePrivateDownloadIntent is called with with a non-private (or regular) download THEN removeDownloadJob must not be called`() { + val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = false) + val downloadJobState = DownloadJobState(state = downloadState, status = COMPLETED) + val browserStore = mock<BrowserStore>() + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + doAnswer { }.`when`(service).removeDownloadJob(any()) + + service.downloadJobs[downloadState.id] = downloadJobState + + service.handleRemovePrivateDownloadIntent(downloadState) + + verify(service, never()).removeDownloadJob(downloadJobState) + verify(browserStore, never()).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id)) + } + + @Test + fun `service redelivers if no download extra is passed `() = runTest(testsDispatcher) { + val downloadIntent = Intent("ACTION_DOWNLOAD") + + val intentCode = service.onStartCommand(downloadIntent, 0, 0) + + assertEquals(Service.START_REDELIVER_INTENT, intentCode) + } + + @Test + fun `verifyDownload sets the download to failed if it is not complete`() = runTest(testsDispatcher) { + val downloadState = DownloadState( + url = "mozilla.org/mozilla.txt", + contentLength = 50L, + currentBytesCopied = 5, + status = DOWNLOADING, + ) + + val downloadJobState = DownloadJobState( + job = null, + state = downloadState, + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 5, + status = DOWNLOADING, + ) + + service.verifyDownload(downloadJobState) + + assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState)) + verify(service).setDownloadJobStatus(downloadJobState, FAILED) + verify(service).updateDownloadState(downloadState.copy(status = FAILED)) + } + + @Test + fun `verifyDownload does NOT set the download to failed if it is paused`() = runTest(testsDispatcher) { + val downloadState = DownloadState( + url = "mozilla.org/mozilla.txt", + contentLength = 50L, + currentBytesCopied = 5, + status = DownloadState.Status.PAUSED, + ) + + val downloadJobState = DownloadJobState( + job = null, + state = downloadState, + currentBytesCopied = 5, + status = DownloadState.Status.PAUSED, + foregroundServiceId = 1, + downloadDeleted = false, + ) + + service.verifyDownload(downloadJobState) + + assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState)) + verify(service, times(0)).setDownloadJobStatus(downloadJobState, DownloadState.Status.FAILED) + verify(service, times(0)).updateDownloadState(downloadState.copy(status = DownloadState.Status.FAILED)) + } + + @Test + fun `verifyDownload does NOT set the download to failed if it is complete`() = runTest(testsDispatcher) { + val downloadState = DownloadState( + url = "mozilla.org/mozilla.txt", + contentLength = 50L, + currentBytesCopied = 50, + status = DOWNLOADING, + ) + + val downloadJobState = DownloadJobState( + job = null, + state = downloadState, + currentBytesCopied = 50, + status = DOWNLOADING, + foregroundServiceId = 1, + downloadDeleted = false, + ) + + service.verifyDownload(downloadJobState) + + assertNotEquals(FAILED, service.getDownloadJobStatus(downloadJobState)) + verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED) + verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED)) + } + + @Test + fun `verifyDownload does NOT set the download to failed if it is cancelled`() = runTest(testsDispatcher) { + val downloadState = DownloadState( + url = "mozilla.org/mozilla.txt", + contentLength = 50L, + currentBytesCopied = 50, + status = DownloadState.Status.CANCELLED, + ) + + val downloadJobState = DownloadJobState( + job = null, + state = downloadState, + currentBytesCopied = 50, + status = DownloadState.Status.CANCELLED, + foregroundServiceId = 1, + downloadDeleted = false, + ) + + service.verifyDownload(downloadJobState) + + assertNotEquals(FAILED, service.getDownloadJobStatus(downloadJobState)) + verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED) + verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED)) + } + + @Test + fun `verifyDownload does NOT set the download to failed if it is status COMPLETED`() = runTest(testsDispatcher) { + val downloadState = DownloadState( + url = "mozilla.org/mozilla.txt", + contentLength = 50L, + currentBytesCopied = 50, + status = DownloadState.Status.COMPLETED, + ) + + val downloadJobState = DownloadJobState( + job = null, + state = downloadState, + currentBytesCopied = 50, + status = DownloadState.Status.COMPLETED, + foregroundServiceId = 1, + downloadDeleted = false, + ) + + service.verifyDownload(downloadJobState) + + verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED) + verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED)) + } + + @Test + fun `verify that a COMPLETED download contains a file size`() { + val downloadState = DownloadState( + url = "mozilla.org/mozilla.txt", + contentLength = 0L, + currentBytesCopied = 50, + status = DOWNLOADING, + ) + val downloadJobState = DownloadJobState( + job = null, + state = downloadState, + currentBytesCopied = 50, + status = DOWNLOADING, + foregroundServiceId = 1, + downloadDeleted = false, + ) + + browserStore.dispatch(DownloadAction.AddDownloadAction(downloadState)).joinBlocking() + service.downloadJobs[downloadJobState.state.id] = downloadJobState + service.verifyDownload(downloadJobState) + browserStore.waitUntilIdle() + + assertEquals(downloadJobState.state.contentLength, service.downloadJobs[downloadJobState.state.id]!!.state.contentLength) + assertEquals(downloadJobState.state.contentLength, browserStore.state.downloads.values.first().contentLength) + } + + @Test + fun `broadcastReceiver handles ACTION_PAUSE`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + val pauseIntent = Intent(ACTION_PAUSE).apply { + setPackage(testContext.applicationContext.packageName) + putExtra(DownloadNotification.EXTRA_DOWNLOAD_ID, providedDownload.value.state.id) + } + + CollectionProcessor.withFactCollection { facts -> + service.broadcastReceiver.onReceive(testContext, pauseIntent) + + val pauseFact = facts[0] + assertEquals(Action.PAUSE, pauseFact.action) + assertEquals(NOTIFICATION, pauseFact.item) + } + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState)) + } + + @Test + fun `broadcastReceiver handles ACTION_CANCEL`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + val cancelIntent = Intent(ACTION_CANCEL).apply { + setPackage(testContext.applicationContext.packageName) + putExtra(DownloadNotification.EXTRA_DOWNLOAD_ID, providedDownload.value.state.id) + } + + assertFalse(service.downloadJobs[providedDownload.value.state.id]!!.downloadDeleted) + + CollectionProcessor.withFactCollection { facts -> + service.broadcastReceiver.onReceive(testContext, cancelIntent) + + val cancelFact = facts[0] + assertEquals(Action.CANCEL, cancelFact.action) + assertEquals(NOTIFICATION, cancelFact.item) + } + } + + @Test + fun `broadcastReceiver handles ACTION_RESUME`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + + val downloadResponse = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + val resumeResponse = Response( + "https://example.com/file.txt", + 206, + MutableHeaders("Content-Range" to "1-67589/67589"), + Response.Body(mock()), + ) + doReturn(downloadResponse).`when`(client) + .fetch(Request("https://example.com/file.txt")) + doReturn(resumeResponse).`when`(client) + .fetch(Request("https://example.com/file.txt", headers = MutableHeaders("Range" to "bytes=1-"))) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + // Simulate a pause + var downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + downloadJobState.currentBytesCopied = 1 + service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED) + + service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED) + service.downloadJobs[providedDownload.value.state.id]?.job?.cancel() + + val resumeIntent = Intent(ACTION_RESUME).apply { + setPackage(testContext.applicationContext.packageName) + putExtra(DownloadNotification.EXTRA_DOWNLOAD_ID, providedDownload.value.state.id) + } + + CollectionProcessor.withFactCollection { facts -> + service.broadcastReceiver.onReceive(testContext, resumeIntent) + + val resumeFact = facts[0] + assertEquals(Action.RESUME, resumeFact.action) + assertEquals(NOTIFICATION, resumeFact.item) + } + + downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState)) + + // Make sure the download job is completed (break out of copyInChunks) + service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED) + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + + verify(service).startDownloadJob(providedDownload.value) + } + + @Test + fun `broadcastReceiver handles ACTION_TRY_AGAIN`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + + // Simulate a failure + var downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + service.setDownloadJobStatus(downloadJobState, FAILED) + service.downloadJobs[providedDownload.value.state.id]?.job?.cancel() + + val tryAgainIntent = Intent(ACTION_TRY_AGAIN).apply { + setPackage(testContext.applicationContext.packageName) + putExtra(DownloadNotification.EXTRA_DOWNLOAD_ID, providedDownload.value.state.id) + } + + CollectionProcessor.withFactCollection { facts -> + service.broadcastReceiver.onReceive(testContext, tryAgainIntent) + + val tryAgainFact = facts[0] + assertEquals(Action.TRY_AGAIN, tryAgainFact.action) + assertEquals(NOTIFICATION, tryAgainFact.item) + } + + downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState)) + + // Make sure the download job is completed (break out of copyInChunks) + service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED) + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + + verify(service).startDownloadJob(providedDownload.value) + } + + @Test + fun `download fails on a bad network response`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 400, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState)) + } + + @Test + fun `makeUniqueFileNameIfNecessary transforms fileName when appending FALSE`() { + folder.newFile("example.apk") + + val download = DownloadState( + url = "mozilla.org", + fileName = "example.apk", + destinationDirectory = folder.root.path, + ) + val transformedDownload = service.makeUniqueFileNameIfNecessary(download, false) + + assertNotEquals(download.fileName, transformedDownload.fileName) + } + + @Test + fun `makeUniqueFileNameIfNecessary does NOT transform fileName when appending TRUE`() { + folder.newFile("example.apk") + + val download = DownloadState( + url = "mozilla.org", + fileName = "example.apk", + destinationDirectory = folder.root.path, + ) + val transformedDownload = service.makeUniqueFileNameIfNecessary(download, true) + + assertEquals(download, transformedDownload) + } + + @Test + fun `notification is shown when download status is ACTIVE`() = runBlocking { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + service.setDownloadJobStatus(downloadJobState, DOWNLOADING) + assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState)) + + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + + // The additional notification is the summary one (the notification group). + assertEquals(2, shadowNotificationService.size()) + } + + @Test + fun `onStartCommand must change status of INITIATED downloads to DOWNLOADING`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt", status = INITIATED) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + doNothing().`when`(service).performDownload(any(), anyBoolean()) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.first().job!!.joinBlocking() + + verify(service).startDownloadJob(any()) + assertEquals(DOWNLOADING, service.downloadJobs.values.first().status) + } + + @Test + fun `onStartCommand must change the status only for INITIATED downloads`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt", status = FAILED) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + + verify(service, never()).startDownloadJob(any()) + assertEquals(FAILED, service.downloadJobs.values.first().status) + } + + @Test + fun `onStartCommand sets the notification foreground`() = runTest(testsDispatcher) { + val download = DownloadState("https://example.com/file.txt", "file.txt") + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + doNothing().`when`(service).performDownload(any(), anyBoolean()) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + + verify(service).setForegroundNotification(any()) + } + + @Test + fun `sets the notification foreground in devices that support notification group`() = runTest(testsDispatcher) { + val download = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ) + val downloadState = DownloadJobState( + state = download, + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + val notification = mock<Notification>() + + doReturn(notification).`when`(service).updateNotificationGroup() + + service.downloadJobs["1"] = downloadState + + service.setForegroundNotification(downloadState) + + verify(service).startForeground(NOTIFICATION_DOWNLOAD_GROUP_ID, notification) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `sets the notification foreground in devices that DO NOT support notification group`() { + val download = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ) + val downloadState = DownloadJobState( + state = download, + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + val notification = mock<Notification>() + + doReturn(notification).`when`(service).createCompactForegroundNotification(downloadState) + + service.downloadJobs["1"] = downloadState + + service.setForegroundNotification(downloadState) + + verify(service).startForeground(downloadState.foregroundServiceId, notification) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun createCompactForegroundNotification() { + val download = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ) + val downloadState = DownloadJobState( + state = download, + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + + assertEquals(0, shadowNotificationService.size()) + + val notification = service.createCompactForegroundNotification(downloadState) + + service.downloadJobs["1"] = downloadState + + service.setForegroundNotification(downloadState) + + assertNull(notification.group) + assertEquals(1, shadowNotificationService.size()) + assertNotNull(shadowNotificationService.getNotification(downloadState.foregroundServiceId)) + } + + @Test + fun `getForegroundId in devices that support notification group will return NOTIFICATION_DOWNLOAD_GROUP_ID`() { + val download = DownloadState(id = "1", url = "https://example.com/file.txt", fileName = "file.txt") + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + doNothing().`when`(service).performDownload(any(), anyBoolean()) + + service.onStartCommand(downloadIntent, 0, 0) + + assertEquals(NOTIFICATION_DOWNLOAD_GROUP_ID, service.getForegroundId()) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `getForegroundId in devices that support DO NOT notification group will return the latest active download`() { + val download = DownloadState(id = "1", url = "https://example.com/file.txt", fileName = "file.txt") + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + doNothing().`when`(service).performDownload(any(), anyBoolean()) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + + val foregroundId = service.downloadJobs.values.first().foregroundServiceId + assertEquals(foregroundId, service.getForegroundId()) + assertEquals(foregroundId, service.compatForegroundNotificationId) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `updateNotificationGroup will do nothing on devices that do not support notificaiton groups`() = runTest(testsDispatcher) { + val download = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ) + val downloadState = DownloadJobState( + state = download, + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + + service.downloadJobs["1"] = downloadState + + val notificationGroup = service.updateNotificationGroup() + + assertNull(notificationGroup) + assertEquals(0, shadowNotificationService.size()) + } + + @Test + fun `removeDownloadJob will update the background notification if there are other pending downloads`() { + val download = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ) + val downloadState = DownloadJobState( + state = download, + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + + service.downloadJobs["1"] = downloadState + service.downloadJobs["2"] = mock() + + doNothing().`when`(service).updateForegroundNotificationIfNeeded(downloadState) + + service.removeDownloadJob(downloadJobState = downloadState) + + assertEquals(1, service.downloadJobs.size) + verify(service).updateForegroundNotificationIfNeeded(downloadState) + verify(service).removeNotification(testContext, downloadState) + } + + @Test + fun `WHEN all downloads are completed stopForeground must be called`() { + val download1 = DownloadState( + id = "1", + url = "https://example.com/file1.txt", + fileName = "file1.txt", + status = COMPLETED, + ) + val download2 = DownloadState( + id = "2", + url = "https://example.com/file2.txt", + fileName = "file2.txt", + status = COMPLETED, + ) + val downloadState1 = DownloadJobState( + state = download1, + status = COMPLETED, + foregroundServiceId = Random.nextInt(), + ) + + val downloadState2 = DownloadJobState( + state = download2, + status = COMPLETED, + foregroundServiceId = Random.nextInt(), + ) + + service.downloadJobs["1"] = downloadState1 + service.downloadJobs["2"] = downloadState2 + + service.updateForegroundNotificationIfNeeded(downloadState1) + + verify(service).stopForegroundCompat(false) + } + + @Test + fun `Until all downloads are NOT completed stopForeground must NOT be called`() { + val download1 = DownloadState( + id = "1", + url = "https://example.com/file1.txt", + fileName = "file1.txt", + status = COMPLETED, + ) + val download2 = DownloadState( + id = "2", + url = "https://example.com/file2.txt", + fileName = "file2.txt", + status = DOWNLOADING, + ) + val downloadState1 = DownloadJobState( + state = download1, + status = COMPLETED, + foregroundServiceId = Random.nextInt(), + ) + + val downloadState2 = DownloadJobState( + state = download2, + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + + service.downloadJobs["1"] = downloadState1 + service.downloadJobs["2"] = downloadState2 + + service.updateForegroundNotificationIfNeeded(downloadState1) + + verify(service, never()).stopForeground(Service.STOP_FOREGROUND_DETACH) + } + + @Test + fun `removeDownloadJob will stop the service if there are none pending downloads`() { + val download = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ) + val downloadState = DownloadJobState( + state = download, + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + + doNothing().`when`(service).stopForeground(Service.STOP_FOREGROUND_DETACH) + doNothing().`when`(service).clearAllDownloadsNotificationsAndJobs() + doNothing().`when`(service).stopSelf() + + service.downloadJobs["1"] = downloadState + + service.removeDownloadJob(downloadJobState = downloadState) + + assertTrue(service.downloadJobs.isEmpty()) + verify(service).stopSelf() + verify(service, times(0)).updateForegroundNotificationIfNeeded(downloadState) + } + + @Test + fun `updateForegroundNotification will update the notification group for devices that support it`() { + doReturn(null).`when`(service).updateNotificationGroup() + + service.updateForegroundNotificationIfNeeded(mock()) + + verify(service).updateNotificationGroup() + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `updateForegroundNotification will select a new foreground notification`() { + val downloadState1 = DownloadJobState( + state = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DownloadState.Status.COMPLETED, + ), + status = DownloadState.Status.COMPLETED, + foregroundServiceId = Random.nextInt(), + ) + val downloadState2 = DownloadJobState( + state = DownloadState( + id = "2", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ), + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + + service.compatForegroundNotificationId = downloadState1.foregroundServiceId + + service.downloadJobs["1"] = downloadState1 + service.downloadJobs["2"] = downloadState2 + + service.updateForegroundNotificationIfNeeded(downloadState1) + + verify(service).setForegroundNotification(downloadState2) + assertEquals(downloadState2.foregroundServiceId, service.compatForegroundNotificationId) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `updateForegroundNotification will NOT select a new foreground notification`() { + val downloadState1 = DownloadJobState( + state = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ), + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + val downloadState2 = DownloadJobState( + state = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ), + status = DOWNLOADING, + foregroundServiceId = Random.nextInt(), + ) + + service.compatForegroundNotificationId = downloadState1.foregroundServiceId + + service.downloadJobs["1"] = downloadState1 + service.downloadJobs["2"] = downloadState2 + + service.updateForegroundNotificationIfNeeded(downloadState1) + + verify(service, times(0)).setForegroundNotification(downloadState2) + verify(service, times(0)).updateNotificationGroup() + assertEquals(downloadState1.foregroundServiceId, service.compatForegroundNotificationId) + } + + @Test + fun `notification is shown when download status is PAUSED`() = runBlocking { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED) + assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState)) + + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + + // one of the notifications it is the group notification only for devices the support it + assertEquals(2, shadowNotificationService.size()) + } + + @Test + fun `notification is shown when download status is COMPLETED`() = runBlocking { + performSuccessfulCompleteDownload() + + assertEquals(2, shadowNotificationService.size()) + } + + @Test + fun `completed download notification avoids notification trampoline restrictions by using an activity based PendingIntent to open the file`() = runBlocking { + val downloadJobState = performSuccessfulCompleteDownload() + + val notification = shadowNotificationService.getNotification(downloadJobState.foregroundServiceId) + val shadowNotificationContentPendingIntent = shadowOf(notification.contentIntent) + assertTrue(shadowNotificationContentPendingIntent.isActivity) + } + + private suspend fun performSuccessfulCompleteDownload(): DownloadJobState { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + service.setDownloadJobStatus(downloadJobState, COMPLETED) + assertEquals(COMPLETED, service.getDownloadJobStatus(downloadJobState)) + + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + return downloadJobState + } + + @Test + fun `notification is shown when download status is FAILED`() = runBlocking { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + service.setDownloadJobStatus(downloadJobState, FAILED) + assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState)) + + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + + // one of the notifications it is the group notification only for devices the support it + assertEquals(2, shadowNotificationService.size()) + } + + @Test + fun `notification is not shown when download status is CANCELLED`() = runBlocking { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + service.setDownloadJobStatus(downloadJobState, DownloadState.Status.CANCELLED) + assertEquals(DownloadState.Status.CANCELLED, service.getDownloadJobStatus(downloadJobState)) + + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + + // The additional notification is the summary one (the notification group). + assertEquals(1, shadowNotificationService.size()) + } + + @Test + fun `job status is set to failed when an Exception is thrown while performDownload`() = runTest(testsDispatcher) { + doThrow(IOException()).`when`(client).fetch(any()) + val download = DownloadState("https://example.com/file.txt", "file.txt") + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState)) + } + + @Test + fun `WHEN a download is from a private session the request must be private`() = runTest(testsDispatcher) { + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(any()) + val download = DownloadState("https://example.com/file.txt", "file.txt", private = true) + val downloadJob = DownloadJobState(state = download, status = DOWNLOADING) + val providedRequest = argumentCaptor<Request>() + + service.performDownload(downloadJob) + verify(client).fetch(providedRequest.capture()) + assertTrue(providedRequest.value.private) + + downloadJob.state = download.copy(private = false) + service.performDownload(downloadJob) + + verify(client, times(2)).fetch(providedRequest.capture()) + + assertFalse(providedRequest.value.private) + } + + @Test + fun `performDownload - use the download response when available`() { + val responseFromDownloadState = mock<Response>() + val responseFromClient = mock<Response>() + val download = DownloadState("https://example.com/file.txt", "file.txt", response = responseFromDownloadState) + val downloadJob = DownloadJobState(state = download, status = DOWNLOADING) + + doReturn(404).`when`(responseFromDownloadState).status + doReturn(responseFromClient).`when`(client).fetch(any()) + + service.performDownload(downloadJob) + + verify(responseFromDownloadState, atLeastOnce()).status + verifyNoInteractions(client) + } + + @Test + fun `performDownload - use the client response when the download response NOT available`() { + val responseFromClient = mock<Response>() + val download = spy(DownloadState("https://example.com/file.txt", "file.txt", response = null)) + val downloadJob = DownloadJobState(state = download, status = DOWNLOADING) + + doReturn(404).`when`(responseFromClient).status + doReturn(responseFromClient).`when`(client).fetch(any()) + + service.performDownload(downloadJob) + + verify(responseFromClient, atLeastOnce()).status + } + + @Test + fun `performDownload - use the client response when resuming a download`() { + val responseFromDownloadState = mock<Response>() + val responseFromClient = mock<Response>() + val download = spy(DownloadState("https://example.com/file.txt", "file.txt", response = responseFromDownloadState)) + val downloadJob = DownloadJobState(currentBytesCopied = 100, state = download, status = DOWNLOADING) + + doReturn(404).`when`(responseFromClient).status + doReturn(responseFromClient).`when`(client).fetch(any()) + + service.performDownload(downloadJob) + + verify(responseFromClient, atLeastOnce()).status + verifyNoInteractions(responseFromDownloadState) + } + + @Test + fun `onDestroy cancels all running jobs`() = runBlocking { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + // Simulate a long running reading operation by sleeping for 5 seconds. + Response.Body( + object : InputStream() { + override fun read(): Int { + Thread.sleep(5000) + return 0 + } + }, + ), + ) + // Call the real method to force the reading of the response's body. + doCallRealMethod().`when`(service).useFileStream(any(), anyBoolean(), any()) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.registerNotificationActionsReceiver() + service.onStartCommand(downloadIntent, 0, 0) + + service.downloadJobs.values.forEach { assertTrue(it.job!!.isActive) } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + // Advance the clock so that the puller posts a notification. + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + // One of the notifications it is the group notification only for devices the support it + assertEquals(2, shadowNotificationService.size()) + + // Now destroy + service.onDestroy() + + // Assert that jobs were cancelled rather than completed. + service.downloadJobs.values.forEach { + assertTrue(it.job!!.isCancelled) + assertFalse(it.job!!.isCompleted) + } + + // Assert that all currently shown notifications are gone. + assertEquals(0, shadowNotificationService.size()) + } + + @Test + fun `updateDownloadState must update the download state in the store and in the downloadJobs`() { + val download = DownloadState( + "https://example.com/file.txt", + "file1.txt", + status = DOWNLOADING, + ) + val downloadJob = DownloadJobState(state = mock(), status = DOWNLOADING) + val mockStore = mock<BrowserStore>() + val mockNotificationsDelegate = mock<NotificationsDelegate>() + + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = mockStore + override val notificationsDelegate = mockNotificationsDelegate + }, + ) + + service.downloadJobs[download.id] = downloadJob + + service.updateDownloadState(download) + + assertEquals(download, service.downloadJobs[download.id]!!.state) + verify(mockStore).dispatch(DownloadAction.UpdateDownloadAction(download)) + } + + @Test + fun `onTaskRemoved cancels all notifications on the shadow notification manager`() = runBlocking { + val download = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.registerNotificationActionsReceiver() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + + service.setDownloadJobStatus(service.downloadJobs[download.id]!!, DownloadState.Status.PAUSED) + + // Advance the clock so that the poller posts a notification. + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + assertEquals(2, shadowNotificationService.size()) + + // Now simulate onTaskRemoved. + service.onTaskRemoved(null) + + verify(service).stopSelf() + } + + @Test + fun `clearAllDownloadsNotificationsAndJobs cancels all running jobs and remove all notifications`() = runTest(testsDispatcher) { + val download = DownloadState( + id = "1", + url = "https://example.com/file.txt", + fileName = "file.txt", + status = DOWNLOADING, + ) + val downloadState = DownloadJobState( + state = download, + foregroundServiceId = Random.nextInt(), + status = DOWNLOADING, + job = CoroutineScope(IO).launch { + @Suppress("ControlFlowWithEmptyBody") + while (true) { } + }, + ) + + service.registerNotificationActionsReceiver() + service.downloadJobs[download.id] = downloadState + + val notificationStyle = AbstractFetchDownloadService.Style() + val notification = DownloadNotification.createOngoingDownloadNotification( + testContext, + downloadState, + notificationStyle.notificationAccentColor, + ) + + NotificationManagerCompat.from(testContext).notify(downloadState.foregroundServiceId, notification) + + // We have a pending notification + assertEquals(1, shadowNotificationService.size()) + + service.clearAllDownloadsNotificationsAndJobs() + + // Assert that all currently shown notifications are gone. + assertEquals(0, shadowNotificationService.size()) + + // Assert that jobs were cancelled rather than completed. + service.downloadJobs.values.forEach { + assertTrue(it.job!!.isCancelled) + assertFalse(it.job!!.isCompleted) + } + } + + @Test + fun `onDestroy will remove all download notifications, jobs and will call unregisterNotificationActionsReceiver`() = runTest(testsDispatcher) { + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + doReturn(testContext).`when`(service).context + + service.registerNotificationActionsReceiver() + + service.onDestroy() + + verify(service).clearAllDownloadsNotificationsAndJobs() + verify(service).unregisterNotificationActionsReceiver() + } + + @Test + fun `register and unregister notification actions receiver`() { + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + doReturn(testContext).`when`(service).context + + service.onCreate() + + verify(service).registerNotificationActionsReceiver() + + service.onDestroy() + + verify(service).unregisterNotificationActionsReceiver() + } + + @Test + fun `WHEN a download is completed and the scoped storage is not used it MUST be added manually to the download system database`() = runTest(testsDispatcher) { + val download = DownloadState( + url = "http://www.mozilla.org", + fileName = "example.apk", + destinationDirectory = folder.root.path, + status = DownloadState.Status.COMPLETED, + ) + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + val downloadJobState = DownloadJobState(state = download, status = DownloadState.Status.COMPLETED) + + doReturn(testContext).`when`(service).context + service.updateDownloadNotification(DownloadState.Status.COMPLETED, downloadJobState, this) + + verify(service).addCompletedDownload( + title = any(), + description = any(), + isMediaScannerScannable = eq(true), + mimeType = any(), + path = any(), + length = anyLong(), + showNotification = anyBoolean(), + download = any(), + ) + } + + @Test + fun `WHEN a download is completed and the scoped storage is used addToDownloadSystemDatabaseCompat MUST NOT be called`() = runTest(testsDispatcher) { + val download = DownloadState( + url = "http://www.mozilla.org", + fileName = "example.apk", + destinationDirectory = folder.root.path, + status = DownloadState.Status.COMPLETED, + ) + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + val downloadJobState = DownloadJobState(state = download, status = DownloadState.Status.COMPLETED) + + doReturn(testContext).`when`(service).context + doNothing().`when`(service).addCompletedDownload( + title = any(), + description = any(), + isMediaScannerScannable = eq(true), + mimeType = any(), + path = any(), + length = anyLong(), + showNotification = anyBoolean(), + download = any(), + ) + doReturn(true).`when`(service).shouldUseScopedStorage() + + service.updateDownloadNotification(DownloadState.Status.COMPLETED, downloadJobState, this) + + verify(service, never()).addCompletedDownload( + title = any(), + description = any(), + isMediaScannerScannable = eq(true), + mimeType = any(), + path = any(), + length = anyLong(), + showNotification = anyBoolean(), + download = any(), + ) + } + + @Test + fun `WHEN we download on devices with version higher than Q THEN we use scoped storage`() { + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + val uniqueFile: DownloadState = mock() + val qSdkVersion = 29 + doReturn(uniqueFile).`when`(service).makeUniqueFileNameIfNecessary(any(), anyBoolean()) + doNothing().`when`(service).updateDownloadState(uniqueFile) + doNothing().`when`(service).useFileStreamScopedStorage(eq(uniqueFile), any()) + doReturn(qSdkVersion).`when`(service).getSdkVersion() + + service.useFileStream(mock(), true) {} + + verify(service).useFileStreamScopedStorage(eq(uniqueFile), any()) + } + + @Test + fun `WHEN we download on devices with version lower than Q THEN we use legacy file stream`() { + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + val uniqueFile: DownloadState = mock() + val qSdkVersion = 27 + doReturn(uniqueFile).`when`(service).makeUniqueFileNameIfNecessary(any(), anyBoolean()) + doNothing().`when`(service).updateDownloadState(uniqueFile) + doNothing().`when`(service).useFileStreamLegacy(eq(uniqueFile), anyBoolean(), any()) + doReturn(qSdkVersion).`when`(service).getSdkVersion() + + service.useFileStream(mock(), true) {} + + verify(service).useFileStreamLegacy(eq(uniqueFile), anyBoolean(), any()) + } + + @Test + @Suppress("Deprecation") + fun `do not pass non-http(s) url to addCompletedDownload`() = runTest(testsDispatcher) { + val download = DownloadState( + url = "blob:moz-extension://d5ea9baa-64c9-4c3d-bb38-49308c47997c/", + fileName = "example.apk", + destinationDirectory = folder.root.path, + ) + + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + val spyContext = spy(testContext) + val downloadManager: DownloadManager = mock() + + doReturn(spyContext).`when`(service).context + doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>() + + service.addToDownloadSystemDatabaseCompat(download, this) + verify(downloadManager).addCompletedDownload(anyString(), anyString(), anyBoolean(), anyString(), anyString(), anyLong(), anyBoolean(), isNull(), any()) + } + + @Test + @Suppress("Deprecation") + fun `GIVEN a download that throws an exception WHEN adding to the system database THEN handle the exception`() = + runTest(testsDispatcher) { + val download = DownloadState( + url = "url", + fileName = "example.apk", + destinationDirectory = folder.root.path, + ) + + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + val spyContext = spy(testContext) + val downloadManager: DownloadManager = mock() + + doReturn(spyContext).`when`(service).context + doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>() + + doAnswer { throw IllegalArgumentException() }.`when`(downloadManager) + .addCompletedDownload( + anyString(), anyString(), anyBoolean(), anyString(), + anyString(), anyLong(), anyBoolean(), isNull(), any(), + ) + + try { + service.addToDownloadSystemDatabaseCompat(download, this) + } catch (e: IOException) { + fail() + } + } + + @Test + @Suppress("Deprecation") + fun `pass http(s) url to addCompletedDownload`() = runTest(testsDispatcher) { + val download = DownloadState( + url = "https://mozilla.com", + fileName = "example.apk", + destinationDirectory = folder.root.path, + ) + + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + + val spyContext = spy(testContext) + val downloadManager: DownloadManager = mock() + + doReturn(spyContext).`when`(service).context + doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>() + + service.addToDownloadSystemDatabaseCompat(download, this) + verify(downloadManager).addCompletedDownload(anyString(), anyString(), anyBoolean(), anyString(), anyString(), anyLong(), anyBoolean(), any(), any()) + } + + @Test + @Suppress("Deprecation") + fun `always call addCompletedDownload with a not empty or null mimeType`() = runTest(testsDispatcher) { + val service = spy( + object : AbstractFetchDownloadService() { + override val httpClient = client + override val store = browserStore + override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate + }, + ) + val spyContext = spy(testContext) + var downloadManager: DownloadManager = mock() + doReturn(spyContext).`when`(service).context + doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>() + val downloadWithNullMimeType = DownloadState( + url = "blob:moz-extension://d5ea9baa-64c9-4c3d-bb38-49308c47997c/", + fileName = "example.apk", + destinationDirectory = folder.root.path, + contentType = null, + ) + val downloadWithEmptyMimeType = downloadWithNullMimeType.copy(contentType = "") + val defaultMimeType = "*/*" + + service.addToDownloadSystemDatabaseCompat(downloadWithNullMimeType, this) + verify(downloadManager).addCompletedDownload( + anyString(), anyString(), anyBoolean(), eq(defaultMimeType), + anyString(), anyLong(), anyBoolean(), isNull(), any(), + ) + + downloadManager = mock() + doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>() + service.addToDownloadSystemDatabaseCompat(downloadWithEmptyMimeType, this) + verify(downloadManager).addCompletedDownload( + anyString(), anyString(), anyBoolean(), eq(defaultMimeType), + anyString(), anyLong(), anyBoolean(), isNull(), any(), + ) + } + + @Test + fun `cancelled download does not prevent other notifications`() = runBlocking { + val cancelledDownload = DownloadState("https://example.com/file.txt", "file.txt") + val response = Response( + "https://example.com/file.txt", + 200, + MutableHeaders(), + Response.Body(mock()), + ) + + doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt")) + val cancelledDownloadIntent = Intent("ACTION_DOWNLOAD") + cancelledDownloadIntent.putExtra(EXTRA_DOWNLOAD_ID, cancelledDownload.id) + + browserStore.dispatch(DownloadAction.AddDownloadAction(cancelledDownload)).joinBlocking() + service.onStartCommand(cancelledDownloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + + val providedDownload = argumentCaptor<DownloadJobState>() + + verify(service).performDownload(providedDownload.capture(), anyBoolean()) + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + + val cancelledDownloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + + service.setDownloadJobStatus(cancelledDownloadJobState, DownloadState.Status.CANCELLED) + assertEquals(DownloadState.Status.CANCELLED, service.getDownloadJobStatus(cancelledDownloadJobState)) + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + // The additional notification is the summary one (the notification group). + assertEquals(1, shadowNotificationService.size()) + + val download = DownloadState("https://example.com/file.txt", "file.txt") + val downloadIntent = Intent("ACTION_DOWNLOAD") + downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id) + + // Start another download to ensure its notifications are presented + browserStore.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + service.onStartCommand(downloadIntent, 0, 0) + service.downloadJobs.values.forEach { it.job?.join() } + verify(service, times(2)).performDownload(providedDownload.capture(), anyBoolean()) + service.downloadJobs[providedDownload.value.state.id]?.job?.join() + + val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!! + + service.setDownloadJobStatus(downloadJobState, DownloadState.Status.COMPLETED) + assertEquals(DownloadState.Status.COMPLETED, service.getDownloadJobStatus(downloadJobState)) + mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL) + mainDispatcher.scheduler.runCurrent() + // one of the notifications it is the group notification only for devices the support it + assertEquals(2, shadowNotificationService.size()) + } + + @Test + fun `createDirectoryIfNeeded - MUST create directory when it does not exists`() = runTest(testsDispatcher) { + val download = DownloadState(destinationDirectory = Environment.DIRECTORY_DOWNLOADS, url = "") + + val file = File(download.directoryPath) + file.delete() + + assertFalse(file.exists()) + + service.createDirectoryIfNeeded(download) + + assertTrue(file.exists()) + } + + @Test + fun `keeps track of how many seconds have passed since the last update to a notification`() = runBlocking { + val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING) + val oneSecond = 1000L + + downloadJobState.lastNotificationUpdate = System.currentTimeMillis() + + delay(oneSecond) + + var seconds = downloadJobState.getSecondsSinceTheLastNotificationUpdate() + + assertEquals(1, seconds) + + delay(oneSecond) + + seconds = downloadJobState.getSecondsSinceTheLastNotificationUpdate() + + assertEquals(2, seconds) + } + + @Test + fun `is a notification under the time limit for updates`() = runBlocking { + val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING) + val oneSecond = 1000L + + downloadJobState.lastNotificationUpdate = System.currentTimeMillis() + + assertFalse(downloadJobState.isUnderNotificationUpdateLimit()) + + delay(oneSecond) + + assertTrue(downloadJobState.isUnderNotificationUpdateLimit()) + } + + @Test + fun `try to update a notification`() = runBlocking { + val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING) + val oneSecond = 1000L + + downloadJobState.lastNotificationUpdate = System.currentTimeMillis() + + // It's over the notification limit + assertFalse(downloadJobState.canUpdateNotification()) + + delay(oneSecond) + + // It's under the notification limit + assertTrue(downloadJobState.canUpdateNotification()) + + downloadJobState.notifiedStopped = true + + assertFalse(downloadJobState.canUpdateNotification()) + + downloadJobState.notifiedStopped = false + + assertTrue(downloadJobState.canUpdateNotification()) + } + + @Test + fun `copyInChunks must alter download currentBytesCopied`() = runTest(testsDispatcher) { + val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING) + val inputStream = mock<InputStream>() + + assertEquals(0, downloadJobState.currentBytesCopied) + + doReturn(15, -1).`when`(inputStream).read(any()) + doNothing().`when`(service).updateDownloadState(any()) + + service.copyInChunks(downloadJobState, inputStream, mock()) + + assertEquals(15, downloadJobState.currentBytesCopied) + } + + @Test + fun `copyInChunks - must return ERROR_IN_STREAM_CLOSED when inStream is closed`() = runTest(testsDispatcher) { + val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING) + val inputStream = mock<InputStream>() + + assertEquals(0, downloadJobState.currentBytesCopied) + + doAnswer { throw IOException() }.`when`(inputStream).read(any()) + doNothing().`when`(service).updateDownloadState(any()) + doNothing().`when`(service).performDownload(any(), anyBoolean()) + + val status = service.copyInChunks(downloadJobState, inputStream, mock()) + + verify(service).performDownload(downloadJobState, true) + assertEquals(ERROR_IN_STREAM_CLOSED, status) + } + + @Test + fun `copyInChunks - must throw when inStream is closed and download was performed using http client`() = runTest(testsDispatcher) { + val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING) + val inputStream = mock<InputStream>() + var exceptionWasThrown = false + + assertEquals(0, downloadJobState.currentBytesCopied) + + doAnswer { throw IOException() }.`when`(inputStream).read(any()) + doNothing().`when`(service).updateDownloadState(any()) + doNothing().`when`(service).performDownload(any(), anyBoolean()) + + try { + service.copyInChunks(downloadJobState, inputStream, mock(), true) + } catch (e: IOException) { + exceptionWasThrown = true + } + + verify(service, times(0)).performDownload(downloadJobState, true) + assertTrue(exceptionWasThrown) + } + + @Test + fun `copyInChunks - must return COMPLETED when finish copying bytes`() = runTest(testsDispatcher) { + val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING) + val inputStream = mock<InputStream>() + + assertEquals(0, downloadJobState.currentBytesCopied) + + doReturn(15, -1).`when`(inputStream).read(any()) + doNothing().`when`(service).updateDownloadState(any()) + + val status = service.copyInChunks(downloadJobState, inputStream, mock()) + + verify(service, never()).performDownload(any(), anyBoolean()) + + assertEquals(15, downloadJobState.currentBytesCopied) + assertEquals(AbstractFetchDownloadService.CopyInChuckStatus.COMPLETED, status) + } + + @Test + fun `getSafeContentType - WHEN the file content type is available THEN use it`() { + val contentTypeFromFile = "application/pdf; qs=0.001" + val spyContext = spy(testContext) + val contentResolver = mock<ContentResolver>() + + doReturn(contentTypeFromFile).`when`(contentResolver).getType(any()) + doReturn(contentResolver).`when`(spyContext).contentResolver + + val result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), "any") + + assertEquals("application/pdf", result) + } + + @Test + fun `getSafeContentType - WHEN the file content type is not available THEN use the provided content type`() { + val contentType = " application/pdf " + val spyContext = spy(testContext) + val contentResolver = mock<ContentResolver>() + doReturn(contentResolver).`when`(spyContext).contentResolver + + doReturn(null).`when`(contentResolver).getType(any()) + var result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), contentType) + assertEquals("application/pdf", result) + + doReturn("").`when`(contentResolver).getType(any()) + result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), contentType) + assertEquals("application/pdf", result) + } + + @Test + fun `getSafeContentType - WHEN none of the provided content types are available THEN return a generic content type`() { + val spyContext = spy(testContext) + val contentResolver = mock<ContentResolver>() + doReturn(contentResolver).`when`(spyContext).contentResolver + + doReturn(null).`when`(contentResolver).getType(any()) + var result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), null) + assertEquals("*/*", result) + + doReturn("").`when`(contentResolver).getType(any()) + result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), null) + assertEquals("*/*", result) + } + + // Following 3 tests use the String version of #getSafeContentType while the above 3 tested the Uri version + // The String version just overloads and delegates the Uri one but being in a companion object we cannot + // verify the delegation so we are left to verify the result to prevent any regressions. + @Test + fun `getSafeContentType2 - WHEN the file content type is available THEN use it`() { + val contentTypeFromFile = "application/pdf; qs=0.001" + val spyContext = spy(testContext) + val contentResolver = mock<ContentResolver>() + + doReturn(contentTypeFromFile).`when`(contentResolver).getType(any()) + doReturn(contentResolver).`when`(spyContext).contentResolver + + val result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", "any") + + assertEquals("application/pdf", result) + } + + @Test + fun `getSafeContentType2 - WHEN the file content type is not available THEN use the provided content type`() { + val contentType = " application/pdf " + val spyContext = spy(testContext) + val contentResolver = mock<ContentResolver>() + doReturn(contentResolver).`when`(spyContext).contentResolver + + doReturn(null).`when`(contentResolver).getType(any()) + var result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", contentType) + assertEquals("application/pdf", result) + + doReturn("").`when`(contentResolver).getType(any()) + result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", contentType) + assertEquals("application/pdf", result) + } + + @Test + fun `getSafeContentType2 - WHEN none of the provided content types are available THEN return a generic content type`() { + val spyContext = spy(testContext) + val contentResolver = mock<ContentResolver>() + doReturn(contentResolver).`when`(spyContext).contentResolver + + doReturn(null).`when`(contentResolver).getType(any()) + var result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", null) + assertEquals("*/*", result) + + doReturn("").`when`(contentResolver).getType(any()) + result = AbstractFetchDownloadService.getSafeContentType(spyContext, "any", null) + assertEquals("*/*", result) + } + + // Hard to test #getFilePathUri since it only returns the result of a certain Android api call. + // But let's try. + @Test + @Config(shadows = [DefaultFileProvider::class]) // use default implementation just for this test + fun `getFilePathUri - WHEN called without a registered provider THEN exception is thrown`() { + // There is no app registered provider that could expose a file from the filesystem of the machine running this test. + // Peeking into the exception would indicate whether the code really called "FileProvider.getUriForFile" as expected. + var exception: IllegalArgumentException? = null + try { + AbstractFetchDownloadService.getFilePathUri(testContext, "test.txt") + } catch (e: IllegalArgumentException) { + exception = e + } + + assertTrue(exception!!.stackTrace[0].fileName.contains("FileProvider")) + assertTrue(exception.stackTrace[0].methodName == "getUriForFile") + } + + @Test + fun `getFilePathUri - WHEN called THEN return a file provider path for the filePath`() { + // Test that the String filePath is passed to the provider from which we expect a Uri path + val result = AbstractFetchDownloadService.getFilePathUri(testContext, "location/test.txt") + + assertTrue(result.toString().endsWith("location/test.txt")) + } +} + +@Implements(FileProvider::class) +object ShadowFileProvider { + @Implementation + @JvmStatic + @Suppress("UNUSED_PARAMETER") + fun getUriForFile( + context: Context?, + authority: String?, + file: File, + ) = "content://authority/random/location/${file.name}".toUri() +} + +@Implements(FileProvider::class) +object DefaultFileProvider diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadCancelDialogFragmentTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadCancelDialogFragmentTest.kt new file mode 100644 index 0000000000..0322416b68 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadCancelDialogFragmentTest.kt @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.graphics.drawable.GradientDrawable +import android.view.Gravity +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.feature.downloads.ui.DownloadCancelDialogFragment +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(application = TestApplication::class) +class DownloadCancelDialogFragmentTest { + + @Test + fun `WHEN accept button is clicked THEN onAcceptClicked must be called`() { + spy(DownloadCancelDialogFragment.newInstance(2)).apply { + doReturn(testContext).`when`(this).requireContext() + doReturn(mockFragmentManager()).`when`(this).parentFragmentManager + var wasAcceptClicked = false + onAcceptClicked = { _, _ -> wasAcceptClicked = true } + + with(onCreateDialog(null)) { + findViewById<Button>(R.id.accept_button).apply { performClick() } + } + + assertTrue(wasAcceptClicked) + } + } + + @Test + fun `WHEN deny button is clicked THEN onDenyClicked must be called`() { + spy(DownloadCancelDialogFragment.newInstance(2)).apply { + doReturn(testContext).`when`(this).requireContext() + doReturn(mockFragmentManager()).`when`(this).parentFragmentManager + var wasDenyCalled = false + onDenyClicked = { wasDenyCalled = true } + + with(onCreateDialog(null)) { + findViewById<Button>(R.id.deny_button).apply { performClick() } + } + + assertTrue(wasDenyCalled) + } + } + + @Test + fun `WHEN overriding strings are provided to the prompt, THEN they are used by the prompt`() { + val testText = DownloadCancelDialogFragment.PromptText( + titleText = R.string.mozac_feature_downloads_cancel_active_private_downloads_warning_content_body, + bodyText = R.string.mozac_feature_downloads_cancel_active_downloads_warning_content_title, + acceptText = R.string.mozac_feature_downloads_cancel_active_private_downloads_deny, + denyText = R.string.mozac_feature_downloads_cancel_active_downloads_accept, + ) + spy( + DownloadCancelDialogFragment.newInstance( + 0, + promptText = testText, + ), + ).apply { + doReturn(testContext).`when`(this).requireContext() + + with(onCreateDialog(null)) { + findViewById<TextView>(R.id.title).apply { + Assert.assertEquals(text, testContext.getString(testText.titleText)) + } + findViewById<TextView>(R.id.body).apply { + Assert.assertEquals(text, testContext.getString(testText.bodyText)) + } + findViewById<Button>(R.id.accept_button).apply { + Assert.assertEquals(text, testContext.getString(testText.acceptText)) + } + findViewById<Button>(R.id.deny_button).apply { + Assert.assertEquals(text, testContext.getString(testText.denyText)) + } + } + } + } + + @Test + fun `WHEN styling is provided to the prompt, THEN it's used by the prompt`() { + val testStyling = DownloadCancelDialogFragment.PromptStyling( + gravity = Gravity.TOP, + shouldWidthMatchParent = false, + positiveButtonBackgroundColor = android.R.color.white, + positiveButtonTextColor = android.R.color.black, + positiveButtonRadius = 4f, + ) + + spy( + DownloadCancelDialogFragment.newInstance( + 0, + promptStyling = testStyling, + ), + ).apply { + doReturn(testContext).`when`(this).requireContext() + + with(onCreateDialog(null)) { + with(window!!.attributes) { + Assert.assertTrue(gravity == Gravity.TOP) + Assert.assertTrue(width == ViewGroup.LayoutParams.WRAP_CONTENT) + } + + with(findViewById<Button>(R.id.accept_button)) { + Assert.assertEquals( + ContextCompat.getColor( + testContext, + testStyling.positiveButtonBackgroundColor!!, + ), + (background as GradientDrawable).color?.defaultColor, + ) + Assert.assertEquals( + testStyling.positiveButtonRadius!!, + (background as GradientDrawable).cornerRadius, + ) + Assert.assertEquals( + ContextCompat.getColor( + testContext, + testStyling.positiveButtonTextColor!!, + ), + textColors.defaultColor, + ) + } + } + } + } + + private fun mockFragmentManager(): FragmentManager { + val fragmentManager: FragmentManager = mock() + val transaction: FragmentTransaction = mock() + doReturn(transaction).`when`(fragmentManager).beginTransaction() + return fragmentManager + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadDialogFragmentTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadDialogFragmentTest.kt new file mode 100644 index 0000000000..d7685c9017 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadDialogFragmentTest.kt @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.BYTES_TO_MB_LIMIT +import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KEY_FILE_NAME +import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.KILOBYTE +import mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.math.roundToLong + +@RunWith(AndroidJUnit4::class) +class DownloadDialogFragmentTest { + + private lateinit var dialog: DownloadDialogFragment + private lateinit var download: DownloadState + + @Before + fun setup() { + dialog = object : DownloadDialogFragment() {} + download = DownloadState( + "http://ipv4.download.thinkbroadband.com/5MB.zip", + "5MB.zip", + "application/zip", + 5242880, + userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36", + ) + } + + @Test + fun `when setDownload must set download metadata`() { + dialog.setDownload(download) + + assertNotNull(dialog.arguments) + val fileName = dialog.arguments!!.getString(KEY_FILE_NAME) + val url = dialog.arguments!!.getString(DownloadDialogFragment.KEY_URL) + val contentLength = dialog.arguments!!.getLong(DownloadDialogFragment.KEY_CONTENT_LENGTH) + + assertEquals(fileName, download.fileName) + assertEquals(url, download.url) + assertEquals(contentLength, download.contentLength) + } + + @Test + fun `extension function 'toMegabyteOrKilobyteString' returns MB string when size is or equal or bigger than the limit `() { + val size = (BYTES_TO_MB_LIMIT * MEGABYTE).roundToLong() + val expectedString = String.format("%.2f MB", size / MEGABYTE) + assertEquals(expectedString, size.toMegabyteOrKilobyteString()) + } + + @Test + fun `extension function 'toMegabyteOrKilobyteString' returns KB string when size is smaller than the limit `() { + val size = (BYTES_TO_MB_LIMIT * MEGABYTE).roundToLong() - 1 + val expectedString = String.format("%.2f KB", size / KILOBYTE) + assertEquals(expectedString, size.toMegabyteOrKilobyteString()) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt new file mode 100644 index 0000000000..c7f38b5e16 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt @@ -0,0 +1,607 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.app.DownloadManager.EXTRA_DOWNLOAD_ID +import android.content.Context +import android.content.Intent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED +import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED +import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED +import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Response +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +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.rule.MainCoroutineRule +import mozilla.components.support.test.rule.runTestOnMain +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class DownloadMiddlewareTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val dispatcher = coroutinesTestRule.testDispatcher + + @Test + fun `service is started when download is queued`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val download = DownloadState("https://mozilla.org/download", destinationDirectory = "") + store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + val intentCaptor = argumentCaptor<Intent>() + verify(downloadMiddleware).startForegroundService(intentCaptor.capture()) + assertEquals(download.id, intentCaptor.value.getStringExtra(EXTRA_DOWNLOAD_ID)) + + reset(downloadMiddleware) + + // We don't store private downloads in the storage. + val privateDownload = download.copy(id = "newId", private = true) + + store.dispatch(DownloadAction.AddDownloadAction(privateDownload)).joinBlocking() + + verify(downloadMiddleware, never()).saveDownload(any(), any()) + verify(downloadMiddleware.downloadStorage, never()).add(privateDownload) + verify(downloadMiddleware).startForegroundService(intentCaptor.capture()) + assertEquals(privateDownload.id, intentCaptor.value.getStringExtra(EXTRA_DOWNLOAD_ID)) + } + + @Test + fun `saveDownload do not store private downloads`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val privateDownload = DownloadState("https://mozilla.org/download", private = true) + + store.dispatch(DownloadAction.AddDownloadAction(privateDownload)).joinBlocking() + + verify(downloadMiddleware.downloadStorage, never()).add(privateDownload) + } + + @Test + fun `restarted downloads MUST not be passed to the downloadStorage`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadStorage: DownloadStorage = mock() + val downloadMiddleware = DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + downloadStorage = downloadStorage, + coroutineContext = dispatcher, + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + var download = DownloadState("https://mozilla.org/download", destinationDirectory = "") + store.dispatch(DownloadAction.RestoreDownloadStateAction(download)).joinBlocking() + + verify(downloadStorage, never()).add(download) + + download = DownloadState("https://mozilla.org/download", destinationDirectory = "") + store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + verify(downloadStorage).add(download) + } + + @Test + fun `previously added downloads MUST be ignored`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadStorage: DownloadStorage = mock() + val download = DownloadState("https://mozilla.org/download") + val downloadMiddleware = DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + downloadStorage = downloadStorage, + coroutineContext = dispatcher, + ) + val store = BrowserStore( + initialState = BrowserState( + downloads = mapOf(download.id to download), + ), + middleware = listOf(downloadMiddleware), + ) + + store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + verify(downloadStorage, never()).add(download) + } + + @Test + fun `RemoveDownloadAction MUST remove from the storage`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadStorage: DownloadStorage = mock() + val downloadMiddleware = DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + downloadStorage = downloadStorage, + coroutineContext = dispatcher, + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val download = DownloadState("https://mozilla.org/download", destinationDirectory = "") + store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + store.dispatch(DownloadAction.RemoveDownloadAction(download.id)).joinBlocking() + + verify(downloadStorage).remove(download) + } + + @Test + fun `RemoveAllDownloadsAction MUST remove all downloads from the storage`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadStorage: DownloadStorage = mock() + val downloadMiddleware = DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + downloadStorage = downloadStorage, + coroutineContext = dispatcher, + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val download = DownloadState("https://mozilla.org/download", destinationDirectory = "") + store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + store.dispatch(DownloadAction.RemoveAllDownloadsAction).joinBlocking() + + verify(downloadStorage).removeAllDownloads() + } + + @Test + fun `UpdateDownloadAction MUST update the storage when changes are needed`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadStorage: DownloadStorage = mock() + val downloadMiddleware = DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + downloadStorage = downloadStorage, + coroutineContext = dispatcher, + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val download = DownloadState("https://mozilla.org/download", status = INITIATED) + store.dispatch(DownloadAction.AddDownloadAction(download)).joinBlocking() + + val downloadInTheStore = store.state.downloads.getValue(download.id) + + assertEquals(download, downloadInTheStore) + + var updatedDownload = download.copy(status = COMPLETED, skipConfirmation = true) + store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload)).joinBlocking() + + verify(downloadStorage).update(updatedDownload) + + // skipConfirmation is value that we are not storing in the storage, + // changes on it shouldn't trigger an update on the storage. + updatedDownload = updatedDownload.copy(skipConfirmation = false) + store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload)).joinBlocking() + + verify(downloadStorage, times(1)).update(any()) + + // Private downloads are not updated in the storage. + updatedDownload = updatedDownload.copy(private = true) + + store.dispatch(DownloadAction.UpdateDownloadAction(updatedDownload)).joinBlocking() + verify(downloadStorage, times(1)).update(any()) + } + + @Test + fun `RestoreDownloadsState MUST populate the store with items in the storage`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadStorage: DownloadStorage = mock() + val downloadMiddleware = DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + downloadStorage = downloadStorage, + coroutineContext = dispatcher, + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val download = DownloadState("https://mozilla.org/download") + whenever(downloadStorage.getDownloadsList()).thenReturn(listOf(download)) + + assertTrue(store.state.downloads.isEmpty()) + + store.dispatch(DownloadAction.RestoreDownloadsStateAction).joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + assertEquals(download, store.state.downloads.values.first()) + } + + @Test + fun `private downloads MUST NOT be restored`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadStorage: DownloadStorage = mock() + val downloadMiddleware = DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + downloadStorage = downloadStorage, + coroutineContext = dispatcher, + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val download = DownloadState("https://mozilla.org/download", private = true) + whenever(downloadStorage.getDownloadsList()).thenReturn(listOf(download)) + + assertTrue(store.state.downloads.isEmpty()) + + store.dispatch(DownloadAction.RestoreDownloadsStateAction).joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + assertTrue(store.state.downloads.isEmpty()) + } + + @Test + fun `sendDownloadIntent MUST call startForegroundService WHEN downloads are NOT COMPLETED, CANCELLED and FAILED`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + ), + ) + + val ignoredStatus = listOf(COMPLETED, CANCELLED, FAILED) + ignoredStatus.forEach { status -> + val download = DownloadState("https://mozilla.org/download", status = status) + downloadMiddleware.sendDownloadIntent(download) + verify(downloadMiddleware, times(0)).startForegroundService(any()) + } + + reset(downloadMiddleware) + + val allowedStatus = DownloadState.Status.values().filter { it !in ignoredStatus } + + allowedStatus.forEachIndexed { index, status -> + val download = DownloadState("https://mozilla.org/download", status = status) + downloadMiddleware.sendDownloadIntent(download) + verify(downloadMiddleware, times(index + 1)).startForegroundService(any()) + } + } + + @Test + fun `WHEN RemoveAllTabsAction and RemoveAllPrivateTabsAction are received THEN removePrivateNotifications must be called`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val actions = listOf(TabListAction.RemoveAllTabsAction(), TabListAction.RemoveAllPrivateTabsAction) + + actions.forEach { + store.dispatch(it).joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + verify(downloadMiddleware, times(1)).removePrivateNotifications(any()) + reset(downloadMiddleware) + } + } + + @Test + fun `WHEN RemoveTabsAction is received AND there is no private tabs THEN removePrivateNotifications MUST be called`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab1"), + createTab("https://www.firefox.com", id = "test-tab2"), + createTab("https://www.wikipedia.com", private = true, id = "test-tab3"), + ), + ), + middleware = listOf(downloadMiddleware), + ) + + store.dispatch(TabListAction.RemoveTabsAction(listOf("test-tab1", "test-tab3"))).joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + verify(downloadMiddleware, times(1)).removePrivateNotifications(any()) + reset(downloadMiddleware) + } + + @Test + fun `WHEN RemoveTabsAction is received AND there is a private tab THEN removePrivateNotifications MUST NOT be called`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab1"), + createTab("https://www.firefox.com", id = "test-tab2"), + createTab("https://www.wikipedia.com", private = true, id = "test-tab3"), + ), + ), + middleware = listOf(downloadMiddleware), + ) + + store.dispatch(TabListAction.RemoveTabsAction(listOf("test-tab1", "test-tab2"))).joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + verify(downloadMiddleware, times(0)).removePrivateNotifications(any()) + reset(downloadMiddleware) + } + + @Test + fun `WHEN RemoveTabAction is received AND there is no private tabs THEN removePrivateNotifications MUST be called`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab1"), + createTab("https://www.firefox.com", id = "test-tab2"), + createTab("https://www.wikipedia.com", private = true, id = "test-tab3"), + ), + ), + middleware = listOf(downloadMiddleware), + ) + + store.dispatch(TabListAction.RemoveTabAction("test-tab3")).joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + verify(downloadMiddleware, times(1)).removePrivateNotifications(any()) + } + + @Test + fun `WHEN RemoveTabAction is received AND there is a private tab THEN removePrivateNotifications MUST NOT be called`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab1"), + createTab("https://www.firefox.com", private = true, id = "test-tab2"), + createTab("https://www.wikipedia.com", private = true, id = "test-tab3"), + ), + ), + middleware = listOf(downloadMiddleware), + ) + + store.dispatch(TabListAction.RemoveTabAction("test-tab3")).joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + verify(downloadMiddleware, times(0)).removePrivateNotifications(any()) + } + + @Test + fun `WHEN removeStatusBarNotification is called THEN an ACTION_REMOVE_PRIVATE_DOWNLOAD intent must be created`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val download = DownloadState("https://mozilla.org/download", notificationId = 100) + val store = mock<BrowserStore>() + + downloadMiddleware.removeStatusBarNotification(store, download) + + verify(store, times(1)).dispatch(DownloadAction.DismissDownloadNotificationAction(download.id)) + verify(applicationContext, times(1)).startService(any()) + } + + @Test + fun `WHEN removePrivateNotifications is called THEN removeStatusBarNotification will be called only for private download`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val download = DownloadState("https://mozilla.org/download", notificationId = 100) + val privateDownload = DownloadState("https://mozilla.org/download", notificationId = 100, private = true) + val store = BrowserStore( + initialState = BrowserState( + downloads = mapOf(download.id to download, privateDownload.id to privateDownload), + ), + middleware = listOf(downloadMiddleware), + ) + + downloadMiddleware.removePrivateNotifications(store) + + verify(downloadMiddleware, times(1)).removeStatusBarNotification(store, privateDownload) + } + + @Test + fun `WHEN removePrivateNotifications is called THEN removeStatusBarNotification will be called for all private downloads`() = runTestOnMain { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val download = DownloadState("https://mozilla.org/download", notificationId = 100, sessionId = "tab1") + val privateDownload = DownloadState("https://mozilla.org/download", notificationId = 100, private = true, sessionId = "tab2") + val anotherPrivateDownload = DownloadState("https://mozilla.org/download", notificationId = 100, private = true, sessionId = "tab3") + val store = BrowserStore( + initialState = BrowserState( + downloads = mapOf(download.id to download, privateDownload.id to privateDownload, anotherPrivateDownload.id to anotherPrivateDownload), + ), + middleware = listOf(downloadMiddleware), + ) + + downloadMiddleware.removePrivateNotifications(store) + + verify(downloadMiddleware, times(2)).removeStatusBarNotification(any(), any()) + } + + @Test + fun `WHEN an action for canceling a download response is received THEN a download response must be canceled`() = runTestOnMain { + val response = mock<Response>() + val download = DownloadState(id = "downloadID", url = "example.com/5MB.zip", response = response) + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val tab = createTab("https://www.mozilla.org") + + store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking() + store.dispatch(ContentAction.UpdateDownloadAction(tab.id, download = download)).joinBlocking() + store.dispatch(ContentAction.CancelDownloadAction(tab.id, download.id)).joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + verify(downloadMiddleware, times(1)).closeDownloadResponse(any(), any()) + verify(response).close() + } + + @Test + fun `WHEN closing a download response THEN the response object must be closed`() { + val applicationContext: Context = mock() + val downloadMiddleware = spy( + DownloadMiddleware( + applicationContext, + AbstractFetchDownloadService::class.java, + coroutineContext = dispatcher, + downloadStorage = mock(), + ), + ) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(downloadMiddleware), + ) + + val tab = createTab("https://www.mozilla.org") + val response = mock<Response>() + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = tab.id, response = response) + + store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking() + store.dispatch(ContentAction.UpdateDownloadAction(tab.id, download = download)).joinBlocking() + + downloadMiddleware.closeDownloadResponse(store, tab.id) + verify(response).close() + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt new file mode 100644 index 0000000000..c7b1abf3ac --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt @@ -0,0 +1,414 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.app.PendingIntent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.EXTRA_PROGRESS +import androidx.core.app.NotificationCompat.EXTRA_PROGRESS_INDETERMINATE +import androidx.core.app.NotificationCompat.EXTRA_PROGRESS_MAX +import androidx.core.content.ContextCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class DownloadNotificationTest { + + @Test + fun getProgress() { + val downloadJobState = DownloadJobState( + job = null, + state = DownloadState( + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ) + + assertEquals("10%", downloadJobState.getProgress()) + + val newDownload = downloadJobState.copy(state = downloadJobState.state.copy(contentLength = null)) + + assertEquals("", newDownload.getProgress()) + + val downloadWithNoSize = downloadJobState.copy(state = downloadJobState.state.copy(contentLength = 0)) + + assertEquals("", downloadWithNoSize.getProgress()) + + val downloadWithNullSize = downloadJobState.copy(state = downloadJobState.state.copy(contentLength = null)) + + assertEquals("", downloadWithNullSize.getProgress()) + } + + @Test + fun setCompatGroup() { + val notificationBuilder = NotificationCompat.Builder(testContext, "") + .setCompatGroup("myGroup").build() + + assertEquals("myGroup", notificationBuilder.group) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `setCompatGroup will not set the group`() { + val notificationBuilder = NotificationCompat.Builder(testContext, "") + .setCompatGroup("myGroup").build() + + assertNotEquals("myGroup", notificationBuilder.group) + } + + @Test + fun getStatusDescription() { + val pausedText = testContext.getString(R.string.mozac_feature_downloads_paused_notification_text) + val completedText = testContext.getString(R.string.mozac_feature_downloads_completed_notification_text2) + val failedText = testContext.getString(R.string.mozac_feature_downloads_failed_notification_text2) + var downloadJobState = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ), + foregroundServiceId = 1, + downloadDeleted = false, + status = DownloadState.Status.DOWNLOADING, + currentBytesCopied = 10, + ) + + assertEquals(downloadJobState.getProgress(), downloadJobState.getStatusDescription(testContext)) + + downloadJobState = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.PAUSED, + ), + foregroundServiceId = 1, + downloadDeleted = false, + status = DownloadState.Status.PAUSED, + ) + + assertEquals(pausedText, downloadJobState.getStatusDescription(testContext)) + + downloadJobState = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.COMPLETED, + ), + foregroundServiceId = 1, + downloadDeleted = false, + status = DownloadState.Status.COMPLETED, + ) + + assertEquals(completedText, downloadJobState.getStatusDescription(testContext)) + + downloadJobState = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.FAILED, + ), + foregroundServiceId = 1, + downloadDeleted = false, + status = DownloadState.Status.FAILED, + ) + + assertEquals(failedText, downloadJobState.getStatusDescription(testContext)) + + downloadJobState = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.CANCELLED, + ), + foregroundServiceId = 1, + downloadDeleted = false, + status = DownloadState.Status.CANCELLED, + ) + + assertEquals("", downloadJobState.getStatusDescription(testContext)) + } + + @Test + fun getDownloadSummary() { + val download1 = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ) + val download2 = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla2.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 20, + status = DownloadState.Status.DOWNLOADING, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 20, + status = DownloadState.Status.DOWNLOADING, + ) + + val summary = DownloadNotification.getSummaryList(testContext, listOf(download1, download2)) + assertEquals(listOf("mozilla.txt 10%", "mozilla2.txt 20%"), summary) + } + + @Test + fun `createOngoingDownloadNotification progress does not overflow`() { + val size = 3 * 1024L * 1024L * 1024L + val copiedSize = size / 2 + val downloadJobState = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = size, + currentBytesCopied = copiedSize, + status = DownloadState.Status.DOWNLOADING, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = copiedSize, + status = DownloadState.Status.DOWNLOADING, + ) + + val style = AbstractFetchDownloadService.Style() + + val notification = DownloadNotification.createOngoingDownloadNotification( + testContext, + downloadJobState, + notificationAccentColor = style.notificationAccentColor, + ) + + assertEquals( + 50L, + 100L * notification.extras.getInt(EXTRA_PROGRESS) / notification.extras.getInt(EXTRA_PROGRESS_MAX), + ) + + assertEquals(false, notification.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE)) + + val notificationNewDownload = DownloadNotification.createOngoingDownloadNotification( + testContext, + downloadJobState.copy(state = downloadJobState.state.copy(contentLength = null)), + notificationAccentColor = style.notificationAccentColor, + ) + + assertEquals(true, notificationNewDownload.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE)) + + val notificationDownloadWithNoSize = DownloadNotification.createOngoingDownloadNotification( + testContext, + downloadJobState.copy(state = downloadJobState.state.copy(contentLength = 0)), + notificationAccentColor = style.notificationAccentColor, + ) + + assertEquals(true, notificationDownloadWithNoSize.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE)) + } + + @Test + fun getOngoingNotificationAccentColor() { + val download = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ) + + val style = AbstractFetchDownloadService.Style() + + val notification = DownloadNotification.createOngoingDownloadNotification( + testContext, + download, + notificationAccentColor = style.notificationAccentColor, + ) + + val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor) + + assertEquals(accentColor, notification.color) + } + + @Test + fun getPausedNotificationAccentColor() { + val download = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.PAUSED, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 10, + status = DownloadState.Status.PAUSED, + ) + + val style = AbstractFetchDownloadService.Style() + + val notification = DownloadNotification.createPausedDownloadNotification( + testContext, + download, + notificationAccentColor = style.notificationAccentColor, + ) + + val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor) + + assertEquals(accentColor, notification.color) + } + + @Test + fun getCompletedNotificationAccentColor() { + val download = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.COMPLETED, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 10, + status = DownloadState.Status.COMPLETED, + ) + + val style = AbstractFetchDownloadService.Style() + + val notification = DownloadNotification.createDownloadCompletedNotification( + testContext, + download, + notificationAccentColor = style.notificationAccentColor, + mock(PendingIntent::class.java), + ) + + val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor) + + assertEquals(accentColor, notification.color) + } + + @Test + fun getFailedNotificationAccentColor() { + val download = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.FAILED, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 10, + status = DownloadState.Status.FAILED, + ) + + val style = AbstractFetchDownloadService.Style() + + val notification = DownloadNotification.createDownloadFailedNotification( + testContext, + download, + notificationAccentColor = style.notificationAccentColor, + ) + + val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor) + + assertEquals(accentColor, notification.color) + } + + @Test + fun getGroupNotificationAccentColor() { + val download1 = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ) + + val download2 = DownloadJobState( + job = null, + state = DownloadState( + fileName = "mozilla.txt", + url = "mozilla.org/mozilla.txt", + contentLength = 100L, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ), + foregroundServiceId = 1, + downloadDeleted = false, + currentBytesCopied = 10, + status = DownloadState.Status.DOWNLOADING, + ) + + val style = AbstractFetchDownloadService.Style() + + val notification = DownloadNotification.createDownloadGroupNotification( + testContext, + listOf(download1, download2), + notificationAccentColor = style.notificationAccentColor, + ) + + val accentColor = ContextCompat.getColor(testContext, style.notificationAccentColor) + + assertEquals(accentColor, notification.color) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadStorageTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadStorageTest.kt new file mode 100644 index 0000000000..ddbfd380f6 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadStorageTest.kt @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.os.Environment +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DownloadStorageTest { + @Test + fun isSameDownload() { + val download = DownloadState( + id = "1", + url = "url", + contentType = "application/zip", + contentLength = 5242880, + status = DownloadState.Status.DOWNLOADING, + destinationDirectory = Environment.DIRECTORY_MUSIC, + ) + + assertTrue(DownloadStorage.isSameDownload(download, download)) + assertFalse(DownloadStorage.isSameDownload(download, download.copy(id = "2"))) + assertFalse(DownloadStorage.isSameDownload(download, download.copy(url = "newUrl"))) + assertFalse(DownloadStorage.isSameDownload(download, download.copy(contentType = "contentType"))) + assertFalse(DownloadStorage.isSameDownload(download, download.copy(contentLength = 0))) + assertFalse(DownloadStorage.isSameDownload(download, download.copy(status = DownloadState.Status.COMPLETED))) + assertFalse(DownloadStorage.isSameDownload(download, download.copy(destinationDirectory = Environment.DIRECTORY_DOWNLOADS))) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadUseCasesTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadUseCasesTest.kt new file mode 100644 index 0000000000..feee7fd022 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadUseCasesTest.kt @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.test.mock +import org.junit.Test +import org.mockito.Mockito.verify + +class DownloadUseCasesTest { + + @Test + fun consumeDownloadUseCase() { + val store: BrowserStore = mock() + val useCases = DownloadsUseCases(store) + + useCases.consumeDownload("tabId", "downloadId") + verify(store).dispatch(ContentAction.ConsumeDownloadAction("tabId", "downloadId")) + } + + @Test + fun restoreDownloadsUseCase() { + val store: BrowserStore = mock() + val useCases = DownloadsUseCases(store) + + useCases.restoreDownloads() + verify(store).dispatch(DownloadAction.RestoreDownloadsStateAction) + } + + @Test + fun removeDownloadUseCase() { + val store: BrowserStore = mock() + val useCases = DownloadsUseCases(store) + + useCases.removeDownload("downloadId") + verify(store).dispatch(DownloadAction.RemoveDownloadAction("downloadId")) + } + + @Test + fun removeAllDownloadsUseCase() { + val store: BrowserStore = mock() + val useCases = DownloadsUseCases(store) + + useCases.removeAllDownloads() + verify(store).dispatch(DownloadAction.RemoveAllDownloadsAction) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt new file mode 100644 index 0000000000..0b956c55be --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt @@ -0,0 +1,1300 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.Manifest.permission.INTERNET +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.downloads.DownloadsUseCases.CancelDownloadRequestUseCase +import mozilla.components.feature.downloads.DownloadsUseCases.ConsumeDownloadUseCase +import mozilla.components.feature.downloads.manager.DownloadManager +import mozilla.components.feature.downloads.ui.DownloadAppChooserDialog +import mozilla.components.feature.downloads.ui.DownloaderApp +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.eq +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.grantPermission +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +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.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.doThrow +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.robolectric.shadows.ShadowToast + +@RunWith(AndroidJUnit4::class) +class DownloadsFeatureTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val dispatcher = coroutinesTestRule.testDispatcher + + private lateinit var store: BrowserStore + + @Before + fun setUp() { + store = BrowserStore( + BrowserState( + tabs = listOf(createTab("https://www.mozilla.org", id = "test-tab")), + selectedTabId = "test-tab", + ), + ) + } + + @Test + fun `Adding a download object will request permissions if needed`() { + val fragmentManager: FragmentManager = mock() + + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + + var requestedPermissions = false + + val feature = DownloadsFeature( + testContext, + store, + useCases = mock(), + onNeedToRequestPermissions = { requestedPermissions = true }, + fragmentManager = mockFragmentManager(), + ) + + feature.start() + + assertFalse(requestedPermissions) + + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download)) + .joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + + assertTrue(requestedPermissions) + verify(fragmentManager, never()).beginTransaction() + } + + @Test + fun `Adding a download when permissions are granted will show dialog`() { + val fragmentManager: FragmentManager = mockFragmentManager() + + grantPermissions() + + val feature = DownloadsFeature( + testContext, + store, + useCases = mock(), + fragmentManager = fragmentManager, + ) + + feature.start() + + verify(fragmentManager, never()).beginTransaction() + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download)) + .joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + + verify(fragmentManager).beginTransaction() + } + + @Test + fun `Try again calls download manager`() { + val fragmentManager: FragmentManager = mockFragmentManager() + + val downloadManager: DownloadManager = mock() + + grantPermissions() + + val feature = DownloadsFeature( + testContext, + store, + useCases = mock(), + fragmentManager = fragmentManager, + downloadManager = downloadManager, + ) + + feature.start() + feature.tryAgain("0") + + verify(downloadManager).tryAgain("0") + } + + @Test + fun `Adding a download without a fragment manager will start download immediately`() { + grantPermissions() + + val downloadManager: DownloadManager = mock() + doReturn( + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE), + ).`when`(downloadManager).permissions + + val feature = DownloadsFeature( + testContext, + store, + useCases = DownloadsUseCases(store), + downloadManager = downloadManager, + ) + + feature.start() + + verify(downloadManager, never()).download(any(), anyString()) + + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + doReturn("id").`when`(downloadManager).download(download) + + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download)) + .joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + + verify(downloadManager).download(eq(download), anyString()) + } + + @Test + fun `Adding a Download with skipConfirmation flag will start download immediately`() { + val fragmentManager: FragmentManager = mockFragmentManager() + + grantPermissions() + + val downloadManager: DownloadManager = mock() + doReturn( + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE), + ).`when`(downloadManager).permissions + + val feature = DownloadsFeature( + testContext, + store, + useCases = DownloadsUseCases(store), + fragmentManager = fragmentManager, + downloadManager = downloadManager, + ) + + feature.start() + + verify(fragmentManager, never()).beginTransaction() + + val download = DownloadState( + url = "https://www.mozilla.org", + skipConfirmation = true, + sessionId = "test-tab", + ) + + doReturn("id").`when`(downloadManager).download(eq(download), anyString()) + + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download)) + .joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + store.waitUntilIdle() + + verify(fragmentManager, never()).beginTransaction() + verify(downloadManager).download(eq(download), anyString()) + + assertNull(store.state.findTab("test-tab")!!.content.download) + } + + @Test + fun `When starting a download an existing dialog is reused`() { + grantPermissions() + + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download)) + .joinBlocking() + + val dialogFragment: DownloadDialogFragment = mock() + val fragmentManager: FragmentManager = mock() + doReturn(dialogFragment).`when`(fragmentManager).findFragmentByTag(DownloadDialogFragment.FRAGMENT_TAG) + + val downloadManager: DownloadManager = mock() + doReturn( + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE), + ).`when`(downloadManager).permissions + + val feature = DownloadsFeature( + testContext, + store, + useCases = mock(), + downloadManager = downloadManager, + fragmentManager = fragmentManager, + ) + + val tab = store.state.findTab("test-tab") + feature.showDownloadDialog(tab!!, download) + + verify(dialogFragment).onStartDownload = any() + verify(dialogFragment).onCancelDownload = any() + verify(dialogFragment).setDownload(download) + verify(dialogFragment, never()).showNow(any(), any()) + } + + @Test + fun `WHEN dismissing a download dialog THEN the download stream should be closed`() { + val downloadsUseCases = spy(DownloadsUseCases(store)) + val closeDownloadResponseUseCase = mock<CancelDownloadRequestUseCase>() + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + val dialogFragment = spy(object : DownloadDialogFragment() {}) + val fragmentManager: FragmentManager = mock() + + doReturn(dialogFragment).`when`(fragmentManager).findFragmentByTag(DownloadDialogFragment.FRAGMENT_TAG) + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download)) + .joinBlocking() + doReturn(closeDownloadResponseUseCase).`when`(downloadsUseCases).cancelDownloadRequest + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = downloadsUseCases, + downloadManager = mock(), + fragmentManager = fragmentManager, + ), + ) + + val tab = store.state.findTab("test-tab") + + feature.showDownloadDialog(tab!!, download) + + dialogFragment.onCancelDownload() + verify(closeDownloadResponseUseCase).invoke(anyString(), anyString()) + } + + @Test + fun `onPermissionsResult will start download if permissions were granted and thirdParty enabled`() { + val downloadsUseCases = spy(DownloadsUseCases(store)) + val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>() + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + val downloadManager: DownloadManager = mock() + val permissionsArray = arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE) + val grantedPermissionsArray = arrayOf(PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED).toIntArray() + + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download)) + .joinBlocking() + + doReturn(permissionsArray).`when`(downloadManager).permissions + doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = downloadsUseCases, + downloadManager = downloadManager, + shouldForwardToThirdParties = { true }, + ), + ) + + doReturn(false).`when`(feature).startDownload(any()) + + grantPermissions() + + feature.onPermissionsResult(permissionsArray, grantedPermissionsArray) + + verify(feature).startDownload(download) + verify(feature, never()).processDownload(any(), eq(download)) + verify(consumeDownloadUseCase).invoke(anyString(), anyString()) + } + + @Test + fun `onPermissionsResult will process download if permissions were granted and thirdParty disabled`() { + val downloadsUseCases = spy(DownloadsUseCases(store)) + val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>() + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + val downloadManager: DownloadManager = mock() + val permissionsArray = arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE) + val grantedPermissionsArray = arrayOf(PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED).toIntArray() + + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download)) + .joinBlocking() + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = downloadsUseCases, + downloadManager = downloadManager, + shouldForwardToThirdParties = { false }, + ), + ) + + doReturn(permissionsArray).`when`(downloadManager).permissions + doReturn(false).`when`(feature).processDownload(any(), any()) + + grantPermissions() + + feature.onPermissionsResult(permissionsArray, grantedPermissionsArray) + + verify(feature).processDownload(any(), eq(download)) + verify(feature, never()).startDownload(download) + verify(consumeDownloadUseCase, never()).invoke(anyString(), anyString()) + } + + @Test + fun `onPermissionsResult will cancel the download if permissions were not granted`() { + val closeDownloadResponseUseCase = mock<CancelDownloadRequestUseCase>() + val store = BrowserStore( + BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab"), + ), + selectedTabId = "test-tab", + ), + ) + val downloadsUseCases = spy(DownloadsUseCases(store)) + + doReturn(closeDownloadResponseUseCase).`when`(downloadsUseCases).cancelDownloadRequest + + store.dispatch( + ContentAction.UpdateDownloadAction( + "test-tab", + DownloadState("https://www.mozilla.org"), + ), + ).joinBlocking() + + val downloadManager: DownloadManager = mock() + doReturn( + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE), + ).`when`(downloadManager).permissions + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = downloadsUseCases, + downloadManager = downloadManager, + ), + ) + + feature.start() + + feature.onPermissionsResult( + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE), + arrayOf(PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_DENIED).toIntArray(), + ) + + store.waitUntilIdle() + + verify(downloadManager, never()).download(any(), anyString()) + verify(closeDownloadResponseUseCase).invoke(anyString(), anyString()) + verify(feature).showPermissionDeniedDialog() + } + + @Test + fun `Calling stop() will unregister listeners from download manager`() { + val downloadManager: DownloadManager = mock() + + val feature = DownloadsFeature( + testContext, + store, + useCases = mock(), + downloadManager = downloadManager, + ) + + feature.start() + + verify(downloadManager, never()).unregisterListeners() + + feature.stop() + + verify(downloadManager).unregisterListeners() + } + + @Test + fun `DownloadManager failing to start download will cause error toast to be displayed`() { + grantPermissions() + + val downloadManager: DownloadManager = mock() + doReturn( + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE), + ).`when`(downloadManager).permissions + + doReturn(null).`when`(downloadManager).download(any(), anyString()) + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = DownloadsUseCases(store), + downloadManager = downloadManager, + ), + ) + + doNothing().`when`(feature).showDownloadNotSupportedError() + + feature.start() + + verify(downloadManager, never()).download(any(), anyString()) + verify(feature, never()).showDownloadNotSupportedError() + + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download)) + .joinBlocking() + + dispatcher.scheduler.advanceUntilIdle() + + verify(downloadManager).download(eq(download), anyString()) + verify(feature).showDownloadNotSupportedError() + } + + @Test + fun `showDownloadNotSupportedError shows toast`() { + grantPermissions() + + val downloadManager: DownloadManager = mock() + doReturn( + arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE), + ).`when`(downloadManager).permissions + + doReturn(null).`when`(downloadManager).download(any(), anyString()) + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = mock(), + downloadManager = downloadManager, + ), + ) + + feature.showDownloadNotSupportedError() + + val toast = ShadowToast.getTextOfLatestToast() + assertNotNull(toast) + assertTrue(toast.contains("can’t download this file type")) + } + + @Test + fun `download dialog must be added once`() { + val fragmentManager = mockFragmentManager() + val dialog = mock<DownloadDialogFragment>() + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = mock(), + downloadManager = mock(), + fragmentManager = fragmentManager, + ), + ) + + feature.showDownloadDialog(mock(), mock(), dialog) + + verify(dialog).showNow(fragmentManager, DownloadDialogFragment.FRAGMENT_TAG) + doReturn(true).`when`(feature).isAlreadyADownloadDialog() + + feature.showDownloadDialog(mock(), mock(), dialog) + verify(dialog, times(1)).showNow(fragmentManager, DownloadDialogFragment.FRAGMENT_TAG) + } + + @Test + fun `download dialog must NOT be shown WHEN the fragmentManager isDestroyed`() { + val fragmentManager = mockFragmentManager() + val dialog = mock<DownloadDialogFragment>() + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = mock(), + downloadManager = mock(), + fragmentManager = fragmentManager, + ), + ) + + doReturn(false).`when`(feature).isAlreadyADownloadDialog() + doReturn(true).`when`(fragmentManager).isDestroyed + + feature.showDownloadDialog(mock(), mock(), dialog) + + verify(dialog, never()).showNow(fragmentManager, DownloadDialogFragment.FRAGMENT_TAG) + } + + @Test + fun `app downloader dialog must NOT be shown WHEN the fragmentManager isDestroyed`() { + val fragmentManager = mockFragmentManager() + val dialog = mock<DownloadAppChooserDialog>() + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = mock(), + downloadManager = mock(), + fragmentManager = fragmentManager, + ), + ) + + doReturn(false).`when`(feature).isAlreadyADownloadDialog() + doReturn(true).`when`(fragmentManager).isDestroyed + + feature.showAppDownloaderDialog(mock(), mock(), emptyList(), dialog) + + verify(dialog, never()).showNow(fragmentManager, DownloadDialogFragment.FRAGMENT_TAG) + } + + @Test + fun `processDownload only forward downloads when shouldForwardToThirdParties is true`() { + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val downloadManager: DownloadManager = mock() + + grantPermissions() + + val feature = spy( + DownloadsFeature( + testContext, + store, + DownloadsUseCases(store), + downloadManager = downloadManager, + shouldForwardToThirdParties = { false }, + ), + ) + + doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions + doReturn(false).`when`(feature).startDownload(download) + + feature.processDownload(tab, download) + + verify(feature, never()).showAppDownloaderDialog(any(), any(), any(), any()) + } + + @Test + fun `processDownload must not forward downloads to third party apps when we are the only app that can handle the download`() { + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = mock<DownloaderApp>() + + grantPermissions() + + val downloadManager: DownloadManager = mock() + doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions + + val feature = spy( + DownloadsFeature( + testContext, + store, + DownloadsUseCases(store), + downloadManager = downloadManager, + shouldForwardToThirdParties = { true }, + ), + ) + + doReturn(false).`when`(feature).startDownload(download) + doReturn(listOf(ourApp)).`when`(feature).getDownloaderApps(testContext, download) + + feature.processDownload(tab, download) + + verify(feature, times(0)).showAppDownloaderDialog(any(), any(), any(), any()) + } + + @Test + fun `processDownload MUST forward downloads to third party apps when there are multiple apps that can handle the download`() { + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = mock<DownloaderApp>() + val anotherApp = mock<DownloaderApp>() + + grantPermissions() + + val downloadManager: DownloadManager = mock() + doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions + + val feature = spy( + DownloadsFeature( + testContext, + store, + DownloadsUseCases(store), + downloadManager = downloadManager, + shouldForwardToThirdParties = { true }, + ), + ) + + doReturn(false).`when`(feature).startDownload(download) + doNothing().`when`(feature).showAppDownloaderDialog(any(), any(), any(), any()) + doReturn(listOf(ourApp, anotherApp)).`when`(feature).getDownloaderApps(testContext, download) + + feature.processDownload(tab, download) + + verify(feature).showAppDownloaderDialog(any(), any(), any(), any()) + } + + @Test + fun `GIVEN download should not be forwarded to third party apps but to a custom delegate WHEN processing a download request THEN forward it to the delegate`() { + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test") + val usecases: DownloadsUseCases = mock() + val consumeDownloadUseCase: ConsumeDownloadUseCase = mock() + val cancelDownloadUseCase: CancelDownloadRequestUseCase = mock() + doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload + doReturn(cancelDownloadUseCase).`when`(usecases).cancelDownloadRequest + val downloadManager: DownloadManager = mock() + var delegateFilename = "" + var delegateContentSize: Long = -1 + var delegatePositiveActionCallback: (() -> Unit)? = null + var delegateNegativeActionCallback: (() -> Unit)? = null + grantPermissions() + doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions + val feature = spy( + DownloadsFeature( + applicationContext = testContext, + store = mock(), + useCases = usecases, + downloadManager = downloadManager, + shouldForwardToThirdParties = { true }, + customFirstPartyDownloadDialog = { filename, contentSize, positiveActionCallback, negativeActionCallback -> + delegateFilename = filename.value + delegateContentSize = contentSize.value + delegatePositiveActionCallback = positiveActionCallback.value + delegateNegativeActionCallback = negativeActionCallback.value + }, + ), + ) + + feature.processDownload(tab, download) + + assertEquals("file.txt", delegateFilename) + assertEquals(0, delegateContentSize) + assertNotNull(delegatePositiveActionCallback) + delegatePositiveActionCallback?.invoke() + verify(consumeDownloadUseCase).invoke(tab.id, download.id) + assertNotNull(delegateNegativeActionCallback) + delegateNegativeActionCallback?.invoke() + verify(cancelDownloadUseCase).invoke(tab.id, download.id) + } + + @Test + fun `GIVEN download should be forwarded to third party apps and a custom delegate is set WHEN processing a download request THEN forward it to the delegate`() { + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test") + val usecases: DownloadsUseCases = mock() + val cancelDownloadUseCase: CancelDownloadRequestUseCase = mock() + doReturn(cancelDownloadUseCase).`when`(usecases).cancelDownloadRequest + val downloadManager: DownloadManager = mock() + var delegateDownloaderApps: List<DownloaderApp> = emptyList() + var delegateChosenAppCallback: ((DownloaderApp) -> Unit)? = null + var delegateNegativeActionCallback: (() -> Unit)? = null + val ourApp = mock<DownloaderApp>() + val anotherApp = mock<DownloaderApp>() + grantPermissions() + doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions + val feature = spy( + DownloadsFeature( + applicationContext = testContext, + store = mock(), + useCases = usecases, + downloadManager = downloadManager, + shouldForwardToThirdParties = { true }, + customThirdPartyDownloadDialog = { apps, chosenAppCallback, dismissCallback -> + delegateDownloaderApps = apps.value + delegateChosenAppCallback = chosenAppCallback.value + delegateNegativeActionCallback = dismissCallback.value + }, + ), + ) + doReturn(listOf(ourApp, anotherApp)).`when`(feature).getDownloaderApps(testContext, download) + doNothing().`when`(feature).onDownloaderAppSelected(anotherApp, tab, download) + + feature.processDownload(tab, download) + + assertEquals(listOf(ourApp, anotherApp), delegateDownloaderApps) + assertNotNull(delegateChosenAppCallback) + delegateChosenAppCallback?.invoke(anotherApp) + verify(feature).onDownloaderAppSelected(anotherApp, tab, download) + assertNotNull(delegateNegativeActionCallback) + delegateNegativeActionCallback?.invoke() + verify(cancelDownloadUseCase).invoke(tab.id, download.id) + } + + @Test + fun `when url is data url return only our app as downloader app`() { + val context = mock<Context>() + val download = DownloadState(url = "data:", sessionId = "test-tab") + val app = mock<ResolveInfo>() + + val activityInfo = mock<ActivityInfo>() + app.activityInfo = activityInfo + val nonLocalizedLabel = "nonLocalizedLabel" + val packageName = "packageName" + val appName = "Fenix" + + activityInfo.packageName = packageName + activityInfo.name = appName + activityInfo.exported = true + + val packageManager = mock<PackageManager>() + whenever(context.packageManager).thenReturn(packageManager) + whenever(context.packageName).thenReturn(packageName) + whenever(app.loadLabel(packageManager)).thenReturn(nonLocalizedLabel) + + val ourApp = DownloaderApp( + nonLocalizedLabel, + app, + packageName, + appName, + download.url, + download.contentType, + ) + + val mockList = listOf(app) + @Suppress("DEPRECATION") + whenever(packageManager.queryIntentActivities(any(), anyInt())).thenReturn(mockList) + + val downloadManager: DownloadManager = mock() + + val feature = DownloadsFeature( + context, + store, + DownloadsUseCases(store), + downloadManager = downloadManager, + shouldForwardToThirdParties = { true }, + ) + + val appList = feature.getDownloaderApps(context, download) + + assertTrue(download.url.startsWith("data:")) + assertEquals(1, appList.size) + assertEquals(ourApp, appList[0]) + } + + @Test + fun `showAppDownloaderDialog MUST setup and show the dialog`() { + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = mock<DownloaderApp>() + val anotherApp = mock<DownloaderApp>() + val apps = listOf(ourApp, anotherApp) + val dialog = mock<DownloadAppChooserDialog>() + val fragmentManager: FragmentManager = mockFragmentManager() + val feature = spy( + DownloadsFeature( + testContext, + store, + DownloadsUseCases(store), + downloadManager = mock(), + shouldForwardToThirdParties = { true }, + fragmentManager = fragmentManager, + ), + ) + + feature.showAppDownloaderDialog(tab, download, apps, dialog) + + verify(dialog).setApps(apps) + verify(dialog).onAppSelected = any() + verify(dialog).onDismiss = any() + verify(dialog).showNow(fragmentManager, DownloadAppChooserDialog.FRAGMENT_TAG) + } + + @Test + fun `WHEN dismissing a downloader app dialog THEN the download should be canceled`() { + val downloadsUseCases = spy(DownloadsUseCases(store)) + val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>() + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = mock<DownloaderApp>() + val anotherApp = mock<DownloaderApp>() + val apps = listOf(ourApp, anotherApp) + val dialog = spy(DownloadAppChooserDialog()) + val fragmentManager: FragmentManager = mockFragmentManager() + val feature = spy( + DownloadsFeature( + testContext, + store, + downloadsUseCases, + downloadManager = mock(), + shouldForwardToThirdParties = { true }, + fragmentManager = fragmentManager, + ), + ) + + doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest + + feature.showAppDownloaderDialog(tab, download, apps, dialog) + dialog.onDismiss() + + verify(cancelDownloadRequestUseCase).invoke(anyString(), anyString()) + } + + @Test + fun `when isAlreadyAppDownloaderDialog we must NOT show the appChooserDialog`() { + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = mock<DownloaderApp>() + val anotherApp = mock<DownloaderApp>() + val apps = listOf(ourApp, anotherApp) + val dialog = mock<DownloadAppChooserDialog>() + val fragmentManager: FragmentManager = mockFragmentManager() + val feature = spy( + DownloadsFeature( + testContext, + store, + DownloadsUseCases(store), + downloadManager = mock(), + shouldForwardToThirdParties = { true }, + fragmentManager = fragmentManager, + ), + ) + + doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG) + doReturn(true).`when`(feature).isAlreadyAppDownloaderDialog() + + feature.showAppDownloaderDialog(tab, download, apps) + + verify(dialog).setApps(apps) + verify(dialog).onAppSelected = any() + verify(dialog).onDismiss = any() + verify(dialog, times(0)).showNow(fragmentManager, DownloadAppChooserDialog.FRAGMENT_TAG) + } + + @Test + fun `when our app is selected for downloading and permission granted then we should perform the download`() { + val spyContext = spy(testContext) + val downloadsUseCases = spy(DownloadsUseCases(store)) + val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>() + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null) + val anotherApp = mock<DownloaderApp>() + val apps = listOf(ourApp, anotherApp) + val dialog = DownloadAppChooserDialog() + val fragmentManager: FragmentManager = mockFragmentManager() + val downloadManager: DownloadManager = mock() + val feature = spy( + DownloadsFeature( + testContext, + store, + downloadsUseCases, + downloadManager = downloadManager, + shouldForwardToThirdParties = { true }, + fragmentManager = fragmentManager, + ), + ) + + grantPermissions() + + doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG) + doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload + doReturn(false).`when`(feature).startDownload(any()) + doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions + + feature.showAppDownloaderDialog(tab, download, apps) + dialog.onAppSelected(ourApp) + + verify(feature).startDownload(any()) + verify(consumeDownloadUseCase).invoke(anyString(), anyString()) + verify(spyContext, times(0)).startActivity(any()) + } + + @Test + fun `GIVEN permissions are granted WHEN our app is selected for download THEN perform the download`() { + val spyContext = spy(testContext) + val usecases: DownloadsUseCases = mock() + val consumeDownloadUseCase: ConsumeDownloadUseCase = mock() + doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test") + val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null) + var wasPermissionsRequested = false + val feature = spy( + DownloadsFeature( + applicationContext = testContext, + store = mock(), + useCases = usecases, + onNeedToRequestPermissions = { wasPermissionsRequested = true }, + ), + ) + doReturn(false).`when`(feature).startDownload(any()) + + grantPermissions() + feature.onDownloaderAppSelected(ourApp, tab, download) + + verify(feature).startDownload(download) + verify(consumeDownloadUseCase).invoke(tab.id, download.id) + assertFalse(wasPermissionsRequested) + verify(spyContext, never()).startActivity(any()) + } + + @Test + fun `GIVEN permissions are not granted WHEN our app is selected for download THEN request the needed permissions`() { + val spyContext = spy(testContext) + val usecases: DownloadsUseCases = mock() + val consumeDownloadUseCase: ConsumeDownloadUseCase = mock() + doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test") + val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null) + var wasPermissionsRequested = false + val feature = spy( + DownloadsFeature( + applicationContext = testContext, + store = mock(), + useCases = usecases, + onNeedToRequestPermissions = { wasPermissionsRequested = true }, + ), + ) + + feature.onDownloaderAppSelected(ourApp, tab, download) + + verify(feature, never()).startDownload(any()) + verify(consumeDownloadUseCase, never()).invoke(anyString(), anyString()) + assertTrue(wasPermissionsRequested) + verify(spyContext, never()).startActivity(any()) + } + + @Test + fun `GIVEN a download WHEN a 3rd party app is selected THEN delegate download to it`() { + val spyContext = spy(testContext) + val usecases: DownloadsUseCases = mock() + val consumeDownloadUseCase: ConsumeDownloadUseCase = mock() + doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test") + val anotherApp = DownloaderApp( + name = "app", + packageName = "test", + resolver = mock(), + activityName = "", + url = download.url, + contentType = null, + ) + val feature = spy( + DownloadsFeature( + applicationContext = spyContext, + store = mock(), + useCases = usecases, + ), + ) + val intentArgumentCaptor = argumentCaptor<Intent>() + val expectedIntent = with(feature) { anotherApp.toIntent() } + + feature.onDownloaderAppSelected(anotherApp, tab, download) + + verify(spyContext).startActivity(intentArgumentCaptor.capture()) + assertEquals(expectedIntent.toUri(0), intentArgumentCaptor.value.toUri(0)) + verify(consumeDownloadUseCase).invoke(tab.id, download.id) + verify(feature, never()).startDownload(any()) + assertNull(ShadowToast.getTextOfLatestToast()) + } + + @Test + fun `GIVEN a download WHEN a 3rd party app is selected and the download fails THEN show a warning toast and consume the download`() { + val spyContext = spy(testContext) + val usecases: DownloadsUseCases = mock() + val consumeDownloadUseCase: ConsumeDownloadUseCase = mock() + doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test") + val anotherApp = DownloaderApp( + name = "app", + packageName = "test", + resolver = mock(), + activityName = "", + url = download.url, + contentType = null, + ) + val feature = spy( + DownloadsFeature( + applicationContext = spyContext, + store = mock(), + useCases = usecases, + ), + ) + val expectedWarningText = testContext.getString( + R.string.mozac_feature_downloads_unable_to_open_third_party_app, + anotherApp.name, + ) + val intentArgumentCaptor = argumentCaptor<Intent>() + val expectedIntent = with(feature) { anotherApp.toIntent() } + doThrow(ActivityNotFoundException()).`when`(spyContext).startActivity(any()) + + feature.onDownloaderAppSelected(anotherApp, tab, download) + + verify(spyContext).startActivity(intentArgumentCaptor.capture()) + assertEquals(expectedIntent.toUri(0), intentArgumentCaptor.value.toUri(0)) + verify(consumeDownloadUseCase).invoke(tab.id, download.id) + verify(feature, never()).startDownload(any()) + assertEquals(expectedWarningText, ShadowToast.getTextOfLatestToast()) + } + + @Test + fun `when an app third party is selected for downloading we MUST forward the download`() { + val spyContext = spy(testContext) + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val downloadsUseCases = spy(DownloadsUseCases(store)) + val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>() + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = DownloaderApp(name = "app", packageName = "thridparty.app", resolver = mock(), activityName = "", url = "", contentType = null) + val anotherApp = mock<DownloaderApp>() + val apps = listOf(ourApp, anotherApp) + val dialog = DownloadAppChooserDialog() + val fragmentManager: FragmentManager = mockFragmentManager() + val feature = spy( + DownloadsFeature( + spyContext, + store, + downloadsUseCases, + downloadManager = mock(), + shouldForwardToThirdParties = { true }, + fragmentManager = fragmentManager, + ), + ) + + doReturn(false).`when`(feature).startDownload(any()) + doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG) + doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload + + feature.showAppDownloaderDialog(tab, download, apps) + dialog.onAppSelected(ourApp) + + verify(feature, times(0)).startDownload(any()) + verify(consumeDownloadUseCase).invoke(anyString(), anyString()) + verify(spyContext).startActivity(any()) + } + + @Test + fun `None exception is thrown when unable to open an app third party for downloading`() { + val spyContext = spy(testContext) + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val downloadsUseCases = spy(DownloadsUseCases(store)) + val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>() + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = DownloaderApp(name = "app", packageName = "thridparty.app", resolver = mock(), activityName = "", url = "", contentType = null) + val anotherApp = mock<DownloaderApp>() + val apps = listOf(ourApp, anotherApp) + val dialog = DownloadAppChooserDialog() + val fragmentManager: FragmentManager = mockFragmentManager() + val feature = spy( + DownloadsFeature( + spyContext, + store, + downloadsUseCases, + downloadManager = mock(), + shouldForwardToThirdParties = { true }, + fragmentManager = fragmentManager, + ), + ) + + doThrow(ActivityNotFoundException()).`when`(spyContext).startActivity(any()) + doReturn(false).`when`(feature).startDownload(any()) + doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG) + doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload + + feature.showAppDownloaderDialog(tab, download, apps) + dialog.onAppSelected(ourApp) + + verify(feature, times(0)).startDownload(any()) + verify(consumeDownloadUseCase).invoke(anyString(), anyString()) + verify(spyContext).startActivity(any()) + } + + @Test + fun `when the appChooserDialog is dismissed THEN the download must be canceled`() { + val spyContext = spy(testContext) + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val downloadsUseCases = spy(DownloadsUseCases(store)) + val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>() + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = mock<DownloaderApp>() + val anotherApp = mock<DownloaderApp>() + val apps = listOf(ourApp, anotherApp) + val dialog = DownloadAppChooserDialog() + val fragmentManager: FragmentManager = mockFragmentManager() + val feature = spy( + DownloadsFeature( + spyContext, + store, + downloadsUseCases, + downloadManager = mock(), + shouldForwardToThirdParties = { true }, + fragmentManager = fragmentManager, + ), + ) + + doReturn(false).`when`(feature).startDownload(any()) + doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG) + doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest + + feature.showAppDownloaderDialog(tab, download, apps) + dialog.onDismiss() + + verify(cancelDownloadRequestUseCase).invoke(anyString(), anyString()) + } + + @Test + fun `ResolveInfo to DownloaderApps`() { + val spyContext = spy(testContext) + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val info = ActivityInfo().apply { + packageName = "thridparty.app" + name = "activityName" + icon = android.R.drawable.btn_default + } + val resolveInfo = ResolveInfo().apply { + labelRes = android.R.string.ok + activityInfo = info + nonLocalizedLabel = "app" + } + + val expectedApp = DownloaderApp(name = "app", packageName = "thridparty.app", resolver = resolveInfo, activityName = "activityName", url = download.url, contentType = download.contentType) + + val app = resolveInfo.toDownloaderApp(spyContext, download) + assertEquals(expectedApp, app) + } + + @Test + fun `previous dialogs MUST be dismissed when navigating to another website`() { + val downloadsUseCases = spy(DownloadsUseCases(store)) + val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>() + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download)) + .joinBlocking() + + doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = downloadsUseCases, + downloadManager = mock(), + ), + ) + + doNothing().`when`(feature).dismissAllDownloadDialogs() + doReturn(true).`when`(feature).processDownload(any(), any()) + + feature.start() + + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download)) + .joinBlocking() + + grantPermissions() + + val tab = createTab("https://www.firefox.com") + store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking() + + verify(feature).dismissAllDownloadDialogs() + verify(downloadsUseCases).cancelDownloadRequest + assertNull(feature.previousTab) + } + + @Test + fun `previous dialogs must NOT be dismissed when navigating on the same website`() { + val downloadsUseCases = spy(DownloadsUseCases(store)) + val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>() + val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab") + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download)) + .joinBlocking() + + doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = downloadsUseCases, + downloadManager = mock(), + ), + ) + + doNothing().`when`(feature).dismissAllDownloadDialogs() + doReturn(true).`when`(feature).processDownload(any(), any()) + + feature.start() + + store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download)) + .joinBlocking() + + grantPermissions() + + val tab = createTab("https://www.mozilla.org/example") + store.dispatch(TabListAction.AddTabAction(tab, select = true)).joinBlocking() + + verify(feature, never()).dismissAllDownloadDialogs() + verify(downloadsUseCases, never()).cancelDownloadRequest + assertNotNull(feature.previousTab) + } + + @Test + fun `when our app is selected for downloading and permission not granted then we should ask for permission`() { + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab") + val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null) + val anotherApp = mock<DownloaderApp>() + val apps = listOf(ourApp, anotherApp) + val downloadManager: DownloadManager = mock() + var permissionsRequested = false + val dialog = DownloadAppChooserDialog() + val downloadsUseCases = spy(DownloadsUseCases(store)) + val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>() + val fragmentManager: FragmentManager = mockFragmentManager() + + val feature = spy( + DownloadsFeature( + testContext, + store, + useCases = downloadsUseCases, + downloadManager = downloadManager, + shouldForwardToThirdParties = { true }, + onNeedToRequestPermissions = { permissionsRequested = true }, + fragmentManager = fragmentManager, + ), + ) + + doReturn(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)).`when`(downloadManager).permissions + doReturn(testContext.packageName).`when`(spy(ourApp)).packageName + doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG) + doReturn(consumeDownloadUseCase).`when`(downloadsUseCases).consumeDownload + + assertFalse(permissionsRequested) + + feature.showAppDownloaderDialog(tab, download, apps, dialog) + dialog.onAppSelected(ourApp) + + assertTrue(permissionsRequested) + + verify(feature, never()).startDownload(any()) + verify(spy(testContext), never()).startActivity(any()) + verify(consumeDownloadUseCase, never()).invoke(anyString(), anyString()) + } +} + +private fun grantPermissions() { + grantPermission(INTERNET, WRITE_EXTERNAL_STORAGE) +} + +private fun mockFragmentManager(): FragmentManager { + val fragmentManager: FragmentManager = mock() + doReturn(mock<FragmentTransaction>()).`when`(fragmentManager).beginTransaction() + return fragmentManager +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragmentTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragmentTest.kt new file mode 100644 index 0000000000..f552c9b0af --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/SimpleDownloadDialogFragmentTest.kt @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads + +import android.app.Application +import android.graphics.drawable.GradientDrawable +import android.view.Gravity +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageButton +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.marginEnd +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.robolectric.annotation.Config +import androidx.appcompat.R as appcompatR + +@RunWith(AndroidJUnit4::class) +@Config(application = TestApplication::class) +class SimpleDownloadDialogFragmentTest { + + private lateinit var dialog: SimpleDownloadDialogFragment + private lateinit var download: DownloadState + private lateinit var mockFragmentManager: FragmentManager + + @Before + fun setup() { + mockFragmentManager = mock() + download = DownloadState( + "http://ipv4.download.thinkbroadband.com/5MB.zip", + "5MB.zip", + "application/zip", + 5242880, + userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36", + ) + dialog = SimpleDownloadDialogFragment.newInstance() + } + + @Test + fun `when the positive button is clicked onStartDownload must be called`() { + var isOnStartDownloadCalled = false + + val onStartDownload = { + isOnStartDownloadCalled = true + } + + val fragment = Mockito.spy(SimpleDownloadDialogFragment.newInstance()) + doNothing().`when`(fragment).dismiss() + + fragment.onStartDownload = onStartDownload + fragment.testingContext = testContext + + doReturn(testContext).`when`(fragment).requireContext() + + val downloadDialog = fragment.onCreateDialog(null) + downloadDialog.show() + + val positiveButton = downloadDialog.findViewById<Button>(R.id.download_button) + positiveButton.performClick() + + assertTrue(isOnStartDownloadCalled) + } + + @Test + fun `when the cancel button is clicked onCancelDownload must be called`() { + var isDownloadCancelledCalled = false + + val onCancelDownload = { + isDownloadCancelledCalled = true + } + + val fragment = Mockito.spy(SimpleDownloadDialogFragment.newInstance()) + doNothing().`when`(fragment).dismiss() + + fragment.onCancelDownload = onCancelDownload + fragment.testingContext = testContext + + doReturn(testContext).`when`(fragment).requireContext() + + val downloadDialog = fragment.onCreateDialog(null) + downloadDialog.show() + + val closeButton = downloadDialog.findViewById<ImageButton>(R.id.close_button) + closeButton.performClick() + + assertTrue(isDownloadCancelledCalled) + } + + @Test + fun `dialog must adhere to promptsStyling`() { + val promptsStyling = DownloadsFeature.PromptsStyling( + gravity = Gravity.TOP, + shouldWidthMatchParent = true, + positiveButtonBackgroundColor = android.R.color.white, + positiveButtonTextColor = android.R.color.black, + positiveButtonRadius = 4f, + fileNameEndMargin = 56, + ) + + val fragment = Mockito.spy( + SimpleDownloadDialogFragment.newInstance( + R.string.mozac_feature_downloads_dialog_title2, + R.string.mozac_feature_downloads_dialog_download, + 0, + promptsStyling, + ), + ) + doReturn(testContext).`when`(fragment).requireContext() + + val dialog = fragment.onCreateDialog(null) + val dialogAttributes = dialog.window!!.attributes + val positiveButton = dialog.findViewById<Button>(R.id.download_button) + val filename = dialog.findViewById<TextView>(R.id.filename) + + assertEquals(ContextCompat.getColor(testContext, promptsStyling.positiveButtonBackgroundColor!!), (positiveButton.background as GradientDrawable).color?.defaultColor) + assertEquals(promptsStyling.positiveButtonRadius!!, (positiveButton.background as GradientDrawable).cornerRadius) + assertEquals(ContextCompat.getColor(testContext, promptsStyling.positiveButtonTextColor!!), positiveButton.textColors.defaultColor) + assertTrue(dialogAttributes.gravity == Gravity.TOP) + assertTrue(dialogAttributes.width == ViewGroup.LayoutParams.MATCH_PARENT) + assertTrue(filename.marginEnd == 56) + } + + private fun mockFragmentManager(): FragmentManager { + val fragmentManager: FragmentManager = mock() + val transaction: FragmentTransaction = mock() + doReturn(transaction).`when`(fragmentManager).beginTransaction() + return fragmentManager + } +} + +class TestApplication : Application() { + override fun onCreate() { + super.onCreate() + setTheme(appcompatR.style.Theme_AppCompat) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/db/DownloadEntityTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/db/DownloadEntityTest.kt new file mode 100644 index 0000000000..84fe5a5310 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/db/DownloadEntityTest.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.downloads.db + +import android.os.Environment +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DownloadEntityTest { + + @Test + fun `convert a DownloadEntity to a DownloadState`() { + val downloadEntity = DownloadEntity( + id = "1", + url = "url", + fileName = "fileName", + contentType = "application/zip", + contentLength = 5242880, + status = DownloadState.Status.DOWNLOADING, + destinationDirectory = Environment.DIRECTORY_MUSIC, + createdAt = 33, + ) + + val downloadState = downloadEntity.toDownloadState() + + assertEquals(downloadEntity.id, downloadState.id) + assertEquals(downloadEntity.url, downloadState.url) + assertEquals(downloadEntity.fileName, downloadState.fileName) + assertEquals(downloadEntity.contentType, downloadState.contentType) + assertEquals(downloadEntity.contentLength, downloadState.contentLength) + assertEquals(downloadEntity.status, downloadState.status) + assertEquals(downloadEntity.destinationDirectory, downloadState.destinationDirectory) + assertEquals(downloadEntity.createdAt, downloadState.createdTime) + } + + @Test + fun `convert a DownloadState to DownloadEntity`() { + val downloadState = DownloadState( + id = "1", + url = "url", + fileName = "fileName", + contentType = "application/zip", + contentLength = 5242880, + status = DownloadState.Status.DOWNLOADING, + destinationDirectory = Environment.DIRECTORY_MUSIC, + private = true, + createdTime = 33, + ) + + val downloadEntity = downloadState.toDownloadEntity() + + assertEquals(downloadState.id, downloadEntity.id) + assertEquals(downloadState.url, downloadEntity.url) + assertEquals(downloadState.fileName, downloadEntity.fileName) + assertEquals(downloadState.contentType, downloadEntity.contentType) + assertEquals(downloadState.contentLength, downloadEntity.contentLength) + assertEquals(downloadState.status, downloadEntity.status) + assertEquals(downloadState.destinationDirectory, downloadEntity.destinationDirectory) + assertEquals(downloadState.createdTime, downloadEntity.createdAt) + } + + @Test + fun `GIVEN a download with data URL WHEN converting a DownloadState to DownloadEntity THEN data url is removed`() { + val downloadState = DownloadState( + id = "1", + url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==", + ) + + val downloadEntity = downloadState.toDownloadEntity() + + assertEquals(downloadState.id, downloadEntity.id) + assertTrue(downloadEntity.url.isEmpty()) + } + + @Test + fun `GIVEN a download with no data URL WHEN converting a DownloadState to DownloadEntity THEN data url is not removed`() { + val downloadState = DownloadState( + id = "1", + url = "url", + ) + + val downloadEntity = downloadState.toDownloadEntity() + + assertEquals(downloadState.id, downloadEntity.id) + assertEquals(downloadState.url, downloadEntity.url) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragmentTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragmentTest.kt new file mode 100644 index 0000000000..60c6580979 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/dialog/DeniedPermissionDialogFragmentTest.kt @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.dialog + +import android.content.DialogInterface.BUTTON_POSITIVE +import android.os.Looper.getMainLooper +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.base.R +import mozilla.components.support.test.ext.appCompatContext +import mozilla.components.support.test.robolectric.testContext +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 +import org.robolectric.Shadows.shadowOf + +@RunWith(AndroidJUnit4::class) +class DeniedPermissionDialogFragmentTest { + + @Test + fun `WHEN showing the dialog THEN it has the provided message`() { + val messageId = R.string.mozac_support_base_permissions_needed_negative_button + val fragment = spy( + DeniedPermissionDialogFragment.newInstance(messageId), + ) + + doReturn(appCompatContext).`when`(fragment).requireContext() + + val dialog = fragment.onCreateDialog(null) + + dialog.show() + + val messageTextView = dialog.findViewById<TextView>(android.R.id.message) + + assertEquals(fragment.message, messageId) + assertEquals(messageTextView.text.toString(), testContext.getString(messageId)) + } + + @Test + fun `WHEN clicking the positive button THEN the settings page will show`() { + val messageId = R.string.mozac_support_base_permissions_needed_negative_button + + val fragment = spy( + DeniedPermissionDialogFragment.newInstance(messageId), + ) + + doNothing().`when`(fragment).dismiss() + doReturn(appCompatContext).`when`(fragment).requireContext() + + val dialog = fragment.onCreateDialog(null) + dialog.show() + + val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE) + positiveButton.performClick() + + shadowOf(getMainLooper()).idle() + + verify(fragment).openSettingsPage() + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ext/DownloadStateKtTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ext/DownloadStateKtTest.kt new file mode 100644 index 0000000000..10364fbcdd --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ext/DownloadStateKtTest.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.downloads.ext + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.support.utils.DownloadUtils +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DownloadStateKtTest { + @Test + fun `GIVEN a download filename is unkwnown WHEN requested with a guessing fallback THEN return a guessed canonical filename`() { + val download = DownloadState( + url = "url", + fileName = null, + ) + val expectedName = with(download) { + DownloadUtils.guessFileName(null, destinationDirectory, url, contentType) + } + + val result = download.realFilenameOrGuessed + + assertEquals(expectedName, result) + } + + @Test + fun `GIVEN a download filename is available WHEN requested with a guessing fallback THEN return the available filename`() { + val download = DownloadState( + url = "http://example.com/file.jpg", + fileName = "test", + contentType = "image/jpeg", + ) + val guessedName = with(download) { + DownloadUtils.guessFileName(null, destinationDirectory, url, contentType) + } + + val result = download.realFilenameOrGuessed + + assertEquals("test", result) + assertNotEquals(guessedName, result) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/AndroidDownloadManagerTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/AndroidDownloadManagerTest.kt new file mode 100644 index 0000000000..8265d376d7 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/AndroidDownloadManagerTest.kt @@ -0,0 +1,212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.manager + +import android.Manifest.permission.INTERNET +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE +import android.app.DownloadManager.Request +import android.content.Intent +import android.os.Build +import android.os.Looper.getMainLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.EXTRA_DOWNLOAD_STATUS +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.grantPermission +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoInteractions +import org.robolectric.Shadows.shadowOf + +@RunWith(AndroidJUnit4::class) +class AndroidDownloadManagerTest { + + private lateinit var store: BrowserStore + private lateinit var download: DownloadState + private lateinit var downloadManager: AndroidDownloadManager + + @Before + fun setup() { + download = DownloadState( + "http://ipv4.download.thinkbroadband.com/5MB.zip", + "", + "application/zip", + 5242880, + userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36", + ) + store = BrowserStore() + downloadManager = AndroidDownloadManager(testContext, store) + } + + @Test(expected = SecurityException::class) + fun `calling download without the right permission must throw an exception`() { + downloadManager.download(download) + } + + @Test + fun `calling download must download the file`() { + var downloadCompleted = false + + downloadManager.onDownloadStopped = { _, _, _ -> downloadCompleted = true } + + grantPermissions() + + assertTrue(store.state.downloads.isEmpty()) + val id = downloadManager.download(download)!! + store.waitUntilIdle() + assertEquals(download.copy(id = id), store.state.downloads[id]) + + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + assertTrue(downloadCompleted) + } + + @Test + fun `calling tryAgain starts the download again`() { + var downloadStopped = false + + downloadManager.onDownloadStopped = { _, _, _ -> downloadStopped = true } + grantPermissions() + + val id = downloadManager.download(download)!! + store.waitUntilIdle() + notifyDownloadFailed(id) + shadowOf(getMainLooper()).idle() + assertTrue(downloadStopped) + + downloadStopped = false + downloadManager.tryAgain(id) + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + assertTrue(downloadStopped) + } + + @Test + fun `trying to download a file with invalid protocol must NOT triggered a download`() { + val invalidDownload = download.copy(url = "ftp://ipv4.download.thinkbroadband.com/5MB.zip") + grantPermissions() + + val id = downloadManager.download(invalidDownload) + assertNull(id) + } + + @Test + fun `GIVEN a device that supports scoped storage THEN permissions must not included file access`() { + val downloadManager = spy(AndroidDownloadManager(testContext, store)) + + doReturn(Build.VERSION_CODES.Q).`when`(downloadManager).getSDKVersion() + println(downloadManager.permissions.joinToString { it }) + assertTrue(WRITE_EXTERNAL_STORAGE !in downloadManager.permissions) + } + + @Test + fun `GIVEN a device does not supports scoped storage THEN permissions must be included file access`() { + val downloadManager = spy(AndroidDownloadManager(testContext, store)) + + doReturn(Build.VERSION_CODES.P).`when`(downloadManager).getSDKVersion() + + assertTrue(WRITE_EXTERNAL_STORAGE in downloadManager.permissions) + + doReturn(Build.VERSION_CODES.O_MR1).`when`(downloadManager).getSDKVersion() + + assertTrue(WRITE_EXTERNAL_STORAGE in downloadManager.permissions) + } + + @Test + fun `sendBroadcast with valid downloadID must call onDownloadStopped after download`() { + var downloadCompleted = false + var downloadStatus: DownloadState.Status? = null + val downloadWithFileName = download.copy(fileName = "5MB.zip") + + grantPermissions() + + val id = downloadManager.download( + downloadWithFileName, + cookie = "yummy_cookie=choco", + )!! + + downloadManager.onDownloadStopped = { _, _, status -> + downloadStatus = status + downloadCompleted = true + } + + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + + assertTrue(downloadCompleted) + assertEquals(DownloadState.Status.COMPLETED, downloadStatus) + } + + @Test + fun `sendBroadcast with completed download`() { + var downloadStatus: DownloadState.Status? = null + val downloadWithFileName = download.copy(fileName = "5MB.zip") + grantPermissions() + + downloadManager.onDownloadStopped = { _, _, status -> + downloadStatus = status + } + + val id = downloadManager.download( + downloadWithFileName, + cookie = "yummy_cookie=choco", + )!! + store.waitUntilIdle() + assertEquals(downloadWithFileName.copy(id = id), store.state.downloads[id]) + + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + assertEquals(DownloadState.Status.COMPLETED, downloadStatus) + } + + @Test + fun `no null or empty headers can be added to the DownloadManager`() { + val mockRequest: Request = mock() + + mockRequest.addRequestHeaderSafely("User-Agent", "") + + verifyNoInteractions(mockRequest) + + mockRequest.addRequestHeaderSafely("User-Agent", null) + + verifyNoInteractions(mockRequest) + + val fireFox = "Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1" + + mockRequest.addRequestHeaderSafely("User-Agent", fireFox) + + verify(mockRequest).addRequestHeader(anyString(), anyString()) + } + + private fun notifyDownloadFailed(id: String) { + val intent = Intent(ACTION_DOWNLOAD_COMPLETE) + intent.putExtra(android.app.DownloadManager.EXTRA_DOWNLOAD_ID, id) + intent.putExtra(EXTRA_DOWNLOAD_STATUS, DownloadState.Status.FAILED) + testContext.sendBroadcast(intent) + } + + private fun notifyDownloadCompleted(id: String) { + val intent = Intent(ACTION_DOWNLOAD_COMPLETE) + intent.putExtra(android.app.DownloadManager.EXTRA_DOWNLOAD_ID, id) + intent.putExtra(EXTRA_DOWNLOAD_STATUS, DownloadState.Status.COMPLETED) + testContext.sendBroadcast(intent) + } + + private fun grantPermissions() { + grantPermission(INTERNET, WRITE_EXTERNAL_STORAGE) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/FetchDownloadManagerTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/FetchDownloadManagerTest.kt new file mode 100644 index 0000000000..e72061db98 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/FetchDownloadManagerTest.kt @@ -0,0 +1,322 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.manager + +import android.Manifest.permission.FOREGROUND_SERVICE +import android.Manifest.permission.INTERNET +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE +import android.app.DownloadManager.EXTRA_DOWNLOAD_ID +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Looper.getMainLooper +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Client +import mozilla.components.feature.downloads.AbstractFetchDownloadService +import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.EXTRA_DOWNLOAD_STATUS +import mozilla.components.support.base.android.NotificationsDelegate +import mozilla.components.support.test.any +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.grantPermission +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.robolectric.Shadows.shadowOf + +@RunWith(AndroidJUnit4::class) +class FetchDownloadManagerTest { + + private lateinit var broadcastManager: LocalBroadcastManager + private lateinit var service: MockDownloadService + private lateinit var download: DownloadState + private lateinit var downloadManager: FetchDownloadManager<MockDownloadService> + private lateinit var store: BrowserStore + private lateinit var notificationsDelegate: NotificationsDelegate + + @Before + fun setup() { + broadcastManager = LocalBroadcastManager.getInstance(testContext) + service = MockDownloadService() + store = BrowserStore() + notificationsDelegate = mock() + download = DownloadState( + "http://ipv4.download.thinkbroadband.com/5MB.zip", + "", + "application/zip", + 5242880, + userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36", + ) + downloadManager = FetchDownloadManager( + testContext, + store, + MockDownloadService::class, + notificationsDelegate = notificationsDelegate, + ) + } + + @Test(expected = SecurityException::class) + fun `calling download without the right permission must throw an exception`() { + downloadManager.download(download) + } + + @Test + fun `calling download must queue the download`() { + var downloadStopped = false + + downloadManager.onDownloadStopped = { _, _, _ -> downloadStopped = true } + + grantPermissions() + + assertTrue(store.state.downloads.isEmpty()) + val id = downloadManager.download(download)!! + store.waitUntilIdle() + assertEquals(download, store.state.downloads[download.id]) + + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + + assertTrue(downloadStopped) + } + + @Test + fun `sending an ACTION_DOWNLOAD_COMPLETE intent without an EXTRA_DOWNLOAD_STATUS should not crash`() { + var downloadStopped = false + + downloadManager.onDownloadStopped = { _, _, _ -> downloadStopped = true } + + grantPermissions() + + assertTrue(store.state.downloads.isEmpty()) + val id = downloadManager.download(download)!! + store.waitUntilIdle() + assertEquals(download, store.state.downloads[download.id]) + + // Excluding the EXTRA_DOWNLOAD_STATUS + val intent = Intent(ACTION_DOWNLOAD_COMPLETE) + intent.putExtra(EXTRA_DOWNLOAD_ID, id) + + testContext.sendBroadcast(intent) + + shadowOf(getMainLooper()).idle() + + assertFalse(downloadStopped) + } + + @Test + fun `calling tryAgain starts the download again`() { + val context = spy(testContext) + downloadManager = FetchDownloadManager( + context, + store, + MockDownloadService::class, + notificationsDelegate = notificationsDelegate, + ) + var downloadStopped = false + + downloadManager.onDownloadStopped = { _, _, _ -> downloadStopped = true } + + grantPermissions() + + val id = downloadManager.download(download)!! + store.waitUntilIdle() + notifyDownloadFailed(id) + shadowOf(getMainLooper()).idle() + assertTrue(downloadStopped) + + downloadStopped = false + downloadManager.tryAgain(id) + verify(context).startService(any()) + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + + assertTrue(downloadStopped) + } + + @Test + fun `GIVEN a device that supports scoped storage THEN permissions must not included file access`() { + val downloadManagerSpy = spy(downloadManager) + + doReturn(Build.VERSION_CODES.Q).`when`(downloadManagerSpy).getSDKVersion() + + assertTrue(WRITE_EXTERNAL_STORAGE !in downloadManagerSpy.permissions) + } + + @Test + fun `GIVEN a device does not supports scoped storage THEN permissions must be included file access`() { + val downloadManagerSpy = spy(downloadManager) + + doReturn(Build.VERSION_CODES.P).`when`(downloadManagerSpy).getSDKVersion() + + assertTrue(WRITE_EXTERNAL_STORAGE in downloadManagerSpy.permissions) + + doReturn(Build.VERSION_CODES.O_MR1).`when`(downloadManagerSpy).getSDKVersion() + + assertTrue(WRITE_EXTERNAL_STORAGE in downloadManagerSpy.permissions) + } + + @Test + fun `try again should not crash when download does not exist`() { + val context: Context = mock() + downloadManager = FetchDownloadManager( + context, + store, + MockDownloadService::class, + notificationsDelegate = notificationsDelegate, + ) + grantPermissions() + val id = downloadManager.download(download)!! + + downloadManager.tryAgain(id + 1) + verify(context, never()).startService(any()) + } + + @Test + fun `trying to download a file with invalid protocol must NOT triggered a download`() { + val invalidDownload = download.copy(url = "ftp://ipv4.download.thinkbroadband.com/5MB.zip") + grantPermissions() + + val id = downloadManager.download(invalidDownload) + assertNull(id) + } + + @Test + fun `trying to download a file with a blob scheme should trigger a download`() { + val validBlobDownload = + download.copy(url = "blob:https://ipv4.download.thinkbroadband.com/5MB.zip") + grantPermissions() + + val id = downloadManager.download(validBlobDownload)!! + assertNotNull(id) + } + + @Test + fun `trying to download a file with a moz-extension scheme should trigger a download`() { + val validBlobDownload = + download.copy(url = "moz-extension://db84fb8b-909c-4270-8567-0e947ffe379f/readerview.html?id=1&url=https%3A%2F%2Fmozilla.org") + grantPermissions() + + val id = downloadManager.download(validBlobDownload)!! + assertNotNull(id) + } + + @Test + fun `sendBroadcast with valid downloadID must call onDownloadStopped after download`() { + var downloadStopped = false + var downloadStatus: DownloadState.Status? = null + val downloadWithFileName = download.copy(fileName = "5MB.zip") + + grantPermissions() + + downloadManager.onDownloadStopped = { _, _, status -> + downloadStatus = status + downloadStopped = true + } + + val id = downloadManager.download( + downloadWithFileName, + cookie = "yummy_cookie=choco", + )!! + store.waitUntilIdle() + + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + + assertTrue(downloadStopped) + assertEquals(DownloadState.Status.COMPLETED, downloadStatus) + } + + @Test + fun `sendBroadcast with completed download`() { + var downloadStatus: DownloadState.Status? = null + val downloadWithFileName = download.copy(fileName = "5MB.zip") + grantPermissions() + + downloadManager.onDownloadStopped = { _, _, status -> + downloadStatus = status + } + + val id = downloadManager.download( + downloadWithFileName, + cookie = "yummy_cookie=choco", + )!! + store.waitUntilIdle() + assertEquals(downloadWithFileName, store.state.downloads[downloadWithFileName.id]) + + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + + store.waitUntilIdle() + assertEquals(DownloadState.Status.COMPLETED, downloadStatus) + } + + @Test + fun `onReceive properly gets download object form sendBroadcast`() { + var downloadStopped = false + var downloadStatus: DownloadState.Status? = null + var downloadName = "" + var downloadSize = 0L + val downloadWithFileName = download.copy(fileName = "5MB.zip", contentLength = 5L) + + grantPermissions() + + downloadManager.onDownloadStopped = { download, _, status -> + downloadStatus = status + downloadStopped = true + downloadName = download.fileName ?: "" + downloadSize = download.contentLength ?: 0 + } + + val id = downloadManager.download(downloadWithFileName)!! + store.waitUntilIdle() + notifyDownloadCompleted(id) + shadowOf(getMainLooper()).idle() + + assertTrue(downloadStopped) + assertEquals("5MB.zip", downloadName) + assertEquals(5L, downloadSize) + assertEquals(DownloadState.Status.COMPLETED, downloadStatus) + } + + private fun notifyDownloadFailed(id: String) { + val intent = Intent(ACTION_DOWNLOAD_COMPLETE) + intent.putExtra(EXTRA_DOWNLOAD_ID, id) + intent.putExtra(EXTRA_DOWNLOAD_STATUS, DownloadState.Status.FAILED) + + testContext.sendBroadcast(intent) + } + + private fun notifyDownloadCompleted(id: String) { + val intent = Intent(ACTION_DOWNLOAD_COMPLETE) + intent.putExtra(EXTRA_DOWNLOAD_ID, id) + intent.putExtra(EXTRA_DOWNLOAD_STATUS, DownloadState.Status.COMPLETED) + + testContext.sendBroadcast(intent) + } + + private fun grantPermissions() { + grantPermission(INTERNET, WRITE_EXTERNAL_STORAGE, FOREGROUND_SERVICE) + } + + class MockDownloadService : AbstractFetchDownloadService() { + override val httpClient: Client = mock() + override val store: BrowserStore = mock() + override val notificationsDelegate: NotificationsDelegate = mock() + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeatureTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeatureTest.kt new file mode 100644 index 0000000000..db89a7cdda --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/CopyDownloadFeatureTest.kt @@ -0,0 +1,340 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.temporary + +import android.content.Context +import android.webkit.MimeTypeMap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.runTest +import mozilla.components.browser.state.action.CopyInternetResourceAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.content.ShareInternetResourceState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE +import mozilla.components.concept.fetch.MutableHeaders +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.After +import org.junit.Assert.assertEquals +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 +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.robolectric.Shadows.shadowOf +import java.io.File +import java.nio.charset.StandardCharsets + +/** + * The 89a gif header as seen on https://www.w3.org/Graphics/GIF/spec-gif89a.txt + */ +private const val GIF_HEADER = "GIF89a" + +@RunWith(AndroidJUnit4::class) +class CopyDownloadFeatureTest { + // When writing new tests initialize CopyDownloadFeature with this class' context property + // When creating new directories use class' context property#cacheDir as a parent + // This will ensure the effectiveness of @After. Otherwise leftover files may be left on the machine running tests. + + private lateinit var context: Context + private val testCacheDirName = "testCacheDir" + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val dispatcher = coroutinesTestRule.testDispatcher + private val scope = coroutinesTestRule.scope + + @Before + fun setup() { + // Effectively reset context mock + context = spy(testContext) + doReturn(File(testCacheDirName)).`when`(context).cacheDir + } + + @After + fun cleanup() { + context.cacheDir.deleteRecursively() + } + + @Test + fun `cleanupCache should automatically be called when this class is initialized`() = runTest { + val cacheDir = File(context.cacheDir, cacheDirName).also { dir -> + dir.mkdirs() + File(dir, "leftoverFile").also { file -> file.createNewFile() } + } + + assertTrue(cacheDir.listFiles()!!.isNotEmpty()) + + CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher) + + assertTrue(cacheDir.listFiles()!!.isEmpty()) + } + + @Test + fun `CopyFeature starts the copy process for AddCopyAction which is immediately consumed`() { + val store = spy( + BrowserStore( + BrowserState( + tabs = listOf( + TabSessionState( + "123", + ContentState(url = "https://www.mozilla.org"), + ), + ), + ), + ), + ) + val copyFeature = + spy(CopyDownloadFeature(context, store, "123", mock(), mock(), dispatcher)) + doNothing().`when`(copyFeature).startCopy(any()) + val download = ShareInternetResourceState(url = "testDownload") + val action = CopyInternetResourceAction.AddCopyAction("123", download) + copyFeature.start() + + store.dispatch(action).joinBlocking() + dispatcher.scheduler.advanceUntilIdle() + + verify(copyFeature).startCopy(download) + verify(store).dispatch(CopyInternetResourceAction.ConsumeCopyAction("123")) + } + + @Test + fun `cleanupCache should delete all files from the cache directory`() = runTest { + val copyFeature = + spy(CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)) + val testDir = File(context.cacheDir, cacheDirName).also { dir -> + dir.mkdirs() + File(dir, "testFile").also { file -> file.createNewFile() } + } + + doReturn(testDir).`when`(copyFeature).getCacheDirectory() + assertTrue(testDir.listFiles()!!.isNotEmpty()) + + copyFeature.cleanupCache() + + assertTrue(testDir.listFiles()!!.isEmpty()) + } + + @Test + fun `startCopy() will download and then copy the selected download`() = runTest { + val confirmationAction = mock<() -> Unit>() + val copyFeature = + spy( + CopyDownloadFeature( + context, + mock(), + null, + confirmationAction, + mock(), + dispatcher, + ), + ) + val shareState = ShareInternetResourceState(url = "testUrl", contentType = "contentType") + val downloadedFile = File("filePath") + doReturn(downloadedFile).`when`(copyFeature).download(any()) + copyFeature.scope = scope + + copyFeature.startCopy(shareState) + + verify(copyFeature).download(shareState) + verify(copyFeature).copy(downloadedFile.canonicalPath, confirmationAction) + } + + @Test + fun `download() will persist in cache the response#body() if available`() { + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher) + val inputStream = "test".byteInputStream(StandardCharsets.UTF_8) + val responseFromShareState = mock<Response>() + doReturn(Response.Body(inputStream)).`when`(responseFromShareState).body + val shareState = + ShareInternetResourceState("randomUrl.jpg", response = responseFromShareState) + doReturn(Response.SUCCESS).`when`(responseFromShareState).status + doReturn(MutableHeaders()).`when`(responseFromShareState).headers + + val result = copyFeature.download(shareState) + + assertTrue(result.exists()) + assertTrue(result.name.endsWith(".$DEFAULT_IMAGE_EXTENSION")) + assertEquals(cacheDirName, result.parentFile!!.name) + assertEquals("test", result.inputStream().bufferedReader().use { it.readText() }) + } + + @Test(expected = RuntimeException::class) + fun `download() will throw an error if the request is not successful`() { + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher) + val inputStream = "test".byteInputStream(StandardCharsets.UTF_8) + val responseFromShareState = mock<Response>() + doReturn(Response.Body(inputStream)).`when`(responseFromShareState).body + val shareState = + ShareInternetResourceState("randomUrl.jpg", response = responseFromShareState) + doReturn(500).`when`(responseFromShareState).status + + copyFeature.download(shareState) + } + + @Test + fun `download() will download from the provided url the response#body() if is unavailable`() { + val client: Client = mock() + val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8) + doAnswer { Response("randomUrl", 200, MutableHeaders(), Response.Body(inputStream)) } + .`when`(client).fetch(any()) + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), client, dispatcher) + val shareState = ShareInternetResourceState("randomUrl") + + val result = copyFeature.download(shareState) + + assertTrue(result.exists()) + assertTrue(result.name.endsWith(".$DEFAULT_IMAGE_EXTENSION")) + assertEquals(cacheDirName, result.parentFile!!.name) + assertEquals("clientTest", result.inputStream().bufferedReader().use { it.readText() }) + } + + @Test + fun `download() will create a not private Request if not in private mode`() { + val client: Client = mock() + val requestCaptor = argumentCaptor<Request>() + val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8) + doAnswer { + Response( + "randomUrl.png", + 200, + MutableHeaders(), + Response.Body(inputStream), + ) + } + .`when`(client).fetch(requestCaptor.capture()) + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), client, dispatcher) + val shareState = ShareInternetResourceState("randomUrl.png", private = false) + + copyFeature.download(shareState) + + assertFalse(requestCaptor.value.private) + } + + @Test + fun `download() will create a private Request if in private mode`() { + val client: Client = mock() + val requestCaptor = argumentCaptor<Request>() + val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8) + doAnswer { + Response( + "randomUrl.png", + 200, + MutableHeaders(), + Response.Body(inputStream), + ) + } + .`when`(client).fetch(requestCaptor.capture()) + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), client, dispatcher) + val shareState = ShareInternetResourceState("randomUrl.png", private = true) + + copyFeature.download(shareState) + + assertTrue(requestCaptor.value.private) + } + + @Test + fun `getFilename(extension) will return a String with the extension suffix`() { + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher) + val testExtension = "testExtension" + + val result = copyFeature.getFilename(testExtension) + + assertTrue(result.endsWith(testExtension)) + assertTrue(result.length > testExtension.length) + } + + @Test + fun `getTempFile(extension) will return a File from the cache dir and with name ending in extension`() { + val copyFeature = + spy(CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)) + val testExtension = "testExtension" + + val result = copyFeature.getTempFile(testExtension) + + assertTrue(result.name.endsWith(testExtension)) + assertEquals(copyFeature.getCacheDirectory().toString(), result.parent) + } + + @Test + fun `getCacheDirectory() will return a new directory in the app's cache`() { + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher) + + val result = copyFeature.getCacheDirectory() + + assertEquals(testCacheDirName, result.parent) + assertEquals(cacheDirName, result.name) + } + + @Test + fun `getMediaShareCacheDirectory creates the needed files if they don't exist`() { + val copyFeature = + spy(CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher)) + assertFalse(context.cacheDir.exists()) + + val result = copyFeature.getMediaShareCacheDirectory() + + assertEquals(cacheDirName, result.name) + assertTrue(result.exists()) + } + + @Test + fun `getFileExtension returns a default extension if one cannot be extracted`() { + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher) + + val result = copyFeature.getFileExtension(mock(), mock()) + + assertEquals(DEFAULT_IMAGE_EXTENSION, result) + } + + @Test + fun `getFileExtension returns an extension based on the media type inferred from the stream`() { + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher) + val gifStream = (GIF_HEADER + "testImage").byteInputStream(StandardCharsets.UTF_8) + // Add the gif mapping to a by default empty shadow of MimeTypeMap. + shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("gif", "image/gif") + + val result = copyFeature.getFileExtension(mock(), gifStream) + + assertEquals("gif", result) + } + + @Test + fun `getFileExtension returns an extension based on the response headers`() { + val copyFeature = + CopyDownloadFeature(context, mock(), null, mock(), mock(), dispatcher) + val gifHeaders = MutableHeaders().apply { set(CONTENT_TYPE, "image/gif") } + // Add the gif mapping to a by default empty shadow of MimeTypeMap. + shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("gif", "image/gif") + + val result = copyFeature.getFileExtension(gifHeaders, mock()) + + assertEquals("gif", result) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeatureTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeatureTest.kt new file mode 100644 index 0000000000..e49ec42285 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/temporary/ShareDownloadFeatureTest.kt @@ -0,0 +1,301 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.downloads.temporary + +import android.content.Context +import android.webkit.MimeTypeMap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.action.ShareInternetResourceAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.content.ShareInternetResourceState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE +import mozilla.components.concept.fetch.MutableHeaders +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.ext.joinBlocking +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.After +import org.junit.Assert.assertEquals +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 +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.robolectric.Shadows.shadowOf +import java.io.File +import java.nio.charset.StandardCharsets + +/** + * The 89a gif header as seen on https://www.w3.org/Graphics/GIF/spec-gif89a.txt + */ +private const val GIF_HEADER = "GIF89a" + +@RunWith(AndroidJUnit4::class) +class ShareDownloadFeatureTest { + // When writing new tests initialize ShareDownloadFeature with this class' context property + // When creating new directories use class' context property#cacheDir as a parent + // This will ensure the effectiveness of @After. Otherwise leftover files may be left on the machine running tests. + + private lateinit var context: Context + private val testCacheDirName = "testCacheDir" + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val dispatcher = coroutinesTestRule.testDispatcher + private val scope = coroutinesTestRule.scope + + @Before + fun setup() { + // Effectively reset context mock + context = spy(testContext) + doReturn(File(testCacheDirName)).`when`(context).cacheDir + } + + @After + fun cleanup() { + context.cacheDir.deleteRecursively() + } + + @Test + fun `cleanupCache should automatically be called when this class is initialized`() = runTestOnMain { + val cacheDir = File(context.cacheDir, cacheDirName).also { dir -> + dir.mkdirs() + File(dir, "leftoverFile").also { file -> + file.createNewFile() + } + } + + assertTrue(cacheDir.listFiles()!!.isNotEmpty()) + + ShareDownloadFeature(context, mock(), null, mock(), dispatcher) + + assertTrue(cacheDir.listFiles()!!.isEmpty()) + } + + @Test + fun `ShareFeature starts the share process for AddShareAction which is immediately consumed`() { + val store = spy( + BrowserStore( + BrowserState( + tabs = listOf(TabSessionState("123", ContentState(url = "https://www.mozilla.org"))), + ), + ), + ) + val shareFeature = spy(ShareDownloadFeature(context, store, "123", mock(), dispatcher)) + doNothing().`when`(shareFeature).startSharing(any()) + val download = ShareInternetResourceState(url = "testDownload") + val action = ShareInternetResourceAction.AddShareAction("123", download) + shareFeature.start() + + store.dispatch(action).joinBlocking() + dispatcher.scheduler.advanceUntilIdle() + + verify(shareFeature).startSharing(download) + verify(store).dispatch(ShareInternetResourceAction.ConsumeShareAction("123")) + } + + @Test + fun `cleanupCache should delete all files from the cache directory`() = runTestOnMain { + val shareFeature = spy(ShareDownloadFeature(context, mock(), null, mock(), dispatcher)) + val testDir = File(context.cacheDir, cacheDirName).also { dir -> + dir.mkdirs() + File(dir, "testFile").also { file -> + file.createNewFile() + } + } + + doReturn(testDir).`when`(shareFeature).getCacheDirectory() + assertTrue(testDir.listFiles()!!.isNotEmpty()) + + shareFeature.cleanupCache() + + assertTrue(testDir.listFiles()!!.isEmpty()) + } + + @Test + fun `startSharing() will download and then share the selected download`() = runTestOnMain { + val shareFeature = spy(ShareDownloadFeature(context, mock(), null, mock(), dispatcher)) + val shareState = ShareInternetResourceState(url = "testUrl", contentType = "contentType") + val downloadedFile = File("filePath") + doReturn(downloadedFile).`when`(shareFeature).download(any()) + shareFeature.scope = scope + + shareFeature.startSharing(shareState) + + verify(shareFeature).download(shareState) + verify(shareFeature).share(downloadedFile.canonicalPath, "contentType", null, null) + } + + @Test + fun `download() will persist in cache the response#body() if available`() { + val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher) + val inputStream = "test".byteInputStream(StandardCharsets.UTF_8) + val responseFromShareState = mock<Response>() + doReturn(Response.Body(inputStream)).`when`(responseFromShareState).body + val shareState = ShareInternetResourceState("randomUrl.jpg", response = responseFromShareState) + doReturn(Response.SUCCESS).`when`(responseFromShareState).status + doReturn(MutableHeaders()).`when`(responseFromShareState).headers + + val result = shareFeature.download(shareState) + + assertTrue(result.exists()) + assertTrue(result.name.endsWith(".$DEFAULT_IMAGE_EXTENSION")) + assertEquals(cacheDirName, result.parentFile!!.name) + assertEquals("test", result.inputStream().bufferedReader().use { it.readText() }) + } + + @Test(expected = RuntimeException::class) + fun `download() will throw an error if the request is not successful`() { + val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher) + val inputStream = "test".byteInputStream(StandardCharsets.UTF_8) + val responseFromShareState = mock<Response>() + doReturn(Response.Body(inputStream)).`when`(responseFromShareState).body + val shareState = + ShareInternetResourceState("randomUrl.jpg", response = responseFromShareState) + doReturn(500).`when`(responseFromShareState).status + + shareFeature.download(shareState) + } + + @Test + fun `download() will download from the provided url the response#body() if is unavailable`() { + val client: Client = mock() + val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8) + doAnswer { Response("randomUrl", 200, MutableHeaders(), Response.Body(inputStream)) } + .`when`(client).fetch(any()) + val shareFeature = ShareDownloadFeature(context, mock(), null, client, dispatcher) + val shareState = ShareInternetResourceState("randomUrl") + + val result = shareFeature.download(shareState) + + assertTrue(result.exists()) + assertTrue(result.name.endsWith(".$DEFAULT_IMAGE_EXTENSION")) + assertEquals(cacheDirName, result.parentFile!!.name) + assertEquals("clientTest", result.inputStream().bufferedReader().use { it.readText() }) + } + + @Test + fun `download() will create a not private Request if not in private mode`() { + val client: Client = mock() + val requestCaptor = argumentCaptor<Request>() + val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8) + doAnswer { Response("randomUrl.png", 200, MutableHeaders(), Response.Body(inputStream)) } + .`when`(client).fetch(requestCaptor.capture()) + val shareFeature = ShareDownloadFeature(context, mock(), null, client, dispatcher) + val shareState = ShareInternetResourceState("randomUrl.png", private = false) + + shareFeature.download(shareState) + + assertFalse(requestCaptor.value.private) + } + + @Test + fun `download() will create a private Request if in private mode`() { + val client: Client = mock() + val requestCaptor = argumentCaptor<Request>() + val inputStream = "clientTest".byteInputStream(StandardCharsets.UTF_8) + doAnswer { Response("randomUrl.png", 200, MutableHeaders(), Response.Body(inputStream)) } + .`when`(client).fetch(requestCaptor.capture()) + val shareFeature = ShareDownloadFeature(context, mock(), null, client, dispatcher) + val shareState = ShareInternetResourceState("randomUrl.png", private = true) + + shareFeature.download(shareState) + + assertTrue(requestCaptor.value.private) + } + + @Test + fun `getFilename(extension) will return a String with the extension suffix`() { + val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher) + val testExtension = "testExtension" + + val result = shareFeature.getFilename(testExtension) + + assertTrue(result.endsWith(testExtension)) + assertTrue(result.length > testExtension.length) + } + + @Test + fun `getTempFile(extension) will return a File from the cache dir and with name ending in extension`() { + val shareFeature = spy(ShareDownloadFeature(context, mock(), null, mock(), dispatcher)) + val testExtension = "testExtension" + + val result = shareFeature.getTempFile(testExtension) + + assertTrue(result.name.endsWith(testExtension)) + assertEquals(shareFeature.getCacheDirectory().toString(), result.parent) + } + + @Test + fun `getCacheDirectory() will return a new directory in the app's cache`() { + val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher) + + val result = shareFeature.getCacheDirectory() + + assertEquals(testCacheDirName, result.parent) + assertEquals(cacheDirName, result.name) + } + + @Test + fun `getMediaShareCacheDirectory creates the needed files if they don't exist`() { + val shareFeature = spy(ShareDownloadFeature(context, mock(), null, mock(), dispatcher)) + assertFalse(context.cacheDir.exists()) + + val result = shareFeature.getMediaShareCacheDirectory() + + assertEquals(cacheDirName, result.name) + assertTrue(result.exists()) + } + + @Test + fun `getFileExtension returns a default extension if one cannot be extracted`() { + val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher) + + val result = shareFeature.getFileExtension(mock(), mock()) + + assertEquals(DEFAULT_IMAGE_EXTENSION, result) + } + + @Test + fun `getFileExtension returns an extension based on the media type inferred from the stream`() { + val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher) + val gifStream = (GIF_HEADER + "testImage").byteInputStream(StandardCharsets.UTF_8) + // Add the gif mapping to a by default empty shadow of MimeTypeMap. + shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("gif", "image/gif") + + val result = shareFeature.getFileExtension(mock(), gifStream) + + assertEquals("gif", result) + } + + @Test + fun `getFileExtension returns an extension based on the response headers`() { + val shareFeature = ShareDownloadFeature(context, mock(), null, mock(), dispatcher) + val gifHeaders = MutableHeaders().apply { + set(CONTENT_TYPE, "image/gif") + } + // Add the gif mapping to a by default empty shadow of MimeTypeMap. + shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("gif", "image/gif") + + val result = shareFeature.getFileExtension(gifHeaders, mock()) + + assertEquals("gif", result) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialogTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialogTest.kt new file mode 100644 index 0000000000..d47fa56571 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloadAppChooserDialogTest.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.downloads.ui + +import android.app.Application +import android.view.Gravity +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.appcompat.widget.AppCompatImageButton +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.feature.downloads.R +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertTrue +import org.junit.Before +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.robolectric.annotation.Config +import androidx.appcompat.R as appcompatR + +@RunWith(AndroidJUnit4::class) +@Config(application = TestApplication::class) +class DownloadAppChooserDialogTest { + + private lateinit var dialog: DownloadAppChooserDialog + private lateinit var download: DownloadState + private lateinit var mockFragmentManager: FragmentManager + + @Before + fun setup() { + mockFragmentManager = mock() + download = DownloadState( + "http://ipv4.download.thinkbroadband.com/5MB.zip", + "5MB.zip", + "application/zip", + 5242880, + userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36", + ) + dialog = DownloadAppChooserDialog.newInstance() + } + + @Test + fun `when an app is selected onAppSelected must be called`() { + var onAppSelectedWasCalled = false + val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null) + val apps = listOf(ourApp) + + val onAppSelected: ((DownloaderApp) -> Unit) = { + onAppSelectedWasCalled = true + } + + val fragment = spy(DownloadAppChooserDialog.newInstance()) + doNothing().`when`(fragment).dismiss() + + fragment.setApps(apps) + fragment.onAppSelected = onAppSelected + + doReturn(testContext).`when`(fragment).requireContext() + + val downloadDialog = fragment.onCreateDialog(null) + downloadDialog.show() + + val adapter = downloadDialog.findViewById<RecyclerView>(R.id.apps_list).adapter + val holder = adapter!!.onCreateViewHolder(LinearLayout(testContext), 0) + adapter.bindViewHolder(holder, 0) + + holder.itemView.performClick() + + assertTrue(onAppSelectedWasCalled) + } + + @Test + fun `when the cancel button is clicked onDismiss must be called`() { + var isDismissCalled = false + + val onDismiss = { + isDismissCalled = true + } + + val fragment = spy(DownloadAppChooserDialog.newInstance()) + doNothing().`when`(fragment).dismiss() + + fragment.onDismiss = onDismiss + + doReturn(testContext).`when`(fragment).requireContext() + + val downloadDialog = fragment.onCreateDialog(null) + downloadDialog.show() + + val closeButton = downloadDialog.findViewById<AppCompatImageButton>(R.id.close_button) + closeButton.performClick() + + assertTrue(isDismissCalled) + } + + @Test + fun `dialog must adhere to promptsStyling`() { + val fragment = spy(DownloadAppChooserDialog.newInstance(Gravity.TOP, true)) + doReturn(testContext).`when`(fragment).requireContext() + + val dialog = fragment.onCreateDialog(null) + val dialogAttributes = dialog.window!!.attributes + + assertTrue(dialogAttributes.gravity == Gravity.TOP) + assertTrue(dialogAttributes.width == ViewGroup.LayoutParams.MATCH_PARENT) + } + + private fun mockFragmentManager(): FragmentManager { + val fragmentManager: FragmentManager = mock() + val transaction: FragmentTransaction = mock() + doReturn(transaction).`when`(fragmentManager).beginTransaction() + return fragmentManager + } +} + +class TestApplication : Application() { + override fun onCreate() { + super.onCreate() + setTheme(appcompatR.style.Theme_AppCompat) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapterTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapterTest.kt new file mode 100644 index 0000000000..2ea423b202 --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/ui/DownloaderAppAdapterTest.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.downloads.ui + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(application = TestApplication::class) +class DownloaderAppAdapterTest { + + @Test + fun `bind apps`() { + val nameLabel = spy(TextView(testContext)) + val iconImage = spy(ImageView(testContext)) + val ourApp = DownloaderApp(name = "app", packageName = "thridparty.app", resolver = mock(), activityName = "", url = "", contentType = null) + val apps = listOf(ourApp) + val view = View(testContext) + var appSelected = false + val viewHolder = DownloaderAppViewHolder(view, nameLabel, iconImage) + + val adapter = DownloaderAppAdapter(testContext, apps) { + appSelected = true + } + + adapter.onBindViewHolder(viewHolder, 0) + view.performClick() + + verify(nameLabel).text = ourApp.name + verify(iconImage).setImageDrawable(any()) + assertTrue(appSelected) + assertEquals(ourApp, view.tag) + } +} diff --git a/mobile/android/android-components/components/feature/downloads/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/downloads/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/downloads/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/downloads/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/downloads/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/feature/downloads/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 |