diff options
Diffstat (limited to 'mobile/android/android-components/components/support/ktx/src')
183 files changed, 7518 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml b/mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..d0418bdaa1 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml @@ -0,0 +1,11 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <application> + <activity + android:name="mozilla.components.support.ktx.TestActivity" + android:exported="false" /> + </application> + +</manifest> diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt new file mode 100644 index 0000000000..ad3d123c10 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx + +import android.app.Activity + +/** + * Empty activity only to be used in UI tests. + */ +internal class TestActivity : Activity() diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt new file mode 100644 index 0000000000..f0ec9bf2d3 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.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.support.ktx.android.net + +import android.content.Context +import androidx.core.net.toUri +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class OnDeviceUriKtTest { + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + @Test + fun isUnderPrivateAppDirectory() { + var uri = "file:///data/user/0/${context.packageName}/any_directory/file.text".toUri() + + assertTrue(uri.isUnderPrivateAppDirectory(context)) + + uri = "file:///data/data/${context.packageName}/any_directory/file.text".toUri() + + assertTrue(uri.isUnderPrivateAppDirectory(context)) + + uri = "file:///data/directory/${context.packageName}/any_directory/file.text".toUri() + + assertFalse(uri.isUnderPrivateAppDirectory(context)) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt new file mode 100644 index 0000000000..93b12d237e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.view + +import android.graphics.Color +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.filters.SdkSuppress +import mozilla.components.support.ktx.TestActivity +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class WindowKtTest { + @get:Rule + internal val activityRule: ActivityScenarioRule<TestActivity> = ActivityScenarioRule(TestActivity::class.java) + + @SdkSuppress(minSdkVersion = 23) + @Test + fun whenALightColorIsAppliedToStatusBarThenSetIsAppearanceLightStatusBarsToTrue() { + activityRule.scenario.onActivity { + it.window.setStatusBarTheme(Color.WHITE) + + assertTrue(it.window.createWindowInsetsController().isAppearanceLightStatusBars) + } + } + + @Test + fun whenADarkColorIsAppliedToStatusBarThenSetIsAppearanceLightStatusBarsToFalse() { + activityRule.scenario.onActivity { + it.window.setStatusBarTheme(Color.BLACK) + + assertFalse(it.window.createWindowInsetsController().isAppearanceLightStatusBars) + } + } + + @SdkSuppress(minSdkVersion = 23) + @Test + fun whenALightColorIsAppliedToNavigationBarThemeThenSetIsAppearanceLightNavigationBarsToTrue() { + activityRule.scenario.onActivity { + it.window.setNavigationBarTheme(Color.WHITE) + + assertTrue(it.window.createWindowInsetsController().isAppearanceLightNavigationBars) + } + } + + @Test + fun whenADarkColorIsAppliedToNavigationBarThemeThenSetIsAppearanceLightNavigationBarsToFalse() { + activityRule.scenario.onActivity { + it.window.setNavigationBarTheme(Color.BLACK) + + assertFalse(it.window.createWindowInsetsController().isAppearanceLightNavigationBars) + } + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c93a4ab93a --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <queries> + <intent> + <action android:name="android.intent.action.SEND" /> + <data android:mimeType="text/plain" /> + </intent> + </queries> +</manifest> diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt new file mode 100644 index 0000000000..5123683117 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.arch.lifecycle + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver + +/** + * Calls [Lifecycle.addObserver] for a variable list of [LifecycleObserver]s. + */ +fun Lifecycle.addObservers(vararg observers: LifecycleObserver) = observers.forEach { addObserver(it) } diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt new file mode 100644 index 0000000000..e1e4dd7299 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt @@ -0,0 +1,344 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.content + +import android.app.ActivityManager +import android.content.ActivityNotFoundException +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_DIAL +import android.content.Intent.ACTION_SEND +import android.content.Intent.ACTION_SENDTO +import android.content.Intent.EXTRA_EMAIL +import android.content.Intent.EXTRA_STREAM +import android.content.Intent.EXTRA_SUBJECT +import android.content.Intent.EXTRA_TEXT +import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.hardware.camera2.CameraManager +import android.net.Uri +import android.os.Build +import android.os.Process +import android.provider.ContactsContract +import android.view.accessibility.AccessibilityManager +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.checkSelfPermission +import androidx.core.content.FileProvider +import androidx.core.content.getSystemService +import mozilla.components.support.base.log.Log +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.R +import mozilla.components.support.ktx.android.content.res.resolveAttribute +import mozilla.components.support.utils.ext.getPackageInfoCompat +import java.io.File + +/** + * The (visible) version name of the application, as specified by the <manifest> tag's versionName + * attribute. E.g. "2.0". + */ +val Context.appVersionName: String + get() = packageManager.getPackageInfoCompat(packageName, 0).versionName + +/** + * Returns the name (label) of the application or the package name as a fallback. + */ +val Context.appName: String + get() = packageManager.getApplicationLabel(applicationInfo).toString() + +/** + * Returns whether or not the operating system is under low memory conditions. + */ +fun Context.isOSOnLowMemory(): Boolean { + val activityManager: ActivityManager = getSystemService()!! + return ActivityManager.MemoryInfo().also { memoryInfo -> + activityManager.getMemoryInfo(memoryInfo) + }.lowMemory +} + +/** + * Returns if a list of permission have been granted, if all the permission have been granted + * returns true otherwise false. + */ +fun Context.isPermissionGranted(permission: Iterable<String>): Boolean { + return permission.all { checkSelfPermission(this, it) == PERMISSION_GRANTED } +} + +fun Context.isPermissionGranted(vararg permission: String): Boolean { + return isPermissionGranted(permission.asIterable()) +} + +/** + * Checks whether or not the device has a camera. + * + * @return true if a camera was found, otherwise false. + */ +@Suppress("TooGenericExceptionCaught") +fun Context.hasCamera(): Boolean { + return try { + val cameraManager: CameraManager? = getSystemService() + cameraManager?.cameraIdList?.isNotEmpty() ?: false + } catch (_: Throwable) { + false + } +} + +/** + * Shares content via [ACTION_SEND] intent. + * + * @param text the data to be shared [EXTRA_TEXT] + * @param subject of the intent [EXTRA_TEXT] + * @return true it is able to share false otherwise. + */ +fun Context.share(text: String, subject: String = getString(R.string.mozac_support_ktx_share_dialog_title)): Boolean { + return try { + val intent = Intent(ACTION_SEND).apply { + type = "text/plain" + putExtra(EXTRA_SUBJECT, subject) + putExtra(EXTRA_TEXT, text) + flags = FLAG_ACTIVITY_NEW_TASK + } + + startActivity( + intent.createChooserExcludingCurrentApp( + this, + getString(R.string.mozac_support_ktx_menu_share_with), + ), + ) + true + } catch (e: ActivityNotFoundException) { + Log.log(Log.Priority.WARN, message = "No activity to share to found", throwable = e, tag = "Reference-Browser") + false + } +} + +/** + * Shares content via [ACTION_SEND] intent. + * + * @param filePath Path of the copied file. + * @param contentType Content type (MIME type) to indicate the media type of the resource. + * @param subject of the intent [EXTRA_SUBJECT] + * @param message of the intent [EXTRA_TEXT] + * + * @return true it is able to share false otherwise. + */ +fun Context.shareMedia( + filePath: String, + contentType: String?, + subject: String? = null, + message: String? = null, +): Boolean { + val contentUri = getContentUriForFile(filePath) + + val intent = Intent().apply { + action = ACTION_SEND + type = contentType ?: contentResolver.getType(contentUri) + flags = FLAG_ACTIVITY_NEW_DOCUMENT or FLAG_GRANT_READ_URI_PERMISSION + putExtra(EXTRA_STREAM, contentUri) + if (subject != null) { + putExtra(EXTRA_SUBJECT, subject) + } + if (message != null) { + putExtra(EXTRA_TEXT, message) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Android Q allows us to show a thumbnail preview of the file to be shared. + clipData = ClipData.newRawUri(contentUri.toString(), contentUri) + } + } + + val shareIntent = Intent.createChooser(intent, getString(R.string.mozac_support_ktx_menu_share_with)).apply { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_GRANT_READ_URI_PERMISSION + } + + return try { + startActivity(shareIntent) + true + } catch (error: ActivityNotFoundException) { + Log.log(Log.Priority.WARN, message = "No activity to share to found", throwable = error, tag = "shareMedia") + + false + } +} + +/** + * Creates a content URI for the given [filePath] to add to the device clipboard and maybe displays + * confirmation feedback. + * + * @param filePath Path of the copied file. + * @param onCopyConfirmation The confirmation action of copying an image. + */ +fun Context.copyImage( + filePath: String, + onCopyConfirmation: () -> Unit, +) { + val contentUri = getContentUriForFile(filePath) + + val clipData = ClipData.newUri(contentResolver, "Copied media URI", contentUri) + getClipboardManager().setPrimaryClip(clipData) + + onCopyConfirmation.invoke() +} + +private fun Context.getContentUriForFile(filePath: String) = FileProvider.getUriForFile( + this, + "${applicationContext.packageName}.feature.downloads.fileprovider", // (packageName + FILE_PROVIDER_EXTENSION) + File(filePath), +) + +private fun Context.getClipboardManager() = + getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + +/** + * Emails content via [ACTION_SENDTO] intent. + * + * @param address the email address to send to [EXTRA_EMAIL] + * @param subject of the intent [EXTRA_TEXT] + * @return true it is able to share email false otherwise. + */ +fun Context.email( + address: String, + subject: String = getString(R.string.mozac_support_ktx_share_dialog_title), +): Boolean { + return try { + val intent = Intent(ACTION_SENDTO, Uri.parse("mailto:$address")) + intent.putExtra(EXTRA_SUBJECT, subject) + + val emailIntent = Intent.createChooser( + intent, + getString(R.string.mozac_support_ktx_menu_email_with), + ).apply { + flags = FLAG_ACTIVITY_NEW_TASK + } + + startActivity(emailIntent) + true + } catch (e: ActivityNotFoundException) { + Logger.warn("No activity found to handle email intent", throwable = e) + false + } +} + +/** + * Calls phone number via [ACTION_DIAL] intent. + * + * Note: we purposely use ACTION_DIAL rather than ACTION_CALL as the latter requires user permission + * @param phoneNumber the phone number to send to [ACTION_DIAL] + * @param subject of the intent [EXTRA_TEXT] + * @return true it is able to share phone call false otherwise. + */ +fun Context.call( + phoneNumber: String, + subject: String = getString(R.string.mozac_support_ktx_share_dialog_title), +): Boolean { + return try { + val intent = Intent(ACTION_DIAL, Uri.parse("tel:$phoneNumber")) + intent.putExtra(EXTRA_SUBJECT, subject) + + val callIntent = Intent.createChooser( + intent, + getString(R.string.mozac_support_ktx_menu_call_with), + ).apply { + flags = FLAG_ACTIVITY_NEW_TASK + } + + startActivity(callIntent) + true + } catch (e: ActivityNotFoundException) { + Logger.warn("No activity found to handle dial intent", throwable = e) + false + } +} + +/** + * Add to contact via [ContactsContract.Intents.Insert.ACTION] + * + * @param address the email address to add to [ContactsContract.Intents.Insert.EMAIL] + * @return true it is able to share email false otherwise. + */ +fun Context.addContact( + address: String, +): Boolean { + return try { + val intent = Intent(ContactsContract.Intents.Insert.ACTION).apply { + type = ContactsContract.RawContacts.CONTENT_TYPE + putExtra(ContactsContract.Intents.Insert.EMAIL, address) + putExtra( + ContactsContract.Intents.Insert.EMAIL_TYPE, + ContactsContract.CommonDataKinds.Email.TYPE_WORK, + ) + addFlags(FLAG_ACTIVITY_NEW_TASK) + } + + startActivity(intent) + true + } catch (e: ActivityNotFoundException) { + Logger.warn("No activity found to handle dial intent", throwable = e) + false + } +} + +/** + * Check if TalkBack service is enabled. + * + * (via https://stackoverflow.com/a/12362545/512580) + */ +inline val Context.isScreenReaderEnabled: Boolean + get() = getSystemService<AccessibilityManager>()?.isTouchExplorationEnabled ?: false + +@VisibleForTesting +internal var isMainProcess: Boolean? = null + +/** + * Returns true if we are running in the main process false otherwise. + */ +fun Context.isMainProcess(): Boolean { + if (isMainProcess != null) return isMainProcess as Boolean + + val pid = Process.myPid() + val activityManager: ActivityManager? = getSystemService() + + isMainProcess = activityManager?.runningAppProcesses.orEmpty().any { processInfo -> + processInfo.pid == pid && processInfo.processName == packageName + } + + return isMainProcess as Boolean +} + +/** + * Takes a function runs it only it if we are running in the main process, otherwise the function will not be executed. + * @param [block] function to be executed in the main process. + */ +inline fun Context.runOnlyInMainProcess(block: () -> Unit) { + if (isMainProcess()) { + block() + } +} + +/** + * Returns the color int corresponding to the attribute. + */ +@ColorInt +fun Context.getColorFromAttr(@AttrRes attr: Int) = + ContextCompat.getColor(this, theme.resolveAttribute(attr)) + +/** + * Returns a tinted drawable for the given resource ID. + * @param resId ID of the drawable to load. + * @param tint Tint color int to apply to the drawable. + */ +fun Context.getDrawableWithTint(@DrawableRes resId: Int, @ColorInt tint: Int) = + AppCompatResources.getDrawable(this, resId)?.apply { + mutate() + setTint(tint) + } diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt new file mode 100644 index 0000000000..041e105c33 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.content + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.LabeledIntent +import android.os.Build +import android.os.Parcelable +import mozilla.components.support.utils.ext.queryIntentActivitiesCompat + +/** + * Modify the current intent to be used in an intent chooser excluding the current app. + * + * @param context Android context used for various system interactions. + * @param title Title that will be displayed in the chooser. + * + * @return a new Intent object that you can hand to Context.startActivity() and related methods. + */ +fun Intent.createChooserExcludingCurrentApp( + context: Context, + title: CharSequence, +): Intent { + val chooserIntent: Intent + val resolveInfos = context.packageManager.queryIntentActivitiesCompat(this, 0).toHashSet() + + val excludedComponentNames = resolveInfos + .map { it.activityInfo } + .filter { it.packageName == context.packageName } + .map { ComponentName(it.packageName, it.name) } + + // Starting with Android N we can use Intent.EXTRA_EXCLUDE_COMPONENTS to exclude components + // other way we are constrained to use Intent.EXTRA_INITIAL_INTENTS. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + chooserIntent = Intent.createChooser(this, title) + .putExtra( + Intent.EXTRA_EXCLUDE_COMPONENTS, + excludedComponentNames.toTypedArray(), + ) + } else { + var targetIntents = resolveInfos + .filterNot { it.activityInfo.packageName == context.packageName } + .map { resolveInfo -> + val activityInfo = resolveInfo.activityInfo + val targetIntent = Intent(this).apply { + component = ComponentName(activityInfo.packageName, activityInfo.name) + } + LabeledIntent( + targetIntent, + activityInfo.packageName, + resolveInfo.labelRes, + resolveInfo.icon, + ) + } + + // Sometimes on Android M and below an empty chooser is displayed, problem reported also here + // https://issuetracker.google.com/issues/37085761 + // To fix that we are creating a chooser with an empty intent + chooserIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent.createChooser(Intent(), title) + } else { + targetIntents = targetIntents.toMutableList() + Intent.createChooser(targetIntents.removeAt(0), title) + } + chooserIntent.putExtra( + Intent.EXTRA_INITIAL_INTENTS, + targetIntents.toTypedArray<Parcelable>(), + ) + } + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return chooserIntent +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt new file mode 100644 index 0000000000..1661c188dd --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:Suppress("MatchingDeclarationName") + +package mozilla.components.support.ktx.android.content + +import android.content.SharedPreferences +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * Represents a class that holds a reference to [SharedPreferences]. + */ +interface PreferencesHolder { + val preferences: SharedPreferences +} + +private class BooleanPreference( + private val key: String, + private val default: Boolean, +) : ReadWriteProperty<PreferencesHolder, Boolean> { + + override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Boolean = + thisRef.preferences.getBoolean(key, default) + + override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Boolean) = + thisRef.preferences.edit().putBoolean(key, value).apply() +} + +private class FloatPreference( + private val key: String, + private val default: Float, +) : ReadWriteProperty<PreferencesHolder, Float> { + + override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Float = + thisRef.preferences.getFloat(key, default) + + override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Float) = + thisRef.preferences.edit().putFloat(key, value).apply() +} + +private class IntPreference( + private val key: String, + private val default: Int, +) : ReadWriteProperty<PreferencesHolder, Int> { + + override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Int = + thisRef.preferences.getInt(key, default) + + override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Int) = + thisRef.preferences.edit().putInt(key, value).apply() +} + +private class LongPreference( + private val key: String, + private val default: Long, +) : ReadWriteProperty<PreferencesHolder, Long> { + + override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Long = + thisRef.preferences.getLong(key, default) + + override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Long) = + thisRef.preferences.edit().putLong(key, value).apply() +} + +private class StringPreference( + private val key: String, + private val default: String, + private val persistDefaultIfNotExists: Boolean = false, +) : ReadWriteProperty<PreferencesHolder, String> { + + override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): String { + return thisRef.preferences.getString(key, null) ?: run { + when (persistDefaultIfNotExists) { + true -> { + thisRef.preferences.edit().putString(key, default).apply() + thisRef.preferences.getString(key, null)!! + } + false -> default + } + } + } + + override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: String) = + thisRef.preferences.edit().putString(key, value).apply() +} + +private class StringSetPreference( + private val key: String, + private val default: Set<String>, +) : ReadWriteProperty<PreferencesHolder, Set<String>> { + + override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Set<String> = + thisRef.preferences.getStringSet(key, default) ?: default + + override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Set<String>) = + thisRef.preferences.edit().putStringSet(key, value).apply() +} + +/** + * Property delegate for getting and setting a boolean shared preference. + * + * Example usage: + * ``` + * class Settings : PreferenceHolder { + * ... + * val isTelemetryOn by booleanPreference("telemetry", default = false) + * } + * ``` + */ +fun booleanPreference(key: String, default: Boolean): ReadWriteProperty<PreferencesHolder, Boolean> = + BooleanPreference(key, default) + +/** + * Property delegate for getting and setting a float number shared preference. + * + * Example usage: + * ``` + * class Settings : PreferenceHolder { + * ... + * var percentage by floatPreference("percentage", default = 0f) + * } + * ``` + */ +fun floatPreference(key: String, default: Float): ReadWriteProperty<PreferencesHolder, Float> = + FloatPreference(key, default) + +/** + * Property delegate for getting and setting an int number shared preference. + * + * Example usage: + * ``` + * class Settings : PreferenceHolder { + * ... + * var widgetNumInvocations by intPreference("widget_number_of_invocations", default = 0) + * } + * ``` + */ +fun intPreference(key: String, default: Int): ReadWriteProperty<PreferencesHolder, Int> = + IntPreference(key, default) + +/** + * Property delegate for getting and setting a long number shared preference. + * + * Example usage: + * ``` + * class Settings : PreferenceHolder { + * ... + * val appInstanceId by longPreference("app_instance_id", default = 123456789L) + * } + * ``` + */ +fun longPreference(key: String, default: Long): ReadWriteProperty<PreferencesHolder, Long> = + LongPreference(key, default) + +/** + * Property delegate for getting and setting a string shared preference. + * Optionally this will persist the default value if one is not already persisted. + * + * Example usage: + * ``` + * class Settings : PreferenceHolder { + * ... + * var permissionsEnabledEnum by stringPreference( + * "permissions_enabled", + * default = "blocked", + * persistDefaultIfNotExists = true, + * ) + * } + * ``` + */ +fun stringPreference( + key: String, + default: String, + persistDefaultIfNotExists: Boolean = false, +): ReadWriteProperty<PreferencesHolder, String> = + StringPreference(key, default, persistDefaultIfNotExists) + +/** + * Property delegate for getting and setting a string set shared preference. + * + * Example usage: + * ``` + * class Settings : PreferenceHolder { + * ... + * var connectedDevices by stringSetPreference("connected_devices", default = emptySet()) + * } + * ``` + */ +fun stringSetPreference(key: String, default: Set<String>): ReadWriteProperty<PreferencesHolder, Set<String>> = + StringSetPreference(key, default) diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt new file mode 100644 index 0000000000..e9a8a9e140 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.content.pm + +import android.content.pm.PackageManager +import mozilla.components.support.utils.ext.getPackageInfoCompat + +/** + * Check if a package is installed + * + * @param packageName The name of the package to check for. + */ +fun PackageManager.isPackageInstalled(packageName: String): Boolean { + return try { + // Turn off all the flags since we don't need the return value + getPackageInfoCompat(packageName, 0) + true + } catch (e: PackageManager.NameNotFoundException) { + false + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt new file mode 100644 index 0000000000..72fa88eba9 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.content.res + +import android.content.res.AssetManager +import org.json.JSONObject + +/** + * Read a file from the "assets" and create a a JSONObject from its content. + * + * @param fileName The name of the asset to open. This name can be + * hierarchical. + */ +fun AssetManager.readJSONObject(fileName: String) = JSONObject( + open(fileName).bufferedReader().use { + it.readText() + }, +) diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt new file mode 100644 index 0000000000..3253c722be --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt @@ -0,0 +1,102 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.content.res + +import android.content.res.Resources +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.text.SpannableStringBuilder +import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE +import android.text.SpannedString +import androidx.annotation.StringRes +import java.util.Formatter +import java.util.Locale + +/** + * Returns the primary locale according to the user's preference. + */ +val Resources.locale: Locale + get() = if (SDK_INT >= Build.VERSION_CODES.N) { + configuration.locales[0] + } else { + @Suppress("Deprecation") + configuration.locale + } + +/** + * Returns the character sequence associated with a given resource [id], + * substituting format arguments with additional styling spans. + * + * Credit to Michael Spitsin https://medium.com/@programmerr47/working-with-spans-in-android-ca4ab1327bc4 + * + * @param id The desired resource identifier, corresponding to a string resource. + * @param spanParts The format arguments that will be used for substitution. + * The first element of each pair is the text to insert, similar to [String.format]. + * The second element of each pair is a span that will be used to style the inserted string. + */ +@Suppress("SpreadOperator") +fun Resources.getSpanned( + @StringRes id: Int, + vararg spanParts: Pair<Any, Any>, +): SpannedString { + val builder = SpannableStringBuilder() + val formatArgs = spanParts.map { (text) -> text }.toTypedArray() + val formatter = Formatter(SpannableAppendable(builder, spanParts), locale) + formatter.format(getString(id), *formatArgs) + return SpannedString(builder) +} + +/** + * [Appendable] implementation that wraps [SpannableStringBuilder] + * and inserts spans from the span parts array. + */ +private class SpannableAppendable( + private val builder: SpannableStringBuilder, + spanParts: Array<out Pair<Any, Any>>, +) : Appendable { + + /** + * Map of values from span parts, with keys converted to char sequences. + */ + private val spansMap = spanParts + .toMap() + .mapKeys { (key) -> key.let { it as? CharSequence ?: it.toString() } } + + override fun append(csq: CharSequence?) = apply { appendSmart(csq) } + + override fun append(csq: CharSequence?, start: Int, end: Int) = apply { + if (csq != null) { + if (start in 0 until end && end <= csq.length) { + append(csq.subSequence(start, end)) + } else { + throw IndexOutOfBoundsException("start " + start + ", end " + end + ", s.length() " + csq.length) + } + } + } + + override fun append(c: Char) = apply { builder.append(c.toString()) } + + /** + * Tries to find [csq] in the [spansMap] and use the corresponding span value. + * If [csq] is not found, the map is searched manually by converting values to strings. + * If no match is found afterwards, [csq] is appended with no corresponding span. + */ + private fun appendSmart(csq: CharSequence?) { + if (csq != null) { + if (csq in spansMap) { + val span = spansMap.getValue(csq) + builder.append(csq, span, SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + val possibleMatchDict = spansMap.filter { (text) -> text.toString() == csq } + if (possibleMatchDict.isNotEmpty()) { + val spanDictEntry = possibleMatchDict.entries.first() + builder.append(spanDictEntry.key, spanDictEntry.value, SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + builder.append(csq) + } + } + } + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt new file mode 100644 index 0000000000..4ad046a05d --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.content.res + +import android.content.res.Resources +import android.util.TypedValue +import androidx.annotation.AnyRes +import androidx.annotation.AttrRes + +/** + * Resolves the resource ID corresponding to the given attribute. + * + * @sample + * context.theme.resolveAttribute(R.attr.textColor) == R.color.light_text_color + */ +@AnyRes +fun Resources.Theme.resolveAttribute(@AttrRes attribute: Int): Int { + val outValue = TypedValue() + resolveAttribute(attribute, outValue, true) + return outValue.resourceId +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt new file mode 100644 index 0000000000..bac132e64e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.graphics + +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Shader.TileMode +import android.util.Base64 +import androidx.annotation.CheckResult +import java.io.ByteArrayOutputStream + +/** + * Transform bitmap into base64 encoded data uri (PNG). + */ +fun Bitmap.toDataUri(): String { + val stream = ByteArrayOutputStream() + compress(Bitmap.CompressFormat.PNG, BITMAP_COMPRESSION_QUALITY, stream) + val encodedImage = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT) + return "data:image/png;base64,$encodedImage" +} + +private const val BITMAP_COMPRESSION_QUALITY = 100 + +/** + * Returns a new bitmap that is the receiver Bitmap with four rounded corners; + * the receiver is unmodified. + * + * This operation is expensive: it requires allocating an identical Bitmap and copying + * all of the Bitmap's pixels. Consider these theoretically cheaper alternatives: + * - android:background= a drawable with rounded corners + * - Wrap your bitmap's ImageView with a layout that masks your view with rounded corners (e.g. CardView) + */ +@CheckResult +fun Bitmap.withRoundedCorners(cornerRadiusPx: Float): Bitmap { + val roundedBitmap = Bitmap.createBitmap(width, height, config) + val canvas = Canvas(roundedBitmap) + val paint = Paint().apply { + isAntiAlias = true + shader = BitmapShader(this@withRoundedCorners, TileMode.CLAMP, TileMode.CLAMP) + } + + canvas.drawRoundRect( + 0.0f, + 0.0f, + width.toFloat(), + height.toFloat(), + cornerRadiusPx, + cornerRadiusPx, + paint, + ) + return roundedBitmap +} + +/** + * Returns true if all pixels have the same value, false otherwise. + */ +fun Bitmap.arePixelsAllTheSame(): Boolean { + val testPixel = getPixel(0, 0) + + // For perf, I expect iteration order is important. Under the hood, the pixels are represented + // by a single array: if you iterate along the buffer, you can take advantage of cache hits + // (since several words in memory are imported each time memory is accessed). + // + // We choose this iteration order (width first) because getPixels writes into a single array + // with index 1 being the same value as getPixel(1, 0) (i.e. it writes width first). + for (y in 0 until height) { + for (x in 0 until width) { + val color = getPixel(x, y) + if (color != testPixel) { + return false + } + } + } + + return true +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt new file mode 100644 index 0000000000..e5d1df1175 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.net + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.os.Build +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import androidx.annotation.VisibleForTesting +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.kotlin.sanitizeFileName +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.util.UUID + +internal val commonPrefixes = listOf("www.", "mobile.", "m.") +internal val mobileSubdomains = listOf("mobile", "m") + +/** + * Returns the host without common prefixes like "www" or "m". + */ +val Uri.hostWithoutCommonPrefixes: String? + get() { + val host = host ?: return null + for (prefix in commonPrefixes) { + if (host.startsWith(prefix)) return host.substring(prefix.length) + } + return host + } + +/** + * Checks that the Uri has the same host as [other], with mobile subdomains removed. + * @param other The Uri to be compared. + */ +fun Uri.sameHostWithoutMobileSubdomainAs(other: Uri): Boolean { + val thisHost = hostWithoutCommonPrefixes?.let { + it.split(".") + .filter { subdomain -> mobileSubdomains.none { mobileSubdomain -> mobileSubdomain == subdomain } } + } ?: return false + val otherHost = other.hostWithoutCommonPrefixes?.let { + it.split(".") + .filter { subdomain -> mobileSubdomains.none { mobileSubdomain -> mobileSubdomain == subdomain } } + } ?: return false + return thisHost == otherHost +} + +/** + * Returns true if the [Uri] uses the "http" or "https" protocol scheme. + */ +val Uri.isHttpOrHttps: Boolean + get() = scheme == "http" || scheme == "https" + +/** + * Checks that the given URL is in one of the given URL [scopes]. + * + * https://www.w3.org/TR/appmanifest/#dfn-within-scope + * + * @param scopes Uris that each represent a scope. + * A Uri is within the scope if the origin matches and it starts with the scope's path. + * @return True if this Uri is within any of the given scopes. + */ +fun Uri.isInScope(scopes: Iterable<Uri>): Boolean { + val path = path.orEmpty() + return scopes.any { scope -> + sameOriginAs(scope) && path.startsWith(scope.path.orEmpty()) + } +} + +/** + * Checks that Uri has the same scheme and host as [other]. + */ +fun Uri.sameSchemeAndHostAs(other: Uri) = scheme == other.scheme && host == other.host + +/** + * Checks that Uri has the same origin as [other]. + * + * https://html.spec.whatwg.org/multipage/origin.html#same-origin + */ +fun Uri.sameOriginAs(other: Uri) = sameSchemeAndHostAs(other) && port == other.port + +/** + * Indicate if the [this] uri is under the application private directory. + */ +fun Uri.isUnderPrivateAppDirectory(context: Context): Boolean { + return when (this.scheme) { + ContentResolver.SCHEME_FILE -> { + try { + val uriPath = path ?: return true + val uriCanonicalPath = File(uriPath).canonicalPath + val dataDirCanonicalPath = File(context.applicationInfo.dataDir).canonicalPath + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + uriCanonicalPath.startsWith(dataDirCanonicalPath) + } else { + // We have to do this manual check on early builds of Android 11 + // as symlink didn't resolve from /data/user/ to data/data + // we have to revise this again once Android 11 is out + // https://github.com/mozilla-mobile/android-components/issues/7750 + uriCanonicalPath.startsWith("/data/data") || uriCanonicalPath.startsWith("/data/user") + } + } catch (e: IOException) { + true + } + } + else -> false + } +} + +/** + * Return a file name for [this] give Uri. + * @return A file name for the content, or generated file name if the URL is invalid or the type is unknown + */ +fun Uri.getFileName(contentResolver: ContentResolver): String { + return when (this.scheme) { + ContentResolver.SCHEME_FILE -> File(path ?: "").name.sanitizeFileName() + ContentResolver.SCHEME_CONTENT -> getFileNameForContentUris(contentResolver) + else -> { + generateFileName(getFileExtension(contentResolver)) + } + } +} + +/** + * Return a file extension for [this] give Uri (only supports content:// schemes). + * @return A file extension for the content, or empty string if the URL is invalid or the type is unknown + */ +fun Uri.getFileExtension(contentResolver: ContentResolver): String { + return MimeTypeMap.getSingleton().getExtensionFromMimeType(contentResolver.getType(this)) ?: "" +} + +/** + * Copy the content of [this] [Uri] into a temporary file in the given [dirToCopy] + * @return A "file://" [Uri] which contains the content of [this] [Uri]. + */ +fun Uri.toFileUri(context: Context, dirToCopy: String = "/temps"): Uri { + val contentResolver = context.contentResolver + val cacheUploadDirectory = File(context.cacheDir, dirToCopy) + + if (!cacheUploadDirectory.exists()) { + cacheUploadDirectory.mkdir() + } + + val temporalFile = File(cacheUploadDirectory, getFileName(contentResolver)) + try { + contentResolver.openInputStream(this)!!.use { inStream -> + copyFile(temporalFile, inStream) + } + } catch (e: IOException) { + Logger("Uri.kt").warn("Could not convert uri to file uri", e) + } + return Uri.parse("file:///${Uri.encode(temporalFile.absolutePath)}") +} + +@VisibleForTesting +internal fun copyFile(temporalFile: File, inStream: InputStream): Long { + return FileOutputStream(temporalFile).use { outStream -> + inStream.copyTo(outStream) + } +} + +@VisibleForTesting +internal fun Uri.getFileNameForContentUris(contentResolver: ContentResolver): String { + var fileName = "" + contentResolver.query(this, null, null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val fileExtension = getFileExtension(contentResolver) + fileName = if (nameIndex == -1) { + generateFileName(fileExtension) + } else { + cursor.moveToFirst() + cursor.getString(nameIndex) ?: generateFileName(fileExtension) + } + } + return fileName.sanitizeFileName() +} + +/** + * Generate a file name using a randomUUID + the current timestamp. + */ +@VisibleForTesting +internal fun generateFileName(fileExtension: String = ""): String { + val randomId = UUID.randomUUID().toString().removePrefix("-").trim() + val timeStamp = System.currentTimeMillis() + return if (fileExtension.isNotEmpty()) { + "$randomId$timeStamp.$fileExtension" + } else { + "$randomId$timeStamp" + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt new file mode 100644 index 0000000000..d4d7063fc0 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:Suppress("MatchingDeclarationName") + +package mozilla.components.support.ktx.android.notification + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.annotation.StringRes +import androidx.core.content.getSystemService + +/** + * Make sure a notification channel exists. + * @param context A [Context], used for creating the notification channel. + * @param onSetupChannel A lambda in the context of the NotificationChannel that gives you the + * opportunity to apply any setup on the channel before gets created. + * @param onCreateChannel A lambda in the context of the NotificationManager that gives you the + * opportunity to perform any operation on the [NotificationManager]. + * @return Returns the channel id to be used. + */ +fun ensureNotificationChannelExists( + context: Context, + channelDate: ChannelData, + onSetupChannel: NotificationChannel.() -> Unit = {}, + onCreateChannel: NotificationManager.() -> Unit = {}, +): String { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = requireNotNull(context.getSystemService<NotificationManager>()) + val channel = NotificationChannel( + channelDate.id, + context.getString(channelDate.name), + channelDate.importance, + ) + onSetupChannel(channel) + notificationManager.createNotificationChannel(channel) + onCreateChannel(notificationManager) + } + + return channelDate.id +} + +/** + * Wraps the data of a NotificationChannel as this class is available after API 26. + */ +class ChannelData(val id: String, @StringRes val name: Int, val importance: Int) diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt new file mode 100644 index 0000000000..de9608508e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.org.json + +import org.json.JSONArray +import org.json.JSONException + +/** + * Convenience method to convert a JSONArray into a sequence. + * + * @param getter callback to get the value for an index in the array. + */ +inline fun <V> JSONArray.asSequence(crossinline getter: JSONArray.(Int) -> V): Sequence<V> { + val indexRange = 0 until length() + return indexRange.asSequence().map { i -> getter(i) } +} + +fun JSONArray.asSequence(): Sequence<Any> = asSequence { i -> get(i) } + +/** + * Convenience method to convert a JSONArray into a List + * + * @return list with the JSONArray values, or an empty list if the JSONArray was null + */ +@Suppress("UNCHECKED_CAST") +fun <T> JSONArray?.toList(): List<T> { + val array = this ?: return emptyList() + return array.asSequence().map { it as T }.toList() +} + +/** + * Returns a list containing only the non-null results of applying the given [transform] function + * to each element in the original collection as returned by [getFromArray]. If [getFromArray] + * or [transform] throws a [JSONException], these elements will also be omitted. + * + * Here's an example call: + * ```kotlin + * jsonArray.mapNotNull(JSONArray::getJSONObject) { jsonObj -> jsonObj.getString("author") } + * ``` + */ +inline fun <T, R : Any> JSONArray.mapNotNull(getFromArray: JSONArray.(index: Int) -> T, transform: (T) -> R?): List<R> { + val transformedResults = mutableListOf<R>() + for (i in 0 until this.length()) { + try { + val transformed = transform(getFromArray(i)) + if (transformed != null) { transformedResults.add(transformed) } + } catch (e: JSONException) { /* Do nothing: we skip bad data. */ } + } + + return transformedResults +} + +fun Iterable<Any>.toJSONArray() = JSONArray().also { array -> + forEach { array.put(it) } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt new file mode 100644 index 0000000000..a939afd2e6 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.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.support.ktx.android.org.json + +import org.json.JSONObject +import java.util.TreeMap + +/** + * Returns the value mapped by {@code key} if it exists, and + * if the value returned is not null. If it's null, it returns null + */ +fun JSONObject.tryGet(key: String): Any? = if (isNull(key)) null else get(key) + +/** + * Returns the value mapped by {@code key} if it exists, and + * if the value returned is not null. If it's null, it returns null + */ +fun JSONObject.tryGetString(key: String): String? = if (isNull(key)) null else getString(key) + +/** + * Returns the value mapped by {@code key} if it exists, and + * if the value returned is not null. If it's null, it returns null + */ +fun JSONObject.tryGetInt(key: String): Int? = if (isNull(key)) null else getInt(key) + +/** + * Returns the value mapped by {@code key} if it exists, and + * if the value returned is not null. If it's null, it returns null + */ +fun JSONObject.tryGetLong(key: String): Long? = if (isNull(key)) null else getLong(key) + +/** + * Puts the specified value under the key if it's not null + */ +fun JSONObject.putIfNotNull(key: String, value: Any?) { + if (value != null) { + put(key, value) + } +} + +/** + * Sorts the keys of a JSONObject (and all of its child JSONObjects) alphabetically + */ +fun JSONObject.sortKeys(): JSONObject { + val map = TreeMap<String, Any>() + for (key in this.keys()) { + map[key] = this[key] + } + val jsonObject = JSONObject() + for (key in map.keys) { + if (map[key] is JSONObject) { + map[key] = (map[key] as JSONObject).sortKeys() + } + jsonObject.put(key, map[key]) + } + return jsonObject +} + +/** + * Convert a Map<String, String> to a JSONObject + */ +fun Map<String, String>.toJSON() = JSONObject().apply { + forEach { (key, value) -> put(key, value) } +} + +/** + * Merge the contents of another [JSONObject] with this object, + * overwriting the colliding keys. + * + * @param other the [JSONObject] providing the data to be + * merged with this one. + */ +fun JSONObject.mergeWith(other: JSONObject) { + for (key in other.keys()) { + put(key, other[key]) + } +} + +/** + * Gets the [JSONObject] value with the given key if it exists. + * Otherwise calls the defaultValue function, adds its + * result to the object, and returns that. + * + * @param key the key to get or create. + * @param defaultValue a function returning a new default value + * @return the existing or new value + */ +fun JSONObject.getOrPutJSONObject(key: String, defaultValue: () -> JSONObject): JSONObject { + optJSONObject(key)?.let { + return it + } ?: run { + val value = defaultValue() + put(key, value) + return value + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt new file mode 100644 index 0000000000..2bd0be117b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.os + +import android.os.Bundle + +/** + * Returns `true` if the two specified bundles are *structurally* equal to one another, + * i.e. contain the same number of the same elements in the same order. + */ +@Suppress("ComplexMethod") +infix fun Bundle.contentEquals(other: Bundle): Boolean { + if (size() != other.size()) return false + + @Suppress("DEPRECATION") // we still need to use get(String) in order to compare any Objects. + return keySet().all { key -> + val valueTwo = other.get(key) + when (val valueOne = get(key)) { + // Compare bundles deeply + is Bundle -> valueTwo is Bundle && valueOne contentEquals valueTwo + + // Compare arrays using contentEquals + is BooleanArray -> valueTwo is BooleanArray && valueOne contentEquals valueTwo + is ByteArray -> valueTwo is ByteArray && valueOne contentEquals valueTwo + is CharArray -> valueTwo is CharArray && valueOne contentEquals valueTwo + is DoubleArray -> valueTwo is DoubleArray && valueOne contentEquals valueTwo + is FloatArray -> valueTwo is FloatArray && valueOne contentEquals valueTwo + is IntArray -> valueTwo is IntArray && valueOne contentEquals valueTwo + is LongArray -> valueTwo is LongArray && valueOne contentEquals valueTwo + is ShortArray -> valueTwo is ShortArray && valueOne contentEquals valueTwo + is Array<*> -> valueTwo is Array<*> && valueOne contentEquals valueTwo + + else -> valueOne == valueTwo + } + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt new file mode 100644 index 0000000000..ffa8f6e3b5 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.os + +import android.os.StrictMode + +/** + * Runs the given [functionBlock] and sets the ThreadPolicy after its completion. + * + * This function is written in the style of [AutoCloseable.use]. + * + * @return the value returned by [functionBlock]. + */ +inline fun <R> StrictMode.ThreadPolicy.resetAfter(functionBlock: () -> R): R = try { + functionBlock() +} finally { + StrictMode.setThreadPolicy(this) +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt new file mode 100644 index 0000000000..8c0cb9a271 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.os + +import android.Manifest.permission.VIBRATE +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.VibrationEffect +import android.os.VibrationEffect.DEFAULT_AMPLITUDE +import android.os.Vibrator +import androidx.annotation.RequiresPermission + +/** + * Vibrate constantly for the specified period of time. + * + * @param milliseconds The number of milliseconds to vibrate. + */ +@RequiresPermission(VIBRATE) +fun Vibrator.vibrateOneShot(milliseconds: Long) { + if (SDK_INT >= Build.VERSION_CODES.O) { + vibrate(VibrationEffect.createOneShot(milliseconds, DEFAULT_AMPLITUDE)) + } else { + @Suppress("Deprecation") + vibrate(milliseconds) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt new file mode 100644 index 0000000000..ffea3eaf64 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.util + +import android.util.Base64 + +object Base64 { + fun encodeToUriString(data: String) = + "data:text/html;base64," + Base64.encodeToString(data.toByteArray(), Base64.DEFAULT) +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt new file mode 100644 index 0000000000..ecb54c7ea3 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.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.support.ktx.android.util + +import android.util.DisplayMetrics +import android.util.TypedValue + +/** + * Converts a value in density independent pixels (dp) to a float value. + */ +fun Int.dpToFloat(displayMetrics: DisplayMetrics) = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + displayMetrics, +) + +/** + * Converts a value in density independent pixels (dp) to the actual pixel values for the display. + */ +fun Int.dpToPx(displayMetrics: DisplayMetrics) = dpToFloat(displayMetrics).toInt() + +/** Converts a value in density independent pixels (dp) to a px value. */ +fun Float.dpToPx(displayMetrics: DisplayMetrics) = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this, + displayMetrics, +) + +/** Converts a value in scale independent pixels (sp) to a px value. */ +fun Float.spToPx(displayMetrics: DisplayMetrics) = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + this, + displayMetrics, +) diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt new file mode 100644 index 0000000000..b1d29d000d --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.util + +import android.util.JsonReader +import android.util.JsonToken + +/** + * Returns the [JsonToken.STRING] value of the next token or `null` if the next token + * is [JsonToken.NULL]. + */ +fun JsonReader.nextStringOrNull(): String? { + return if (peek() == JsonToken.NULL) { + nextNull() + null + } else { + nextString() + } +} + +/** + * Returns the [JsonToken.BOOLEAN] value of the next token or `null` if the next token + * is [JsonToken.NULL]. + */ +fun JsonReader.nextBooleanOrNull(): Boolean? { + return if (peek() == JsonToken.NULL) { + nextNull() + null + } else { + nextBoolean() + } +} + +/** + * Returns the [JsonToken.NUMBER] value of the next token or `null` if the next token + * is [JsonToken.NULL]. + */ +fun JsonReader.nextIntOrNull(): Int? { + return if (peek() == JsonToken.NULL) { + nextNull() + null + } else { + nextInt() + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt new file mode 100644 index 0000000000..283bd30720 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.view + +import android.app.Activity +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES +import android.view.View +import android.view.WindowManager +import androidx.core.view.ViewCompat +import androidx.core.view.ViewCompat.onApplyWindowInsets +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import mozilla.components.support.base.log.logger.Logger + +/** + * Attempts to enter immersive mode - fullscreen with the status bar and navigation buttons hidden, + * expanding itself into the notch area for devices running API 28+. + * + * This will automatically register and use an inset listener: [View.OnApplyWindowInsetsListener] + * to restore immersive mode if interactions with various other widgets like the keyboard or dialogs + * got the activity out of immersive mode without [exitImmersiveMode] being called. + */ +fun Activity.enterImmersiveMode( + insetsController: WindowInsetsControllerCompat = window.createWindowInsetsController(), +) { + insetsController.hideInsets() + + ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, insetsCompat -> + if (insetsCompat.isVisible(WindowInsetsCompat.Type.statusBars())) { + insetsController.hideInsets() + } + // Allow the decor view to have a chance to process the incoming WindowInsets. + onApplyWindowInsets(view, insetsCompat) + } + + if (SDK_INT >= VERSION_CODES.P) { + window.setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + ) + window.attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } +} + +private fun WindowInsetsControllerCompat.hideInsets() { + apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } +} + +/** + * Shows the system UI windows that were hidden, thereby exiting the immersive experience. + * For devices running API 28+, this function also restores the application's use + * of the notch area of the phone to the default behavior. + * + * @param insetsController is an optional [WindowInsetsControllerCompat] object for controlling the + * window insets. + */ +fun Activity.exitImmersiveMode( + insetsController: WindowInsetsControllerCompat = window.createWindowInsetsController(), +) { + insetsController.show(WindowInsetsCompat.Type.systemBars()) + + ViewCompat.setOnApplyWindowInsetsListener(window.decorView, null) + + if (SDK_INT >= VERSION_CODES.P) { + window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + window.attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT + } +} + +/** + * Calls [Activity.reportFullyDrawn] while also preventing crashes under some circumstances. + * + * @param errorLogger the logger to be used if errors are logged. + */ +fun Activity.reportFullyDrawnSafe(errorLogger: Logger) { + try { + reportFullyDrawn() + } catch (e: SecurityException) { + // This exception is throw on some Samsung devices. We were unable to identify the root + // cause but suspect it's related to Samsung security features. See + // https://github.com/mozilla-mobile/fenix/issues/12345#issuecomment-655058864 for details. + // + // We include "Fully drawn" in the log statement so that this error appears when grepping + // for fully drawn time. + errorLogger.error("Fully drawn - unable to call reportFullyDrawn", e) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt new file mode 100644 index 0000000000..7861fc7e64 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.view + +import android.view.MotionEvent + +/** + * Executes the given [functionBlock] function on this resource and then closes it down correctly whether + * an exception is thrown or not. This is inspired by [java.lang.AutoCloseable.use]. + */ +inline fun <R> MotionEvent.use(functionBlock: (MotionEvent) -> R): R { + try { + return functionBlock(this) + } finally { + recycle() + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt new file mode 100644 index 0000000000..0fec74272e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.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/. */ + +@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public APIs. + +package mozilla.components.support.ktx.android.view + +import android.graphics.drawable.Drawable +import android.widget.TextView + +/** + * Sets the [Drawable]s (if any) to appear to the start of, above, to the end of, + * and below the text. Use `null` if you do not want a Drawable there. + * The Drawables must already have had [Drawable.setBounds] called. + * + * Calling this method will overwrite any Drawables previously set using + * [TextView.setCompoundDrawables] or related methods. + */ +inline fun TextView.putCompoundDrawablesRelative( + start: Drawable? = null, + top: Drawable? = null, + end: Drawable? = null, + bottom: Drawable? = null, +) = setCompoundDrawablesRelative(start, top, end, bottom) + +/** + * + * Sets the [Drawable]s (if any) to appear to the start of, above, to the end of, + * and below the text. Use `null` if you do not want a Drawable there. + * The Drawables' bounds will be set to their intrinsic bounds. + * + * Calling this method will overwrite any Drawables previously set using + * [TextView.setCompoundDrawables] or related methods. + */ +inline fun TextView.putCompoundDrawablesRelativeWithIntrinsicBounds( + start: Drawable? = null, + top: Drawable? = null, + end: Drawable? = null, + bottom: Drawable? = null, +) = setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom) diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt new file mode 100644 index 0000000000..625eae2c96 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.view + +import android.graphics.Rect +import android.os.Handler +import android.os.Looper +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.view.inputmethod.InputMethodManager +import androidx.annotation.MainThread +import androidx.core.content.getSystemService +import androidx.core.view.ViewCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import mozilla.components.support.base.android.Padding +import mozilla.components.support.ktx.android.util.dpToPx +import java.lang.ref.WeakReference + +/** + * Is the horizontal layout direction of this view from Right to Left? + */ +val View.isRTL: Boolean + get() = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL + +/** + * Is the horizontal layout direction of this view from Left to Right? + */ +val View.isLTR: Boolean + get() = layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR + +/** + * Tries to focus this view and show the soft input window for it. + * + * @param flags Provides additional operating flags to be used with InputMethodManager.showSoftInput(). + * Currently may be 0, SHOW_IMPLICIT or SHOW_FORCED. + */ +fun View.showKeyboard(flags: Int = InputMethodManager.SHOW_IMPLICIT) { + ShowKeyboard(this, flags).post() +} + +/** + * Hides the soft input window. + */ +fun View.hideKeyboard() { + val imm = context.getSystemService<InputMethodManager>() + imm?.hideSoftInputFromWindow(windowToken, 0) +} + +/** + * Fills the given [Rect] with data about location view in the window. + * + * @see View.getLocationInWindow + */ +fun View.getRectWithViewLocation(): Rect { + val locationInWindow = IntArray(2).apply { getLocationInWindow(this) } + return Rect( + locationInWindow[0], + locationInWindow[1], + locationInWindow[0] + width, + locationInWindow[1] + height, + ) +} + +/** + * Set a padding using [Padding] object. + */ +fun View.setPadding(padding: Padding) { + with(resources) { + setPadding( + padding.left.dpToPx(displayMetrics), + padding.top.dpToPx(displayMetrics), + padding.right.dpToPx(displayMetrics), + padding.bottom.dpToPx(displayMetrics), + ) + } +} + +/** + * Creates a [CoroutineScope] that is active as long as this [View] is attached. Once this [View] + * gets detached this [CoroutineScope] gets cancelled automatically. + * + * By default coroutines dispatched on the created [CoroutineScope] run on the main dispatcher. + * + * Note: This scope gets only cancelled if the [View] gets detached. In cases where the [View] never + * gets attached this may create a scope that never gets cancelled! + */ +@MainThread +fun View.toScope(): CoroutineScope { + val scope = MainScope() + + addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) = Unit + + override fun onViewDetachedFromWindow(view: View) { + scope.cancel() + view.removeOnAttachStateChangeListener(this) + } + }, + ) + + return scope +} + +/** + * Finds the first a view in the hierarchy, for which the provided predicate is true. + */ +fun View.findViewInHierarchy(predicate: (View) -> Boolean): View? { + if (predicate(this)) return this + + if (this is ViewGroup) { + for (i in 0 until this.childCount) { + val childView = this.getChildAt(i).findViewInHierarchy(predicate) + if (childView != null) return childView + } + } + + return null +} + +/** + * Registers a one-time callback to be invoked when the global layout state + * or the visibility of views within the view tree changes. + */ +inline fun View.onNextGlobalLayout(crossinline callback: () -> Unit) { + var listener: ViewTreeObserver.OnGlobalLayoutListener? = null + listener = ViewTreeObserver.OnGlobalLayoutListener { + viewTreeObserver.removeOnGlobalLayoutListener(listener) + callback() + } + viewTreeObserver.addOnGlobalLayoutListener(listener) +} + +private class ShowKeyboard( + view: View, + private val flags: Int = InputMethodManager.SHOW_IMPLICIT, +) : Runnable { + private val weakReference: WeakReference<View> = WeakReference(view) + private val handler: Handler = Handler(Looper.getMainLooper()) + private var tries: Int = TRIES + + override fun run() { + weakReference.get()?.let { view -> + if (!view.isFocusable || !view.isFocusableInTouchMode) { + // The view is not focusable - we can't show the keyboard for it. + return + } + + if (!view.requestFocus()) { + // Focus this view first. + post() + return + } + + view.context?.getSystemService<InputMethodManager>()?.let { imm -> + if (!imm.isActive(view)) { + // This view is not the currently active view for the input method yet. + post() + return + } + + if (!imm.showSoftInput(view, flags)) { + // Showing they keyboard failed. Try again later. + post() + } + } + } + } + + fun post() { + tries-- + + if (tries > 0) { + handler.postDelayed(this, INTERVAL_MS) + } + } + + companion object { + private const val INTERVAL_MS = 100L + private const val TRIES = 10 + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt new file mode 100644 index 0000000000..0e564cf6cf --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.view + +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.view.Window +import androidx.annotation.ColorInt +import androidx.core.view.WindowInsetsControllerCompat +import mozilla.components.support.utils.ColorUtils.isDark + +/** + * Sets the status bar background color. If the color is light enough, a light navigation bar with + * dark icons will be used. + */ +fun Window.setStatusBarTheme(@ColorInt color: Int) { + createWindowInsetsController().isAppearanceLightStatusBars = !isDark(color) + statusBarColor = color +} + +/** + * Set the navigation bar background and divider colors. If the color is light enough, a light + * navigation bar with dark icons will be used. + */ +fun Window.setNavigationBarTheme( + @ColorInt navBarColor: Int? = null, + @ColorInt navBarDividerColor: Int? = null, +) { + navBarColor?.let { + navigationBarColor = it + createWindowInsetsController().isAppearanceLightNavigationBars = !isDark(it) + } + + if (SDK_INT >= Build.VERSION_CODES.P) { + navigationBarDividerColor = navBarDividerColor ?: 0 + } +} + +/** + * Creates a {@link WindowInsetsControllerCompat} for the top-level window decor view. + */ +fun Window.createWindowInsetsController(): WindowInsetsControllerCompat { + return WindowInsetsControllerCompat(this, this.decorView) +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt new file mode 100644 index 0000000000..00a0a80bc9 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.widget + +import android.view.View +import android.widget.TextView + +/* This is the sum of both the default ascender height and the default descender height in Android */ +private const val DEFAULT_FONT_PADDING = 6 + +/** + * Adjusts the text size of the [TextView] according to the height restriction given to the + * [View.MeasureSpec] given in the parameter. + * + * This will take [TextView.getIncludeFontPadding] into account when calculating the available height + */ +fun TextView.adjustMaxTextSize(heightMeasureSpec: Int, ascenderPadding: Int = DEFAULT_FONT_PADDING) { + val maxHeight = View.MeasureSpec.getSize(heightMeasureSpec) + + var availableHeight = maxHeight.toFloat() + if (this.includeFontPadding) { + availableHeight -= ascenderPadding * resources.displayMetrics.density + } + + availableHeight -= (this.paddingBottom + this.paddingTop) * + resources.displayMetrics.density + + if (availableHeight > 0 && this.textSize > availableHeight) { + this.textSize = availableHeight / resources.displayMetrics.density + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt new file mode 100644 index 0000000000..c3e925fc06 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.java.io + +import java.io.File + +/** + * Removes all files in the directory named by this abstract pathname. Does nothing if the [File] is not pointing to + * a directory. + */ +fun File.truncateDirectory() { + if (!isDirectory) { + return + } + + listFiles()?.forEach { file -> + if (file.isDirectory) { + file.truncateDirectory() + file.delete() + } else { + file.delete() + } + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt new file mode 100644 index 0000000000..0d12578d3d --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.kotlin + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import mozilla.components.support.base.log.logger.Logger +import java.security.MessageDigest + +/** + * Checks whether the given [test] byte sequence exists at the [offset] of this [ByteArray] + */ +fun ByteArray.containsAtOffset(offset: Int, test: ByteArray): Boolean { + if (size - offset < test.size) { + return false + } + + for (i in 0 until test.size) { + if (this[offset + i] != test[i]) { + return false + } + } + + return true +} + +fun ByteArray.toBitmap(opts: BitmapFactory.Options? = null): Bitmap? { + return toBitmap(0, size, opts) +} + +fun ByteArray.toBitmap( + offset: Int, + length: Int, + opts: BitmapFactory.Options? = null, +): Bitmap? { + if (length <= 0) { + return null + } + + return try { + val bitmap = BitmapFactory.decodeByteArray(this, offset, length, opts) + + if (bitmap == null) { + null + } else if (bitmap.width <= 0 || bitmap.height <= 0) { + Logger.warn("Decoded bitmap jas dimensions: ${bitmap.width} x ${bitmap.height}") + null + } else { + bitmap + } + } catch (e: OutOfMemoryError) { + Logger.warn("OutOfMemoryError while decoding byte array", e) + null + } +} + +fun ByteArray.toSha256Digest(): ByteArray { + return MessageDigest.getInstance("SHA-256").digest(this) +} + +/** + * @return A SHA-1 digest. + */ +fun ByteArray.toSha1Digest(): ByteArray { + return MessageDigest.getInstance("SHA-1").digest(this) +} + +/** + * @return An unpadded byte array, according to PKCS#7. + */ +@Suppress("MagicNumber") +fun ByteArray.pkcs7unpad(): ByteArray { + // Last byte tells us the padding length. + val paddingLength = this.last() + // Padding can't be more than 15 bytes. + if (paddingLength in 0..16) { + return this.copyOfRange(0, this.size - paddingLength) + } + return this +} + +fun ByteArray.toHexString(): String { + return toHexString(2 * this.size) +} + +@Suppress("MagicNumber") +fun ByteArray.toHexString(hexLength: Int): String { + val hs = StringBuilder(Math.max(2 * this.size, hexLength)) + var stmp: String + + for (n in 0 until hexLength - 2 * this.size) { + hs.append("0") + } + + for (n in this.indices) { + stmp = Integer.toHexString(this[n].toInt() and 0XFF) + + if (stmp.length == 1) { + hs.append("0") + } + hs.append(stmp) + } + + return hs.toString() +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt new file mode 100644 index 0000000000..03425446ab --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:SuppressWarnings("TopLevelPropertyNaming") + +package mozilla.components.support.ktx.kotlin + +/** + * A series of dots (typically three, such as "…") that usually indicates an intentional omission of + * a word, sentence, or whole section from a text without altering its original meaning. + */ +val Char.Companion.ELLIPSIS: Char + get() = '…' diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt new file mode 100644 index 0000000000..9cda9e3212 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.kotlin + +/** + * Performs a cartesian product of all the elements in two collections and returns each pair to + * the [block] function. + * + * Example: + * + * ```kotlin + * val numbers = listOf(1, 2, 3) + * val letters = listOf('a', 'b', 'c') + * numbers.crossProduct(letters) { number, letter -> + * // Each combination of (1, a), (1, b), (1, c), (2, a), (2, b), etc. + * } + * ``` + */ +inline fun <T, U, R> Collection<T>.crossProduct( + other: Collection<U>, + block: (T, U) -> R, +) = flatMap { first -> + other.map { second -> first to second }.map { block(it.first, it.second) } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt new file mode 100644 index 0000000000..abcd1a741c --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt @@ -0,0 +1,442 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:Suppress("TooManyFunctions") + +package mozilla.components.support.ktx.kotlin + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.InetAddresses +import android.net.Uri +import android.os.Build +import android.util.Base64 +import android.util.Patterns +import android.webkit.URLUtil +import androidx.annotation.VisibleForTesting +import androidx.core.net.toUri +import mozilla.components.lib.publicsuffixlist.PublicSuffixList +import mozilla.components.support.ktx.android.net.commonPrefixes +import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes +import mozilla.components.support.ktx.util.URLStringUtils +import java.io.File +import java.net.IDN +import java.net.MalformedURLException +import java.net.URL +import java.net.URLEncoder +import java.security.MessageDigest +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.text.RegexOption.IGNORE_CASE + +/** + * A collection of regular expressions used in the `is*` methods below. + */ +private val re = object { + val phoneish = "^\\s*tel:\\S?\\d+\\S*\\s*$".toRegex(IGNORE_CASE) + val emailish = "^\\s*mailto:\\w+\\S*\\s*$".toRegex(IGNORE_CASE) + val geoish = "^\\s*geo:\\S*\\d+\\S*\\s*$".toRegex(IGNORE_CASE) +} + +private const val MAILTO = "mailto:" + +// Number of last digits to be shown when credit card number is obfuscated. +private const val LAST_VISIBLE_DIGITS_COUNT = 4 + +// This is used for truncating URLs to prevent extreme cases from +// slowing down UI rendering e.g. in case of a bookmarklet or a data URI. +// https://github.com/mozilla-mobile/android-components/issues/5249 +const val MAX_URI_LENGTH = 25000 + +private const val FILE_PREFIX = "file://" +private const val MAX_VALID_PORT = 65_535 + +/** + * Shortens URLs to be more user friendly. + * + * The algorithm used to generate these strings is a combination of FF desktop 'top sites', + * feedback from the security team, and documentation regarding url elision. See + * StringTest.kt for details. + * + * This method is complex because URLs have a lot of edge cases. Be sure to thoroughly unit + * test any changes you make to it. + */ +// Unused Parameter: We may resume stripping eTLD, depending on conversations between security and UX +// Return count: This is a complex method, but it would not be more understandable if broken up +// ComplexCondition: Breaking out the complex condition would make this logic harder to follow +@Suppress("UNUSED_PARAMETER", "ReturnCount", "ComplexCondition") +fun String.toShortUrl(publicSuffixList: PublicSuffixList): String { + val inputString = this + val uri = inputString.toUri() + + if ( + inputString.isEmpty() || + !URLUtil.isValidUrl(inputString) || + inputString.startsWith(FILE_PREFIX) || + uri.port !in -1..MAX_VALID_PORT + ) { + return inputString + } + + if (uri.host?.isIpv4OrIpv6() == true || + // If inputString is just a hostname and not a FQDN, use the entire hostname. + uri.host?.contains(".") == false + ) { + return uri.host ?: inputString + } + + fun String.stripUserInfo(): String { + val userInfo = this.toUri().encodedUserInfo + return if (userInfo != null) { + val infoIndex = this.indexOf(userInfo) + this.removeRange(infoIndex..infoIndex + userInfo.length) + } else { + this + } + } + fun String.stripPrefixes(): String = this.toUri().hostWithoutCommonPrefixes ?: this + fun String.toUnicode() = IDN.toUnicode(this) + + return inputString + .stripUserInfo() + .lowercase(Locale.getDefault()) + .stripPrefixes() + .toUnicode() +} + +// impl via FFTV https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/main/java/org/mozilla/focus/utils/FormattedDomain.java#129 +@Suppress("DEPRECATION") +internal fun String.isIpv4(): Boolean = Patterns.IP_ADDRESS.matcher(this).matches() + +// impl via FFiOS: https://github.com/mozilla-mobile/firefox-ios/blob/deb9736c905cdf06822ecc4a20152df7b342925d/Shared/Extensions/NSURLExtensions.swift#L292 +// True IPv6 validation is difficult. This is slightly better than nothing +internal fun String.isIpv6(): Boolean { + return this.isNotEmpty() && this.contains(":") +} + +/** + * Returns true if the string represents a valid Ipv4 or Ipv6 IP address. + * Note: does not validate a dual format Ipv6 ( "y:y:y:y:y:y:x.x.x.x" format). + * + */ +@Suppress("TooManyFunctions") +fun String.isIpv4OrIpv6(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + InetAddresses.isNumericAddress(this) + } else { + this.isIpv4() || this.isIpv6() + } +} + +/** + * Checks if this String is a URL. + */ +fun String.isUrl() = URLStringUtils.isURLLike(this) + +/** + * Checks if this String is a URL of an extension page. + */ +fun String.isExtensionUrl() = this.startsWith("moz-extension://") + +/** + * Checks if this String is a URL of a resource. + */ +fun String.isResourceUrl() = this.startsWith("resource://") + +/** + * Appends `http` scheme if no scheme is present in this String. + */ +fun String.toNormalizedUrl(): String { + val s = this.trim() + // Most commonly we'll encounter http or https schemes. + // For these, avoid running through toNormalizedURL as an optimization. + return if (!s.startsWith("http://") && + !s.startsWith("https://") + ) { + URLStringUtils.toNormalizedURL(s) + } else { + s + } +} + +fun String.isPhone() = re.phoneish.matches(this) + +fun String.isEmail() = re.emailish.matches(this) + +fun String.isGeoLocation() = re.geoish.matches(this) + +/** + * Converts a [String] to a [Date] object. + * @param format date format used for formatting the this given [String] object. + * @param locale the locale to use when converting the String, defaults to [Locale.ROOT]. + * @return a [Date] object with the values in the provided in this string, if empty string was provided, a current date + * will be returned. + */ +fun String.toDate(format: String, locale: Locale = Locale.ROOT): Date { + val formatter = SimpleDateFormat(format, locale) + return if (isNotEmpty()) { + formatter.parse(this) ?: Date() + } else { + Date() + } +} + +/** + * Calculates a SHA1 hash for this string. + */ +@Suppress("MagicNumber") +fun String.sha1(): String { + val characters = "0123456789abcdef" + val digest = MessageDigest.getInstance("SHA-1").digest(toByteArray()) + return digest.joinToString( + separator = "", + transform = { byte -> + String(charArrayOf(characters[byte.toInt() shr 4 and 0x0f], characters[byte.toInt() and 0x0f])) + }, + ) +} + +/** + * Tries to convert a [String] to a [Date] using a list of [possibleFormats]. + * @param possibleFormats one ore more possible format. + * @return a [Date] object with the values in the provided in this string, + * if the conversion is not possible null will be returned. + */ +fun String.toDate( + vararg possibleFormats: String = arrayOf( + "yyyy-MM-dd'T'HH:mm", + "yyyy-MM-dd", + "yyyy-'W'ww", + "yyyy-MM", + "HH:mm", + ), +): Date? { + possibleFormats.forEach { + try { + return this.toDate(it) + } catch (pe: ParseException) { + // move to next possible format + } + } + return null +} + +/** + * Tries to parse and get host part if this [String] is valid URL. + * Otherwise returns the string. + */ +fun String.tryGetHostFromUrl(): String = try { + URL(this).host +} catch (e: MalformedURLException) { + this +} + +/** + * Returns `true` if this string is a valid URL that contains [searchParameters] in its query parameters. + */ +fun String.urlContainsQueryParameters(searchParameters: String): Boolean = try { + URL(this).query?.split("&")?.any { it == searchParameters } ?: false +} catch (e: MalformedURLException) { + false +} + +/** + * Compares 2 URLs and returns true if they have the same origin, + * which means: same protocol, same host, same port. + * It will return false if either this or [other] is not a valid URL. + */ +fun String.isSameOriginAs(other: String): Boolean { + fun canonicalizeOrigin(urlStr: String): String { + val url = URL(urlStr) + val port = if (url.port == -1) url.defaultPort else url.port + val canonicalized = URL(url.protocol, url.host, port, "") + return canonicalized.toString() + } + return try { + canonicalizeOrigin(this) == canonicalizeOrigin(other) + } catch (e: MalformedURLException) { + false + } +} + +/** + * Returns an origin (protocol, host and port) from an URL string. + */ +fun String.getOrigin(): String? { + return try { + val url = URL(this) + val port = if (url.port == -1) url.defaultPort else url.port + URL(url.protocol, url.host, port, "").toString() + } catch (e: MalformedURLException) { + null + } +} + +/** + * Returns an origin without the default port. + * For example for an input of "https://mozilla.org:443" you will get "https://mozilla.org". + */ +fun String.stripDefaultPort(): String { + return try { + val url = URL(this) + val port = if (url.port == url.defaultPort) -1 else url.port + URL(url.protocol, url.host, port, "").toString() + } catch (e: MalformedURLException) { + this + } +} + +/** + * Remove any unwanted character in url like spaces at the beginning or end. + */ +fun String.sanitizeURL(): String { + return this.trim() +} + +/** + * Remove any unwanted character from string containing file name. + * For example for an input of "/../../../../../../directory/file.txt" you will get "file.txt" + */ +fun String.sanitizeFileName(): String { + val file = File(this.substringAfterLast(File.separatorChar)) + // Remove unwanted subsequent dots in the file name. + return if (file.extension.trim().isNotEmpty() && file.nameWithoutExtension.isNotEmpty()) { + file.name.replace("\\.\\.+".toRegex(), ".") + } else { + file.name.replace(".", "") + } +} + +/** + * Remove leading mailto from the string. + * For example for an input of "mailto:example@example.com" you will get "example@example.com" + */ +fun String.stripMailToProtocol(): String { + return if (this.startsWith(MAILTO)) { + this.replaceFirst(MAILTO, "") + } else { + this + } +} + +/** + * Translates the string into {@code application/x-www-form-urlencoded} string. + */ +fun String.urlEncode(): String { + return URLEncoder.encode(this, Charsets.UTF_8.name()) +} + +/** + * Returns the string if it's length is not higher than @param[maximumLength] or + * a @param[replacement] string if String length is higher than @param[maximumLength] + */ +fun String.takeOrReplace(maximumLength: Int, replacement: String): String { + return if (this.length > maximumLength) replacement else this +} + +/** + * Returns the extension (without ".") declared in the mime type of this data url. + * In the event that this data url does not contain a mime type or image extension could be read + * for any reason [defaultExtension] will be returned + * + * @param defaultExtension default extension if one could not be read from the mime type. Default is "jpg". + */ +fun String.getDataUrlImageExtension(defaultExtension: String = "jpg"): String { + return ("data:image\\/([a-zA-Z0-9-.+]+).*").toRegex() + .find(this)?.groups?.get(1)?.value ?: defaultExtension +} + +/** + * Returns this char sequence if it's not null or empty + * or the result of calling [defaultValue] function if the char sequence is null or empty. + */ +inline fun <C, R> C?.ifNullOrEmpty(defaultValue: () -> R): C where C : CharSequence, R : C = + if (isNullOrEmpty()) defaultValue() else this + +/** + * Get the representative part of the URL. Usually this is the eTLD part of the host. + * + * For example this method will return "facebook.com" for "https://www.facebook.com/foobar". + */ +fun String.getRepresentativeSnippet(): String { + val uri = Uri.parse(this) + + val host = uri.hostWithoutCommonPrefixes + if (!host.isNullOrEmpty()) { + return host + } + + val path = uri.path + if (!path.isNullOrEmpty()) { + return path + } + + return this +} + +/** + * Get a representative character for the given URL. + * + * For example this method will return "f" for "https://m.facebook.com/foobar". + */ +fun String.getRepresentativeCharacter(): String { + val snippet = this.getRepresentativeSnippet() + + snippet.forEach { character -> + if (character.isLetterOrDigit()) { + return character.uppercase() + } + } + + return "?" +} + +/** + * Strips common mobile subdomains from a [String]. + */ +fun String.stripCommonSubdomains(): String { + for (prefix in commonPrefixes) { + if (this.startsWith(prefix)) return this.substring(prefix.length) + } + return this +} + +/** + * Returns the last 4 digits from a formatted credit card number string. + */ +fun String.last4Digits(): String { + return this.takeLast(LAST_VISIBLE_DIGITS_COUNT) +} + +/** + * Returns a trimmed string. This is used to prevent extreme cases + * from slowing down UI rendering with large strings. + */ +fun String.trimmed(): String { + return this.take(MAX_URI_LENGTH) +} + +/** + * Returns a bitmap from its base64 representation. + * Returns null if the string is not a valid base64 representation of a bitmap + */ +fun String.base64ToBitmap(): Bitmap? = + extractBase6RawString()?.let { rawString -> + val raw = Base64.decode(rawString, Base64.DEFAULT) + BitmapFactory.decodeByteArray(raw, 0, raw.size) + } + +@VisibleForTesting +internal fun String.extractBase6RawString(): String? { + // Regex that identifies if the strings starts with: + // "(data:image/[ANY_FORMAT];base64," + // For example, "data:image/png;base64," + val base64BitmapRegex = "(data:image/[^;]+;base64,)(.*)".toRegex() + return base64BitmapRegex.find(this)?.let { + val (_, contentString) = it.destructured + contentString + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt new file mode 100644 index 0000000000..ca31cf7b87 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.kotlinx.coroutines + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * + * Returns a function that limits the executions of the [block] function, until the [skipTimeInMs] + * passes, then the latest value passed to [block] will be used. Any calls before [skipTimeInMs] + * passes will be ignored. All calls to the returned function must happen on the same thread. + * + * Credit to Terenfear https://gist.github.com/Terenfear/a84863be501d3399889455f391eeefe5 + * + * @param skipTimeInMs the time to wait until the next call to [block] be processed. + * @param coroutineScope the coroutine scope where [block] will executed. + * @param block function to be execute. + */ +fun <T> throttleLatest( + skipTimeInMs: Long = 300L, + coroutineScope: CoroutineScope, + block: (T) -> Unit, +): (T) -> Unit { + var throttleJob: Job? = null + var latestParam: T + return { param: T -> + latestParam = param + if (throttleJob?.isCompleted != false) { + throttleJob = coroutineScope.launch { + block(latestParam) + delay(skipTimeInMs) + } + } + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt new file mode 100644 index 0000000000..eeaf0afa85 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.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.support.ktx.kotlinx.coroutines.flow + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapConcat + +/** + * Returns a [Flow] containing only changed elements of the lists of the original [Flow]. + * + * ``` + * Example: Identity function + * Transform: x -> x (transformed values are the same as original) + * Original Flow: list(0), list(0, 1), list(0, 1, 2, 3), list(4), list(5, 6, 7, 8) + * Transformed: + * (0) -> (0 emitted because it is a new value) + * + * (0, 1) -> (0 not emitted because same as previous value, + * 1 emitted because it is a new value), + * + * (0, 1, 2, 3) -> (0 and 1 not emitted because same as previous values, + * 2 and 3 emitted because they are new values), + * + * (4) -> (4 emitted because because it is a new value) + * + * (5, 6, 7, 8) -> (5, 6, 7, 8 emitted because they are all new values) + * Returned Flow: 0, 1, 2, 3, 4, 5, 6, 7, 8 + * --- + * + * Example: Modulo 2 + * Transform: x -> x % 2 (emit changed values if the result of modulo 2 changed) + * Original Flow: listOf(1), listOf(1, 2), listOf(3, 4, 5), listOf(3, 4) + * Transformed: + * (1) -> (1 emitted because it is a new value) + * + * (1, 0) -> (1 not emitted because same as previous value with the same transformed value, + * 2 emitted because it is a new value), + * + * (1, 0, 1) -> (3, 4, 5 emitted because they are all new values) + * + * (1, 0) -> (3, 4 not emitted because same as previous values with same transformed values) + * + * Returned Flow: 1, 2, 3, 4, 5 + * --- + * ``` + */ +fun <T, R> Flow<List<T>>.filterChanged(transform: (T) -> R): Flow<T> { + var lastMappedValues: Map<T, R>? = null + return flatMapConcat { values -> + val lastMapped = lastMappedValues + val changed = if (lastMapped == null) { + values + } else { + values.filter { + !lastMapped.containsKey(it) || lastMapped[it] != transform(it) + } + } + lastMappedValues = values.associateWith { transform(it) } + changed.asFlow() + } +} + +/** + * Returns a [Flow] containing only values of the original [Flow] where the result array + * of calling [transform] contains at least one different value. + * + * Example: + * ``` + * Block: x -> [x[0], x[1]] // Map to first two characters of input + * Original Flow: "banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home" + * Mapped: [b, a], [b, a], [b, u], [a, p], [b, i], [c, o], [c, i], [h, o] + * Returned Flow: "banana", "bus, "apple", "big", "coconut", "circle", "home" + * `` + */ +fun <T, R> Flow<T>.ifAnyChanged(transform: (T) -> Array<R>): Flow<T> { + var observedValueOnce = false + var lastMappedValues: Array<R>? = null + + return filter { value -> + val mapped = transform(value) + val hasChanges = lastMappedValues + ?.asSequence() + ?.filterIndexed { i, r -> mapped[i] != r } + ?.any() + + if (!observedValueOnce || hasChanges == true) { + lastMappedValues = mapped + observedValueOnce = true + true + } else { + false + } + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt new file mode 100644 index 0000000000..fb7609db5f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.util + +import android.util.AtomicFile +import android.util.JsonReader +import android.util.JsonWriter +import org.json.JSONException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStreamWriter + +/** + * Reads an [AtomicFile] and provides a deserialized version of its content. + * @param block A function to be executed after the file is read and provides the content as + * a [String]. It is expected that this function returns a deserialized version of the content + * of the file. + */ +inline fun <T> AtomicFile.readAndDeserialize(block: (String) -> T): T? { + return try { + openRead().use { + val text = it.bufferedReader().use { reader -> reader.readText() } + block(text) + } + } catch (_: IOException) { + null + } catch (_: JSONException) { + null + } +} + +/** + * Writes an [AtomicFile] and indicates if the file was wrote. + * @param block A function with provides the content of the file as a [String] + * @return true if the file wrote otherwise false + */ +inline fun AtomicFile.writeString(block: () -> String): Boolean { + return stream { writer -> + writer.write(block()) + } +} + +/** + * Opens the [AtomicFile] for writing and provides a [JsonWriter] to [block] for writing JSON + * directly to the file. + * + * At the end of [block] the writer will be flushed and the file closed. + */ +inline fun AtomicFile.streamJSON(block: JsonWriter.() -> Unit): Boolean { + return stream { writer -> + val jsonWriter = JsonWriter(writer) + block(jsonWriter) + jsonWriter.flush() + } +} + +/** + * Opens the [AtomicFile] for reading and provides a [JsonReader] to [block] for reading JSON from + * the file. + */ +inline fun <R> AtomicFile.readJSON(block: JsonReader.() -> R): R? { + var reader: InputStream? = null + + return try { + reader = openRead() + + val jsonReader = JsonReader(reader.bufferedReader()) + block(jsonReader) + } catch (e: IOException) { + null + } finally { + reader?.close() + } +} + +/** + * Opens the [AtomicFile] for writing and provides an [OutputStreamWriter] to [block] for writing + * directly to the file. + * + * At the end of [block] the writer will be flushed and the file closed. + */ +inline fun AtomicFile.stream(block: (OutputStreamWriter) -> Unit): Boolean { + var outputStream: FileOutputStream? = null + return try { + outputStream = startWrite() + + outputStream.buffered().writer().apply { + block(this) + flush() + } + + finishWrite(outputStream) + true + } catch (_: IOException) { + failWrite(outputStream) + false + } catch (_: JSONException) { + failWrite(outputStream) + false + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml new file mode 100644 index 0000000000..f1dc322160 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">ይደውሉ በ…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">ኢሜይል በ…</string> + <string name="mozac_support_ktx_menu_share_with">ያጋሩ ከ…</string> + <string name="mozac_support_ktx_share_dialog_title">በ በኩል አጋራ</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml new file mode 100644 index 0000000000..b8d3e21fae --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Gritar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Ninviar un correu con…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir per</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000..329232b470 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">اتصل به عبر…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">أبرِد عبر…</string> + <string name="mozac_support_ktx_menu_share_with">شارِك مع…</string> + <string name="mozac_support_ktx_share_dialog_title">شارِك عبر</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml new file mode 100644 index 0000000000..d99964cf72 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Llamar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Unviar un corréu electrónicu con…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir per</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000..164b24b301 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Bununla zəng et…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Bununla e-poçt göndər…</string> + <string name="mozac_support_ktx_menu_share_with">Paylaş…</string> + <string name="mozac_support_ktx_share_dialog_title">Paylaş</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml new file mode 100644 index 0000000000..af07dcca5b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">… -دان تماس توت</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">… ایله ایمیل یوللا</string> + <string name="mozac_support_ktx_menu_share_with">… ایله پایلاش</string> + <string name="mozac_support_ktx_share_dialog_title">پایلاش</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml new file mode 100644 index 0000000000..5702b6d03f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Panggil sareng…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Rerepél sareng…</string> + <string name="mozac_support_ktx_menu_share_with">Wagiang sareng…</string> + <string name="mozac_support_ktx_share_dialog_title">Wagiang anggen</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml new file mode 100644 index 0000000000..6a4bb46bb3 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Тэлефанаваць праз…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Адправіць электроннай поштай праз…</string> + <string name="mozac_support_ktx_menu_share_with">Падзяліцца з…</string> + <string name="mozac_support_ktx_share_dialog_title">Падзяліцца праз</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000..aba8b7418d --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Позвъняване чрез…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Имейл чрез…</string> + <string name="mozac_support_ktx_menu_share_with">Споделяне с …</string> + <string name="mozac_support_ktx_share_dialog_title">Споделяне чрез</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000000..e438eeb8a1 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">যার মাধ্যমে কল করবেন…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">যার মাধ্যমে ইমেইল করবেন…</string> + <string name="mozac_support_ktx_menu_share_with">যার মাধ্যমে শেয়ার করবেন…</string> + <string name="mozac_support_ktx_share_dialog_title">শেয়ারের মাধ্যম</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml new file mode 100644 index 0000000000..ae6e869874 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Gervel gant…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Kas ur postel gant…</string> + <string name="mozac_support_ktx_menu_share_with">Rannañ gant…</string> + <string name="mozac_support_ktx_share_dialog_title">Rannañ dre</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000..2f60e05edd --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Nazovite sa…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Pošaljite email sa…</string> + <string name="mozac_support_ktx_menu_share_with">Podijeli sa…</string> + <string name="mozac_support_ktx_share_dialog_title">Podijeli putem</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000..3a4689607f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Truca amb…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Envia un correu electrònic amb…</string> + <string name="mozac_support_ktx_menu_share_with">Comparteix amb…</string> + <string name="mozac_support_ktx_share_dialog_title">Comparteix mitjançant</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml new file mode 100644 index 0000000000..4085d2da6e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Katoyon rik\'in…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Titaq rik\'in…</string> + <string name="mozac_support_ktx_menu_share_with">Tikomonïx rik\'in…</string> + <string name="mozac_support_ktx_share_dialog_title">Tikomonïx rik\'in</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml new file mode 100644 index 0000000000..06103d7468 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Tawag sa</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">i-Email sa</string> + <string name="mozac_support_ktx_menu_share_with">i-Share sa</string> + <string name="mozac_support_ktx_share_dialog_title">i-Share agi sa</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml new file mode 100644 index 0000000000..1dffae627e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">پەیوەندی بکە بەهۆی…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">ئیمێڵ بکە بەهۆی…</string> + <string name="mozac_support_ktx_menu_share_with">بڵاوەپێکردن لەگەڵ…</string> + <string name="mozac_support_ktx_share_dialog_title">بڵاوکردنەوە لە ڕێگەی</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml new file mode 100644 index 0000000000..09f88611fe --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Chjamà cù…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Mandà un messaghju cù…</string> + <string name="mozac_support_ktx_menu_share_with">Sparte cù…</string> + <string name="mozac_support_ktx_share_dialog_title">Sparte via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000..7af3f9ecf4 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Volat pomocí…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Poslat e-mail pomocí…</string> + <string name="mozac_support_ktx_menu_share_with">Sdílet pomocí…</string> + <string name="mozac_support_ktx_share_dialog_title">Sdílet pomocí</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml new file mode 100644 index 0000000000..434de6b1d5 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Galw gyda…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">E-bostio gyda…</string> + <string name="mozac_support_ktx_menu_share_with">Rhannu gyda…</string> + <string name="mozac_support_ktx_share_dialog_title">Rhannu drwy</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml new file mode 100644 index 0000000000..4521e128e9 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Ring med…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Send mail med…</string> + <string name="mozac_support_ktx_menu_share_with">Del med…</string> + <string name="mozac_support_ktx_share_dialog_title">Del via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000..5a33b1ebf3 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Anrufen mit…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Per E-Mail versenden mit…</string> + <string name="mozac_support_ktx_menu_share_with">Teilen mit…</string> + <string name="mozac_support_ktx_share_dialog_title">Teilen über</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml new file mode 100644 index 0000000000..b24d4b53ac --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Wołaś z …</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Mejlki słaś z …</string> + <string name="mozac_support_ktx_menu_share_with">Źěliś z…</string> + <string name="mozac_support_ktx_share_dialog_title">Źěliś pśez</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml new file mode 100644 index 0000000000..c1348e15a0 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Κλήση με…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Email με…</string> + <string name="mozac_support_ktx_menu_share_with">Κοινή χρήση με…</string> + <string name="mozac_support_ktx_share_dialog_title">Κοινή χρήση μέσω</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml new file mode 100644 index 0000000000..e22fa27c94 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Call with…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Email with…</string> + <string name="mozac_support_ktx_menu_share_with">Share with…</string> + <string name="mozac_support_ktx_share_dialog_title">Share via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..e22fa27c94 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Call with…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Email with…</string> + <string name="mozac_support_ktx_menu_share_with">Share with…</string> + <string name="mozac_support_ktx_share_dialog_title">Share via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000000..cb1ad4d987 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Voki per…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Sendi retpoŝton per…</string> + <string name="mozac_support_ktx_menu_share_with">Dividi kun…</string> + <string name="mozac_support_ktx_share_dialog_title">Dividi per</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml new file mode 100644 index 0000000000..eec2afd893 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Llamar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar por correo electrónico…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir vía</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml new file mode 100644 index 0000000000..e0b2c75b94 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Llamar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir mediante</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml new file mode 100644 index 0000000000..0abf4d574f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Llamar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir a través de</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml new file mode 100644 index 0000000000..7b8ad93650 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Llamar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir vía</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..0abf4d574f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Llamar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir a través de</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..f795eeda04 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Helista äpiga…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Saada e-kiri äpiga…</string> + <string name="mozac_support_ktx_menu_share_with">Jaga…</string> + <string name="mozac_support_ktx_share_dialog_title">Jagamine kasutades</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..7bf9f4b2b2 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Deitu honekin…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Bidali mezu elektronikoa honekin…</string> + <string name="mozac_support_ktx_menu_share_with">Partekatu honekin…</string> + <string name="mozac_support_ktx_share_dialog_title">Partekatu honen bidez</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000000..2923d03c60 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">تماس با…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">رایانامه کردن با…</string> + <string name="mozac_support_ktx_menu_share_with">همرسانی با…</string> + <string name="mozac_support_ktx_share_dialog_title">همرسانی از طریق</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml new file mode 100644 index 0000000000..d5ee6e1699 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <string name="mozac_support_ktx_menu_share_with">Lollin e…</string> + <string name="mozac_support_ktx_share_dialog_title">Lollin rewrude e</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000..571ff1bcd7 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Soita…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Sähköposti…</string> + <string name="mozac_support_ktx_menu_share_with">Jaa…</string> + <string name="mozac_support_ktx_share_dialog_title">Jaa</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..5396ace76c --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Appeler avec…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Envoyer un e-mail avec…</string> + <string name="mozac_support_ktx_menu_share_with">Partager avec…</string> + <string name="mozac_support_ktx_share_dialog_title">Partager à l’aide de</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml new file mode 100644 index 0000000000..4e4364b228 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Clame cun…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Mande e-mail cun…</string> + <string name="mozac_support_ktx_menu_share_with">Condivît cun…</string> + <string name="mozac_support_ktx_share_dialog_title">Condivît vie</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml new file mode 100644 index 0000000000..67936f7e19 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Belje mei…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">E-maile mei…</string> + <string name="mozac_support_ktx_menu_share_with">Diele mei…</string> + <string name="mozac_support_ktx_share_dialog_title">Diele fia</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml new file mode 100644 index 0000000000..e215f3efce --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <string name="mozac_support_ktx_menu_share_with">Comhroinn le…</string> + <string name="mozac_support_ktx_share_dialog_title">Comhroinn trí</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml new file mode 100644 index 0000000000..bc0227a440 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Cuir fòn le…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Cuir air a‘ phost-d le…</string> + <string name="mozac_support_ktx_menu_share_with">Co-roinn le…</string> + <string name="mozac_support_ktx_share_dialog_title">Co-roinn slighe</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000..6899c78f10 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Chamar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar por correo con…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir mediante</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml new file mode 100644 index 0000000000..f3dd2ad99b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Ehenói… ndive</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Emondo ñanduti veve… ndive</string> + <string name="mozac_support_ktx_menu_share_with">Emoherakuã…</string> + <string name="mozac_support_ktx_share_dialog_title">Emoherakuã amóva rupi</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml new file mode 100644 index 0000000000..50fec04dc8 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">… સાથે કૉલ કરો</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">… સાથે ઇમેલ કરો</string> + <string name="mozac_support_ktx_menu_share_with">સાથે શેર કરો…</string> + <string name="mozac_support_ktx_share_dialog_title">દ્વારા શેર કરો</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml new file mode 100644 index 0000000000..6c5ef983bd --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">इसके साथ कॉल करें…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">इसके साथ ईमेल भेजें…</string> + <string name="mozac_support_ktx_menu_share_with">के साथ साझा करें…</string> + <string name="mozac_support_ktx_share_dialog_title">इससे साझा करें</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000000..03edef01db --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Nazovi s…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Pošalji e-poštu s…</string> + <string name="mozac_support_ktx_menu_share_with">Dijeli s …</string> + <string name="mozac_support_ktx_share_dialog_title">Dijeli putem</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml new file mode 100644 index 0000000000..99e16cb09f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Wołać z …</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Mejlki słać z …</string> + <string name="mozac_support_ktx_menu_share_with">Dźělić z…</string> + <string name="mozac_support_ktx_share_dialog_title">Dźělić přez</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..a738cdeee9 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Hívás ezzel…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">E-mail ezzel…</string> + <string name="mozac_support_ktx_menu_share_with">Megosztás…</string> + <string name="mozac_support_ktx_share_dialog_title">Megosztás ezzel</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000000..7c11d7060f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Զանգահարել՝</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Էլ. նամակ ուղարկել՝</string> + <string name="mozac_support_ktx_menu_share_with">Տարածել հետևյալով…</string> + <string name="mozac_support_ktx_share_dialog_title">Տարածել միջոցով</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml new file mode 100644 index 0000000000..bf9d4a2856 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Appellar con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Inviar email con…</string> + <string name="mozac_support_ktx_menu_share_with">Compartir con…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartir per</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml new file mode 100644 index 0000000000..6e0376c98e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Panggil dengan…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Surelkan dengan…</string> + <string name="mozac_support_ktx_menu_share_with">Bagikan dengan…</string> + <string name="mozac_support_ktx_share_dialog_title">Bagikan lewat</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000..8006dd8621 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Hringja með…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Senda tölvupóst með…</string> + <string name="mozac_support_ktx_menu_share_with">Deila með…</string> + <string name="mozac_support_ktx_share_dialog_title">Deila með</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..22d4fd0735 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Chiama con…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Invia email con…</string> + <string name="mozac_support_ktx_menu_share_with">Condividi con…</string> + <string name="mozac_support_ktx_share_dialog_title">Condividi con</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml new file mode 100644 index 0000000000..f551cf77cf --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">חיוג באמצעות…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">שליחה בדוא״ל באמצעות…</string> + <string name="mozac_support_ktx_menu_share_with">שיתוף עם…</string> + <string name="mozac_support_ktx_share_dialog_title">שיתוף באמצעות</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..13c7a83ef0 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">電話をかける…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">メール送信…</string> + <string name="mozac_support_ktx_menu_share_with">共有先…</string> + <string name="mozac_support_ktx_share_dialog_title">共有方法</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml new file mode 100644 index 0000000000..2abbdb068d --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">დარეკვა…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">ელფოსტის გაგზავნა…</string> + <string name="mozac_support_ktx_menu_share_with">გაზიარება…</string> + <string name="mozac_support_ktx_share_dialog_title">გაზიარება პროგრამით</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml new file mode 100644 index 0000000000..7bc33f728b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">…arqalı qońıraw etiw</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Elektron pochta járdeminde…</string> + <string name="mozac_support_ktx_menu_share_with">…járdeminde bólisiw</string> + <string name="mozac_support_ktx_share_dialog_title">Arqalı bólisiw</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml new file mode 100644 index 0000000000..e64f651208 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Siwel s…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Azen imayl s…</string> + <string name="mozac_support_ktx_menu_share_with">Bḍu d…</string> + <string name="mozac_support_ktx_share_dialog_title">Bḍu s</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml new file mode 100644 index 0000000000..49590be901 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Көмегімен қоңырау шалу…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Көмегімен эл. пошта хатын жіберу…</string> + <string name="mozac_support_ktx_menu_share_with">Көмегімен бөлісу…</string> + <string name="mozac_support_ktx_share_dialog_title">Арқылы бөлісу</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml new file mode 100644 index 0000000000..c2ac17ebfc --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Bigere bi…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Emaîl bi…</string> + <string name="mozac_support_ktx_menu_share_with">Parve bike bi…</string> + <string name="mozac_support_ktx_share_dialog_title">Parve bike bi rêya</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml new file mode 100644 index 0000000000..cdecfceeef --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">ಇದರೊಂದಿಗೆ ಕರೆ ಮಾಡಿ…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">ಇದರೊಂದಿಗೆ ಇಮೇಲ್ ಮಾಡಿ…</string> + <string name="mozac_support_ktx_menu_share_with">ಜೊತೆ ಹಂಚಿಕೊ…</string> + <string name="mozac_support_ktx_share_dialog_title">ಇವುಗಳ ಮೂಲಕ ಹಂಚು</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000000..e942f9e254 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">통화 앱…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">이메일 앱…</string> + <string name="mozac_support_ktx_menu_share_with">공유…</string> + <string name="mozac_support_ktx_share_dialog_title">다음으로 공유</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml new file mode 100644 index 0000000000..b516969167 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <string name="mozac_support_ktx_menu_share_with">Condividdi con…</string> + <string name="mozac_support_ktx_share_dialog_title">Condividdi via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..e719b84028 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">ໂທດ້ວຍ…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">ອີເມລດ້ວຍ…</string> + <string name="mozac_support_ktx_menu_share_with">ແບ່ງປັນກັບ…</string> + <string name="mozac_support_ktx_share_dialog_title">ແບ່ງປັນຜ່ານທາງ</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000000..e8cf2fc4f5 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Skambinti naudojant…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Siųsti el. laišką naudojant…</string> + <string name="mozac_support_ktx_menu_share_with">Dalintis su…</string> + <string name="mozac_support_ktx_share_dialog_title">Dalintis per</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml new file mode 100644 index 0000000000..8d8d7316e9 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <string name="mozac_support_ktx_menu_share_with">ഇവരുമായി പങ്കിടുക…</string> + <string name="mozac_support_ktx_share_dialog_title">ഇതുവഴി പങ്കിടൂ</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml new file mode 100644 index 0000000000..ebb1cc7071 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">यासह कॉल करा…</string> + <string name="mozac_support_ktx_menu_share_with">यासह शेअर करा…</string> + <string name="mozac_support_ktx_share_dialog_title">याद्वारे शेअर करा</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml new file mode 100644 index 0000000000..8faa96afa1 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">ခေါ်ဆိုခြင်း ဖြင့်…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">အီးမေး ဖြင့်…</string> + <string name="mozac_support_ktx_menu_share_with">… နှင့်မျှဝေပါ</string> + <string name="mozac_support_ktx_share_dialog_title">အခြားကနေ မျှဝေ</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 0000000000..a47a064ec3 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Ring med…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Send e-post med…</string> + <string name="mozac_support_ktx_menu_share_with">Del med…</string> + <string name="mozac_support_ktx_share_dialog_title">Del via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml new file mode 100644 index 0000000000..423ed59fd2 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">यससँग कल गर्नुहोस्…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">यससँग इमेल गर्नुहोस्…</string> + <string name="mozac_support_ktx_menu_share_with">यससँग साझेदारी गर्नुहोस्…</string> + <string name="mozac_support_ktx_share_dialog_title">मार्फत साझेदार गर्नुहोस्</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000..005f02df51 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Bellen met…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">E-mailen met…</string> + <string name="mozac_support_ktx_menu_share_with">Delen met…</string> + <string name="mozac_support_ktx_share_dialog_title">Delen via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml new file mode 100644 index 0000000000..a47a064ec3 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Ring med…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Send e-post med…</string> + <string name="mozac_support_ktx_menu_share_with">Del med…</string> + <string name="mozac_support_ktx_share_dialog_title">Del via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml new file mode 100644 index 0000000000..11b8ea1372 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Sonar amb…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar un corrièl amb…</string> + <string name="mozac_support_ktx_menu_share_with">Partejar amb…</string> + <string name="mozac_support_ktx_share_dialog_title">Partejar via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml new file mode 100644 index 0000000000..e13aee47b5 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">…ਨਾਲ ਕਾਲ ਕਰੋ</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">…ਨਾਲ ਈਮੇਲ ਭੇਜੋ</string> + <string name="mozac_support_ktx_menu_share_with">…ਨਾਲ ਸਾਂਝਾ ਕਰੋ</string> + <string name="mozac_support_ktx_share_dialog_title">ਇਸ ਰਾਹੀਂ ਸਾਂਝਾ ਕਰੋ</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml new file mode 100644 index 0000000000..8ed66cb6de --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">کیہتھوں کال کرو…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">کیہتھوں ایمیل بھیجو…</string> + <string name="mozac_support_ktx_menu_share_with">کیہنوں سانجھا کرو…</string> + <string name="mozac_support_ktx_share_dialog_title">کیہتھوں سانجھا کرو</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000000..ec1f77ca41 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Zadzwoń za pomocą…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Wyślij e-mail za pomocą…</string> + <string name="mozac_support_ktx_menu_share_with">Udostępnij za pomocą…</string> + <string name="mozac_support_ktx_share_dialog_title">Udostępnij przez</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..3027936bdc --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Chamar com…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar email com…</string> + <string name="mozac_support_ktx_menu_share_with">Compartilhar com…</string> + <string name="mozac_support_ktx_share_dialog_title">Compartilhar via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000..395aa28f03 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Chamar com …</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Enviar e-mail com…</string> + <string name="mozac_support_ktx_menu_share_with">Partilhar com…</string> + <string name="mozac_support_ktx_share_dialog_title">Partilhar via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml new file mode 100644 index 0000000000..529df8d789 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Telefonar cun…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Trametter l\'e-mail cun…</string> + <string name="mozac_support_ktx_menu_share_with">Cundivider cun…</string> + <string name="mozac_support_ktx_share_dialog_title">Cundivider via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000000..58b1a2121d --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Sună cu…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Trimite mesaj pe e-mail cu…</string> + <string name="mozac_support_ktx_menu_share_with">Partajează cu…</string> + <string name="mozac_support_ktx_share_dialog_title">Partajează prin</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000..bd99e3a695 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Позвонить через…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Отправить по почте через…</string> + <string name="mozac_support_ktx_menu_share_with">Поделиться с помощью…</string> + <string name="mozac_support_ktx_share_dialog_title">Поделиться через</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml new file mode 100644 index 0000000000..6caae0a0b5 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">ᱱᱚᱶᱟ ᱛᱮ ᱠᱚᱞ ᱢᱮ …</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">ᱱᱚᱶᱟ ᱛᱮ ᱤᱢᱮᱞ ᱢᱮ …</string> + <string name="mozac_support_ktx_menu_share_with">ᱱᱚᱶᱟ ᱛᱮ ᱦᱟᱹᱴᱤᱧ ᱢᱮ…</string> + <string name="mozac_support_ktx_share_dialog_title">ᱫᱟᱨᱟᱭ ᱛᱮ ᱦᱟᱹᱴᱤᱧ</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml new file mode 100644 index 0000000000..57751f5529 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Muti cun…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Imbia cun posta eletrònica…</string> + <string name="mozac_support_ktx_menu_share_with">Cumpartzi cun…</string> + <string name="mozac_support_ktx_share_dialog_title">Cumpartzi cun</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml new file mode 100644 index 0000000000..639bdcbbb2 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">සමඟ අමතන්න…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">මගින් තැපෑලට…</string> + <string name="mozac_support_ktx_menu_share_with">සමඟ බෙදාගන්න…</string> + <string name="mozac_support_ktx_share_dialog_title">හරහා බෙදාගන්න</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000..393a11ab4a --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Zavolať pomocou…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Poslať e‑mail pomocou…</string> + <string name="mozac_support_ktx_menu_share_with">Zdieľať pomocou…</string> + <string name="mozac_support_ktx_share_dialog_title">Zdieľať cez</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml new file mode 100644 index 0000000000..e855302218 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">این٘دے نال فون کرو۔۔۔</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">این٘دے نال ای میل کرو۔۔۔</string> + <string name="mozac_support_ktx_menu_share_with">این٘دے نال شیئر کرو۔۔۔</string> + <string name="mozac_support_ktx_share_dialog_title">شیئر بذریعہ</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000000..827a16fc3c --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Pokliči z …</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Pošlji po e-pošti z …</string> + <string name="mozac_support_ktx_menu_share_with">Deli z …</string> + <string name="mozac_support_ktx_share_dialog_title">Deli preko</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000..1da3e21d02 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Thirre me…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Dërgoni Email me…</string> + <string name="mozac_support_ktx_menu_share_with">Ndajeni me të tjerët përmes…</string> + <string name="mozac_support_ktx_share_dialog_title">Ndajeni përmes</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000000..e009ea8e49 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Позови са…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Пошаљи мејл са…</string> + <string name="mozac_support_ktx_menu_share_with">Дели са…</string> + <string name="mozac_support_ktx_share_dialog_title">Дели преко</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml new file mode 100644 index 0000000000..c46f025160 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Gero maké…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Surélékan maké…</string> + <string name="mozac_support_ktx_menu_share_with">Bagikeun sareng…</string> + <string name="mozac_support_ktx_share_dialog_title">Bagikeun kana</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml new file mode 100644 index 0000000000..109c410ba7 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Ring med…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Skicka e-post med…</string> + <string name="mozac_support_ktx_menu_share_with">Dela med…</string> + <string name="mozac_support_ktx_share_dialog_title">Dela via</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml new file mode 100644 index 0000000000..345e1f965a --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">உடன் அழைக்கவும்…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">இதனுடன் மின்னஞ்சல்…</string> + <string name="mozac_support_ktx_menu_share_with">இதனுடன் பகிர்…</string> + <string name="mozac_support_ktx_share_dialog_title">இதன்வழியாக பகிரவும்</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000..20d323b226 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">దీనితో కాల్ చేయి…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">దీనితో ఈమెయిలు చేయి…</string> + <string name="mozac_support_ktx_menu_share_with">దీనితో పంచుకో…</string> + <string name="mozac_support_ktx_share_dialog_title">దీని ద్వారా పంచుకోండి</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml new file mode 100644 index 0000000000..81fcf1c654 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Занг задан тавассути…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Ирсоли паёми эл. тавассути…</string> + <string name="mozac_support_ktx_menu_share_with">Мубодила кардан тавассути…</string> + <string name="mozac_support_ktx_share_dialog_title">Мубодила тавассути</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..6bda999442 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">โทรด้วย…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">อีเมลด้วย…</string> + <string name="mozac_support_ktx_menu_share_with">แบ่งปันด้วย…</string> + <string name="mozac_support_ktx_share_dialog_title">แบ่งปันผ่าน</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml new file mode 100644 index 0000000000..ac8f553b1b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Tumawag gamit ang…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">i-Email kasama…</string> + <string name="mozac_support_ktx_menu_share_with">Ibahagi sa…</string> + <string name="mozac_support_ktx_share_dialog_title">Ibahagi sa pamamagitan ng</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000000..fe7f209fc5 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Bununla çağrı…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Bununla e-posta…</string> + <string name="mozac_support_ktx_menu_share_with">Paylaş…</string> + <string name="mozac_support_ktx_share_dialog_title">Paylaş</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml new file mode 100644 index 0000000000..8a1414fe57 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Ga\'mīn ngà…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Gā\'nïnj gān\'ānj korrêo ngà…</string> + <string name="mozac_support_ktx_menu_share_with">Dūyingô\' ngà…</string> + <string name="mozac_support_ktx_share_dialog_title">Dūyingô\' riña</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml new file mode 100644 index 0000000000..92ab1ac020 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">… ярдәмендә шалтырату</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">… ярдәмендә эл. почта җибәрү</string> + <string name="mozac_support_ktx_menu_share_with">Уртаклашу…</string> + <string name="mozac_support_ktx_share_dialog_title">Аша уртаклашу</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml new file mode 100644 index 0000000000..63e9286a19 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Ɣer s…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Azen imayl s…</string> + <string name="mozac_support_ktx_menu_share_with">Bḍu d…</string> + <string name="mozac_support_ktx_share_dialog_title">Bḍu s</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml new file mode 100644 index 0000000000..7b79f14b4a --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">تېلېفون قىلىش…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">ئېلخەت يوللاش…</string> + <string name="mozac_support_ktx_menu_share_with">ھەمبەھىرلەش…</string> + <string name="mozac_support_ktx_share_dialog_title">ھەمبەھىرلەش</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..528ab00907 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Зателефонувати через…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Надіслати електронним листом через…</string> + <string name="mozac_support_ktx_menu_share_with">Поділитися з…</string> + <string name="mozac_support_ktx_share_dialog_title">Поділитись через</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml new file mode 100644 index 0000000000..027be3feed --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">اس کے ذریعہ کال کریں…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">اس کے ذریعہ ای میل بھیجیں…</string> + <string name="mozac_support_ktx_menu_share_with">… کے ساتھ شیئر کریں</string> + <string name="mozac_support_ktx_share_dialog_title">کے زریعے شیئر کریں</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml new file mode 100644 index 0000000000..85303338d4 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Qoʻngʻiroq qilish</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Xat yozish</string> + <string name="mozac_support_ktx_menu_share_with">Ulashish</string> + <string name="mozac_support_ktx_share_dialog_title">Ulashish:</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml new file mode 100644 index 0000000000..908be2008a --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <string name="mozac_support_ktx_menu_share_with">Condividi co…</string> + <string name="mozac_support_ktx_share_dialog_title">Condividi con</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..94efea291f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Gọi với…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Gửi email với…</string> + <string name="mozac_support_ktx_menu_share_with">Chia sẻ với…</string> + <string name="mozac_support_ktx_share_dialog_title">Chia sẻ qua</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml new file mode 100644 index 0000000000..3fd0eac24b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Pè pẹ̀lú…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Ímeelì pẹ̀lú…</string> + <string name="mozac_support_ktx_menu_share_with">Pín-in pẹ̀lú…</string> + <string name="mozac_support_ktx_share_dialog_title">Pín-in nípasẹ̀</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..68e8298dca --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">使用下列程序拨打电话…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">使用下列程序发送邮件…</string> + <string name="mozac_support_ktx_menu_share_with">使用下列方式分享…</string> + <string name="mozac_support_ktx_share_dialog_title">分享到</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..8e97f07a1c --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">使用下列程式撥打電話…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">使用下列程式發送郵件…</string> + <string name="mozac_support_ktx_menu_share_with">使用下列方式分享…</string> + <string name="mozac_support_ktx_share_dialog_title">分享到</string> +</resources> diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml new file mode 100644 index 0000000000..d20d60e1f4 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ This Source Code Form is subject to the terms of the Mozilla Public + ~ License, v. 2.0. If a copy of the MPL was not distributed with this + ~ file, You can obtain one at http://mozilla.org/MPL/2.0/. + --> + +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto"> + <!-- Text displayed when choosing which app to call with after selecting a phone number--> + <string name="mozac_support_ktx_menu_call_with">Call with…</string> + <!-- Text displayed when choosing which app to email with after selecting an email address--> + <string name="mozac_support_ktx_menu_email_with">Email with…</string> + <string name="mozac_support_ktx_menu_share_with">Share with…</string> + <string name="mozac_support_ktx_share_dialog_title">Share via</string> +</resources>
\ No newline at end of file diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt new file mode 100644 index 0000000000..205afd049b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.arch.lifecycle + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import mozilla.components.support.test.mock +import org.junit.Test +import org.mockito.Mockito.verify + +class LifecycleTest { + + @Test + fun addObservers() { + val observer1: LifecycleObserver = mock() + val observer2: LifecycleObserver = mock() + val lifecycle: Lifecycle = mock() + + lifecycle.addObservers(observer1, observer2) + + verify(lifecycle).addObserver(observer1) + verify(lifecycle).addObserver(observer2) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt new file mode 100644 index 0000000000..7288467a3e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.content + +import android.content.Context +import android.view.accessibility.AccessibilityManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Shadows.shadowOf +import org.robolectric.shadows.ShadowAccessibilityManager + +@RunWith(AndroidJUnit4::class) +class ContextKtTest { + + lateinit var accessibilityManager: ShadowAccessibilityManager + + @Before + fun setUp() { + accessibilityManager = shadowOf( + testContext + .getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager, + ) + } + + @Test + fun `screen reader enabled`() { + // Given + accessibilityManager.setTouchExplorationEnabled(true) + + // When + val isEnabled = testContext.isScreenReaderEnabled + + // Then + assertTrue(isEnabled) + } + + @Test + fun `screen reader disabled`() { + // Given + accessibilityManager.setTouchExplorationEnabled(false) + + // When + val isEnabled = testContext.isScreenReaderEnabled + + // Then + assertFalse(isEnabled) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt new file mode 100644 index 0000000000..aa6782f4f5 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt @@ -0,0 +1,304 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.content + +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.app.Activity +import android.app.ActivityManager +import android.content.ActivityNotFoundException +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_SEND +import android.content.Intent.EXTRA_INTENT +import android.content.Intent.EXTRA_STREAM +import android.content.Intent.EXTRA_SUBJECT +import android.content.Intent.EXTRA_TEXT +import android.content.Intent.EXTRA_TITLE +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.hardware.camera2.CameraManager +import android.net.Uri +import android.os.Build +import androidx.core.content.FileProvider +import androidx.core.content.getSystemService +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.ktx.R +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.fakes.android.FakeContext +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.robolectric.Robolectric +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements +import org.robolectric.shadows.ShadowApplication +import org.robolectric.shadows.ShadowCameraCharacteristics +import org.robolectric.shadows.ShadowProcess +import java.io.File + +@RunWith(AndroidJUnit4::class) +class ContextTest { + + @Before + fun setup() { + isMainProcess = null + } + + @Test + fun `isOSOnLowMemory() should return the same as getMemoryInfo() lowMemory`() { + val extensionFunctionResult = testContext.isOSOnLowMemory() + + val activityManager: ActivityManager? = testContext.getSystemService() + + val normalMethodResult = ActivityManager.MemoryInfo().also { memoryInfo -> + activityManager?.getMemoryInfo(memoryInfo) + }.lowMemory + + assertEquals(extensionFunctionResult, normalMethodResult) + } + + @Test + fun `isPermissionGranted() returns same service as checkSelfPermission()`() { + val application = ShadowApplication() + + assertEquals( + testContext.isPermissionGranted(WRITE_EXTERNAL_STORAGE), + testContext.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED, + ) + + application.grantPermissions(WRITE_EXTERNAL_STORAGE) + + assertEquals( + testContext.isPermissionGranted(WRITE_EXTERNAL_STORAGE), + testContext.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED, + ) + } + + @Test + fun `share invokes startActivity`() { + val context = spy(testContext) + val argCaptor = argumentCaptor<Intent>() + + val result = context.share("https://mozilla.org") + + verify(context).startActivity(argCaptor.capture()) + + assertTrue(result) + assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags) + } + + @Test + @Config(shadows = [ShadowFileProvider::class]) + fun `shareMedia invokes startActivity`() { + val context = spy(testContext) + val argCaptor = argumentCaptor<Intent>() + + val result = context.shareMedia("filePath", "*/*", "subject", "message") + + verify(context).startActivity(argCaptor.capture()) + assertTrue(result) + // verify all the properties we set for the share Intent + val chooserIntent = argCaptor.value + val chooserTitle: String = chooserIntent.extras!!.getString(EXTRA_TITLE) as String + + @Suppress("DEPRECATION") + val shareIntent: Intent = chooserIntent.extras!!.get(EXTRA_INTENT) as Intent + + assertTrue(chooserIntent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0) + assertTrue(chooserIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0) + assertEquals(context.getString(R.string.mozac_support_ktx_menu_share_with), chooserTitle) + assertEquals(ACTION_SEND, shareIntent.action) + + @Suppress("DEPRECATION") + assertEquals(ShadowFileProvider.FAKE_URI_RESULT, shareIntent.extras!![EXTRA_STREAM]) + assertEquals("subject", shareIntent.extras!!.getString(EXTRA_SUBJECT)) + assertEquals("message", shareIntent.extras!!.getString(EXTRA_TEXT)) + assertTrue(shareIntent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0) + assertTrue(shareIntent.flags and Intent.FLAG_ACTIVITY_NEW_DOCUMENT != 0) + } + + @Suppress("UNREACHABLE_CODE") + @Test + @Config(shadows = [ShadowFileProvider::class]) + fun `shareMedia returns false if the chooser could not be shown`() { + val context = spy( + object : FakeContext() { + override fun startActivity(intent: Intent?) = throw ActivityNotFoundException() + override fun getApplicationContext() = testContext + }, + ) + doReturn(testContext.resources).`when`(context).resources + + val result = context.shareMedia("filePath", "*/*", "subject", "message") + + assertFalse(result) + } + + @Test + @Config(shadows = [ShadowFileProvider::class], sdk = [Build.VERSION_CODES.Q]) + fun `shareMedia will show a thumbnail starting with Android 10`() { + val context = spy(testContext) + val argCaptor = argumentCaptor<Intent>() + + val result = context.shareMedia("filePath", "*/*", "subject", "message") + + verify(context).startActivity(argCaptor.capture()) + assertTrue(result) + // verify all the properties we set for the share Intent + val chooserIntent = argCaptor.value + assertEquals(1, chooserIntent.clipData!!.itemCount) + assertEquals(ShadowFileProvider.FAKE_URI_RESULT, chooserIntent.clipData!!.getItemAt(0).uri) + } + + @Test + @Config(shadows = [ShadowFileProvider::class], sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P]) + fun `shareMedia will not show a thumbnail prior to Android 10`() { + val context = spy(testContext) + val argCaptor = argumentCaptor<Intent>() + + val result = context.shareMedia("filePath", "*/*", "subject", "message") + + verify(context).startActivity(argCaptor.capture()) + assertTrue(result) + // verify all the properties we set for the share Intent + val chooserIntent = argCaptor.value + assertNull(chooserIntent.clipData) + } + + @Test + @Config(shadows = [ShadowFileProvider::class]) + fun `copyImage will copy the file URI to the clipboard & invoke the confirmation action`() { + val context = spy(testContext) + val confirmationAction = mock<() -> Unit>() + + context.copyImage("filePath", confirmationAction) + + val clipboardManager = + testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + assertEquals( + ShadowFileProvider.FAKE_URI_RESULT, + clipboardManager.primaryClip!!.getItemAt(0).uri, + ) + verify(confirmationAction).invoke() + } + + @Test + fun `email invokes startActivity`() { + val context = spy(testContext) + val argCaptor = argumentCaptor<Intent>() + + val result = context.email("test@mozilla.org") + + verify(context).startActivity(argCaptor.capture()) + + assertTrue(result) + assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags) + } + + @Test + fun `call invokes startActivity`() { + val context = spy(testContext) + val argCaptor = argumentCaptor<Intent>() + + val result = context.call("555-5555") + + verify(context).startActivity(argCaptor.capture()) + + assertTrue(result) + assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags) + } + + @Test + fun `isMainProcess must only return true if we are in the main process`() { + val myPid = Int.MAX_VALUE + + assertTrue(testContext.isMainProcess()) + + ShadowProcess.setPid(myPid) + isMainProcess = null + + assertFalse(testContext.isMainProcess()) + } + + @Test + fun `runOnlyInMainProcess must only run if we are in the main process`() { + val myPid = Int.MAX_VALUE + var wasExecuted = false + + testContext.runOnlyInMainProcess { + wasExecuted = true + } + + assertTrue(wasExecuted) + + wasExecuted = false + ShadowProcess.setPid(myPid) + isMainProcess = false + + testContext.runOnlyInMainProcess { + wasExecuted = true + } + + assertFalse(wasExecuted) + } + + @Test + fun `hasCamera returns true if the device has a camera`() { + val context = Robolectric.buildActivity(Activity::class.java).get() + assertFalse(context.hasCamera()) + + val cameraManager: CameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + shadowOf(cameraManager).addCamera("camera0", ShadowCameraCharacteristics.newCameraCharacteristics()) + assertTrue(context.hasCamera()) + } + + @Test + fun `hasCamera returns false if exception is thrown`() { + val context = spy(testContext) + val cameraManager: CameraManager = mock() + whenever(context.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager) + + whenever(cameraManager.cameraIdList).thenThrow(IllegalStateException("Test")) + assertFalse(context.hasCamera()) + } + + @Test + fun `hasCamera returns false if assertion is thrown`() { + val context = spy(testContext) + val cameraManager: CameraManager = mock() + whenever(context.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager) + + whenever(cameraManager.cameraIdList).thenThrow(AssertionError("Test")) + assertFalse(context.hasCamera()) + } +} + +@Implements(FileProvider::class) +object ShadowFileProvider { + val FAKE_URI_RESULT: Uri = "fakeUri".toUri() + + @Implementation + @JvmStatic + @Suppress("UNUSED_PARAMETER") + fun getUriForFile( + context: Context?, + authority: String?, + file: File, + ) = FAKE_URI_RESULT +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt new file mode 100644 index 0000000000..20f32b799f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.content + +import android.content.Context +import android.content.SharedPreferences +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.robolectric.testContext +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SharedPreferencesStringTest { + private val key = "key" + private val defaultValue = "defaultString" + private lateinit var preferencesHolder: StringTestPreferenceHolder + private lateinit var testPreferences: SharedPreferences + + @Before + fun setup() { + testPreferences = testContext.getSharedPreferences("test", Context.MODE_PRIVATE) + } + + @After + fun tearDown() { + testPreferences.edit().clear().apply() + } + + @Test + fun `GIVEN string does not exist and asked to persist the default WHEN asked for it THEN persist the default and return it`() { + preferencesHolder = StringTestPreferenceHolder( + persistDefaultIfNotExists = true, + ) + + val result = preferencesHolder.string + + assertEquals(defaultValue, result) + assertEquals(defaultValue, testPreferences.getString(key, null)) + } + + @Test + fun `GIVEN string does not exist and not asked to persist the default WHEN asked for it THEN return the default but not persist it`() { + preferencesHolder = StringTestPreferenceHolder( + persistDefaultIfNotExists = false, + ) + + val result = preferencesHolder.string + + assertEquals(defaultValue, result) + assertNull(testPreferences.getString(key, null)) + } + + @Test + fun `GIVEN string exists and asked to persist the default WHEN asked for it THEN return the existing string and don't persist the default`() { + testPreferences.edit().putString(key, "test").apply() + preferencesHolder = StringTestPreferenceHolder( + persistDefaultIfNotExists = true, + ) + + val result = preferencesHolder.string + + assertEquals("test", result) + } + + @Test + fun `GIVEN string exists and not asked to persist the default WHEN asked for it THEN return the existing string and don't persist the default`() { + testPreferences.edit().putString(key, "test").apply() + preferencesHolder = StringTestPreferenceHolder( + persistDefaultIfNotExists = true, + ) + + val result = preferencesHolder.string + + assertEquals("test", result) + } + + @Test + fun `GIVEN a value exists WHEN asked to persist a new value THEN update the persisted value`() { + testPreferences.edit().putString(key, "test").apply() + preferencesHolder = StringTestPreferenceHolder() + + preferencesHolder.string = "update" + + assertEquals( + "update", + testPreferences.getString(key, null), + ) + } + + @Test + fun `GIVEN a value does not exist WHEN asked to persist a new value THEN persist the requested value`() { + preferencesHolder = StringTestPreferenceHolder() + + preferencesHolder.string = "test" + + assertEquals( + "test", + testPreferences.getString(key, null), + ) + } + + private inner class StringTestPreferenceHolder( + persistDefaultIfNotExists: Boolean = false, + ) : PreferencesHolder { + override val preferences = testPreferences + + var string by stringPreference(key, defaultValue, persistDefaultIfNotExists) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt new file mode 100644 index 0000000000..7ab36893ae --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.content + +import android.content.SharedPreferences +import mozilla.components.support.test.any +import mozilla.components.support.test.eq +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mock +import org.mockito.Mockito.anyString +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations.openMocks + +class SharedPreferencesTest { + @Mock private lateinit var sharedPrefs: SharedPreferences + + @Mock private lateinit var editor: SharedPreferences.Editor + + @Before + fun setup() { + openMocks(this) + + `when`(sharedPrefs.edit()).thenReturn(editor) + `when`(editor.putBoolean(anyString(), anyBoolean())).thenReturn(editor) + `when`(editor.putFloat(anyString(), anyFloat())).thenReturn(editor) + `when`(editor.putInt(anyString(), anyInt())).thenReturn(editor) + `when`(editor.putLong(anyString(), anyLong())).thenReturn(editor) + `when`(editor.putString(anyString(), anyString())).thenReturn(editor) + `when`(editor.putStringSet(anyString(), any())).thenReturn(editor) + } + + @Test + fun `getter returns boolean from shared preferences`() { + val holder = MockPreferencesHolder() + `when`(sharedPrefs.getBoolean(eq("boolean"), anyBoolean())).thenReturn(true) + + assertTrue(holder.boolean) + verify(sharedPrefs).getBoolean("boolean", false) + } + + @Test + fun `setter applies boolean to shared preferences`() { + val holder = MockPreferencesHolder(defaultBoolean = true) + holder.boolean = false + + verify(editor).putBoolean("boolean", false) + verify(editor).apply() + } + + @Test + fun `getter uses default boolean value`() { + val holderFalse = MockPreferencesHolder(defaultBoolean = false) + // Call the getter for the test + holderFalse.boolean + + verify(sharedPrefs).getBoolean("boolean", false) + + val holderTrue = MockPreferencesHolder(defaultBoolean = true) + // Call the getter for the test + holderTrue.boolean + + verify(sharedPrefs).getBoolean("boolean", true) + } + + @Test + fun `getter returns float from shared preferences`() { + val holder = MockPreferencesHolder() + `when`(sharedPrefs.getFloat(eq("float"), anyFloat())).thenReturn(2.4f) + + assertEquals(2.4f, holder.float) + verify(sharedPrefs).getFloat("float", 0f) + } + + @Test + fun `setter applies float to shared preferences`() { + val holder = MockPreferencesHolder(defaultFloat = 1f) + holder.float = 0f + + verify(editor).putFloat("float", 0f) + verify(editor).apply() + } + + @Test + fun `getter uses default float value`() { + val holderDefault = MockPreferencesHolder(defaultFloat = 0f) + // Call the getter for the test + holderDefault.float + + verify(sharedPrefs).getFloat("float", 0f) + + val holderOther = MockPreferencesHolder(defaultFloat = 12f) + // Call the getter for the test + holderOther.float + + verify(sharedPrefs).getFloat("float", 12f) + } + + @Test + fun `getter returns int from shared preferences`() { + val holder = MockPreferencesHolder() + `when`(sharedPrefs.getInt(eq("int"), anyInt())).thenReturn(5) + + assertEquals(5, holder.int) + verify(sharedPrefs).getInt("int", 0) + } + + @Test + fun `setter applies int to shared preferences`() { + val holder = MockPreferencesHolder(defaultInt = 1) + holder.int = 0 + + verify(editor).putInt("int", 0) + verify(editor).apply() + } + + @Test + fun `getter uses default int value`() { + val holderDefault = MockPreferencesHolder(defaultInt = 0) + // Call the getter for the test + holderDefault.int + + verify(sharedPrefs).getInt("int", 0) + + val holderOther = MockPreferencesHolder(defaultInt = 23) + // Call the getter for the test + holderOther.int + + verify(sharedPrefs).getInt("int", 23) + } + + @Test + fun `getter returns long from shared preferences`() { + val holder = MockPreferencesHolder() + `when`(sharedPrefs.getLong(eq("long"), anyLong())).thenReturn(200L) + + assertEquals(200L, holder.long) + verify(sharedPrefs).getLong("long", 0) + } + + @Test + fun `setter applies long to shared preferences`() { + val holder = MockPreferencesHolder(defaultLong = 1) + holder.long = 0 + + verify(editor).putLong("long", 0) + verify(editor).apply() + } + + @Test + fun `getter uses default long value`() { + val holderDefault = MockPreferencesHolder(defaultLong = 0) + // Call the getter for the test + holderDefault.long + + verify(sharedPrefs).getLong("long", 0) + + val holderOther = MockPreferencesHolder(defaultLong = 23) + // Call the getter for the test + holderOther.long + + verify(sharedPrefs).getLong("long", 23) + } + + @Test + fun `getter returns string set from shared preferences`() { + val holder = MockPreferencesHolder() + `when`(sharedPrefs.getStringSet(eq("string_set"), any())).thenReturn(setOf("foo")) + + assertEquals(setOf("foo"), holder.stringSet) + verify(sharedPrefs).getStringSet("string_set", emptySet()) + } + + @Test + fun `setter applies string set to shared preferences`() { + val holder = MockPreferencesHolder(defaultString = "foo") + holder.stringSet = setOf("bar") + + verify(editor).putStringSet("string_set", setOf("bar")) + verify(editor).apply() + } + + @Test + fun `getter uses default string set value`() { + val holderDefault = MockPreferencesHolder() + // Call the getter for the test + holderDefault.stringSet + + verify(sharedPrefs).getStringSet("string_set", emptySet()) + + val holderOther = MockPreferencesHolder(defaultSet = setOf("hello", "world")) + // Call the getter for the test + holderOther.stringSet + + verify(sharedPrefs).getStringSet("string_set", setOf("hello", "world")) + } + + private inner class MockPreferencesHolder( + defaultBoolean: Boolean = false, + defaultFloat: Float = 0f, + defaultInt: Int = 0, + defaultLong: Long = 0L, + defaultString: String = "", + defaultSet: Set<String> = emptySet(), + ) : PreferencesHolder { + override val preferences = sharedPrefs + + var boolean by booleanPreference("boolean", default = defaultBoolean) + + var float by floatPreference("float", default = defaultFloat) + + var int by intPreference("int", default = defaultInt) + + var long by longPreference("long", default = defaultLong) + + var string by stringPreference("string", default = defaultString) + + var stringSet by stringSetPreference("string_set", default = defaultSet) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt new file mode 100644 index 0000000000..4e15aa6dbc --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.content.pm + +import android.content.Context +import android.content.pm.PackageInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertFalse +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.robolectric.Shadows.shadowOf + +@RunWith(AndroidJUnit4::class) +class PackageManagerTest { + private fun createContext( + installedApps: List<String> = emptyList(), + ): Context { + val pm = testContext.packageManager + val packageManager = shadowOf(pm) + val context = mock<Context>() + `when`(context.packageManager).thenReturn(pm) + installedApps.forEach { name -> + val packageInfo = PackageInfo().apply { + packageName = name + } + packageManager.addPackageNoDefaults(packageInfo) + } + + return context + } + + /** + * Verify that PackageManager.isPackageInstalled works correctly. + */ + @Test + fun `isPackageInstalled() returns true when package is installed, false otherwise`() { + val context = createContext(listOf("com.example", "com.test")) + + assert(context.packageManager.isPackageInstalled("com.example")) + assert(context.packageManager.isPackageInstalled("com.test")) + assertFalse(context.packageManager.isPackageInstalled("com.mozilla")) + assertFalse(context.packageManager.isPackageInstalled("com.example.com")) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt new file mode 100644 index 0000000000..dcb087ccae --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt @@ -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/. */ + +package mozilla.components.support.ktx.android.content.res + +import android.content.Context +import android.content.res.AssetManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import java.io.ByteArrayInputStream + +@RunWith(AndroidJUnit4::class) +class AssetManagerTest { + + /** + * Verify that AssetManager.readJSONObject() closes its stream. + */ + @Test + fun readJSONObjectClosesStream() { + // Setup + + val stream = Mockito.spy(ByteArrayInputStream("{}".toByteArray())) + + val assetManager = mock<AssetManager>() + Mockito.`when`(assetManager.open(ArgumentMatchers.anyString())).thenReturn(stream) + + val context = mock<Context>() + Mockito.`when`(context.assets).thenReturn(assetManager) + + // Now use our mock classes to call readJSONObject() + + context.assets.readJSONObject("test.txt") + + // Verify that the stream was opened and closed + + Mockito.verify(assetManager).open("test.txt") + Mockito.verify(stream).close() + } + + /** + * The stream returned by the AssetManager will be read and converted into a JSONObject instance. + */ + @Test + fun streamsIsTransformedIntoJSONObject() { + // Setup + + val stream = Mockito.spy(ByteArrayInputStream("{'firstName': 'John', 'lastName': 'Smith'}".toByteArray())) + + val assetManager = mock<AssetManager>() + Mockito.`when`(assetManager.open(ArgumentMatchers.anyString())).thenReturn(stream) + + val context = mock<Context>() + Mockito.`when`(context.assets).thenReturn(assetManager) + + // Now read the stream into an JSONObject + + val data = context.assets.readJSONObject("test.txt") + + // Assert that the JSONObject was constructed correctly. + + Mockito.verify(assetManager).open("test.txt") + + assertNotNull(data) + assertEquals(2, data.length()) + assertTrue(data.has("firstName")) + assertTrue(data.has("lastName")) + assertEquals("John", data.getString("firstName")) + assertEquals("Smith", data.getString("lastName")) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt new file mode 100644 index 0000000000..c8be2da46c --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.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.support.ktx.android.content.res + +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Typeface.BOLD +import android.graphics.Typeface.ITALIC +import android.os.Build +import android.os.LocaleList +import android.text.Html +import android.text.style.StyleSpan +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.robolectric.annotation.Config +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +class ResourcesTest { + + private lateinit var resources: Resources + private lateinit var configuration: Configuration + + @Before + fun setup() { + resources = mock() + configuration = spy(Configuration()) + + whenever(resources.configuration).thenReturn(configuration) + } + + @Config(sdk = [Build.VERSION_CODES.N]) + @Test + fun `locale returns first item in locales list`() { + whenever(configuration.locales).thenReturn(LocaleList(Locale.CANADA, Locale.ENGLISH)) + assertEquals(Locale.CANADA, resources.locale) + } + + @Suppress("Deprecation") + @Config(sdk = [Build.VERSION_CODES.M]) + @Test + fun `locale returns locale from configuration`() { + configuration.locale = Locale.FRENCH + assertEquals(Locale.FRENCH, resources.locale) + } + + @Config(sdk = [Build.VERSION_CODES.N]) + @Test + fun `getSpanned formats corresponding string`() { + val id = 100 + whenever(configuration.locales).thenReturn(LocaleList(Locale.ROOT)) + whenever(resources.getString(id)).thenReturn("Allow %1\$s to open %2\$s") + + assertEquals( + "<p dir=\"ltr\">Allow <b>App</b> to open <i>Website</i></p>\n", + Html.toHtml( + resources.getSpanned( + id, + "App" to StyleSpan(BOLD), + "Website" to StyleSpan(ITALIC), + ), + Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE, + ), + ) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt new file mode 100644 index 0000000000..acd57c1194 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt @@ -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/. */ + +package mozilla.components.support.ktx.android.graphics + +import android.graphics.Bitmap +import android.graphics.Color +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BitmapKtTest { + + private lateinit var subject: Bitmap + + @Before + fun setUp() { + subject = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + } + + @Ignore("convert to integration test. Robolectric's shadows are incomplete and cause this to fail.") + @Test + fun `WHEN withRoundedCorners is called THEN returned bitmap's corners should be transparent and center with color`() { + val dimen = 200 + val fillColor = Color.RED + + val bitmap = Bitmap.createBitmap(dimen, dimen, Bitmap.Config.ARGB_8888).apply { + eraseColor(fillColor) + } + val roundedBitmap = bitmap.withRoundedCorners(40f) + + fun assertCornersAreTransparent() { + val cornerLocations = listOf(0, dimen - 1) + + cornerLocations.forEach { x -> + cornerLocations.forEach { y -> + assertEquals(Color.TRANSPARENT, roundedBitmap.getPixel(x, y)) + } + } + } + + fun assertCenterIsFilled() { + val center = dimen / 2 + assertEquals(fillColor, roundedBitmap.getPixel(center, center)) + } + + assertNotSame(bitmap, roundedBitmap) + assertCornersAreTransparent() + assertCenterIsFilled() + } + + @Test + fun `GIVEN an all red bitmap THEN pixels are all the same`() { + subject.eraseColor(Color.RED) + assertTrue(subject.arePixelsAllTheSame()) + } + + @Test + fun `GIVEN an all transparent bitmap THEN pixels are all the same`() { + subject.eraseColor(Color.TRANSPARENT) + assertTrue(subject.arePixelsAllTheSame()) + } + + @Test + fun `GIVEN an all red bitmap with one pixel not red THEN pixels are not all the same`() { + subject.eraseColor(Color.RED) + subject.setPixel(0, 1, Color.rgb(244, 0, 0)) + assertFalse(subject.arePixelsAllTheSame()) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt new file mode 100644 index 0000000000..213e440e56 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt @@ -0,0 +1,243 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.net + +import android.content.ContentResolver +import android.database.Cursor +import android.webkit.MimeTypeMap +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.any +import org.mockito.Mockito.doReturn +import org.robolectric.Shadows + +@RunWith(AndroidJUnit4::class) +class UriTest { + + @Test + fun hostWithoutCommonPrefixes() { + assertEquals( + "mozilla.org", + "https://www.mozilla.org".toUri().hostWithoutCommonPrefixes, + ) + + assertEquals( + "twitter.com", + "https://mobile.twitter.com/home".toUri().hostWithoutCommonPrefixes, + ) + + assertNull("".toUri().hostWithoutCommonPrefixes) + + assertEquals( + "", + "http://".toUri().hostWithoutCommonPrefixes, + ) + + assertEquals( + "facebook.com", + "https://m.facebook.com/".toUri().hostWithoutCommonPrefixes, + ) + + assertEquals( + "github.com", + "https://github.com/mozilla-mobile/android-components".toUri().hostWithoutCommonPrefixes, + ) + } + + @Test + fun testIsHttpOrHttps() { + // No value + assertFalse("".toUri().isHttpOrHttps) + + // Garbage + assertFalse("lksdjflasuf".toUri().isHttpOrHttps) + + // URLs with http/https + assertTrue("https://www.google.com".toUri().isHttpOrHttps) + assertTrue("http://www.facebook.com".toUri().isHttpOrHttps) + assertTrue("https://mozilla.org/en-US/firefox/products/".toUri().isHttpOrHttps) + + // IP addresses + assertTrue("https://192.168.0.1".toUri().isHttpOrHttps) + assertTrue("http://63.245.215.20/en-US/firefox/products".toUri().isHttpOrHttps) + + // Other protocols + assertFalse("ftp://people.mozilla.org".toUri().isHttpOrHttps) + assertFalse("javascript:window.google.com".toUri().isHttpOrHttps) + assertFalse("tel://1234567890".toUri().isHttpOrHttps) + + // No scheme + assertFalse("google.com".toUri().isHttpOrHttps) + assertFalse("git@github.com:mozilla/gecko-dev.git".toUri().isHttpOrHttps) + } + + @Test + fun testIsInScope() { + val url = "https://mozilla.github.io/my-app/".toUri() + val prefix = "https://mozilla.github.io/prefix-of/resource.html".toUri() + assertFalse(url.isInScope(emptyList())) + assertTrue(url.isInScope(listOf("https://mozilla.github.io/my-app/".toUri()))) + assertFalse(url.isInScope(listOf("https://firefox.com/out-of-scope/".toUri()))) + assertFalse(url.isInScope(listOf("https://mozilla.github.io/my-app-almost-in-scope".toUri()))) + assertTrue(prefix.isInScope(listOf("https://mozilla.github.io/prefix".toUri()))) + assertTrue(prefix.isInScope(listOf("https://mozilla.github.io/prefix-of/".toUri()))) + } + + @Test + fun testSameSchemeAndHostAs() { + // Host mismatch. + assertFalse("https://foo.bar".toUri().sameSchemeAndHostAs("https://foo.baz".toUri())) + // Scheme mismatch. + assertFalse("http://127.0.0.1".toUri().sameSchemeAndHostAs("https://127.0.0.1".toUri())) + // Port mismatch. + assertTrue("https://foo.bar:444".toUri().sameSchemeAndHostAs("https://foo.bar:555".toUri())) + // Port OK but scheme different. + assertFalse("https://foo.bar:443".toUri().sameSchemeAndHostAs("ftp://foo.bar:443".toUri())) + + assertTrue("https://foo.bar/bobo".toUri().sameSchemeAndHostAs("https://foo.bar:443/obob".toUri())) + assertTrue("https://foo.bar:333".toUri().sameSchemeAndHostAs("https://foo.bar:443:333".toUri())) + } + + @Test + fun testSameOriginAs() { + // Host mismatch. + assertFalse("https://foo.bar".toUri().sameOriginAs("https://foo.baz".toUri())) + // Scheme mismatch. + assertFalse("http://127.0.0.1".toUri().sameOriginAs("https://127.0.0.1".toUri())) + // Port mismatch. + assertFalse("https://foo.bar:444".toUri().sameOriginAs("https://foo.bar:555".toUri())) + // Port OK but scheme different. + assertFalse("https://foo.bar:443".toUri().sameOriginAs("ftp://foo.bar:443".toUri())) + + assertTrue("https://foo.bar:443/bobo".toUri().sameOriginAs("https://foo.bar:443/obob".toUri())) + assertTrue("https://foo.bar:333".toUri().sameOriginAs("https://foo.bar:333".toUri())) + } + + @Test + fun testGenerateFileName() { + val fileExtension = "txt" + var fileName = generateFileName(fileExtension) + + assertTrue(fileName.contains(fileExtension)) + + fileName = generateFileName() + + assertFalse(fileName.contains(".")) + } + + @Test + fun testGetFileExtension() { + val resolver = mock<ContentResolver>() + val uri = "content://media/external/file/37162".toUri() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain") + + doReturn("text/plain").`when`(resolver).getType(any()) + + assertEquals("txt", uri.getFileExtension(resolver)) + } + + @Test + fun `getFileNameForContentUris for urls with DISPLAY_NAME`() { + val resolver = mock<ContentResolver>() + val uri = "content://media/external/file/37162".toUri() + val cursor = mock<Cursor>() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any()) + doReturn(1).`when`(cursor).getColumnIndex(any()) + doReturn("myFile.txt").`when`(cursor).getString(anyInt()) + + assertEquals("myFile.txt", uri.getFileNameForContentUris(resolver)) + } + + @Test + fun `getFileNameForContentUris for urls without DISPLAY_NAME`() { + val resolver = mock<ContentResolver>() + val uri = "content://media/external/file/37162".toUri() + val cursor = mock<Cursor>() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any()) + doReturn(-1).`when`(cursor).getColumnIndex(any()) + + val fileName = uri.getFileNameForContentUris(resolver) + + assertTrue(fileName.contains(".txt")) + assertTrue(fileName.isNotEmpty()) + } + + @Test + fun `getFileNameForContentUris for urls with null DISPLAY_NAME`() { + val resolver = mock<ContentResolver>() + val uri = "content://media/external/file/37162".toUri() + val cursor = mock<Cursor>() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any()) + doReturn(1).`when`(cursor).getColumnIndex(any()) + doReturn(null).`when`(cursor).getString(anyInt()) + + val fileName = uri.getFileNameForContentUris(resolver) + + assertTrue(fileName.contains(".txt")) + assertTrue(fileName.isNotEmpty()) + } + + @Test + fun `getFileName for file uri schemes`() { + val resolver = mock<ContentResolver>() + val uri = "file:///home/user/myfile.html".toUri() + + assertEquals("myfile.html", uri.getFileName(resolver)) + } + + @Test + fun `getFileName for content uri schemes`() { + val resolver = mock<ContentResolver>() + val uri = "content://media/external/file/37162".toUri() + val cursor = mock<Cursor>() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any()) + doReturn(1).`when`(cursor).getColumnIndex(any()) + doReturn(null).`when`(cursor).getString(anyInt()) + + val fileName = uri.getFileName(resolver) + + assertTrue(fileName.contains(".txt")) + assertTrue(fileName.isNotEmpty()) + } + + @Test + fun `getFileName for UNKNOWN uri schemes will generate file name`() { + val resolver = mock<ContentResolver>() + val uri = "UNKNOWN://media/external/file/37162".toUri() + + Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain") + doReturn("text/plain").`when`(resolver).getType(any()) + + val fileName = uri.getFileName(resolver) + + assertTrue(fileName.contains(".txt")) + assertTrue(fileName.isNotEmpty()) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt new file mode 100644 index 0000000000..5b1a53bce6 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.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.support.ktx.android.org.json + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class JSONArrayTest { + + private lateinit var testData2Elements: JSONArray + + @Before + fun setUp() { + testData2Elements = JSONArray().apply { + put(JSONObject("""{"a": 1}""")) + put(JSONObject("""{"b": 2}""")) + } + } + + @Test + fun itCanBeIterated() { + val array = JSONArray("[1, 2, 3]") + + val sum = array.asSequence() + .map { it as Int } + .sum() + + assertEquals(6, sum) + } + + @Test + fun toListNull() { + val jsonArray: JSONArray? = null + val list = jsonArray.toList<Any>() + assertEquals(0, list.size) + } + + @Test + fun toListEmpty() { + val jsonArray = JSONArray() + val list = jsonArray.toList<Any>() + assertEquals(0, list.size) + } + + @Test + fun toListNotEmpty() { + val jsonArray = JSONArray() + jsonArray.put("value") + jsonArray.put("another-value") + val list = jsonArray.toList<String>() + assertEquals(2, list.size) + assertTrue(list.contains("value")) + assertTrue(list.contains("another-value")) + } + + @Test + fun `WHEN mapNotNull on an empty jsonArray THEN an empty list is returned`() { + assertEquals(emptyList<Int>(), JSONArray().mapNotNull(JSONArray::getJSONObject) { 1 }) + } + + @Test + fun `WHEN mapNotNull getFromArray throws a JSONException THEN that item is ignored`() { + val expected = listOf("a", "b") + testData2Elements.put(404) + val actual = testData2Elements.mapNotNull(JSONArray::getJSONObject) { it.keys().asSequence().first() } + assertEquals(expected, actual) + } + + @Test + fun `WHEN mapNotNull transform throws a JSONException THEN that item is ignored`() { + val actual = testData2Elements.mapNotNull(JSONArray::getJSONObject) { + it.get("b") // key not found for first item: throws an exception. + } + assertEquals(1, actual.size) + assertEquals(2, actual[0]) + } + + @Test + fun `WHEN mapNotNull getFromArray uses casted classes THEN data is mapped`() { + val expected = listOf(JSONArrayTest()) + val input = JSONArray().apply { put(expected[0]) } + val actual = input.mapNotNull(getFromArray = { i -> get(i) as JSONArrayTest }) { it } + assertEquals(expected, actual) + } + + @Test + fun `WHEN mapNotNull on an array of Int THEN nulls are removed and data is mapped`() { + val expected = listOf(2, 4, 6, 8, 10) + + // Convert expected to input: [2, null, 4, null, ...] + val input = JSONArray() + expected.forEach { + input.put(it) + input.put(null) + } + val actual = input.mapNotNull(JSONArray::getInt) { it } + + assertEquals(expected, actual) + } + + @Test + fun `WHEN mapNotNull on an array of JSONObject THEN nulls are removed and data is mapped`() { + val expected = listOf( + "a" to 1, + "b" to 2, + "c" to 3, + ) + + // Convert expected to input: [JSONObject("a" to 1), null, JSONObject("b" to 2), null, ...] + val input = JSONArray() + expected.forEach { + val obj = JSONObject().apply { put(it.first, it.second) } + input.put(obj) + input.put(null) + } + + val actual = input.mapNotNull(JSONArray::getJSONObject) { + val keys = it.keys().asSequence().toList() + assertEquals(it.toString(), 1, keys.size) + + val key = keys.first() + val value = it.get(key) as Int + Pair(key, value) + } + + assertEquals(expected, actual) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt new file mode 100644 index 0000000000..1f724f0b55 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.org.json + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class JSONObjectTest { + + @Test + fun sortKeys() { + val jsonObject = JSONObject() + jsonObject.put("second-key", "second-value") + jsonObject.put( + "third-key", + JSONArray().apply { + put(1) + put(2) + put(3) + }, + ) + jsonObject.put( + "first-key", + JSONObject().apply { + put("one-key", "one-value") + put("a-key", "a-value") + put("second-key", "second") + }, + ) + assertEquals("""{"first-key":{"a-key":"a-value","one-key":"one-value","second-key":"second"},"second-key":"second-value","third-key":[1,2,3]}""", jsonObject.sortKeys().toString()) + } + + @Test + fun putIfNotNull() { + val jsonObject = JSONObject() + assertEquals(0, jsonObject.length()) + jsonObject.putIfNotNull("key", null) + assertEquals(0, jsonObject.length()) + jsonObject.putIfNotNull("key", "value") + assertEquals(1, jsonObject.length()) + assertEquals("value", jsonObject["key"]) + } + + @Test + fun tryGetNull() { + val jsonObject = JSONObject("""{"key":null}""") + assertNull(jsonObject.tryGet("key")) + assertNull(jsonObject.tryGet("another-key")) + } + + @Test + fun tryGetNotNull() { + val jsonObject = JSONObject("""{"key":"value"}""") + assertEquals("value", jsonObject.tryGet("key")) + } + + @Test + fun tryGetStringNull() { + val jsonObject = JSONObject("""{"key":null}""") + assertNull(jsonObject.tryGetString("key")) + assertNull(jsonObject.tryGetString("another-key")) + } + + @Test + fun tryGetStringNotNull() { + val jsonObject = JSONObject("""{"key":"value"}""") + assertEquals("value", jsonObject.tryGetString("key")) + } + + @Test + fun tryGetLongNull() { + val jsonObject = JSONObject("""{"key":null}""") + assertNull(jsonObject.tryGetLong("key")) + assertNull(jsonObject.tryGetLong("another-key")) + } + + @Test + fun tryGetLongNotNull() { + val jsonObject = JSONObject("""{"key":218728173837192717}""") + assertEquals(218728173837192717, jsonObject.tryGetLong("key")) + } + + @Test + fun tryGetIntNull() { + val jsonObject = JSONObject("""{"key":null}""") + assertNull(jsonObject.tryGetInt("key")) + assertNull(jsonObject.tryGetInt("another-key")) + } + + @Test + fun tryGetIntNotNull() { + val jsonObject = JSONObject("""{"key":3}""") + assertEquals(3, jsonObject.tryGetInt("key")) + } + + @Test + fun mergeWith() { + val merged = JSONObject( + mapOf( + "toKeep" to 3, + "toOverride" to "OHNOZ", + ), + ) + + merged.mergeWith( + JSONObject( + mapOf( + "newKey" to 5, + "toOverride" to "YAY", + ), + ), + ) + + val expectedObject = JSONObject( + mapOf( + "toKeep" to 3, + "toOverride" to "YAY", + "newKey" to 5, + ), + ) + assertEquals(expectedObject.toString(), merged.toString()) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt new file mode 100644 index 0000000000..b41997bc19 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.os + +import androidx.core.os.bundleOf +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.concept.base.crash.Breadcrumb +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class BundleTest { + + @Test + fun `bundles with different sizes should not be equals`() { + val small = bundleOf( + "hello" to "world", + ) + val big = bundleOf( + "hello" to "world", + "foo" to "bar", + ) + assertFalse(small.contentEquals(big)) + } + + @Test + fun `bundles with arrays should be equal`() { + val (bundle1, bundle2) = (0..1).map { + bundleOf( + "str" to "world", + "int" to 0, + "boolArray" to booleanArrayOf(true, false), + "byteArray" to "test".toByteArray(), + "charArray" to "test".toCharArray(), + "doubleArray" to doubleArrayOf(0.0, 1.1), + "floatArray" to floatArrayOf(1f, 2f), + "intArray" to intArrayOf(0, 1, 2), + "longArray" to longArrayOf(0L, 1L), + "shortArray" to shortArrayOf(1, 2), + "typedArray" to arrayOf("foo", "bar"), + "nestedBundle" to bundleOf(), + ) + } + assertTrue(bundle1.contentEquals(bundle2)) + } + + @Test + fun `bundles with parcelables should be equal`() { + val date = Date() + val (bundle1, bundle2) = (0..1).map { + bundleOf( + "crumbs" to Breadcrumb( + message = "msg", + level = Breadcrumb.Level.DEBUG, + date = date, + ), + ) + } + assertTrue(bundle1.contentEquals(bundle2)) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt new file mode 100644 index 0000000000..4854110c2f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.os + +import android.os.StrictMode +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class StrictModeTest { + + @Test + fun `strict mode policy should be restored`() { + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .penaltyLog() + .build(), + ) + val policy = StrictMode.getThreadPolicy() + assertEquals( + 27, + StrictMode.allowThreadDiskReads().resetAfter { + // Comparing via toString because StrictMode.ThreadPolicy does not redefine equals() and each time + // setThreadPolicy is called a new ThreadPolicy object is created (although the mask is the same) + assertNotEquals(policy.toString(), StrictMode.getThreadPolicy().toString()) + 27 + }, + ) + assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString()) + } + + @Test + fun `strict mode policy should be restored if function block throws an error`() { + val policy = StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .penaltyLog() + .build().apply { + StrictMode.setThreadPolicy(this) + } + + val exceptionCaught: Boolean + + assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString()) + + try { + StrictMode.allowThreadDiskReads().resetAfter { + assertNotEquals(policy.toString(), StrictMode.getThreadPolicy().toString()) + throw RuntimeException("Boing!") + } + } catch (e: RuntimeException) { + exceptionCaught = true + } + + assertTrue(exceptionCaught) + assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString()) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt new file mode 100644 index 0000000000..5661998034 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.os + +import android.os.Build +import android.os.VibrationEffect +import android.os.VibrationEffect.DEFAULT_AMPLITUDE +import android.os.Vibrator +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class VibratorTest { + + private lateinit var vibrator: Vibrator + + @Before + fun setup() { + vibrator = mock() + } + + @Config(sdk = [Build.VERSION_CODES.O]) + @Test + fun `vibrateOneShot uses VibrationEffect on new APIs`() { + vibrator.vibrateOneShot(50L) + + verify(vibrator).vibrate(VibrationEffect.createOneShot(50L, DEFAULT_AMPLITUDE)) + } + + @Suppress("Deprecation") + @Config(sdk = [Build.VERSION_CODES.LOLLIPOP]) + @Test + fun `vibrateOneShot uses vibrate on new APIs`() { + vibrator.vibrateOneShot(100L) + + verify(vibrator).vibrate(100L) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt new file mode 100644 index 0000000000..353761d7ba --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.util + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Base64Test { + + @Test + fun `encodeToUriString contains required data URI format`() { + val s = Base64.encodeToUriString("foo") + Assert.assertTrue(s.contains("data:text/html;base64,")) + Assert.assertTrue(s.contains("Zm9v")) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt new file mode 100644 index 0000000000..fe92f15c4b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.util + +import android.util.DisplayMetrics +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DisplayMetricsTest { + + @Test + fun `dp returns same value as manual conversion`() { + val metrics = testContext.resources.displayMetrics + + for (i in 1..500) { + val px = (i * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() + Assert.assertEquals(px, i.dpToPx(metrics)) + Assert.assertNotEquals(0, i.dpToPx(metrics)) + } + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt new file mode 100644 index 0000000000..341588f695 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.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.support.ktx.android.util + +import android.util.JsonReader +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class JsonReaderKtTest { + @Test + fun `nextStringOrNull - string`() { + val json = """{ "key": "value" }""" + val reader = JsonReader(json.reader()) + + reader.beginObject() + reader.nextName() + + assertEquals("value", reader.nextStringOrNull()) + } + + @Test + fun `nextStringOrNull - null`() { + val json = """{ "key": null }""" + val reader = JsonReader(json.reader()) + + reader.beginObject() + reader.nextName() + + assertNull(reader.nextStringOrNull()) + } + + @Test + fun `nextBooleanOrNull - true`() { + val json = """{ "key": true }""" + val reader = JsonReader(json.reader()) + + reader.beginObject() + reader.nextName() + + assertTrue(reader.nextBooleanOrNull()!!) + } + + @Test + fun `nextBooleanOrNull - false`() { + val json = """{ "key": false }""" + val reader = JsonReader(json.reader()) + + reader.beginObject() + reader.nextName() + + assertFalse(reader.nextBooleanOrNull()!!) + } + + @Test + fun `nextBooleanOrNull - null`() { + val json = """{ "key": null }""" + val reader = JsonReader(json.reader()) + + reader.beginObject() + reader.nextName() + + assertNull(reader.nextBooleanOrNull()) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt new file mode 100644 index 0000000000..f438039b2b --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.view + +import android.app.Activity +import android.os.Build +import android.view.View +import android.view.ViewTreeObserver +import android.view.Window +import android.view.WindowInsets +import android.view.WindowManager +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +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.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.robolectric.annotation.Config + +@Suppress("DEPRECATION") +@RunWith(AndroidJUnit4::class) +class ActivityTest { + + private lateinit var activity: Activity + private lateinit var window: Window + private lateinit var decorView: View + private lateinit var viewTreeObserver: ViewTreeObserver + private lateinit var windowInsets: WindowInsets + private lateinit var insetsController: WindowInsetsControllerCompat + private lateinit var layoutParams: WindowManager.LayoutParams + + @Before + fun setup() { + activity = mock() + window = mock() + decorView = mock() + viewTreeObserver = mock() + windowInsets = mock() + insetsController = mock() + layoutParams = WindowManager.LayoutParams() + + `when`(activity.window).thenReturn(window) + `when`(window.decorView).thenReturn(decorView) + `when`(window.decorView.viewTreeObserver).thenReturn(viewTreeObserver) + `when`(window.decorView.onApplyWindowInsets(windowInsets)).thenReturn(windowInsets) + `when`(window.attributes).thenReturn(layoutParams) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.P]) + fun `GIVEN Android version P WHEN enterImmersiveMode is called THEN systems bars are hidden, inset listener is set and notch flags are set to extend view into notch area`() { + activity.enterImmersiveMode(insetsController) + + verify(insetsController).hide(WindowInsetsCompat.Type.systemBars()) + verify(insetsController).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + verify(window.decorView).setOnApplyWindowInsetsListener(any()) + + verify(window).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, layoutParams.layoutInDisplayCutoutMode) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.O_MR1]) + fun `GIVEN Android version O_MR1 WHEN enterImmersiveMode is called THEN systems bars are hidden, inset listener is set and notch flags are not being set`() { + activity.enterImmersiveMode(insetsController) + + verify(insetsController).hide(WindowInsetsCompat.Type.systemBars()) + verify(insetsController).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + verify(window.decorView).setOnApplyWindowInsetsListener(any()) + + verify(window, never()).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + } + + @Test + fun `GIVEN enterImmersiveMode was called WHEN window insets are changed THEN insetsController hides system bars and sets bars behaviour again`() { + val insetListenerCaptor = argumentCaptor<View.OnApplyWindowInsetsListener>() + doReturn(30).`when`(windowInsets).systemWindowInsetTop + + activity.enterImmersiveMode(insetsController) + + verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars()) + verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + + verify(window.decorView).setOnApplyWindowInsetsListener(insetListenerCaptor.capture()) + insetListenerCaptor.value.onApplyWindowInsets(window.decorView, windowInsets) + + verify(insetsController, times(2)).hide(WindowInsetsCompat.Type.systemBars()) + verify(insetsController, times(2)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + @Test + fun `GIVEN enterImmersiveMode was called WHEN window insets are not changed THEN insetsController does nothing`() { + val insetListenerCaptor = argumentCaptor<View.OnApplyWindowInsetsListener>() + doReturn(0).`when`(windowInsets).systemWindowInsetTop + + activity.enterImmersiveMode(insetsController) + + verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars()) + verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + + verify(window.decorView).setOnApplyWindowInsetsListener(insetListenerCaptor.capture()) + insetListenerCaptor.value.onApplyWindowInsets(window.decorView, windowInsets) + + verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars()) + verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + @Test + fun `WHEN exitImmersiveMode is called THEN insetsController shows system bars and OnApplyWindowInsetsListener is cleared`() { + activity.exitImmersiveMode(insetsController) + + verify(insetsController).show(WindowInsetsCompat.Type.systemBars()) + verify(window.decorView).setOnApplyWindowInsetsListener(null) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.P]) + fun `GIVEN Android version P WHEN exitImmersiveMode is called THEN notch flags are reset to defaults`() { + activity.exitImmersiveMode() + + verify(window).clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT, layoutParams.layoutInDisplayCutoutMode) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.O_MR1]) + fun `GIVEN Android version O_MR1 WHEN exitImmersiveMode is called THEN notch flags were not being set`() { + activity.exitImmersiveMode() + + verify(window, never()).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt new file mode 100644 index 0000000000..355ae9f9bb --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.view + +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class MotionEventKtTest { + + private lateinit var subject: MotionEvent + private lateinit var subjectSpy: MotionEvent + + @Before + fun setUp() { + subject = MotionEvent.obtain(100, 100, ACTION_DOWN, 0f, 0f, 0) + subjectSpy = spy(subject) + } + + @Test + fun `WHEN use is called without an exception THEN the object is recycled`() { + subjectSpy.use {} + verify(subjectSpy, times(1)).recycle() + } + + @Test + fun `WHEN use is called with an exception THEN the object is recycled`() { + try { subjectSpy.use { throw IllegalStateException("Catch me!") } } catch (e: Exception) { /* Do nothing */ } + verify(subjectSpy, times(1)).recycle() + } + + @Test + fun `WHEN use is called and its function returns a value THEN that value is returned`() { + val expected = 47 + assertEquals(expected, subject.use { expected }) + } + + @Test(expected = IllegalStateException::class) + fun `WHEN use is called and its function throws an exception THEN that exception is thrown`() { + subject.use { throw IllegalStateException() } + } + + @Test + fun `WHEN use is called THEN the use function's argument is the use receiver`() { + subject.use { assertEquals(subject, it) } + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt new file mode 100644 index 0000000000..e8c4c46321 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.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.support.ktx.android.view + +import android.graphics.drawable.Drawable +import android.widget.EditText +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertArrayEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class TextViewTest { + + @Test + fun `putCompoundDrawablesRelative defaults to all null`() { + val view = TextView(testContext) + + view.putCompoundDrawablesRelative() + + assertArrayEquals( + arrayOf<Drawable?>(null, null, null, null), + view.compoundDrawablesRelative, + ) + } + + @Test + fun `putCompoundDrawablesRelativeWithIntrinsicBounds defaults to all null`() { + val view = TextView(testContext) + + view.putCompoundDrawablesRelativeWithIntrinsicBounds() + + assertArrayEquals( + arrayOf<Drawable?>(null, null, null, null), + view.compoundDrawablesRelative, + ) + } + + @Test + fun `putCompoundDrawablesRelative should set drawableStart and drawableEnd`() { + val view = EditText(testContext) + val drawable: Drawable = mock() + + view.putCompoundDrawablesRelative(start = drawable) + + assertArrayEquals( + arrayOf(drawable, null, null, null), + view.compoundDrawablesRelative, + ) + + view.putCompoundDrawablesRelative(end = drawable) + + assertArrayEquals( + arrayOf(null, null, drawable, null), + view.compoundDrawablesRelative, + ) + } + + @Test + fun `putCompoundDrawablesRelativeWithIntrinsicBounds should set drawableStart and drawableEnd`() { + val view = EditText(testContext) + val drawable: Drawable = mock() + + view.putCompoundDrawablesRelativeWithIntrinsicBounds(start = drawable) + + assertArrayEquals( + arrayOf(drawable, null, null, null), + view.compoundDrawablesRelative, + ) + + view.putCompoundDrawablesRelativeWithIntrinsicBounds(end = drawable) + + assertArrayEquals( + arrayOf(null, null, drawable, null), + view.compoundDrawablesRelative, + ) + } + + @Test + fun `putCompoundDrawablesRelative should call setCompoundDrawablesRelative`() { + val view: TextView = mock() + val drawable: Drawable = mock() + + view.putCompoundDrawablesRelative(top = drawable) + + verify(view).setCompoundDrawablesRelative(null, drawable, null, null) + + view.putCompoundDrawablesRelativeWithIntrinsicBounds(bottom = drawable) + + verify(view).setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, drawable) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt new file mode 100644 index 0000000000..d2e537232f --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt @@ -0,0 +1,221 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.view + +import android.app.Activity +import android.content.Context +import android.os.Looper.getMainLooper +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import mozilla.components.support.base.android.Padding +import mozilla.components.support.test.any +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.robolectric.Robolectric +import org.robolectric.Shadows.shadowOf +import org.robolectric.shadows.ShadowLooper +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +class ViewTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + + @Test + fun `showKeyboard should request focus`() { + val view = EditText(testContext) + assertFalse(view.hasFocus()) + + view.showKeyboard() + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + + assertTrue(view.hasFocus()) + } + + @Test + fun `hideKeyboard should hide soft keyboard`() { + val view = mock<View>() + val context = mock<Context>() + val imm = mock<InputMethodManager>() + `when`(view.context).thenReturn(context) + `when`(context.getSystemService(InputMethodManager::class.java)).thenReturn(imm) + + view.hideKeyboard() + + verify(imm).hideSoftInputFromWindow(view.windowToken, 0) + } + + @Test + fun `setPadding should set padding`() { + val view = TextView(testContext) + + assertEquals(view.paddingLeft, 0) + assertEquals(view.paddingTop, 0) + assertEquals(view.paddingRight, 0) + assertEquals(view.paddingBottom, 0) + + view.setPadding(Padding(16, 20, 24, 28)) + + assertEquals(view.paddingLeft, 16) + assertEquals(view.paddingTop, 20) + assertEquals(view.paddingRight, 24) + assertEquals(view.paddingBottom, 28) + } + + @Test + fun `getRectWithViewLocation should transform getLocationInWindow method values`() { + val view = spy(View(testContext)) + doAnswer { invocation -> + val locationInWindow = (invocation.getArgument(0) as IntArray) + locationInWindow[0] = 100 + locationInWindow[1] = 200 + locationInWindow + }.`when`(view).getLocationInWindow(any()) + + `when`(view.width).thenReturn(150) + `when`(view.height).thenReturn(250) + + val outRect = view.getRectWithViewLocation() + + assertEquals(100, outRect.left) + assertEquals(200, outRect.top) + assertEquals(250, outRect.right) + assertEquals(450, outRect.bottom) + } + + @Test + fun `called after next layout`() { + val view = View(testContext) + + var callbackInvoked = false + view.onNextGlobalLayout { + callbackInvoked = true + } + + assertFalse(callbackInvoked) + + view.viewTreeObserver.dispatchOnGlobalLayout() + + assertTrue(callbackInvoked) + } + + @Test + fun `remove listener after next layout`() { + val view = spy(View(testContext)) + val viewTreeObserver = spy(view.viewTreeObserver) + doReturn(viewTreeObserver).`when`(view).viewTreeObserver + + view.onNextGlobalLayout {} + + verify(viewTreeObserver, never()).removeOnGlobalLayoutListener(any()) + + viewTreeObserver.dispatchOnGlobalLayout() + + verify(viewTreeObserver).removeOnGlobalLayoutListener(any()) + } + + @Test + fun `can dispatch coroutines to view scope`() { + val activity = Robolectric.buildActivity(Activity::class.java).create().get() + val view = View(testContext) + activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100)) + shadowOf(getMainLooper()).idle() + + assertTrue(view.isAttachedToWindow) + + val latch = CountDownLatch(1) + var coroutineExecuted = false + + view.toScope().launch { + coroutineExecuted = true + latch.countDown() + } + + latch.await(10, TimeUnit.SECONDS) + + assertTrue(coroutineExecuted) + } + + @Test + fun `scope is cancelled when view is detached`() { + val activity = Robolectric.buildActivity(Activity::class.java).create().get() + val view = View(testContext) + activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100)) + shadowOf(getMainLooper()).idle() + + val scope = view.toScope() + + assertTrue(view.isAttachedToWindow) + assertTrue(scope.isActive) + + activity.windowManager.removeView(view) + shadowOf(getMainLooper()).idle() + + assertFalse(view.isAttachedToWindow) + assertFalse(scope.isActive) + + val latch = CountDownLatch(1) + var coroutineExecuted = false + + scope.launch { + coroutineExecuted = true + latch.countDown() + } + + assertFalse(latch.await(5, TimeUnit.SECONDS)) + assertFalse(coroutineExecuted) + } + + @Test + fun `correct view is found in the hierarchy matching the predicate`() { + val root = LinearLayout(testContext) + val layout = RelativeLayout(testContext) + val testView = TestView(testContext) + + layout.addView(testView) + root.addView(layout) + + val rootFound = root.findViewInHierarchy { it is LinearLayout } + + assertNotNull(rootFound) + assertTrue(rootFound is LinearLayout) + + val layoutFound = root.findViewInHierarchy { it is RelativeLayout } + + assertNotNull(layoutFound) + assertTrue(layoutFound is RelativeLayout) + + val testViewFound = root.findViewInHierarchy { it is TestView } + + assertNotNull(testViewFound) + assertTrue(testViewFound is TestView) + } + + private class TestView(context: Context) : View(context) +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt new file mode 100644 index 0000000000..24e49b9ce5 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.android.view + +import android.graphics.Color +import android.os.Build +import android.view.View +import android.view.Window +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations.openMocks +import org.robolectric.util.ReflectionHelpers.setStaticField +import kotlin.reflect.jvm.javaField + +/** + * **Note** Tests for isAppearanceLightStatusBars are in WindowKtTest. + */ +@RunWith(AndroidJUnit4::class) +class WindowTest { + + @Mock private lateinit var window: Window + + @Mock private lateinit var decorView: View + + @Before + fun setup() { + setStaticField(Build.VERSION::SDK_INT.javaField, 0) + + openMocks(this) + + `when`(window.decorView).thenAnswer { decorView } + } + + @After + fun teardown() = setStaticField(Build.VERSION::SDK_INT.javaField, 0) + + @Test + fun `GIVEN a color WHEN setStatusBarTheme THEN sets the status bar color`() { + window.setStatusBarTheme(Color.BLUE) + verify(window).statusBarColor = Color.BLUE + + window.setStatusBarTheme(Color.RED) + verify(window).statusBarColor = Color.RED + } + + @Test + fun `GIVEN Android 8 & no args WHEN setNavigationBarTheme THEN no colors are set`() { + setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1) + window.setNavigationBarTheme() + + verifyNoMoreInteractions(window) + } + + @Test + fun `GIVEN Android 8 & has nav bar color WHEN setNavigationBarTheme THEN only the nav bar color is set`() { + setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1) + window.setNavigationBarTheme(navBarColor = Color.MAGENTA) + + // We can't verify against the navigationBarDividerColor directly due to using SDK O_MR1 so we'll verify using ordering. + val inOrder = inOrder(window) + inOrder.verify(window).navigationBarColor = Color.MAGENTA + // Called for createWindowInsetsController() + inOrder.verify(window, times(2)).decorView + inOrder.verifyNoMoreInteractions() + } + + @Test + fun `GIVEN Android 8 & has nav bar divider color WHEN setNavigationBarTheme THEN no colors are set`() { + setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1) + window.setNavigationBarTheme(navBarDividerColor = Color.DKGRAY) + + verifyNoMoreInteractions(window) + } + + @Test + fun `GIVEN Android 8 & all args WHEN setNavigationBarTheme THEN only the nav bar color is set`() { + setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1) + window.setNavigationBarTheme(navBarColor = Color.MAGENTA, navBarDividerColor = Color.DKGRAY) + + // We can't verify against the navigationBarDividerColor directly due to using SDK O_MR1 so we'll verify using ordering. + val inOrder = inOrder(window) + inOrder.verify(window).navigationBarColor = Color.MAGENTA + // Called for createWindowInsetsController() + inOrder.verify(window, times(2)).decorView + inOrder.verifyNoMoreInteractions() + } + + @Test + fun `GIVEN Android 9 & no args WHEN setNavigationBarTheme THEN the nav bar divider color is set to default`() { + setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P) + window.setNavigationBarTheme() + + verify(window, never()).navigationBarColor + verify(window).navigationBarDividerColor = 0 + } + + @Test + fun `GIVEN Android 9 has nav bar color WHEN setNavigationBarTheme THEN the nav bar color is set & nav bar divider color set to default`() { + setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P) + window.setNavigationBarTheme(navBarColor = Color.BLUE) + + verify(window).navigationBarColor = Color.BLUE + verify(window).navigationBarDividerColor = 0 + } + + @Test + fun `GIVEN Android 9 has nav bar divider color WHEN setNavigationBarTheme THEN only the nav bar divider color is set`() { + setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P) + window.setNavigationBarTheme(navBarDividerColor = Color.GREEN) + + verify(window, never()).navigationBarColor + verify(window).navigationBarDividerColor = Color.GREEN + } + + @Test + fun `GIVEN Android 9 & all args WHEN setNavigationBarTheme THEN the nav bar & nav bar divider colors are set`() { + setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P) + window.setNavigationBarTheme(navBarColor = Color.YELLOW, navBarDividerColor = Color.CYAN) + + verify(window).navigationBarColor = Color.YELLOW + verify(window).navigationBarDividerColor = Color.CYAN + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt new file mode 100644 index 0000000000..a03e88ba87 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.android.widget + +import android.view.View +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TextViewTest { + + @Test + fun `check text size is set to the maximum allowable size specified by the height`() { + val textView = TextView(testContext) + val heightSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + + textView.textSize = 200f + textView.adjustMaxTextSize(heightSpec) + assertEquals(94f, textView.textSize) + } + + @Test + fun `check text size is not adjusted if it is not larger than the allowable size`() { + val textView = TextView(testContext) + val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY) + + textView.textSize = 10f + textView.adjustMaxTextSize(heightSpec) + assertEquals(10f, textView.textSize) + } + + @Test + fun `check adjusted text size takes the default ascender padding into account`() { + val textView = TextView(testContext) + val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY) + + textView.textSize = 100f + textView.includeFontPadding = false + textView.adjustMaxTextSize(heightSpec) + assertEquals(50f, textView.textSize) + + textView.textSize = 100f + textView.includeFontPadding = true + textView.adjustMaxTextSize(heightSpec) + assertEquals(44f, textView.textSize) + + textView.textSize = 100f + textView.includeFontPadding = true + textView.adjustMaxTextSize(heightSpec, 25) + assertEquals(25f, textView.textSize) + + textView.textSize = 100f + textView.includeFontPadding = false + textView.adjustMaxTextSize(heightSpec, 25) + assertEquals(50f, textView.textSize) + } + + @Test + fun `check text size is the same as the maximum adjusted text size`() { + val textView = TextView(testContext) + val heightSpec = View.MeasureSpec.makeMeasureSpec(56, View.MeasureSpec.EXACTLY) + + textView.textSize = 50f + textView.adjustMaxTextSize(heightSpec) + assertEquals(50f, textView.textSize) + + textView.textSize = 56f + textView.includeFontPadding = false + textView.adjustMaxTextSize(heightSpec) + assertEquals(56f, textView.textSize) + } + + @Test + fun `check custom padding affects text size`() { + val textView = TextView(testContext) + val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY) + + textView.textSize = 100f + textView.includeFontPadding = false + textView.setPadding(5, 5, 5, 5) + textView.adjustMaxTextSize(heightSpec) + assertEquals(40f, textView.textSize) + + textView.textSize = 100f + textView.includeFontPadding = true + textView.setPadding(0, 5, 0, 5) + textView.adjustMaxTextSize(heightSpec) + assertEquals(34f, textView.textSize) + + textView.textSize = 100f + textView.setPadding(5, 0, 5, 0) + textView.adjustMaxTextSize(heightSpec) + assertEquals(44f, textView.textSize) + + textView.textSize = 100f + textView.setPadding(0, 5, 0, 0) + textView.adjustMaxTextSize(heightSpec) + assertEquals(39f, textView.textSize) + } + + @Test + fun `check negative available height results in text size 0`() { + val textView = TextView(testContext) + val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY) + + textView.textSize = 100f + textView.includeFontPadding = false + textView.setPadding(0, 25, 0, 25) + textView.adjustMaxTextSize(heightSpec) + assertEquals(100f, textView.textSize) + + textView.textSize = 100f + textView.includeFontPadding = true + textView.setPadding(0, 0, 0, 0) + textView.adjustMaxTextSize(heightSpec, 51) + assertEquals(100f, textView.textSize) + + textView.textSize = 100f + textView.includeFontPadding = false + textView.setPadding(0, 26, 0, 25) + textView.adjustMaxTextSize(heightSpec) + assertEquals(100f, textView.textSize) + + textView.textSize = 100f + textView.includeFontPadding = true + textView.setPadding(0, 25, 0, 25) + textView.adjustMaxTextSize(heightSpec) + assertEquals(100f, textView.textSize) + + textView.textSize = 100f + textView.includeFontPadding = true + textView.setPadding(0, 1000, 0, 1000) + textView.adjustMaxTextSize(heightSpec) + assertEquals(100f, textView.textSize) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt new file mode 100644 index 0000000000..2122ed9562 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.java.io + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File +import java.util.* +class FileKtTest { + + @Test + fun truncateDirectory() { + val root = File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString()) + assertTrue(root.mkdir()) + + val file1 = File(root, "file1") + assertTrue(file1.createNewFile()) + + val file2 = File(root, "file2") + assertTrue(file2.createNewFile()) + + val dir1 = File(root, "dir1") + assertTrue(dir1.mkdir()) + + val dir2 = File(root, "dir2") + assertTrue(dir2.mkdir()) + + val file3 = File(dir2, "file3") + file3.createNewFile() + + assertEquals(4, root.listFiles()?.size) + + root.truncateDirectory() + + assertEquals(0, root.listFiles()?.size) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt new file mode 100644 index 0000000000..34b9b15e5e --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.kotlin + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ByteArrayTest { + + @Test + fun toHexString() { + val stringValue = "Android Components" + assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString()) + assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(-1)) + assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(36)) + assertEquals("00416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(38)) + } + + @Test + fun toSha256Digest() { + val stringValue = "Android Components" + assertEquals( + "d2d01f10a9700b60740bdd20c60839dcf6b82be9e6a02719d564146cbe32d68f", + stringValue.toByteArray().toSha256Digest().toHexString(), + ) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt new file mode 100644 index 0000000000..669dc01088 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.kotlin + +import org.junit.Assert +import org.junit.Test + +class CollectionKtTest { + + @Test + fun `cross product of each element is called exactly`() { + val numbers = listOf(1, 2, 3) + val letters = listOf('a', 'b', 'c') + var counter = 0 + numbers.crossProduct(letters) { _, _ -> + counter++ + } + + Assert.assertEquals(numbers.size * letters.size, counter) + } + + @Test + fun `cross product of each element is of the same type`() { + val numbers = listOf(1, 2, 3) + val letters = listOf('a', 'b', 'c') + numbers.crossProduct(letters) { number, letter -> + Assert.assertEquals(Int::class, number::class) + Assert.assertEquals(Char::class, letter::class) + } + } + + @Test + fun `cross product of each pair is in order`() { + val numbers = listOf(1, 2, 3) + val letters = listOf('a', 'b', 'c') + val assertions = arrayOf( + 1 to 'a', + 1 to 'b', + 1 to 'c', + 2 to 'a', + 2 to 'b', + 2 to 'c', + 3 to 'a', + 3 to 'b', + 3 to 'c', + ) + var position = 0 + numbers.crossProduct(letters) { number, letter -> + Assert.assertEquals(assertions[position].first, number) + Assert.assertEquals(assertions[position].second, letter) + position++ + } + } + + @Suppress("USELESS_IS_CHECK") + @Test + fun `cross product result is list of return type`() { + val numbers = listOf(1, 2, 3) + val letters = listOf('a', 'b', 'c') + val result = numbers.crossProduct(letters) { number, letter -> + number to letter + } + Assert.assertTrue(result is List) + Assert.assertEquals(Pair::class, result[0]::class) + Assert.assertEquals(Int::class, result[0].first::class) + Assert.assertEquals(Char::class, result[0].second::class) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt new file mode 100644 index 0000000000..89ade2aace --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt @@ -0,0 +1,595 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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.support.ktx.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.lib.publicsuffixlist.PublicSuffixList +import mozilla.components.support.test.robolectric.testContext +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.assertSame +import org.junit.Assert.assertTrue +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Calendar +import java.util.Calendar.MILLISECOND + +const val PUNYCODE = "xn--kpry57d" +const val IDN = "台灣" + +@RunWith(AndroidJUnit4::class) +class StringTest { + + private val publicSuffixList = PublicSuffixList(testContext) + + @Test + fun isUrl() { + assertTrue("mozilla.org".isUrl()) + assertTrue(" mozilla.org ".isUrl()) + assertTrue("http://mozilla.org".isUrl()) + assertTrue("https://mozilla.org".isUrl()) + assertTrue("file://somefile.txt".isUrl()) + assertTrue("http://mozilla".isUrl()) + assertTrue("http://192.168.255.255".isUrl()) + assertTrue("about:crashcontent".isUrl()) + assertTrue(" about:crashcontent ".isUrl()) + assertTrue("sample:about ".isUrl()) + + assertFalse("mozilla".isUrl()) + assertFalse("mozilla android".isUrl()) + assertFalse(" mozilla android ".isUrl()) + assertFalse("Tweet:".isUrl()) + assertFalse("inurl:mozilla.org advanced search".isUrl()) + assertFalse("what is about:crashes".isUrl()) + + val extraText = "Check out @asa’s Tweet: https://twitter.com/asa/status/123456789?s=09" + val url = extraText.split(" ").find { it.isUrl() } + assertNotEquals("Tweet:", url) + } + + @Test + fun toNormalizedUrl() { + val expectedUrl = "http://mozilla.org" + assertEquals(expectedUrl, "http://mozilla.org".toNormalizedUrl()) + assertEquals(expectedUrl, " http://mozilla.org ".toNormalizedUrl()) + assertEquals(expectedUrl, "mozilla.org".toNormalizedUrl()) + } + + @Test + fun isPhone() { + assertTrue("tel:+1234567890".isPhone()) + assertTrue(" tel:+1234567890".isPhone()) + assertTrue("tel:+1234567890 ".isPhone()) + assertTrue("tel:+1234567890 ".isPhone()) + assertTrue("TEL:+1234567890".isPhone()) + assertTrue("Tel:+1234567890".isPhone()) + + assertFalse("tel:word".isPhone()) + } + + @Test + fun isEmail() { + assertTrue("mailto:asa@mozilla.com".isEmail()) + assertTrue(" mailto:asa@mozilla.com".isEmail()) + assertTrue("mailto:asa@mozilla.com ".isEmail()) + assertTrue("MAILTO:asa@mozilla.com".isEmail()) + assertTrue("Mailto:asa@mozilla.com".isEmail()) + } + + @Test + fun geoLocation() { + assertTrue("geo:1,-1".isGeoLocation()) + assertTrue("geo:1,-1;u=1".isGeoLocation()) + assertTrue("geo:1,-1,0.5;u=1".isGeoLocation()) + assertTrue(" geo:1,-1".isGeoLocation()) + assertTrue("geo:1,-1 ".isGeoLocation()) + assertTrue("GEO:1,-1".isGeoLocation()) + assertTrue("Geo:1,-1".isGeoLocation()) + } + + @Test + fun toDate() { + val calendar = Calendar.getInstance() + calendar.set(2019, 10, 29, 0, 0, 0) + calendar[MILLISECOND] = 0 + assertEquals(calendar.time, "2019-11-29".toDate("yyyy-MM-dd")) + calendar.set(2019, 10, 28, 0, 0, 0) + assertEquals(calendar.time, "2019-11-28".toDate("yyyy-MM-dd")) + assertNotNull("".toDate("yyyy-MM-dd")) + } + + @Test + fun `string to date conversion using multiple formats`() { + assertEquals("2019-08-16T01:02".toDate("yyyy-MM-dd'T'HH:mm"), "2019-08-16T01:02".toDate()) + + assertEquals("2019-08-16T01:02:03".toDate("yyyy-MM-dd'T'HH:mm"), "2019-08-16T01:02:03".toDate()) + + assertEquals("2019-08-16".toDate("yyyy-MM-dd"), "2019-08-16".toDate()) + } + + @Test + fun sha1() { + assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", "".sha1()) + + assertEquals("0a4d55a8d778e5022fab701977c5d840bbc486d0", "Hello World".sha1()) + + assertEquals("8de545c123907e9f886ba2313560a0abef530594", "ßüöä@!§\$".sha1()) + } + + @Test + fun `Try Get Host From Url`() { + val urlTest = "http://www.example.com:1080/docs/resource1.html" + val new = urlTest.tryGetHostFromUrl() + assertEquals(new, "www.example.com") + } + + @Test + fun `Try Get Host From Malformed Url`() { + val urlTest = "notarealurl" + val new = urlTest.tryGetHostFromUrl() + assertEquals(new, "notarealurl") + } + + @Test + fun isSameOriginAs() { + // Host mismatch. + assertFalse("https://foo.bar".isSameOriginAs("https://foo.baz")) + // Scheme mismatch. + assertFalse("http://127.0.0.1".isSameOriginAs("https://127.0.0.1")) + // Port mismatch (implicit + explicit). + assertFalse("https://foo.bar:444".isSameOriginAs("https://foo.bar")) + // Port mismatch (explicit). + assertFalse("https://foo.bar:444".isSameOriginAs("https://foo.bar:555")) + // Port OK but scheme different. + assertFalse("https://foo.bar".isSameOriginAs("http://foo.bar:443")) + // Port OK (explicit) but scheme different. + assertFalse("https://foo.bar:443".isSameOriginAs("ftp://foo.bar:443")) + + assertTrue("https://foo.bar".isSameOriginAs("https://foo.bar")) + assertTrue("https://foo.bar/bobo".isSameOriginAs("https://foo.bar/obob")) + assertTrue("https://foo.bar".isSameOriginAs("https://foo.bar:443")) + assertTrue("https://foo.bar:333".isSameOriginAs("https://foo.bar:333")) + } + + @Test + fun isExtensionUrl() { + assertTrue("moz-extension://1232-abcd".isExtensionUrl()) + assertFalse("mozilla.org".isExtensionUrl()) + assertFalse("https://mozilla.org".isExtensionUrl()) + assertFalse("http://mozilla.org".isExtensionUrl()) + } + + @Test + fun sanitizeURL() { + val expectedUrl = "http://mozilla.org" + assertEquals(expectedUrl, "\nhttp://mozilla.org\n".sanitizeURL()) + } + + @Test + fun isResourceUrl() { + assertTrue("resource://1232-abcd".isResourceUrl()) + assertFalse("mozilla.org".isResourceUrl()) + assertFalse("https://mozilla.org".isResourceUrl()) + assertFalse("http://mozilla.org".isResourceUrl()) + } + + @Test + fun sanitizeFileName() { + var file = "/../../../../../../../../../../directory/file.......txt" + val fileName = "file.txt" + + assertEquals(fileName, file.sanitizeFileName()) + + file = "/root/directory/file.txt" + + assertEquals(fileName, file.sanitizeFileName()) + + assertEquals("file", "file".sanitizeFileName()) + + assertEquals("file", "file..".sanitizeFileName()) + + assertEquals("file", "file.".sanitizeFileName()) + + assertEquals("file", ".file".sanitizeFileName()) + + assertEquals("test.2020.12.01.txt", "test.2020.12.01.txt".sanitizeFileName()) + } + + @Test + fun `getDataUrlImageExtension returns a default extension if one cannot be extracted from the data url`() { + val base64Image = "data:;base64,testImage" + + val result = base64Image.getDataUrlImageExtension() + + assertEquals("jpg", result) + } + + @Test + fun `getDataUrlImageExtension returns an extension based on the media type included in the the data url`() { + val base64Image = "data:image/gif;base64,testImage" + + val result = base64Image.getDataUrlImageExtension() + + assertEquals("gif", result) + } + + @Test + fun `ifNullOrEmpty returns the same if this CharSequence is not null and not empty`() { + val randomString = "something" + + assertSame(randomString, randomString.ifNullOrEmpty { "something else" }) + } + + @Test + fun `ifNullOrEmpty returns the invocation of the passed in argument if this CharSequence is null`() { + val nullString: String? = null + val validResult = "notNullString" + + assertSame(validResult, nullString.ifNullOrEmpty { validResult }) + } + + @Test + fun `ifNullOrEmpty returns the invocation of the passed in argument if this CharSequence is empty`() { + val nullString = "" + val validResult = "notEmptyString" + + assertSame(validResult, nullString.ifNullOrEmpty { validResult }) + } + + @Test + fun `getRepresentativeCharacter returns the correct representative character for the given urls`() { + assertEquals("M", "https://mozilla.org".getRepresentativeCharacter()) + assertEquals("W", "http://wikipedia.org".getRepresentativeCharacter()) + assertEquals("P", "http://plus.google.com".getRepresentativeCharacter()) + assertEquals("E", "https://en.m.wikipedia.org/wiki/Main_Page".getRepresentativeCharacter()) + + // Stripping common prefixes + assertEquals("T", "http://www.theverge.com".getRepresentativeCharacter()) + assertEquals("F", "https://m.facebook.com".getRepresentativeCharacter()) + assertEquals("T", "https://mobile.twitter.com".getRepresentativeCharacter()) + + // Special urls + assertEquals("?", "file:///".getRepresentativeCharacter()) + assertEquals("S", "file:///system/".getRepresentativeCharacter()) + assertEquals("P", "ftp://people.mozilla.org/test".getRepresentativeCharacter()) + + // No values + assertEquals("?", "".getRepresentativeCharacter()) + + // Rubbish + assertEquals("Z", "zZz".getRepresentativeCharacter()) + assertEquals("Ö", "ölkfdpou3rkjaslfdköasdfo8".getRepresentativeCharacter()) + assertEquals("?", "_*+*'##".getRepresentativeCharacter()) + assertEquals("ツ", "¯\\_(ツ)_/¯".getRepresentativeCharacter()) + assertEquals("ಠ", "ಠ_ಠ Look of Disapproval".getRepresentativeCharacter()) + + // Non-ASCII + assertEquals("Ä", "http://www.ätzend.de".getRepresentativeCharacter()) + assertEquals("名", "http://名がドメイン.com".getRepresentativeCharacter()) + assertEquals("C", "http://√.com".getRepresentativeCharacter()) + assertEquals("SS", "http://ß.de".getRepresentativeCharacter()) + assertEquals("Ԛ", "http://ԛәлп.com/".getRepresentativeCharacter()) // cyrillic + + // Punycode + assertEquals("X", "http://xn--tzend-fra.de".getRepresentativeCharacter()) // ätzend.de + assertEquals("X", "http://xn--V8jxj3d1dzdz08w.com".getRepresentativeCharacter()) // 名がドメイン.com + + // Numbers + assertEquals("1", "https://www.1and1.com/".getRepresentativeCharacter()) + + // IP + assertEquals("1", "https://192.168.0.1".getRepresentativeCharacter()) + } + + @Test + fun `last4Digits returns a string with only last 4 digits `() { + assertEquals("8431", "371449635398431".last4Digits()) + assertEquals("2345", "12345".last4Digits()) + assertEquals("1234", "1234".last4Digits()) + assertEquals("123", "123".last4Digits()) + assertEquals("1", "1".last4Digits()) + assertEquals("", "".last4Digits()) + } + + @Test + fun `when the full hostname cannot be displayed, elide labels starting from the front`() { + // See https://url.spec.whatwg.org/#url-rendering-elision + // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#eliding-urls + + val display = "http://1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.com" + .shortened() + + val split = display.split(".") + + // If the list ends with 25.com... + assertEquals("25", split.dropLast(1).last()) + // ...and each value is 1 larger than the last... + split.dropLast(1) + .map { it.toInt() } + .windowed(2, 1) + .forEach { (prev, next) -> + assertEquals(next, prev + 1) + } + // ...that means that all removed values came from the front of the list + } + + @Test + fun `the registrable domain is always displayed`() { + // https://url.spec.whatwg.org/#url-rendering-elision + // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#eliding-urls + + val bigRegistrableDomain = "evil-but-also-shockingly-long-registrable-domain.com" + assertTrue( + "https://login.your-bank.com.$bigRegistrableDomain/enter/your/password".shortened() + .contains(bigRegistrableDomain), + ) + } + + @Test + fun `url username and password fields should not be displayed`() { + // See https://url.spec.whatwg.org/#url-rendering-simplification + // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#simplify + + assertFalse("https://examplecorp.com@attacker.example/".shortened().contains("examplecorp")) + assertFalse("https://examplecorp.com@attacker.example/".shortened().contains("com")) + assertFalse("https://user:password@example.com/".shortened().contains("user")) + assertFalse("https://user:password@example.com/".shortened().contains("password")) + } + + @Test + fun `eTLDs should not be dropped`() { + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11 + "http://mozfreddyb.github.io/" shortenedShouldBecome "mozfreddyb.github.io" + "http://web.security.plumbing/" shortenedShouldBecome "web.security.plumbing" + } + + @Test + fun `ipv4 addresses should be returned as is`() { + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11 + "192.168.85.1" shortenedShouldBecome "192.168.85.1" + } + + @Test + fun `about buildconfig should not be modified`() { + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11 + "about:buildconfig" shortenedShouldBecome "about:buildconfig" + } + + @Test + fun `encoded userinfo should still be considered userinfo`() { + "https://user:password%40really.evil.domain%2F@mail.google.com" shortenedShouldBecome "mail.google.com" + } + + @Test + @Ignore("This would be more correct, but does not appear to be an attack vector") + fun `should decode DWORD IP addresses`() { + "https://16843009" shortenedShouldBecome "1.1.1.1" + } + + @Test + @Ignore("This would be more correct, but does not appear to be an attack vector") + fun `should decode octal IP addresses`() { + "https://000010.000010.000010.000010" shortenedShouldBecome "8.8.8.8" + } + + @Test + @Ignore("This would be more correct, but does not appear to be an attack vector") + fun `should decode hex IP addresses`() { + "http://0x01010101" shortenedShouldBecome "1.1.1.1" + } + + // BEGIN test cases borrowed from desktop (shortUrl is used for Top Sites on new tab) + // Test cases are modified, as we show the eTLD + // (https://searchfox.org/mozilla-central/source/browser/components/newtab/test/unit/lib/ShortUrl.test.js) + @Test + fun `should return a blank string if url is blank`() { + "" shortenedShouldBecome "" + } + + @Test + fun `should return the 'url' if not a valid url`() { + "something" shortenedShouldBecome "something" + "http:" shortenedShouldBecome "http:" + "http::double-colon" shortenedShouldBecome "http::double-colon" + // The largest allowed port is 65,535 + "http://badport:65536/" shortenedShouldBecome "http://badport:65536/" + } + + @Test + fun `should convert host to idn when calling shortURL`() { + "http://$PUNYCODE.blah.com" shortenedShouldBecome "$IDN.blah.com" + } + + @Test + fun `should get the hostname from url`() { + "http://bar.com" shortenedShouldBecome "bar.com" + } + + @Test + fun `should not strip out www if not first subdomain`() { + "http://foo.www.com" shortenedShouldBecome "foo.www.com" + "http://www.foo.www.com" shortenedShouldBecome "foo.www.com" + } + + @Test + fun `should convert to lowercase`() { + "HTTP://FOO.COM" shortenedShouldBecome "foo.com" + } + + @Test + fun `should not include the port`() { + "http://foo.com:8888" shortenedShouldBecome "foo.com" + } + + @Test + fun `should return hostname for localhost`() { + "http://localhost:8000/" shortenedShouldBecome "localhost" + } + + @Test + fun `should return hostname for ip address`() { + "http://127.0.0.1/foo" shortenedShouldBecome "127.0.0.1" + } + + @Test + fun `should return etld for www gov uk (www-only non-etld)`() { + "https://www.gov.uk/countersigning" shortenedShouldBecome "gov.uk" + } + + @Test + fun `should return idn etld for www-only non-etld`() { + "https://www.$PUNYCODE/foo" shortenedShouldBecome IDN + } + + @Test + fun `file uri should return input`() { + "file:///foo/bar.txt" shortenedShouldBecome "file:///foo/bar.txt" + } + + @Test + @Ignore("This behavior conflicts with https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11") + fun `should return not the protocol for about`() { + "about:newtab" shortenedShouldBecome "newtab" + } + + @Test + fun `should fall back to full url as a last resort`() { + "about:" shortenedShouldBecome "about:" + } + // END test cases borrowed from desktop + + // BEGIN test cases borrowed from FFTV + // (https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/test/java/org/mozilla/focus/utils/TestFormattedDomain.java#228) + @Test + fun testIsIPv4RealAddress() { + assertTrue("192.168.1.1".isIpv4()) + assertTrue("8.8.8.8".isIpv4()) + assertTrue("63.245.215.20".isIpv4()) + } + + @Test + fun testIsIPv4WithProtocol() { + assertFalse("http://8.8.8.8".isIpv4()) + assertFalse("https://8.8.8.8".isIpv4()) + } + + @Test + fun testIsIPv4WithPort() { + assertFalse("8.8.8.8:400".isIpv4()) + assertFalse("8.8.8.8:1337".isIpv4()) + } + + @Test + fun testIsIPv4WithPath() { + assertFalse("8.8.8.8/index.html".isIpv4()) + assertFalse("8.8.8.8/".isIpv4()) + } + + @Test + fun testIsIPv4WithIPv6() { + assertFalse("2001:db8::1 ".isIpv4()) + assertFalse("2001:db8:0:1:1:1:1:1".isIpv4()) + assertFalse("[2001:db8:a0b:12f0::1]".isIpv4()) + assertFalse("2001:db8: 3333:4444:5555:6666:1.2.3.4".isIpv4()) + } + + @Test + fun testIsIPv6WithIPv6() { + assertTrue("2001:db8::1".isIpv6()) + assertTrue("2001:db8:0:1:1:1:1:1".isIpv6()) + } + + @Test + fun testIsIPv6WithIPv4() { + assertFalse("192.168.1.1".isIpv6()) + assertFalse("8.8.8.8".isIpv6()) + assertFalse("63.245.215.20".isIpv6()) + } + // END test cases borrowed from FFTV + + @Test + fun testStripCommonSubdomains() { + assertEquals("mozilla.org", ("mozilla.org").stripCommonSubdomains()) + assertEquals("mozilla.org", ("www.mozilla.org").stripCommonSubdomains()) + assertEquals("mozilla.org", ("m.mozilla.org").stripCommonSubdomains()) + assertEquals("mozilla.org", ("mobile.mozilla.org").stripCommonSubdomains()) + assertEquals("random.mozilla.org", ("random.mozilla.org").stripCommonSubdomains()) + } + + @Test + fun `GIVEN an invalid base64 image string WHEN converting it into bitmap THEN the result is null`() { + val invalidBase64BitmapString = "aa" + assertNull(invalidBase64BitmapString.base64ToBitmap()) + } + + @Test + fun `GIVEN a valid base64 png string WHEN converting it into bitmap THEN the result is not null and no exception is thrown`() { + val validBase64BitmapString = "data:image/png;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + assertNotNull(validBase64BitmapString.base64ToBitmap()) + } + + @Test + fun `GIVEN a valid base64 image string WHEN converting it into bitmap THEN the result is not null and no exception is thrown`() { + val validBase64JpegString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + val validBase64JpgString = "data:image/jpg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + val validBase64AnythingString = "data:image/anything;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + assertNotNull(validBase64JpegString.base64ToBitmap()) + assertNotNull(validBase64JpgString.base64ToBitmap()) + assertNotNull(validBase64AnythingString.base64ToBitmap()) + } + + @Test + fun `GIVEN invalid base64 image strings WHEN converting them into bitmap THEN the result is null`() { + val invalidBase64String = "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + val invalidBase64String2 = "data:image/jpg;base64;R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + val invalidBase64String3 = "image/jpg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + assertNull(invalidBase64String.base64ToBitmap()) + assertNull(invalidBase64String2.base64ToBitmap()) + assertNull(invalidBase64String3.base64ToBitmap()) + } + + @Test + fun `GIVEN a valid or invalid base64 image string WHEN extracting its raw content string THEN the result is correct`() { + val validBase64JpegString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + val validBase64JpgString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + val validBase64PngString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + val invalidBase64String = "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + val invalidBase64String2 = "data:image/jpeg;base64;R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" + assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64JpegString.extractBase6RawString()) + assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64JpgString.extractBase6RawString()) + assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64PngString.extractBase6RawString()) + assertNull(invalidBase64String.extractBase6RawString()) + assertNull(invalidBase64String2.extractBase6RawString()) + } + + @Test + fun `GIVEN a URL with matching parameters WHEN testing if a URL contains query parameters THEN the result is true`() { + assertTrue("http://example.com?a".urlContainsQueryParameters("a")) + assertTrue("http://example.com?a&b&c".urlContainsQueryParameters("b")) + assertTrue("http://example.com?a=b".urlContainsQueryParameters("a=b")) + assertTrue("http://example.com?a=b&c=d&e=f".urlContainsQueryParameters("c=d")) + assertTrue("http://example.com?a=b&c=d&e=f#g=h".urlContainsQueryParameters("e=f")) + } + + @Test + fun `GIVEN a URL without matching parameters WHEN testing if a URL contains query parameters THEN the result is false`() { + assertFalse("".urlContainsQueryParameters("a")) + assertFalse("!@#$%^&*()-+".urlContainsQueryParameters("a")) + assertFalse("http://example.com".urlContainsQueryParameters("a")) + assertFalse("http://example.com?a&b".urlContainsQueryParameters("c")) + assertFalse("http://example.com?a=b".urlContainsQueryParameters("a")) + assertFalse("http://example.com?a=b&c=d&e=f#g=h".urlContainsQueryParameters("g=h")) + } + + private infix fun String.shortenedShouldBecome(expect: String) { + assertEquals(expect, this.shortened()) + } + + private fun String.shortened() = this.toShortUrl(publicSuffixList) +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt new file mode 100644 index 0000000000..6c987b9a67 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.ktx.kotlinx.coroutines + +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class UtilsKtTest { + + @Test + fun throttle() = runTest(UnconfinedTestDispatcher()) { + val skipTime = 300L + var value = 0 + val throttleBlock = throttleLatest<Int>(skipTime, coroutineScope = this) { + value = it + } + + for (n in 1..300) { + throttleBlock(n) + } + assertNotEquals(300, value) + + value = 0 + + for (n in 1..300) { + delay(skipTime) + throttleBlock(n) + } + + assertEquals(300, value) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt new file mode 100644 index 0000000000..11156c780a --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt @@ -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/. */ + +package mozilla.components.support.ktx.kotlinx.coroutines.flow + +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +class FlowKtTest { + + data class CharState(val value: Char) + data class IntState(val value: Int) + + @Test + fun `ifAnyChanged operator with block`() = runTest { + val originalFlow = flowOf("banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home") + + val items = originalFlow.ifAnyChanged { item -> arrayOf(item[0], item[1]) }.toList() + + assertEquals( + listOf("banana", "bus", "apple", "big", "coconut", "circle", "home"), + items, + ) + } + + @Test + fun `ifAnyChanged operator uses structural equality`() = runTest { + val originalFlow = flowOf("banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home") + + val items = + originalFlow.ifAnyChanged { + item -> + arrayOf(CharState(item[0]), CharState(item[1])) + }.toList() + + assertEquals( + listOf("banana", "bus", "apple", "big", "coconut", "circle", "home"), + items, + ) + } + + @Test + fun `filterChanged operator`() = runTest { + val intFlow = flowOf(listOf(0), listOf(0, 1), listOf(0, 1, 2, 3), listOf(4), listOf(5, 6, 7, 8)) + val identityItems = intFlow.filterChanged { item -> item }.toList() + assertEquals(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8), identityItems) + + val moduloFlow = flowOf(listOf(1), listOf(1, 2), listOf(3, 4, 5), listOf(3, 4)) + val moduloItems = moduloFlow.filterChanged { item -> item % 2 }.toList() + assertEquals(listOf(1, 2, 3, 4, 5), moduloItems) + + // Here we simulate a non-pure transform function (a function with a side-effect), causing + // the transformed values to be different for the same input. + var counter = 0 + val sideEffectFlow = flowOf(listOf(0), listOf(0, 1), listOf(0, 1, 2, 3), listOf(4), listOf(5, 6, 7, 8)) + val sideEffectItems = sideEffectFlow.filterChanged { item -> item + counter++ }.toList() + assertEquals(listOf(0, 0, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8), sideEffectItems) + } + + @Test + fun `filterChanged operator check for structural equality`() = runTest { + val intFlow = flowOf( + listOf(IntState(0)), + listOf(IntState(0), IntState(1)), + listOf(IntState(0), IntState(1), IntState(2), IntState(3)), + listOf(IntState(4)), + listOf(IntState(5), IntState(6), IntState(7), IntState(8)), + ) + + val identityItems = intFlow.filterChanged { item -> item }.toList() + assertEquals( + listOf( + IntState(0), + IntState(1), + IntState(2), + IntState(3), + IntState(4), + IntState(5), + IntState(6), + IntState(7), + IntState(8), + ), + identityItems, + ) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt new file mode 100644 index 0000000000..a842a05c83 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.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/. */ + +@file:Suppress("SameParameterValue") + +package mozilla.components.support.ktx.notification + +import android.app.NotificationChannel +import android.app.NotificationManager +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.getSystemService +import androidx.test.ext.junit.runners.AndroidJUnit4 +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import mozilla.components.support.ktx.android.notification.ChannelData +import mozilla.components.support.ktx.android.notification.ensureNotificationChannelExists +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith + +internal const val NOTIFICATION_CHANNEL_ID = "NOTIFICATION_CHANNEL_ID" + +@RunWith(AndroidJUnit4::class) +class NotificationTest { + + @Test + fun `ensureChannelExists - creates a notification channel`() { + var setupChannelWasCalled = false + var afterCreatedChannelWasCalled = false + + assertFalse(exists(channelId = NOTIFICATION_CHANNEL_ID)) + + val channelData = ChannelData( + NOTIFICATION_CHANNEL_ID, + android.R.string.ok, + NotificationManagerCompat.IMPORTANCE_LOW, + ) + + val setupChannel: NotificationChannel.() -> Unit = { + assertFalse(exists(channelId = NOTIFICATION_CHANNEL_ID)) + setupChannelWasCalled = true + lockscreenVisibility = NotificationCompat.VISIBILITY_SECRET + } + + val afterCreatedChannel: NotificationManager.() -> Unit = { + assertTrue(exists(channelId = NOTIFICATION_CHANNEL_ID)) + afterCreatedChannelWasCalled = true + + val channel = getChannel(NOTIFICATION_CHANNEL_ID)!! + + assertTrue(channel.lockscreenVisibility == NotificationCompat.VISIBILITY_SECRET) + } + + val channelId = + ensureNotificationChannelExists(testContext, channelData, setupChannel, afterCreatedChannel) + + assertTrue(setupChannelWasCalled) + assertTrue(afterCreatedChannelWasCalled) + assertTrue(exists(channelId = NOTIFICATION_CHANNEL_ID)) + assertEquals(channelId, NOTIFICATION_CHANNEL_ID) + } + + private fun exists(channelId: String) = getChannel(channelId) != null + + private fun getChannel(channelId: String): NotificationChannel? { + val notificationManager = testContext.getSystemService<NotificationManager>()!! + return notificationManager.getNotificationChannel(channelId) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt new file mode 100644 index 0000000000..48cd9137f8 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.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.support.ktx.util + +import android.util.AtomicFile +import androidx.core.util.writeText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.whenever +import org.json.JSONException +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.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.any +import org.mockito.Mockito.doThrow +import org.mockito.Mockito.verify +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class AtomicFileTest { + + @Test + fun `writeString - Fails write on IOException`() { + val mockedFile: AtomicFile = mock() + doThrow(IOException::class.java).`when`(mockedFile).startWrite() + + val result = mockedFile.writeString { "file_content" } + + assertFalse(result) + verify(mockedFile).failWrite(any()) + } + + @Test + fun `writeString - Fails write on JSONException`() { + val mockedFile: AtomicFile = mock() + whenever(mockedFile.startWrite()).thenAnswer { + throw JSONException("") + } + + val result = mockedFile.writeString { "file_content" } + + assertFalse(result) + verify(mockedFile).failWrite(any()) + } + + @Test + fun `writeString - writes the content of the file`() { + val tempFile = File.createTempFile("temp", ".tmp") + val atomicFile = AtomicFile(tempFile) + atomicFile.writeString { "file_content" } + + val result = atomicFile.writeString { "file_content" } + assertTrue(result) + } + + @Test + fun `readAndDeserialize - Returns the content of the file`() { + val tempFile = File.createTempFile("temp", ".tmp") + val atomicFile = AtomicFile(tempFile) + atomicFile.writeText("file_content") + + val fileContent = atomicFile.readAndDeserialize { it } + assertNotNull(fileContent) + assertEquals("file_content", fileContent) + } + + @Test + fun `readAndDeserialize - Returns null on FileNotFoundException`() { + val mockedFile: AtomicFile = mock() + doThrow(FileNotFoundException::class.java).`when`(mockedFile).openRead() + + val content = mockedFile.readAndDeserialize { it } + assertNull(content) + } + + @Test + fun `readAndDeserialize - Returns null on JSONException`() { + val mockedFile: AtomicFile = mock() + whenever(mockedFile.openRead()).thenAnswer { + throw JSONException("") + } + + val content = mockedFile.readAndDeserialize { it } + assertNull(content) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt new file mode 100644 index 0000000000..b07e11f698 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.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.support.ktx.util + +import android.content.res.Resources +import android.util.DisplayMetrics +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.ktx.android.util.dpToPx +import mozilla.components.support.ktx.android.util.spToPx +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` + +@RunWith(AndroidJUnit4::class) +class DisplayMetricsTest { + private lateinit var metrics: DisplayMetrics + + @Before + fun setUp() { + metrics = mock(DisplayMetrics::class.java) + metrics.density = 3f + metrics.setToDefaults() + + val resources: Resources = mock(Resources::class.java) + `when`(resources.displayMetrics).thenReturn(metrics) + } + + @Test + fun `Float dpToPx returns correct value`() { + val floatValue = 10f + + val result = floatValue.dpToPx(metrics) + + assertEquals(metrics.density * floatValue, result) + } + + @Test + fun `Float spToPx returns correct value`() { + val floatValue = 10f + + val result = floatValue.spToPx(metrics) + + @Suppress("DEPRECATION") + assertEquals(metrics.scaledDensity * floatValue, result) + } +} diff --git a/mobile/android/android-components/components/support/ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/support/ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..cf1c399ea8 --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/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/support/ktx/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 |