diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index dc415c0f6b..d5a1ed5765 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -57,11 +57,6 @@ object FeatureFlags { */ const val composeTabsTray = false - /** - * Enables the wallpaper v2 enhancements. - */ - const val wallpaperV2Enabled = true - /** * Enables the save to PDF feature. */ diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 37ce03604f..aaa7710191 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -90,6 +90,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder +import org.mozilla.fenix.components.metrics.GrowthDataWorker import org.mozilla.fenix.databinding.ActivityHomeBinding import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment @@ -452,6 +453,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { Events.defaultBrowserChanged.record(NoExtras()) } + GrowthDataWorker.sendActivatedSignalIfNeeded(applicationContext) ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext) MessageNotificationWorker.setMessageNotificationWorker(applicationContext) } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt index 48213b60a4..862f7fe31a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt @@ -88,9 +88,13 @@ class AdjustMetricsService( override fun track(event: Event) { CoroutineScope(dispatcher).launch { try { - if (event is Event.GrowthData && storage.shouldTrack(event)) { - Adjust.trackEvent(AdjustEvent(event.tokenName)) - storage.updateSentState(event) + if (event is Event.GrowthData) { + if (storage.shouldTrack(event)) { + Adjust.trackEvent(AdjustEvent(event.tokenName)) + storage.updateSentState(event) + } else { + storage.updatePersistentState(event) + } } } catch (e: Exception) { crashReporter.submitCaughtException(e) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index f932fbb604..29d4079c59 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -46,5 +46,12 @@ sealed class Event { * Event recording the first time a URI is loaded in Firefox in a 24 hour period. */ object FirstUriLoadForDay : GrowthData("ja86ek") + + /** + * Event recording when User is "activated" in first week of usage. + * Activated = if the user is active 3 days in their first week and + * if they search once in the latter half of that week (days 4-7). + */ + data class UserActivated(val fromSearch: Boolean) : GrowthData("imgpmr") } } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GrowthDataWorker.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GrowthDataWorker.kt new file mode 100644 index 0000000000..7287523955 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GrowthDataWorker.kt @@ -0,0 +1,74 @@ +package org.mozilla.fenix.components.metrics + +import android.content.Context +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import mozilla.components.support.utils.ext.getPackageInfoCompat +import org.mozilla.fenix.ext.metrics +import org.mozilla.fenix.ext.settings +import java.util.concurrent.TimeUnit + +/** + * Worker that will send the User Activated event at the end of the first week. + */ +class GrowthDataWorker( + context: Context, + workerParameters: WorkerParameters, +) : Worker(context, workerParameters) { + + override fun doWork(): Result { + val settings = applicationContext.settings() + + if (!System.currentTimeMillis().isAfterFirstWeekFromInstall(applicationContext) || + settings.growthUserActivatedSent + ) { + return Result.success() + } + + applicationContext.metrics.track(Event.GrowthData.UserActivated(fromSearch = false)) + + return Result.success() + } + + companion object { + private const val GROWTH_USER_ACTIVATED_WORK_NAME = "org.mozilla.fenix.growth.work" + private const val DAY_MILLIS: Long = 1000 * 60 * 60 * 24 + private const val FULL_WEEK_MILLIS: Long = DAY_MILLIS * 7 + + /** + * Schedules the Activated User event if needed. + */ + fun sendActivatedSignalIfNeeded(context: Context) { + val instanceWorkManager = WorkManager.getInstance(context) + + if (context.settings().growthUserActivatedSent) { + return + } + + val growthSignalWork = OneTimeWorkRequest.Builder(GrowthDataWorker::class.java) + .setInitialDelay(FULL_WEEK_MILLIS, TimeUnit.MILLISECONDS) + .build() + + instanceWorkManager.beginUniqueWork( + GROWTH_USER_ACTIVATED_WORK_NAME, + ExistingWorkPolicy.KEEP, + growthSignalWork, + ).enqueue() + } + + /** + * Returns [Boolean] value signaling if current time is after the first week after install. + */ + private fun Long.isAfterFirstWeekFromInstall(context: Context): Boolean { + val timeDifference = this - getInstalledTime(context) + return (FULL_WEEK_MILLIS <= timeDifference) + } + + private fun getInstalledTime(context: Context): Long = context.packageManager + .getPackageInfoCompat(context.packageName, 0) + .firstInstallTime + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt index bbac2287fc..0fdfc47aa3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt @@ -277,6 +277,7 @@ internal class ReleaseMetricController( } Component.FEATURE_SEARCH to InContentTelemetry.IN_CONTENT_SEARCH -> { BrowserSearch.inContent[value!!].add() + track(Event.GrowthData.UserActivated(fromSearch = true)) } Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> { metadata?.get("installed")?.let { installedAddons -> diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsMiddleware.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsMiddleware.kt index 4b35527f33..f51031da41 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsMiddleware.kt @@ -30,6 +30,7 @@ class MetricsMiddleware( metrics.track(Event.GrowthData.FirstAppOpenForDay) metrics.track(Event.GrowthData.FirstWeekSeriesActivity) metrics.track(Event.GrowthData.UsageThreshold) + metrics.track(Event.GrowthData.UserActivated(fromSearch = false)) } else -> Unit } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsStorage.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsStorage.kt index cfa9121a95..8602e77ea2 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsStorage.kt @@ -33,6 +33,11 @@ interface MetricsStorage { */ suspend fun updateSentState(event: Event) + /** + * Updates locally-stored data related to an [event] that has just been sent. + */ + suspend fun updatePersistentState(event: Event) + /** * Will try to register this as a recorder of app usage based on whether usage recording is still * needed. It will measure usage by to monitoring lifecycle callbacks from [application]'s @@ -60,6 +65,7 @@ internal class DefaultMetricsStorage( /** * Checks local state to see whether the [event] should be sent. */ + @Suppress("ComplexMethod", "CyclomaticComplexMethod") override suspend fun shouldTrack(event: Event): Boolean = withContext(dispatcher) { // The side-effect of storing days of use always needs to happen. @@ -91,6 +97,9 @@ internal class DefaultMetricsStorage( currentTime.duringFirstMonth() && settings.uriLoadGrowthLastSent.hasBeenMoreThanDaySince() } + is Event.GrowthData.UserActivated -> { + hasUserReachedActivatedThreshold() + } } } @@ -114,6 +123,23 @@ internal class DefaultMetricsStorage( Event.GrowthData.FirstUriLoadForDay -> { settings.uriLoadGrowthLastSent = System.currentTimeMillis() } + is Event.GrowthData.UserActivated -> { + settings.growthUserActivatedSent = true + } + } + } + + override suspend fun updatePersistentState(event: Event) { + when (event) { + is Event.GrowthData.UserActivated -> { + if (event.fromSearch && shouldUpdateSearchUsage()) { + settings.growthEarlySearchUsed = true + } else if (!event.fromSearch && shouldUpdateUsageCount()) { + settings.growthEarlyUseCount.increment() + settings.growthEarlyUseCountLastIncrement = System.currentTimeMillis() + } + } + else -> Unit } } @@ -176,6 +202,8 @@ internal class DefaultMetricsStorage( private fun Long.duringFirstDay() = this < getInstalledTime() + dayMillis + private fun Long.afterThirdDay() = this > getInstalledTime() + threeDayMillis + private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis @@ -184,6 +212,25 @@ internal class DefaultMetricsStorage( calendar.add(Calendar.DAY_OF_MONTH, 1) } + private fun hasUserReachedActivatedThreshold(): Boolean { + return !settings.growthUserActivatedSent && + settings.growthEarlyUseCount.value >= daysActivatedThreshold && + settings.growthEarlySearchUsed + } + + private fun shouldUpdateUsageCount(): Boolean { + val currentTime = System.currentTimeMillis() + return currentTime.afterFirstDay() && + currentTime.duringFirstWeek() && + settings.growthEarlyUseCountLastIncrement.hasBeenMoreThanDaySince() + } + + private fun shouldUpdateSearchUsage(): Boolean { + val currentTime = System.currentTimeMillis() + return currentTime.afterThirdDay() && + currentTime.duringFirstWeek() + } + /** * This will store app usage time to disk, based on Resume and Pause lifecycle events. Currently, * there is only interest in usage during the first day after install. @@ -208,6 +255,7 @@ internal class DefaultMetricsStorage( companion object { private const val dayMillis: Long = 1000 * 60 * 60 * 24 + private const val threeDayMillis: Long = 3 * dayMillis private const val shortestMonthMillis: Long = dayMillis * 28 // Note this is 8 so that recording of FirstWeekSeriesActivity happens throughout the length @@ -217,6 +265,9 @@ internal class DefaultMetricsStorage( // The usage threshold we are interested in is currently 340 seconds. private const val usageThresholdMillis = 1000 * 340 + // The usage threshold for "activated" growth users. + private const val daysActivatedThreshold = 3 + /** * Determines whether events should be tracked based on some general criteria: * - user has installed as a result of a campaign diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt index 14850c2319..dcb1d6db3f 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt @@ -86,7 +86,7 @@ private fun createOnboardingPageState( onNegativeButtonClick: () -> Unit, onUrlClick: (String) -> Unit = {}, ): OnboardingPageState = OnboardingPageState( - image = onboardingPageUiData.imageRes, + imageRes = onboardingPageUiData.imageRes, title = onboardingPageUiData.title, description = onboardingPageUiData.description, linkTextState = onboardingPageUiData.linkText?.let { diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/NotificationPermissionDialogScreen.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/NotificationPermissionDialogScreen.kt index 9154c56232..702b98a1fb 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/view/NotificationPermissionDialogScreen.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/NotificationPermissionDialogScreen.kt @@ -28,7 +28,7 @@ fun NotificationPermissionDialogScreen( ) { OnboardingPage( pageState = OnboardingPageState( - image = R.drawable.ic_notification_permission, + imageRes = R.drawable.ic_notification_permission, title = stringResource( id = R.string.onboarding_home_enable_notifications_title, formatArgs = arrayOf(stringResource(R.string.app_name)), diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPage.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPage.kt index fc2c962e6d..1ad5ca6d7e 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPage.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPage.kt @@ -70,7 +70,7 @@ private const val URL_TAG = "URL_TAG" * @param modifier The modifier to be applied to the Composable. * @param onDismiss Invoked when the user clicks the close button. This defaults to null. When null, * it doesn't show the close button. - * @param imageResContentScale The [ContentScale] for the [OnboardingPageState.image]. + * @param imageResContentScale The [ContentScale] for the [OnboardingPageState.imageRes]. */ @Composable @Suppress("LongMethod") @@ -114,7 +114,7 @@ fun OnboardingPage( horizontalAlignment = Alignment.CenterHorizontally, ) { Image( - painter = painterResource(id = pageState.image), + painter = painterResource(id = pageState.imageRes), contentDescription = null, contentScale = imageResContentScale, modifier = Modifier @@ -171,7 +171,7 @@ private fun DescriptionText( description: String, linkTextState: LinkTextState?, ) { - if (linkTextState != null) { + if (linkTextState != null && description.contains(linkTextState.text, ignoreCase = true)) { LinkText( text = description, linkTextState = linkTextState, @@ -198,7 +198,7 @@ private fun LinkText( linkTextState: LinkTextState, ) { val annotatedString = buildAnnotatedString { - val startIndex = text.indexOf(linkTextState.text) + val startIndex = text.indexOf(linkTextState.text, ignoreCase = true) val endIndex = startIndex + linkTextState.text.length append(text) addStyle( @@ -249,7 +249,7 @@ private fun OnboardingPagePreview() { FirefoxTheme { OnboardingPage( pageState = OnboardingPageState( - image = R.drawable.ic_notification_permission, + imageRes = R.drawable.ic_notification_permission, title = stringResource( id = R.string.onboarding_home_enable_notifications_title, formatArgs = arrayOf(stringResource(R.string.app_name)), diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt index cc720c211d..39138af1a8 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt @@ -9,16 +9,16 @@ import androidx.annotation.DrawableRes /** * Model containing data for [OnboardingPage]. * - * @param image [DrawableRes] displayed on the page. - * @param title [String] title of the page. - * @param description [String] description of the page. - * @param linkTextState [LinkTextState] part of description text with a link. - * @param primaryButton [Action] action for the primary button. - * @param secondaryButton [Action] action for the secondary button. - * @param onRecordImpressionEvent Callback for recording impression event. + * @property imageRes [DrawableRes] displayed on the page. + * @property title [String] title of the page. + * @property description [String] description of the page. + * @property linkTextState [LinkTextState] part of description text with a link. + * @property primaryButton [Action] action for the primary button. + * @property secondaryButton [Action] action for the secondary button. + * @property onRecordImpressionEvent Callback for recording impression event. */ data class OnboardingPageState( - @DrawableRes val image: Int, + @DrawableRes val imageRes: Int, val title: String, val description: String, val linkTextState: LinkTextState? = null, diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/UpgradeOnboarding.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/UpgradeOnboarding.kt index 81e2d50430..ba0078c58c 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/view/UpgradeOnboarding.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/UpgradeOnboarding.kt @@ -69,7 +69,7 @@ fun UpgradeOnboarding( OnboardingPage( pageState = when (onboardingState) { UpgradeOnboardingState.Welcome -> OnboardingPageState( - image = R.drawable.ic_onboarding_welcome, + imageRes = R.drawable.ic_onboarding_welcome, title = stringResource(id = R.string.onboarding_home_welcome_title_2), description = stringResource(id = R.string.onboarding_home_welcome_description), primaryButton = Action( @@ -88,7 +88,7 @@ fun UpgradeOnboarding( }, ) UpgradeOnboardingState.SyncSignIn -> OnboardingPageState( - image = R.drawable.ic_onboarding_sync, + imageRes = R.drawable.ic_onboarding_sync, title = stringResource(id = R.string.onboarding_home_sync_title_3), description = stringResource(id = R.string.onboarding_home_sync_description), primaryButton = Action( diff --git a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt index b566fe0d43..e867be39f2 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -26,7 +26,7 @@ object SupportUtils { const val WIKIPEDIA_URL = "https://www.wikipedia.org/" const val FENIX_PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}" const val GOOGLE_URL = "https://www.google.com/" - const val BAIDU_URL = "https://m.baidu.com/?from=1000969a" + const val BAIDU_URL = "https://m.baidu.com/" const val JD_URL = "https://union-click.jd.com/jdc" + "?e=&p=AyIGZRprFDJWWA1FBCVbV0IUWVALHFRBEwQAQB1AWQkFVUVXfFkAF14lRFRbJXstVWR3WQ1rJ08AZnhS" + "HDJBYh4LZR9eEAMUBlccWCUBEQZRGFoXCxc3ZRteJUl8BmUZWhQ" + diff --git a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt index 9161cfac80..073f3caaf9 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/wallpaper/WallpaperSettingsFragment.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.launch import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.service.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.GleanMetrics.Wallpapers import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -59,11 +58,7 @@ class WallpaperSettingsFragment : Fragment() { val coroutineScope = rememberCoroutineScope() WallpaperSettings( - wallpaperGroups = if (FeatureFlags.wallpaperV2Enabled) { - wallpapers.groupByDisplayableCollection() - } else { - mapOf(Wallpaper.ClassicFirefoxCollection to wallpapers) - }, + wallpaperGroups = wallpapers.groupByDisplayableCollection(), defaultWallpaper = Wallpaper.Default, selectedWallpaper = currentWallpaper, loadWallpaperResource = { diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 2fb6fc4ab1..32f2e60355 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -1735,4 +1735,33 @@ class Settings(private val appContext: Context) : PreferencesHolder { key = appContext.getPreferenceKey(R.string.pref_key_enable_tabs_tray_to_compose), default = FeatureFlags.composeTabsTray, ) + + /** + * Adjust Activated User sent + */ + var growthUserActivatedSent by booleanPreference( + key = appContext.getPreferenceKey(R.string.pref_key_growth_user_activated_sent), + default = false, + ) + + /** + * Indicates how many days in the first week user opened the app. + */ + val growthEarlyUseCount = counterPreference( + appContext.getPreferenceKey(R.string.pref_key_growth_early_browse_count), + maxCount = 3, + ) + + var growthEarlyUseCountLastIncrement by longPreference( + key = appContext.getPreferenceKey(R.string.pref_key_growth_early_browse_count_last_increment), + default = 0L, + ) + + /** + * Indicates how many days in the first week user searched in the app. + */ + var growthEarlySearchUsed by booleanPreference( + key = appContext.getPreferenceKey(R.string.pref_key_growth_early_search), + default = false, + ) } diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperDownloader.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperDownloader.kt deleted file mode 100644 index d521c78f9e..0000000000 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperDownloader.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.wallpapers - -import android.content.Context -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import mozilla.components.concept.fetch.Client -import mozilla.components.concept.fetch.Request -import mozilla.components.concept.fetch.isSuccess -import mozilla.components.support.base.log.logger.Logger -import org.mozilla.fenix.BuildConfig -import java.io.File - -/** - * Can download wallpapers from a remote host. - * - * @param context Required for writing files to local storage. - * @param client Required for fetching files from network. - */ -class LegacyWallpaperDownloader( - private val context: Context, - private val client: Client, -) { - private val logger = Logger("WallpaperDownloader") - private val remoteHost = BuildConfig.WALLPAPER_URL - - /** - * Downloads a wallpaper from the network. Will try to fetch 4 versions of each wallpaper: - * portrait/light - portrait/dark - landscape/light - landscape/dark. These are expected to be - * found at a remote path in the form: - * /////.png - */ - suspend fun downloadWallpaper(wallpaper: Wallpaper) = withContext(Dispatchers.IO) { - if (remoteHost.isNullOrEmpty()) { - return@withContext - } - - for (metadata in wallpaper.toMetadata(context)) { - val localFile = File(context.filesDir.absolutePath, metadata.localPath) - if (localFile.exists()) continue - val request = Request( - url = "$remoteHost/${metadata.remotePath}", - method = Request.Method.GET, - ) - Result.runCatching { - val response = client.fetch(request) - if (!response.isSuccess) { - logger.error("Download response failure code: ${response.status}") - return@withContext - } - File(localFile.path.substringBeforeLast("/")).mkdirs() - response.body.useStream { input -> - input.copyTo(localFile.outputStream()) - } - }.onFailure { - Result.runCatching { - if (localFile.exists()) { - localFile.delete() - } - }.onFailure { e -> - logger.error("Failed to delete stale wallpaper bitmaps while downloading", e) - } - - logger.error(it.message ?: "Download failed: no throwable message included.", it) - } - } - } - - private data class WallpaperMetadata(val remotePath: String, val localPath: String) - - private fun Wallpaper.toMetadata(context: Context): List = - listOf("landscape", "portrait").flatMap { orientation -> - listOf("light", "dark").map { theme -> - val localPath = "wallpapers/$orientation/$theme/$name.png" - val remotePath = "${context.resolutionSegment()}/" + - "$orientation/" + - "$theme/" + - "${collection.name}/" + - "$name.png" - WallpaperMetadata(remotePath, localPath) - } - } - - @Suppress("MagicNumber") - private fun Context.resolutionSegment(): String = when (resources.displayMetrics.densityDpi) { - // targeting hdpi and greater density resolutions https://developer.android.com/training/multiscreen/screendensities - in 0..240 -> "low" - in 240..320 -> "medium" - else -> "high" - } -} diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperFileManager.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperFileManager.kt deleted file mode 100644 index 68dbb48b83..0000000000 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperFileManager.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.wallpapers - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File - -/** - * Manages various functions related to the locally-stored wallpaper assets. - * - * @property rootDirectory The top level app-local storage directory. - * @param coroutineDispatcher Dispatcher used to execute suspending functions. Default parameter - * should be likely be used except for when under test. - */ -class LegacyWallpaperFileManager( - private val rootDirectory: File, - coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, -) { - private val scope = CoroutineScope(coroutineDispatcher) - private val portraitDirectory = File(rootDirectory, "wallpapers/portrait") - private val landscapeDirectory = File(rootDirectory, "wallpapers/landscape") - - /** - * Lookup all the files for a wallpaper name. This lookup will fail if there are not - * files for each of the following orientation and theme combinations: - * light/portrait - light/landscape - dark/portrait - dark/landscape - */ - suspend fun lookupExpiredWallpaper(name: String): Wallpaper? = withContext(Dispatchers.IO) { - if (getAllLocalWallpaperPaths(name).all { File(rootDirectory, it).exists() }) { - Wallpaper( - name = name, - collection = Wallpaper.DefaultCollection, - textColor = null, - cardColorLight = null, - cardColorDark = null, - thumbnailFileState = Wallpaper.ImageFileState.Unavailable, - assetsFileState = Wallpaper.ImageFileState.Downloaded, - ) - } else { - null - } - } - - private fun getAllLocalWallpaperPaths(name: String): List = - listOf("landscape", "portrait").flatMap { orientation -> - listOf("light", "dark").map { theme -> - Wallpaper.legacyGetLocalPath(orientation, theme, name) - } - } - - /** - * Remove all wallpapers that are not the [currentWallpaper] or in [availableWallpapers]. - */ - fun clean(currentWallpaper: Wallpaper, availableWallpapers: List) { - scope.launch { - val wallpapersToKeep = (listOf(currentWallpaper) + availableWallpapers).map { it.name } - cleanChildren(portraitDirectory, wallpapersToKeep) - cleanChildren(landscapeDirectory, wallpapersToKeep) - } - } - - private fun cleanChildren(dir: File, wallpapersToKeep: List) { - for (file in dir.walkTopDown()) { - if (file.isDirectory || file.nameWithoutExtension in wallpapersToKeep) continue - file.delete() - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt index 56e33cbebd..cc86e92a6c 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpapersUseCases.kt @@ -12,11 +12,8 @@ import androidx.annotation.VisibleForTesting import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import mozilla.components.concept.fetch.Client -import org.mozilla.fenix.FeatureFlags -import org.mozilla.fenix.R import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction -import org.mozilla.fenix.ext.isSystemInDarkTheme import org.mozilla.fenix.ext.settings import org.mozilla.fenix.utils.Settings import java.io.File @@ -46,57 +43,33 @@ class WallpapersUseCases( private val downloader = WallpaperDownloader(storageRootDirectory, client) private val fileManager = WallpaperFileManager(storageRootDirectory) val initialize: InitializeWallpapersUseCase by lazy { - if (FeatureFlags.wallpaperV2Enabled) { - val metadataFetcher = WallpaperMetadataFetcher(client) - val migrationHelper = LegacyWallpaperMigration( - storageRootDirectory = storageRootDirectory, - settings = context.settings(), - selectWallpaper::invoke, - ) - DefaultInitializeWallpaperUseCase( - appStore = appStore, - downloader = downloader, - fileManager = fileManager, - metadataFetcher = metadataFetcher, - migrationHelper = migrationHelper, - settings = context.settings(), - currentLocale = currentLocale, - ) - } else { - val fileManager = LegacyWallpaperFileManager(storageRootDirectory) - val downloader = LegacyWallpaperDownloader(context, client) - LegacyInitializeWallpaperUseCase( - appStore = appStore, - downloader = downloader, - fileManager = fileManager, - settings = context.settings(), - currentLocale = currentLocale, - ) - } + val metadataFetcher = WallpaperMetadataFetcher(client) + val migrationHelper = LegacyWallpaperMigration( + storageRootDirectory = storageRootDirectory, + settings = context.settings(), + selectWallpaper::invoke, + ) + DefaultInitializeWallpaperUseCase( + appStore = appStore, + downloader = downloader, + fileManager = fileManager, + metadataFetcher = metadataFetcher, + migrationHelper = migrationHelper, + settings = context.settings(), + currentLocale = currentLocale, + ) } val loadBitmap: LoadBitmapUseCase by lazy { - if (FeatureFlags.wallpaperV2Enabled) { - DefaultLoadBitmapUseCase( - filesDir = context.filesDir, - getOrientation = { context.resources.configuration.orientation }, - ) - } else { - LegacyLoadBitmapUseCase(context) - } + DefaultLoadBitmapUseCase( + filesDir = context.filesDir, + getOrientation = { context.resources.configuration.orientation }, + ) } val loadThumbnail: LoadThumbnailUseCase by lazy { - if (FeatureFlags.wallpaperV2Enabled) { - DefaultLoadThumbnailUseCase(storageRootDirectory) - } else { - LegacyLoadThumbnailUseCase(context) - } + DefaultLoadThumbnailUseCase(storageRootDirectory) } val selectWallpaper: SelectWallpaperUseCase by lazy { - if (FeatureFlags.wallpaperV2Enabled) { - DefaultSelectWallpaperUseCase(context.settings(), appStore, fileManager, downloader) - } else { - LegacySelectWallpaperUseCase(context.settings(), appStore) - } + DefaultSelectWallpaperUseCase(context.settings(), appStore, fileManager, downloader) } /** @@ -110,133 +83,6 @@ class WallpapersUseCases( suspend operator fun invoke() } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal class LegacyInitializeWallpaperUseCase( - private val appStore: AppStore, - private val downloader: LegacyWallpaperDownloader, - private val fileManager: LegacyWallpaperFileManager, - private val settings: Settings, - private val currentLocale: String, - private val possibleWallpapers: List = allWallpapers, - ) : InitializeWallpapersUseCase { - - /** - * Downloads the currently available wallpaper metadata from a remote source. - * Updates the [appStore] with that metadata and with the selected wallpaper found in storage. - * Removes any unused promotional or time-limited assets from local storage. - * Should usually be called early the app's lifetime to ensure that metadata and thumbnails - * are available as soon as they are needed. - */ - override suspend operator fun invoke() { - // Quite a bit of code needs to be executed off the main thread in some of this setup. - // This should be cleaned up as improvements are made to the storage, file management, - // and download utilities. - withContext(Dispatchers.IO) { - val dispatchedCurrent = Wallpaper.getCurrentWallpaperFromSettings(settings)?.let { - // Dispatch this ASAP so the home screen can render. - appStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(it)) - true - } ?: false - val availableWallpapers = possibleWallpapers.getAvailableWallpapers() - val currentWallpaperName = settings.currentWallpaperName - val currentWallpaper = possibleWallpapers.find { it.name == currentWallpaperName } - ?: fileManager.lookupExpiredWallpaper(currentWallpaperName) - ?: Wallpaper.Default - - fileManager.clean( - currentWallpaper, - possibleWallpapers, - ) - downloadAllRemoteWallpapers(availableWallpapers) - appStore.dispatch(AppAction.WallpaperAction.UpdateAvailableWallpapers(availableWallpapers)) - if (!dispatchedCurrent) { - appStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(currentWallpaper)) - } - } - } - - private fun List.getAvailableWallpapers() = - this.filter { !it.isExpired() && it.isAvailableInLocale() } - - private suspend fun downloadAllRemoteWallpapers(availableWallpapers: List) { - for (wallpaper in availableWallpapers) { - if (wallpaper != Wallpaper.Default) { - downloader.downloadWallpaper(wallpaper) - } - } - } - - private fun Wallpaper.isExpired(): Boolean { - val expired = this.collection.endDate?.let { Date().after(it) } ?: false - return expired && this.name != settings.currentWallpaperName - } - - private fun Wallpaper.isAvailableInLocale(): Boolean = - this.collection.availableLocales?.contains(currentLocale) ?: true - - companion object { - private val firefoxClassicCollection = Wallpaper.Collection( - name = Wallpaper.firefoxCollectionName, - heading = null, - description = null, - availableLocales = null, - startDate = null, - endDate = null, - learnMoreUrl = null, - ) - private val localWallpapers: List = listOf( - Wallpaper( - name = Wallpaper.amethystName, - collection = firefoxClassicCollection, - textColor = null, - cardColorLight = null, - cardColorDark = null, - thumbnailFileState = Wallpaper.ImageFileState.Unavailable, - assetsFileState = Wallpaper.ImageFileState.Downloaded, - ), - Wallpaper( - name = Wallpaper.ceruleanName, - collection = firefoxClassicCollection, - textColor = null, - cardColorLight = null, - cardColorDark = null, - thumbnailFileState = Wallpaper.ImageFileState.Unavailable, - assetsFileState = Wallpaper.ImageFileState.Downloaded, - ), - Wallpaper( - name = Wallpaper.sunriseName, - collection = firefoxClassicCollection, - textColor = null, - cardColorLight = null, - cardColorDark = null, - thumbnailFileState = Wallpaper.ImageFileState.Unavailable, - assetsFileState = Wallpaper.ImageFileState.Downloaded, - ), - ) - private val remoteWallpapers: List = listOf( - Wallpaper( - name = Wallpaper.twilightHillsName, - collection = firefoxClassicCollection, - textColor = null, - cardColorLight = null, - cardColorDark = null, - thumbnailFileState = Wallpaper.ImageFileState.Unavailable, - assetsFileState = Wallpaper.ImageFileState.Downloaded, - ), - Wallpaper( - name = Wallpaper.beachVibeName, - collection = firefoxClassicCollection, - textColor = null, - cardColorLight = null, - cardColorDark = null, - thumbnailFileState = Wallpaper.ImageFileState.Unavailable, - assetsFileState = Wallpaper.ImageFileState.Downloaded, - ), - ) - val allWallpapers = listOf(Wallpaper.Default) + localWallpapers + remoteWallpapers - } - } - @Suppress("LongParameterList") @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal class DefaultInitializeWallpaperUseCase( @@ -316,64 +162,6 @@ class WallpapersUseCases( suspend operator fun invoke(wallpaper: Wallpaper): Bitmap? } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal class LegacyLoadBitmapUseCase(private val context: Context) : LoadBitmapUseCase { - /** - * Load the bitmap for a [wallpaper], if available. - * - * @param wallpaper The wallpaper to load a bitmap for. - */ - override suspend operator fun invoke(wallpaper: Wallpaper): Bitmap? = when (wallpaper.name) { - Wallpaper.amethystName, Wallpaper.ceruleanName, Wallpaper.sunriseName -> { - loadWallpaperFromDrawable(context, wallpaper) - } - Wallpaper.twilightHillsName, Wallpaper.beachVibeName -> { - loadWallpaperFromDisk(context, wallpaper) - } - else -> null - } - - private suspend fun loadWallpaperFromDrawable( - context: Context, - wallpaper: Wallpaper, - ): Bitmap? = Result.runCatching { - val drawableRes = when (wallpaper.name) { - Wallpaper.amethystName -> R.drawable.amethyst - Wallpaper.ceruleanName -> R.drawable.cerulean - Wallpaper.sunriseName -> R.drawable.sunrise - else -> return@runCatching null - } - withContext(Dispatchers.IO) { - BitmapFactory.decodeResource(context.resources, drawableRes) - } - }.getOrNull() - - private suspend fun loadWallpaperFromDisk( - context: Context, - wallpaper: Wallpaper, - ): Bitmap? = Result.runCatching { - val path = wallpaper.getLocalPathFromContext(context) - withContext(Dispatchers.IO) { - val file = File(context.filesDir, path) - BitmapFactory.decodeStream(file.inputStream()) - } - }.getOrNull() - - /** - * Get the expected local path on disk for a wallpaper. This will differ depending - * on orientation and app theme. - */ - private fun Wallpaper.getLocalPathFromContext(context: Context): String { - val orientation = if (context.isLandscape()) "landscape" else "portrait" - val theme = if (context.isSystemInDarkTheme()) "dark" else "light" - return Wallpaper.legacyGetLocalPath(orientation, theme, name) - } - - private fun Context.isLandscape(): Boolean { - return resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - } - } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal class DefaultLoadBitmapUseCase( private val filesDir: File, @@ -422,12 +210,6 @@ class WallpapersUseCases( suspend operator fun invoke(wallpaper: Wallpaper): Bitmap? } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal class LegacyLoadThumbnailUseCase(private val context: Context) : LoadThumbnailUseCase { - override suspend fun invoke(wallpaper: Wallpaper): Bitmap? = - LegacyLoadBitmapUseCase(context).invoke(wallpaper) - } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal class DefaultLoadThumbnailUseCase(private val filesDir: File) : LoadThumbnailUseCase { override suspend fun invoke(wallpaper: Wallpaper): Bitmap? = withContext(Dispatchers.IO) { @@ -453,26 +235,6 @@ class WallpapersUseCases( suspend operator fun invoke(wallpaper: Wallpaper): Wallpaper.ImageFileState } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal class LegacySelectWallpaperUseCase( - private val settings: Settings, - private val appStore: AppStore, - ) : SelectWallpaperUseCase { - /** - * Select a new wallpaper. Storage and the app store will be updated appropriately. - * - * @param wallpaper The selected wallpaper. - */ - override suspend fun invoke(wallpaper: Wallpaper): Wallpaper.ImageFileState { - settings.currentWallpaperName = wallpaper.name - settings.currentWallpaperTextColor = wallpaper.textColor ?: 0 - settings.currentWallpaperCardColorLight = wallpaper.cardColorLight ?: 0 - settings.currentWallpaperCardColorDark = wallpaper.cardColorDark ?: 0 - appStore.dispatch(AppAction.WallpaperAction.UpdateCurrentWallpaper(wallpaper)) - return Wallpaper.ImageFileState.Downloaded - } - } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal class DefaultSelectWallpaperUseCase( private val settings: Settings, diff --git a/app/src/main/res/drawable-hdpi/amethyst.png b/app/src/main/res/drawable-hdpi/amethyst.png deleted file mode 100644 index 623583084a..0000000000 Binary files a/app/src/main/res/drawable-hdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/cerulean.png b/app/src/main/res/drawable-hdpi/cerulean.png deleted file mode 100644 index d664f76d05..0000000000 Binary files a/app/src/main/res/drawable-hdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/sunrise.jpg b/app/src/main/res/drawable-hdpi/sunrise.jpg deleted file mode 100644 index d949505278..0000000000 Binary files a/app/src/main/res/drawable-hdpi/sunrise.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-land-hdpi/amethyst.png b/app/src/main/res/drawable-land-hdpi/amethyst.png deleted file mode 100644 index 5f312131bd..0000000000 Binary files a/app/src/main/res/drawable-land-hdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-hdpi/cerulean.png b/app/src/main/res/drawable-land-hdpi/cerulean.png deleted file mode 100644 index 6b06934bc5..0000000000 Binary files a/app/src/main/res/drawable-land-hdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-hdpi/sunrise.jpg b/app/src/main/res/drawable-land-hdpi/sunrise.jpg deleted file mode 100644 index cd45b34bba..0000000000 Binary files a/app/src/main/res/drawable-land-hdpi/sunrise.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-land-night-hdpi/amethyst.png b/app/src/main/res/drawable-land-night-hdpi/amethyst.png deleted file mode 100644 index 994271806d..0000000000 Binary files a/app/src/main/res/drawable-land-night-hdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-night-hdpi/cerulean.png b/app/src/main/res/drawable-land-night-hdpi/cerulean.png deleted file mode 100644 index 27138a6e7a..0000000000 Binary files a/app/src/main/res/drawable-land-night-hdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-night-xhdpi/amethyst.png b/app/src/main/res/drawable-land-night-xhdpi/amethyst.png deleted file mode 100644 index e1073ab588..0000000000 Binary files a/app/src/main/res/drawable-land-night-xhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-night-xhdpi/cerulean.png b/app/src/main/res/drawable-land-night-xhdpi/cerulean.png deleted file mode 100644 index 116fea523d..0000000000 Binary files a/app/src/main/res/drawable-land-night-xhdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-night-xxhdpi/amethyst.png b/app/src/main/res/drawable-land-night-xxhdpi/amethyst.png deleted file mode 100644 index a2d72e6cd5..0000000000 Binary files a/app/src/main/res/drawable-land-night-xxhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-night-xxhdpi/cerulean.png b/app/src/main/res/drawable-land-night-xxhdpi/cerulean.png deleted file mode 100644 index e63fab3cfd..0000000000 Binary files a/app/src/main/res/drawable-land-night-xxhdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-xhdpi/amethyst.png b/app/src/main/res/drawable-land-xhdpi/amethyst.png deleted file mode 100644 index 68aaa2c70e..0000000000 Binary files a/app/src/main/res/drawable-land-xhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-xhdpi/cerulean.png b/app/src/main/res/drawable-land-xhdpi/cerulean.png deleted file mode 100644 index 61c5410663..0000000000 Binary files a/app/src/main/res/drawable-land-xhdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-xhdpi/sunrise.jpg b/app/src/main/res/drawable-land-xhdpi/sunrise.jpg deleted file mode 100644 index bf857df901..0000000000 Binary files a/app/src/main/res/drawable-land-xhdpi/sunrise.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-land-xxhdpi/amethyst.png b/app/src/main/res/drawable-land-xxhdpi/amethyst.png deleted file mode 100644 index 360a1d5317..0000000000 Binary files a/app/src/main/res/drawable-land-xxhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-xxhdpi/cerulean.png b/app/src/main/res/drawable-land-xxhdpi/cerulean.png deleted file mode 100644 index 6db8e3d4af..0000000000 Binary files a/app/src/main/res/drawable-land-xxhdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-land-xxhdpi/sunrise.jpg b/app/src/main/res/drawable-land-xxhdpi/sunrise.jpg deleted file mode 100644 index 9c5197b97a..0000000000 Binary files a/app/src/main/res/drawable-land-xxhdpi/sunrise.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-night-hdpi/amethyst.png b/app/src/main/res/drawable-night-hdpi/amethyst.png deleted file mode 100644 index 4d9e5b24bd..0000000000 Binary files a/app/src/main/res/drawable-night-hdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-night-hdpi/cerulean.png b/app/src/main/res/drawable-night-hdpi/cerulean.png deleted file mode 100644 index c74eba843e..0000000000 Binary files a/app/src/main/res/drawable-night-hdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-night-xhdpi/amethyst.png b/app/src/main/res/drawable-port-night-xhdpi/amethyst.png deleted file mode 100644 index 385606a02c..0000000000 Binary files a/app/src/main/res/drawable-port-night-xhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-night-xhdpi/cerulean.png b/app/src/main/res/drawable-port-night-xhdpi/cerulean.png deleted file mode 100644 index 55fd436993..0000000000 Binary files a/app/src/main/res/drawable-port-night-xhdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-night-xxhdpi/amethyst.png b/app/src/main/res/drawable-port-night-xxhdpi/amethyst.png deleted file mode 100644 index 5c0087ef5c..0000000000 Binary files a/app/src/main/res/drawable-port-night-xxhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-night-xxhdpi/cerulean.png b/app/src/main/res/drawable-port-night-xxhdpi/cerulean.png deleted file mode 100644 index cff49206a9..0000000000 Binary files a/app/src/main/res/drawable-port-night-xxhdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-xhdpi/amethyst.png b/app/src/main/res/drawable-port-xhdpi/amethyst.png deleted file mode 100644 index 0965e2b233..0000000000 Binary files a/app/src/main/res/drawable-port-xhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-xhdpi/cerulean.png b/app/src/main/res/drawable-port-xhdpi/cerulean.png deleted file mode 100644 index 5f00f9ec3c..0000000000 Binary files a/app/src/main/res/drawable-port-xhdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-xhdpi/sunrise.jpg b/app/src/main/res/drawable-port-xhdpi/sunrise.jpg deleted file mode 100644 index 62067bc240..0000000000 Binary files a/app/src/main/res/drawable-port-xhdpi/sunrise.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-port-xxhdpi/amethyst.png b/app/src/main/res/drawable-port-xxhdpi/amethyst.png deleted file mode 100644 index a3266444ac..0000000000 Binary files a/app/src/main/res/drawable-port-xxhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-xxhdpi/cerulean.png b/app/src/main/res/drawable-port-xxhdpi/cerulean.png deleted file mode 100644 index a421261c35..0000000000 Binary files a/app/src/main/res/drawable-port-xxhdpi/cerulean.png and /dev/null differ diff --git a/app/src/main/res/drawable-port-xxhdpi/sunrise.jpg b/app/src/main/res/drawable-port-xxhdpi/sunrise.jpg deleted file mode 100644 index 9e88027bdd..0000000000 Binary files a/app/src/main/res/drawable-port-xxhdpi/sunrise.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/amethyst.png b/app/src/main/res/drawable-xhdpi/amethyst.png deleted file mode 100644 index 0965e2b233..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/amethyst.png b/app/src/main/res/drawable-xxhdpi/amethyst.png deleted file mode 100644 index a3266444ac..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/amethyst.png and /dev/null differ diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 1e53e25767..6d7c8bbf9c 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -125,6 +125,9 @@ Nyt privat faneblad + + Genvej til adgangskoder + Hop tilbage til @@ -406,15 +409,23 @@ Annuller - Anmod om understøttelse + Anmod om understøttelse + + Send anmodning + + Reduktion af cookie-bannere - Reduktion af cookie-bannere + Anmod om understøttelse af dette websted? - Anmodning om understøttelse af websted indsendt. + Anmodning om understøttelse af websted indsendt. + + Anmodning sendt Slået til for dette websted - Anmodning om understøttelse af websted indsendt + Anmodning om understøttelse af websted indsendt + + Anmodning om understøttelse sendt Webstedet understøttes ikke i øjeblikket @@ -423,38 +434,22 @@ Slå reduktion af cookie-bannere fra for %1$s? - Dette websted understøttes i øjeblikket ikke af reduktion af cookie-bannere. Vil du have vores team til at gennemgå dette websted for at det kan understøttes i fremtiden? + Dette websted understøttes i øjeblikket ikke af reduktion af cookie-bannere. Vil du have vores team til at gennemgå dette websted for at det kan understøttes i fremtiden? + + %1$s kan ikke afvise cookie-anmodninger automatisk på dette websted. Du kan sende en anmodning om understøttelse af dette websted i fremtiden. %1$s vil rydde dette websteds cookies og genindlæse siden. Rydning af alle cookies kan logge dig ud eller tømme indkøbskurve. %1$s forsøger automatisk at afvise alle cookie-anmodninger på understøttede websteder. - - Slut med cookie-bannere! Tillad %1$s at afvise cookie-bannere? - - Tillad %1$s at automatisk afvise cookie-anmodninger, når det er muligt? %1$s kan automatisk afvise mange cookie-banner-anmodninger. Ikke nu - - Avis bannere Du vil få vist færre cookie-anmodninger - - Se færre pop op-beskeder om cookies - - Besvar automatisk cookie pop op-beskeder, så du kan bruge nettet uden forstyrrelser. %1$s vil afvise alle forespørgsler, når det er muligt. - - Afvis pop op-beskeder om cookies - - Reduktion af cookie-bannere - - Tillad %1$s at afvise et websteds anmodning om samtykke til anvendelse af cookies, når det er muligt? - - Tillad Tillad @@ -1358,7 +1353,9 @@ Tidsinterval der skal slettes - Fjerner historik (herunder historik synkroniseret fra andre enheder), cookies og andre browserdata. + Fjerner historik (herunder historik synkroniseret fra andre enheder), cookies og andre browserdata. + + Fjerner historik (herunder historik synkroniseret fra andre enheder) Den sidste time diff --git a/app/src/main/res/values-dsb/strings.xml b/app/src/main/res/values-dsb/strings.xml index 1e0d9e0d02..0b934f9711 100644 --- a/app/src/main/res/values-dsb/strings.xml +++ b/app/src/main/res/values-dsb/strings.xml @@ -125,6 +125,9 @@ Nowy priwatny rejtarik + + Skrotconka gronidłow + Slědk skócyś @@ -406,15 +409,23 @@ Pśetergnuś - Pomoc póžedaś + Pomoc póžedaś + + Napšašowanje pósłaś + + Reducěrowanje cookiejowych chórgojow - Reducěrowanje cookiejowych chórgojow + Wó pódpěru za toś to sedło pšosyś? - Napšašowanje na sedło pomocy jo se wótpósłało. + Napšašowanje na sedło pomocy jo se wótpósłało. + + Napšašowanje pósłane Za toś to sedło zmóžnjony - Napšašowanje na sedło pomocy jo se wótpósłało + Napšašowanje na sedło pomocy jo se wótpósłało + + Pšosba wó pódpěru pósłana Sedło se tuchylu njepódpěra @@ -422,37 +433,21 @@ Reducěrowanje cookiejowych chórgojow za %1$s znjemóžniś? - Toś to sedło se tuchylu pśez redukciju cookiejowych chórgojow. Cośo, až naš team toś to websedło pśeglědujo a pomoc w pśichoźe pśidawa? + Toś to sedło se tuchylu pśez redukciju cookiejowych chórgojow. Cośo, až naš team toś to websedło pśeglědujo a pomoc w pśichoźe pśidawa? + + %1$s njamóžo cookiejowe napšašowanja na toś tom sedle awtomatiski wótpokazaś. Móžośo pšosbu wó pódpěru toś togo sedła w pśichoźe pósłaś. %1$s cookieje sedła lašujo a buźo bok aktualizěrowaś. Lašowanje wšych cookiejow móžo was pśizjawiś abo nakupowańske wózyki wuprozniś. %1$s wopytujo wšykne cookiejowe napšašowanja na pódprětych sedłach awtomatiski wótpokazaś. - - Firefox dowóliś, cookiejowe chórgoji wótpokazaś? %1$s dowóliś, cookiejowe chórgoji wótpokazaś? - - %1$s dowóliś, cookiejowe napšašowanja awtomatiski wótpokazaś, jolic móžno? %1$s móžo wjele napšašowanjow wó cookiejowych chórgojach awtomatiski wótpokazaś. Nic něnto - - Chórgoje zachyśiś Buźośo mjenjej cookiejowych napšašowanjow wiźeś - - Mjenjej cookiejowych wuskokujucych woknow wiźeś - - Wótegrońśo awtomatiski na cookiejowe wuskokujuce wokna za pśeglědowanje bźez wótchylenja. %1$s wšykne napšašowanja wótpokažo, jolic móžno. - - Wuskokujuce wokna zachyśiś - - Reducěrowanje cookiejowych chórgojow - - %1$s dowóliś, napšašowanje sedła za cookiejowym pśizwólenim wótpokazaś, jolic trjeba? - - Dowóliś Dowóliś @@ -1365,7 +1360,9 @@ Casowy wótrězk za lašowanje - Wótwónoźijo historiju (mjazy njeju historiju, kótaraž jo se synchronizěrowała z drugich rědow), cookieje a druge pśeglědowańske daty. + Wótwónoźijo historiju (mjazy njeju historiju, kótaraž jo se synchronizěrowała z drugich rědow), cookieje a druge pśeglědowańske daty. + + Wótwónoźijo historiju (mjazy njeju historiju, kótaraž jo se synchronizěrowała z drugimi rědami) Zachadna góźina diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index c754ec6e25..c0be74c490 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -408,7 +408,9 @@ Cancel - Request support + Request support + + Send request Cookie Banner Reduction diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml index 7c02636777..9f14138eb9 100644 --- a/app/src/main/res/values-es-rCL/strings.xml +++ b/app/src/main/res/values-es-rCL/strings.xml @@ -409,7 +409,9 @@ Cancelar - Pedir que funcione + Pedir que funcione + + Enviar solicitud Reducción de anuncios de cookies diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 077952eab4..b921fc0b7e 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -1636,7 +1636,7 @@ نام میانبر - شما به راحتی می‌توانید این وبگاه را به صفحهٔ خانگی تلفن خود اضافه کنید تا دسترسی مستقیم به آن داشته باشید و مرور سریع‌تری را مانند تجربه کردن یک برنامه داشته باشید. + به‌راحتی می‌توانید این وبگاه را به صفحهٔ خانگی تلفن خود اضافه کنید تا بی‌درنگ به آن دسترسی داشته باشید و همچون کار با یک برنامه مرور سریع‌تری را تجربه کنید. ورودها و گذرواژه‌ها diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 90874f2103..6f11a4fecd 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -406,7 +406,9 @@ Cancelar - Pedir soporte + Pedir soporte + + Enviar solicitude Redución do aviso de cookies diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 96c7c951c0..f5f84f1a5b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -416,16 +416,20 @@ キャンセル - 対処を依頼 + 対処を依頼 - Cookie バナーの削減 + Cookie バナーの削減 + + このサイトのサポートをリクエストしますか? + + サイトへの対処依頼が送信されました。 - サイトへの対処依頼が送信されました。 + リクエストを送信しました このサイトでオン - 対処を依頼済み + 対処を依頼済み 現在サポートされていないサイトです @@ -433,40 +437,21 @@ %1$s で Cookie バナー削減を無効にしますか? - 現在、Cookie バナー削減機能はこのサイトに対応していません。このウェブサイトに対処するためのレビューを開発チームに依頼しますか? + 現在、Cookie バナー削減機能はこのサイトに対応していません。このウェブサイトに対処するためのレビューを開発チームに依頼しますか? %1$s はこのサイトの Cookie を消去してページを読み込み直します。すべての Cookie を消去すると、ログアウトしたり、ショッピングカートが空になったりする場合があります。 %1$s はサポートされたサイト上の Cookie 要求を自動的に拒否しようとします。 - - Cookie バナーよ、去れ! %1$s に Cookie バナーを拒否させますか? - - 可能な場合、%1$s に自動的に Cookie 要求を拒否させますか? %1$s は多くの Cookie バナーの同意確認を自動的に拒否できます。 後で - - バナーを閉じる Cookie 要求が少なくなります - - Cookie ポップアップを減らす - - - 目障りな Cookie ポップアップに自動的に応答します。%1$s が可能な限りすべての要求を拒否します。 - - ポップアップを閉じる - - Cookie バナーの削減 - - 可能な場合、サイトの Cookie 同意要求を %1$s に拒否させますか? - - 許可 許可 @@ -1391,7 +1376,7 @@ 削除する期間 - 履歴 (他の端末から同期されたものを含む)、Cookie および他のブラウジングデータを削除します。 + 履歴 (他の端末から同期されたものを含む)、Cookie および他のブラウジングデータを削除します。 1 時間以内 diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 2508e56228..9d4e6863e2 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -406,7 +406,9 @@ გაუქმება - მხარდაჭერის მოთხოვნა + მხარდაჭერის მოთხოვნა + + მოთხოვნის გაგზავნა ფუნთუშის მოთხოვნების შემცირება diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 4256e2779e..face9cfba3 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -407,7 +407,9 @@ Бас тарту - Қолдау сұрау + Қолдау сұрау + + Сұранымды жіберу Cookie баннерлерін азайту diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 7c9bebf0f1..baccb72bba 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -36,4 +36,162 @@ ପୃଷ୍ଠାରେ ଖୋଜି ପାଆନ୍ତୁ + + %1$sରେ ଖୋଲନ୍ତୁ + + ଯୋଡ଼ନ୍ତୁ + + ସମ୍ପାଦନା + + + + ମନୋନୀତ ଭାଷା + + ଡିଭାଇସ୍ ର ଭାଷାକୁ ଅନୁସରଣ କରନ୍ତୁ + + + ସନ୍ଧାନ ଇଞ୍ଜିନ୍ ସେଟିଂସ୍ + + + + ଗୋଟିଏ ନୂଆ %1$s ଟ୍ୟାବ୍ ଖୋଲନ୍ତୁ + + + + ସେଟିଂସମୂହ + + ସାଧାରଣ + + ଡିଫଲ୍ଟ ସନ୍ଧାନ ଇଞ୍ଜିନ୍ + + ସନ୍ଧାନ + + ଠିକଣା ଦଣ୍ଡିକା + + ବିକଶିତ + + ଗୋପନୀୟତା ଏବଂ ସୁରକ୍ଷା + + + ବନ୍ଦ + + ଅଧିକ ଜାଣନ୍ତୁ + + ଅଭିଗମ୍ୟତା + + ଥିମ୍ + + ଭାଷା + + + ଇତିହାସ + + + + ହାଲୁକା + + ଗାଢ଼ + + ଡିଭାଇସ୍ ର ଥିମକୁ ଅନୁସରଣ କରନ୍ତୁ + + + ଇତିହାସ + + ନୂଆ ଟ୍ୟାବ୍ + + ସେଟିଂସମୂହ + + + ଗୋଟେ ମାସ ପରେ + + + ସକ୍ରିୟ + + + ଟ୍ୟାବ ଯୋଡ଼ନ୍ତୁ + + ବ୍ୟକ୍ତିଗତ ଟ୍ୟାବ୍ ଯୋଡ଼ନ୍ତୁ + + ବ୍ୟକ୍ତିଗତ + + + ଆଜି + + ଗତକାଲି + + ଗତ ୭ ଦିନ + + ଗତ ୩୦ ଦିନ + + + + ନୂଆ ଟ୍ୟାବରେ ଖୋଲନ୍ତୁ + + + ନୂଆ ସଂଗ୍ରହ ଯୋଡ଼ନ୍ତୁ + + + କ୍ଲିପବୋର୍ଡରେ କପି କରିନିଅନ୍ତୁ + + କ୍ଲିପବୋର୍ଡରେ କପି କରିନିଆଗଲା + + ଡିଭାଇସକୁ ପଠାନ୍ତୁ + + + + ଆପଣଙ୍କ ଥିମ୍ ବାଛନ୍ତୁ + + ସ୍ୱୟଂଚାଳିତ + + ଆପଣଙ୍କ ଡିଭାଇସ୍ ସେଟିଂସ୍ ସହ ଖାପ ଖାଇଥାଏ + + ଗାଢ଼ ଥିମ୍ + + ହାଲୁକା ଥିମ୍ + + + ଏହା ପରିବର୍ତ୍ତେ ଇମେଲ୍ ବ୍ୟବହାର କରନ୍ତୁ + + + + ସୁରକ୍ଷା ସେଟିଂସ୍ + + ମାନକ (ଡିଫଲ୍ଟ) + + ଗୋପନୀୟତା ଏବଂ କାର୍ଯ୍ୟଦକ୍ଷତା ପାଇଁ ସନ୍ତୁଳିତ। ପୃଷ୍ଠାଗୁଡିକ ସାଧାରଣ ଭାବରେ ଲୋଡ୍ ହୁଏ। + + କଡା + + + ବାତିଲ୍ କରନ୍ତୁ + + ଯୋଡ଼ନ୍ତୁ + + + ବ୍ୟତିକ୍ରମଗୁଡ଼ିକ + + ସବୁ ବ୍ୟତିକ୍ରମ ବିଲୋପ କରନ୍ତୁ + + ପାସ୍‍ୱାର୍ଡ଼ + + ପାସ୍‍ୱାର୍ଡ଼ କ୍ଲିପବୋର୍ଡରେ କପି କରିନିଆଗଲା + + ପାସ୍‍ୱାର୍ଡ଼ କପିକରନ୍ତୁ + + ପାସ୍‍ୱାର୍ଡ଼ ଦେଖାନ୍ତୁ + + ପାସ୍‍ୱାର୍ଡ଼ ଲୁଚାନ୍ତୁ + + + କୌଣସି ସାଇଟ୍ ବ୍ୟତିକ୍ରମ ହୋଇନାହିଁ + + ପାସ୍‍ୱାର୍ଡ଼ ଆବଶ୍ୟକ + + + ଠିକ୍ ଅଛି + + + Firefox ପରିବାରର ଏକ ଅଂଶ। %s + diff --git a/app/src/main/res/values-pa-rIN/strings.xml b/app/src/main/res/values-pa-rIN/strings.xml index 2c7cea952a..a35af30303 100644 --- a/app/src/main/res/values-pa-rIN/strings.xml +++ b/app/src/main/res/values-pa-rIN/strings.xml @@ -419,7 +419,9 @@ ਰੱਦ ਕਰੋ - ਸਹਿਯੋਗ ਲਈ ਬੇਨਤੀ + ਸਹਿਯੋਗ ਲਈ ਬੇਨਤੀ + + ਬੇਨਤੀ ਭੇਜੀ ਕੂਕੀ ਬੈਨਰ ਘਟਾਉਣਾ diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 654cdacb76..8a0dd55053 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -400,6 +400,10 @@ ඉල්ලීම යැවිණි මෙම අඩවියට සක්‍රියයි + + අඩවිය සඳහා සහාය ඉල්ලීම යොමු කෙරිණි + + සහාය ඉල්ලීම යැවිණි අඩවියට සහාය නොදක්වයි @@ -497,6 +501,8 @@ යෙදුම්වල සබැඳි අරින්න සැමවිටම + + ඇරීමට පෙර අසන්න කවදාවත් @@ -1193,6 +1199,9 @@ අළෙවිකරණය + + ෆයර්ෆොක්ස් වේගවත් හා පෞද්ගලිකයි ෆයර්ෆොක්ස් පෙරනිමි අතිරික්සුව කරන්න @@ -1201,8 +1210,15 @@ - %1$s හි සුරැකින ලද දත්තකඩ හෝ ඉතිහාසයක් නැතිව පිරික්සන්න + %1$s හි සුරැකෙන දත්තකඩ හෝ ඉතිහාසයකින් තොරව පිරික්සන්න + + + හෝඩුවාවකින් තොරව පිරික්සන්න + + ප්‍රථම සෙවීම අරඹන්න + + සමීක්‍ෂණය ගන්න එපා, ස්තුතියි @@ -1433,6 +1449,8 @@ සම්මත (පෙරනිමි) ආරක්‍ෂාව හා කාර්ය සාධනය අතර සංතුලිතයි. පිටු සාමාන්‍ය ලෙස පූරණය වේ. + + පිටු සුපුරුදු පරිදි පූරණය වුවත් ලුහුබැඳීම් අඩුවෙන් අවහිර කරයි. සම්මත ලුහුබැඳීමේ රැකවරණය මගින් අවහිර කළ දෑ diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 9d5f813133..681708b73d 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -122,6 +122,9 @@ แท็บส่วนตัวใหม่ + + ทางลัดรหัสผ่าน + กลับเข้าไป @@ -407,15 +410,23 @@ ยกเลิก ขอการรองรับ + + ส่งคำขอ การลดแบนเนอร์คุกกี้ + + ขอการรองรับไซต์นี้ไหม? ส่งคำขอรองรับไซต์แล้ว + + ส่งคำขอแล้ว เปิดสำหรับไซต์นี้ ส่งคำขอรองรับไซต์แล้ว + + ส่งคำขอรองรับแล้ว ไม่รองรับไซต์ในขณะนี้ @@ -424,6 +435,8 @@ ต้องการปิดการลดแบนเนอร์คุกกี้สำหรับ %1$s หรือไม่? ขณะนี้ไซต์นี้ไม่รองรับการลดแบนเนอร์คุกกี้ คุณต้องการให้ทีมงานของเราตรวจสอบเว็บไซต์นี้และเพิ่มการรองรับในอนาคตหรือไม่? + + %1$s ไม่สามารถปฏิเสธคำขอคุกกี้โดยอัตโนมัติบนไซต์นี้ คุณสามารถส่งคำขอให้รองรับไซต์นี้ในอนาคตได้ %1$s จะล้างคุกกี้ของไซต์นี้และรีเฟรชหน้า การล้างคุกกี้ทั้งหมดอาจนำคุณออกจากระบบหรือล้างรถเข็นช็อปปิ้ง @@ -1349,6 +1362,8 @@ ช่วงเวลาที่จะลบ ลบประวัติ (รวมถึงประวัติที่ซิงค์จากอุปกรณ์อื่น) คุกกี้ และข้อมูลการเรียกดูอื่น ๆ + + ลบประวัติ (รวมถึงประวัติที่ซิงค์จากอุปกรณ์อื่น) ชั่วโมงที่แล้ว diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 921bddf90a..d52d133adb 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -290,6 +290,12 @@ pref_key_show_collections_home + + pref_key_growth_user_activated_sent + pref_key_growth_early_browse_count + pref_key_growth_early_browse_count_last_increment + pref_key_growth_early_search + pref_key_tab_view_list diff --git a/app/src/test/java/org/mozilla/fenix/components/metrics/DefaultMetricsStorageTest.kt b/app/src/test/java/org/mozilla/fenix/components/metrics/DefaultMetricsStorageTest.kt index 018e680627..d07620b85b 100644 --- a/app/src/test/java/org/mozilla/fenix/components/metrics/DefaultMetricsStorageTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/metrics/DefaultMetricsStorageTest.kt @@ -6,7 +6,9 @@ package org.mozilla.fenix.components.metrics import android.app.Activity import android.app.Application +import io.mockk.Runs import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.slot import io.mockk.verify @@ -382,6 +384,114 @@ class DefaultMetricsStorageTest { assertTrue(updateSlot.captured > 0) } + @Test + fun `GIVEN first week activated days of use and search use thresholds reached THEN will be sent`() = runTest(dispatcher) { + val currentTime = System.currentTimeMillis() + installTime = currentTime - (dayMillis * 5) + every { settings.growthEarlyUseCount.value } returns 3 + every { settings.growthEarlySearchUsed } returns true + every { settings.growthUserActivatedSent } returns false + + val result = storage.shouldTrack(Event.GrowthData.UserActivated(fromSearch = false)) + + assertTrue(result) + } + + @Test + fun `GIVEN first week activated days of use threshold not reached THEN will not be sent`() = runTest(dispatcher) { + val currentTime = System.currentTimeMillis() + installTime = currentTime - (dayMillis * 5) + every { settings.growthEarlyUseCount.value } returns 1 + every { settings.growthEarlySearchUsed } returns true + every { settings.growthUserActivatedSent } returns false + + val result = storage.shouldTrack(Event.GrowthData.UserActivated(fromSearch = false)) + + assertFalse(result) + } + + @Test + fun `GIVEN first week activated search use threshold not reached THEN will not be sent`() = runTest(dispatcher) { + val currentTime = System.currentTimeMillis() + installTime = currentTime - (dayMillis * 5) + every { settings.growthEarlyUseCount.value } returns 3 + every { settings.growthEarlySearchUsed } returns false + every { settings.growthUserActivatedSent } returns false + + val result = storage.shouldTrack(Event.GrowthData.UserActivated(fromSearch = false)) + + assertFalse(result) + } + + @Test + fun `GIVEN first week activated already sent WHEN first week activated signal sent THEN userActivated will not be sent`() = runTest(dispatcher) { + val currentTime = System.currentTimeMillis() + installTime = currentTime - (dayMillis * 5) + every { settings.growthEarlyUseCount.value } returns 3 + every { settings.growthEarlySearchUsed } returns true + every { settings.growthUserActivatedSent } returns true + + val result = storage.shouldTrack(Event.GrowthData.UserActivated(fromSearch = false)) + + assertFalse(result) + } + + @Test + fun `WHEN first week usage signal is sent a full day after last sent THEN settings will be updated accordingly`() = runTest(dispatcher) { + val captureSent = slot() + val currentTime = System.currentTimeMillis() + installTime = currentTime - (dayMillis * 3) + every { settings.growthEarlyUseCount.value } returns 1 + every { settings.growthEarlyUseCount.increment() } just Runs + every { settings.growthEarlyUseCountLastIncrement } returns 0L + every { settings.growthEarlyUseCountLastIncrement = capture(captureSent) } returns Unit + + storage.updatePersistentState(Event.GrowthData.UserActivated(fromSearch = false)) + + assertTrue(captureSent.captured > 0L) + } + + @Test + fun `WHEN first week usage signal is sent less than a full day after last sent THEN settings will not be updated`() = runTest(dispatcher) { + val captureSent = slot() + val currentTime = System.currentTimeMillis() + installTime = currentTime - (dayMillis * 3) + val lastUsageIncrementTime = currentTime - (dayMillis / 2) + every { settings.growthEarlyUseCount.value } returns 1 + every { settings.growthEarlyUseCountLastIncrement } returns lastUsageIncrementTime + every { settings.growthEarlyUseCountLastIncrement = capture(captureSent) } returns Unit + + storage.updatePersistentState(Event.GrowthData.UserActivated(fromSearch = false)) + + assertFalse(captureSent.isCaptured) + } + + @Test + fun `WHEN first week search activity is sent in second half of first week THEN settings will be updated`() = runTest(dispatcher) { + val captureSent = slot() + val currentTime = System.currentTimeMillis() + installTime = currentTime - (dayMillis * 3) - 100 + every { settings.growthEarlySearchUsed } returns false + every { settings.growthEarlySearchUsed = capture(captureSent) } returns Unit + + storage.updatePersistentState(Event.GrowthData.UserActivated(fromSearch = true)) + + assertTrue(captureSent.captured) + } + + @Test + fun `WHEN first week search activity is sent in first half of first week THEN settings will not be updated`() = runTest(dispatcher) { + val captureSent = slot() + val currentTime = System.currentTimeMillis() + installTime = currentTime - (dayMillis * 3) + 100 + every { settings.growthEarlySearchUsed } returns false + every { settings.growthEarlySearchUsed = capture(captureSent) } returns Unit + + storage.updatePersistentState(Event.GrowthData.UserActivated(fromSearch = true)) + + assertFalse(captureSent.isCaptured) + } + private fun Calendar.copy() = clone() as Calendar private fun Calendar.createNextDay() = copy().apply { add(Calendar.DAY_OF_MONTH, 1) diff --git a/app/src/test/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt b/app/src/test/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt index 8b55b81f37..bf0cc11a03 100644 --- a/app/src/test/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt +++ b/app/src/test/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt @@ -15,7 +15,7 @@ class JunoOnboardingMapperTest { @Test fun `GIVEN a default browser page WHEN mapToOnboardingPageState is called THEN creates the expected OnboardingPageState`() { val expected = OnboardingPageState( - image = R.drawable.ic_onboarding_welcome, + imageRes = R.drawable.ic_onboarding_welcome, title = "default browser title", description = "default browser body with link text", linkTextState = LinkTextState( @@ -54,7 +54,7 @@ class JunoOnboardingMapperTest { @Test fun `GIVEN a sync page WHEN mapToOnboardingPageState is called THEN creates the expected OnboardingPageState`() { val expected = OnboardingPageState( - image = R.drawable.ic_onboarding_sync, + imageRes = R.drawable.ic_onboarding_sync, title = "sync title", description = "sync body", primaryButton = Action("sync primary button text", unitLambda), @@ -88,7 +88,7 @@ class JunoOnboardingMapperTest { @Test fun `GIVEN a notification page WHEN mapToOnboardingPageState is called THEN creates the expected OnboardingPageState`() { val expected = OnboardingPageState( - image = R.drawable.ic_notification_permission, + imageRes = R.drawable.ic_notification_permission, title = "notification title", description = "notification body", primaryButton = Action("notification primary button text", unitLambda), diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/LegacyWallpaperFileManagerTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/LegacyWallpaperFileManagerTest.kt deleted file mode 100644 index 0798794494..0000000000 --- a/app/src/test/java/org/mozilla/fenix/wallpapers/LegacyWallpaperFileManagerTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.wallpapers - -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import java.io.File - -class LegacyWallpaperFileManagerTest { - @Rule - @JvmField - val tempFolder = TemporaryFolder() - private lateinit var portraitLightFolder: File - private lateinit var portraitDarkFolder: File - private lateinit var landscapeLightFolder: File - private lateinit var landscapeDarkFolder: File - - private val dispatcher = UnconfinedTestDispatcher() - - private lateinit var fileManager: LegacyWallpaperFileManager - - @Before - fun setup() { - portraitLightFolder = tempFolder.newFolder("wallpapers", "portrait", "light") - portraitDarkFolder = tempFolder.newFolder("wallpapers", "portrait", "dark") - landscapeLightFolder = tempFolder.newFolder("wallpapers", "landscape", "light") - landscapeDarkFolder = tempFolder.newFolder("wallpapers", "landscape", "dark") - fileManager = LegacyWallpaperFileManager( - rootDirectory = tempFolder.root, - coroutineDispatcher = dispatcher, - ) - } - - @Test - fun `GIVEN files exist in all directories WHEN expired wallpaper looked up THEN expired wallpaper returned`() = runTest { - val wallpaperName = "name" - createAllFiles(wallpaperName) - - val result = fileManager.lookupExpiredWallpaper(wallpaperName) - - val expected = generateWallpaper(name = wallpaperName) - assertEquals(expected, result) - } - - @Test - fun `GIVEN any missing file in directories WHEN expired wallpaper looked up THEN null returned`() = runTest { - val wallpaperName = "name" - File(landscapeLightFolder, "$wallpaperName.png").createNewFile() - File(landscapeDarkFolder, "$wallpaperName.png").createNewFile() - - val result = fileManager.lookupExpiredWallpaper(wallpaperName) - - assertEquals(null, result) - } - - @Test - fun `WHEN cleaned THEN current wallpaper and available wallpapers kept`() { - val currentName = "current" - val currentWallpaper = generateWallpaper(name = currentName) - val availableName = "available" - val available = generateWallpaper(name = availableName) - val unavailableName = "unavailable" - createAllFiles(currentName) - createAllFiles(availableName) - createAllFiles(unavailableName) - - fileManager.clean(currentWallpaper, listOf(available)) - - assertTrue(getAllFiles(currentName).all { it.exists() }) - assertTrue(getAllFiles(availableName).all { it.exists() }) - assertTrue(getAllFiles(unavailableName).none { it.exists() }) - } - - private fun createAllFiles(name: String) { - for (file in getAllFiles(name)) { - file.createNewFile() - } - } - - private fun getAllFiles(name: String): List { - return listOf( - File(portraitLightFolder, "$name.png"), - File(portraitDarkFolder, "$name.png"), - File(landscapeLightFolder, "$name.png"), - File(landscapeDarkFolder, "$name.png"), - ) - } - - private fun generateWallpaper(name: String) = Wallpaper( - name = name, - textColor = null, - cardColorLight = null, - cardColorDark = null, - thumbnailFileState = Wallpaper.ImageFileState.Unavailable, - assetsFileState = Wallpaper.ImageFileState.Downloaded, - collection = Wallpaper.Collection( - name = Wallpaper.defaultName, - heading = null, - description = null, - availableLocales = null, - startDate = null, - endDate = null, - learnMoreUrl = null, - ), - ) -} diff --git a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt index 04f78957b3..d11d1f1709 100644 --- a/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt +++ b/app/src/test/java/org/mozilla/fenix/wallpapers/WallpapersUseCasesTest.kt @@ -48,10 +48,6 @@ class WallpapersUseCasesTest { every { shouldMigrateLegacyWallpaperCardColors } returns false every { shouldMigrateLegacyWallpaperCardColors = any() } just Runs } - private val mockLegacyDownloader = mockk(relaxed = true) - private val mockLegacyFileManager = mockk { - every { clean(any(), any()) } just runs - } private lateinit var mockMigrationHelper: LegacyWallpaperMigration private val mockMetadataFetcher = mockk() @@ -76,187 +72,6 @@ class WallpapersUseCasesTest { ) } - @Test - fun `GIVEN legacy use case WHEN initializing THEN the default wallpaper is not downloaded`() = runTest { - val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> - makeFakeRemoteWallpaper(TimeRelation.LATER, name) - } - every { mockSettings.currentWallpaperName } returns "" - coEvery { mockLegacyFileManager.lookupExpiredWallpaper(any()) } returns null - - WallpapersUseCases.LegacyInitializeWallpaperUseCase( - appStore, - mockLegacyDownloader, - mockLegacyFileManager, - mockSettings, - "en-US", - possibleWallpapers = listOf(Wallpaper.Default) + fakeRemoteWallpapers, - ).invoke() - - appStore.waitUntilIdle() - coVerify(exactly = 0) { mockLegacyDownloader.downloadWallpaper(Wallpaper.Default) } - } - - @Test - fun `GIVEN legacy use case WHEN initializing THEN default wallpaper is included in available wallpapers`() = runTest { - val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> - makeFakeRemoteWallpaper(TimeRelation.LATER, name) - } - every { mockSettings.currentWallpaperName } returns "" - coEvery { mockLegacyFileManager.lookupExpiredWallpaper(any()) } returns null - - WallpapersUseCases.LegacyInitializeWallpaperUseCase( - appStore, - mockLegacyDownloader, - mockLegacyFileManager, - mockSettings, - "en-US", - possibleWallpapers = listOf(Wallpaper.Default) + fakeRemoteWallpapers, - ).invoke() - - appStore.waitUntilIdle() - assertTrue(appStore.state.wallpaperState.availableWallpapers.contains(Wallpaper.Default)) - } - - @Test - fun `GIVEN legacy use case and wallpapers that expired WHEN invoking initialize use case THEN expired wallpapers are filtered out and cleaned up`() = runTest { - val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> - makeFakeRemoteWallpaper(TimeRelation.LATER, name) - } - val fakeExpiredRemoteWallpapers = listOf("expired").map { name -> - makeFakeRemoteWallpaper(TimeRelation.BEFORE, name) - } - val possibleWallpapers = fakeRemoteWallpapers + fakeExpiredRemoteWallpapers - every { mockSettings.currentWallpaperName } returns "" - coEvery { mockLegacyFileManager.lookupExpiredWallpaper(any()) } returns null - - WallpapersUseCases.LegacyInitializeWallpaperUseCase( - appStore, - mockLegacyDownloader, - mockLegacyFileManager, - mockSettings, - "en-US", - possibleWallpapers = possibleWallpapers, - ).invoke() - - val expectedFilteredWallpaper = fakeExpiredRemoteWallpapers[0] - appStore.waitUntilIdle() - assertFalse(appStore.state.wallpaperState.availableWallpapers.contains(expectedFilteredWallpaper)) - verify { mockLegacyFileManager.clean(Wallpaper.Default, possibleWallpapers) } - } - - @Test - fun `GIVEN legacy use case and wallpapers that expired and an expired one is selected WHEN invoking initialize use case THEN selected wallpaper is not filtered out`() = runTest { - val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> - makeFakeRemoteWallpaper(TimeRelation.LATER, name) - } - val expiredWallpaper = makeFakeRemoteWallpaper(TimeRelation.BEFORE, "expired") - every { mockSettings.currentWallpaperName } returns expiredWallpaper.name - coEvery { mockLegacyFileManager.lookupExpiredWallpaper(any()) } returns null - - WallpapersUseCases.LegacyInitializeWallpaperUseCase( - appStore, - mockLegacyDownloader, - mockLegacyFileManager, - mockSettings, - "en-US", - possibleWallpapers = fakeRemoteWallpapers + listOf(expiredWallpaper), - ).invoke() - - appStore.waitUntilIdle() - assertTrue(appStore.state.wallpaperState.availableWallpapers.contains(expiredWallpaper)) - assertEquals(expiredWallpaper, appStore.state.wallpaperState.currentWallpaper) - } - - @Test - fun `GIVEN legacy use case and wallpapers that are in promotions outside of locale WHEN invoking initialize use case THEN promotional wallpapers are filtered out`() = runTest { - val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> - makeFakeRemoteWallpaper(TimeRelation.LATER, name) - } - val locale = "en-CA" - every { mockSettings.currentWallpaperName } returns "" - coEvery { mockLegacyFileManager.lookupExpiredWallpaper(any()) } returns null - - WallpapersUseCases.LegacyInitializeWallpaperUseCase( - appStore, - mockLegacyDownloader, - mockLegacyFileManager, - mockSettings, - locale, - possibleWallpapers = fakeRemoteWallpapers, - ).invoke() - - appStore.waitUntilIdle() - assertTrue(appStore.state.wallpaperState.availableWallpapers.isEmpty()) - } - - @Test - fun `GIVEN legacy use case and available wallpapers WHEN invoking initialize use case THEN available wallpapers downloaded`() = runTest { - val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> - makeFakeRemoteWallpaper(TimeRelation.LATER, name) - } - every { mockSettings.currentWallpaperName } returns "" - coEvery { mockLegacyFileManager.lookupExpiredWallpaper(any()) } returns null - - WallpapersUseCases.LegacyInitializeWallpaperUseCase( - appStore, - mockLegacyDownloader, - mockLegacyFileManager, - mockSettings, - "en-US", - possibleWallpapers = fakeRemoteWallpapers, - ).invoke() - - for (fakeRemoteWallpaper in fakeRemoteWallpapers) { - coVerify { mockLegacyDownloader.downloadWallpaper(fakeRemoteWallpaper) } - } - } - - @Test - fun `GIVEN legacy use case and a wallpaper has not been selected WHEN invoking initialize use case THEN app store contains default`() = runTest { - val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> - makeFakeRemoteWallpaper(TimeRelation.LATER, name) - } - every { mockSettings.currentWallpaperName } returns "" - coEvery { mockLegacyFileManager.lookupExpiredWallpaper(any()) } returns null - - WallpapersUseCases.LegacyInitializeWallpaperUseCase( - appStore, - mockLegacyDownloader, - mockLegacyFileManager, - mockSettings, - "en-US", - possibleWallpapers = fakeRemoteWallpapers, - ).invoke() - - appStore.waitUntilIdle() - assertTrue(appStore.state.wallpaperState.currentWallpaper == Wallpaper.Default) - } - - @Test - fun `GIVEN legacy use case a wallpaper is selected and there are available wallpapers WHEN invoking initialize use case THEN these are dispatched to the app store`() = runTest { - val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected") - val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> - makeFakeRemoteWallpaper(TimeRelation.LATER, name) - } - val possibleWallpapers = listOf(selectedWallpaper) + fakeRemoteWallpapers - every { mockSettings.currentWallpaperName } returns selectedWallpaper.name - coEvery { mockLegacyFileManager.lookupExpiredWallpaper(any()) } returns null - - WallpapersUseCases.LegacyInitializeWallpaperUseCase( - appStore, - mockLegacyDownloader, - mockLegacyFileManager, - mockSettings, - "en-US", - possibleWallpapers = possibleWallpapers, - ).invoke() - - appStore.waitUntilIdle() - assertEquals(selectedWallpaper, appStore.state.wallpaperState.currentWallpaper) - assertEquals(possibleWallpapers, appStore.state.wallpaperState.availableWallpapers) - } - @Test fun `WHEN initializing THEN the default wallpaper is not downloaded`() = runTest { val fakeRemoteWallpapers = listOf("first", "second", "third").map { name -> @@ -558,24 +373,6 @@ class WallpapersUseCasesTest { assertEquals(expectedWallpapers, appStore.state.wallpaperState.availableWallpapers) } - @Test - fun `WHEN legacy selected wallpaper usecase invoked THEN storage updated and app store receives dispatch`() = runTest { - val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected") - every { mockSettings.currentWallpaperName = any() } just Runs - - val wallpaperFileState = WallpapersUseCases.LegacySelectWallpaperUseCase( - mockSettings, - appStore, - ).invoke(selectedWallpaper) - - appStore.waitUntilIdle() - - verify { mockSettings.currentWallpaperName = selectedWallpaper.name } - verify { mockSettings.currentWallpaperTextColor = selectedWallpaper.textColor!! } - assertEquals(selectedWallpaper, appStore.state.wallpaperState.currentWallpaper) - assertEquals(wallpaperFileState, Wallpaper.ImageFileState.Downloaded) - } - @Test fun `GIVEN wallpaper downloaded WHEN selecting a wallpaper THEN storage updated and app store receives dispatch`() = runTest { val selectedWallpaper = makeFakeRemoteWallpaper(TimeRelation.LATER, "selected")