commit 007eaddbe00b09a792be8bb3c6b0a9b610021e09 parent 734228b42ecccee410707b5bdf2af85202cb153e Author: Gabriel Luong <gabriel.luong@gmail.com> Date: Wed, 12 Nov 2025 22:16:31 +0000 Bug 1998748 - Add link as a shortcut for Japan r=android-reviewers,tthibaud Co-authored-by: Titouan Thibaud <tthibaud@mozilla.com> Differential Revision: https://phabricator.services.mozilla.com/D272306 Diffstat:
10 files changed, 230 insertions(+), 33 deletions(-)
diff --git a/mobile/android/fenix/app/nimbus.fml.yaml b/mobile/android/fenix/app/nimbus.fml.yaml @@ -634,6 +634,20 @@ features: type: Boolean default: false + firefox-jp-guide-default-site: + description: > + This feature is for managing the visibility of the Firefox Japanese Guide default suggested site shortcut + variables: + enabled: + description: > + Enables the feature. + type: Boolean + default: false + defaults: + - channel: developer + value: + enabled: true + microsurveys: description: Feature for microsurveys. variables: diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.compose import android.content.res.Configuration +import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -16,6 +17,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -26,6 +28,8 @@ import mozilla.components.compose.base.utils.inComposePreview import org.mozilla.fenix.components.components import org.mozilla.fenix.theme.FirefoxTheme +internal val FAVICON_ROUNDED_CORNER_SHAPE = RoundedCornerShape(2.dp) + /** * Load and display the favicon of a particular website. * @@ -36,7 +40,7 @@ import org.mozilla.fenix.theme.FirefoxTheme * download the icon (if needed). * @param imageUrl Optional image URL to create an [IconRequest.Resource] from. * @param shape The shape used to clip the favicon. Defaults to a slightly rounded rectangle. - * Use [CircleShape] for a round image. + * Use [CircleShape] for a round image. */ @Composable fun Favicon( @@ -45,15 +49,13 @@ fun Favicon( modifier: Modifier = Modifier, isPrivate: Boolean = false, imageUrl: String? = null, - shape: Shape = RoundedCornerShape(2.dp), + shape: Shape = FAVICON_ROUNDED_CORNER_SHAPE, ) { - if (inComposePreview) { - FaviconPlaceholder( - size = size, - modifier = modifier, - shape = shape, - ) - } else { + Favicon( + size = size, + modifier = modifier, + shape = shape, + ) { val iconResource = imageUrl?.let { IconRequest.Resource( url = imageUrl, @@ -90,6 +92,65 @@ fun Favicon( } /** + * Load and display the favicon of a particular website. + * + * @param imageResource ID of a drawable resource to be shown. + * @param size [Dp] height and width of the image to be displayed. + * @param modifier [Modifier] to be applied to the layout. + * @param shape The shape used to clip the favicon. Defaults to a slightly rounded rectangle. + * Use [CircleShape] for a round image. + */ +@Composable +fun Favicon( + @DrawableRes imageResource: Int, + size: Dp, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(2.dp), +) { + Favicon( + size = size, + modifier = modifier, + shape = shape, + ) { + Image( + painter = painterResource(id = imageResource), + contentDescription = null, + modifier = modifier + .size(size) + .clip(shape), + contentScale = ContentScale.Crop, + ) + } +} + +/** + * Displays a favicon given a [content] slot. + * + * @param size [Dp] height and width of the placeholder to display. + * @param modifier [Modifier] to be applied to the layout. + * @param shape The shape used to clip the favicon. Defaults to a slightly rounded rectangle. + * Use [CircleShape] for a round image. + * @param content The content to be displayed in the favicon. + */ +@Composable +private fun Favicon( + size: Dp, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(2.dp), + content: @Composable () -> Unit, +) { + if (inComposePreview) { + FaviconPlaceholder( + size = size, + modifier = modifier, + shape = shape, + ) + } else { + content() + } +} + +/** * Placeholder used while the Favicon image is loading. * * @param size [Dp] height and width of the image. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/DefaultTopSitesBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/DefaultTopSitesBinding.kt @@ -78,7 +78,16 @@ class DefaultTopSitesBinding( val notExcludedInRegions = item.excludeRegions.isEmpty() || region !in item.excludeRegions - includedInRegions && notExcludedInRegions + val includedInExperiments = + if (item.includeExperiments.isNotEmpty() && + item.includeExperiments.first() == "firefox-jp-guide-default-site" + ) { + settings.showFirefoxJpGuideDefaultSite + } else { + true + } + + includedInRegions && notExcludedInRegions && includedInExperiments }.map { Pair( it.title?.takeIf(String::isNotBlank) ?: it.url.tryGetHostFromUrl(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt @@ -434,28 +434,26 @@ private fun TopSiteFaviconCard( color = backgroundColor, shape = RoundedCornerShape(4.dp), ) { - if (topSite is TopSite.Provided) { - TopSiteFavicon(url = topSite.url, imageUrl = topSite.imageUrl) - } else { - TopSiteFavicon(url = topSite.url, imageUrl = getImageUrl(url = topSite.url)) - } + TopSiteFavicon(url = topSite.url) } } } } -private fun getImageUrl(url: String): String? { - return when (url) { - "https://tenki.jp/" -> "https://tenki.jp/favicon.ico" - "https://m.yahoo.co.jp/" -> "https://s.yimg.jp/c/icon/s/bsc/2.0/favicon.ico" - "https://ameblo.jp/" -> "https://stat100.ameba.jp/common_style/img/favicon.ico" - else -> null - } -} - @Composable -private fun TopSiteFavicon(url: String, imageUrl: String? = null) { - Favicon(url = url, size = TOP_SITES_FAVICON_SIZE.dp, imageUrl = imageUrl) +private fun TopSiteFavicon(url: String) { + when (val favicon = getTopSitesFavicon(url)) { + is TopSitesFavicon.ImageUrl -> Favicon( + url = url, + size = TOP_SITES_FAVICON_SIZE.dp, + imageUrl = favicon.url, + ) + + is TopSitesFavicon.Drawable -> Favicon( + size = TOP_SITES_FAVICON_SIZE.dp, + imageResource = favicon.drawableResId, + ) + } } @Composable diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesFavicon.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesFavicon.kt @@ -0,0 +1,39 @@ +/* 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.home.topsites + +import androidx.annotation.DrawableRes +import org.mozilla.fenix.R + +/** + * Represents the favicon of a top site. + */ +sealed class TopSitesFavicon { + /** + * An image URL. + * + * @property url The URL of the image to use. + */ + data class ImageUrl(val url: String?) : TopSitesFavicon() + + /** + * A drawable background. + * + * @property drawableResId The drawable resource ID to use. + */ + data class Drawable(@param:DrawableRes val drawableResId: Int) : TopSitesFavicon() +} + +internal fun getTopSitesFavicon(url: String): TopSitesFavicon { + return when (url) { + "https://tenki.jp/" -> TopSitesFavicon.ImageUrl(url = "https://tenki.jp/favicon.ico") + "https://m.yahoo.co.jp/" -> TopSitesFavicon.ImageUrl(url = "https://s.yimg.jp/c/icon/s/bsc/2.0/favicon.ico") + "https://ameblo.jp/" -> TopSitesFavicon.ImageUrl(url = "https://stat100.ameba.jp/common_style/img/favicon.ico") + "https://blog.mozilla.org/ja/firefox-ja/android-guide/" -> + TopSitesFavicon.Drawable(R.drawable.ic_japan_onboarding_favicon) + + else -> TopSitesFavicon.ImageUrl(url = null) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -251,6 +251,12 @@ class Settings(private val appContext: Context) : PreferencesHolder { get() = FxNimbus.features.homescreen.value().sectionsEnabled[HomeScreenSection.COLLECTIONS] == true /** + * Indicates whether or not the Firefox Japan Guide default site should be shown. + */ + val showFirefoxJpGuideDefaultSite: Boolean + get() = FxNimbus.features.firefoxJpGuideDefaultSite.value().enabled + + /** * Indicates whether or not the homepage header should be shown. */ var showHomepageHeader by lazyFeatureFlagPreference( diff --git a/mobile/android/fenix/app/src/main/res/drawable/ic_japan_onboarding_favicon.webp b/mobile/android/fenix/app/src/main/res/drawable/ic_japan_onboarding_favicon.webp Binary files differ. diff --git a/mobile/android/fenix/app/src/main/res/raw/initial_shortcuts.json b/mobile/android/fenix/app/src/main/res/raw/initial_shortcuts.json @@ -38,8 +38,26 @@ "last_modified": 1 }, { - "url": "https://tenki.jp/", + "url": "https://blog.mozilla.org/ja/firefox-ja/android-guide/", "order": 1, + "title": "Firefoxガイド", + "schema": 1, + "exclude_locales": [], + "exclude_regions": [], + "include_locales": [], + "include_regions": [ + "JP" + ], + "exclude_experiments": [], + "include_experiments": [ + "firefox-jp-guide-default-site" + ], + "id": "", + "last_modified": 1 + }, + { + "url": "https://tenki.jp/", + "order": 2, "title": "tenki.jp", "schema": 1, "exclude_locales": [], @@ -55,7 +73,7 @@ }, { "url": "https://m.yahoo.co.jp/", - "order": 2, + "order": 3, "title": "Yahoo! JAPAN", "schema": 1, "exclude_locales": [], @@ -71,7 +89,7 @@ }, { "url": "https://ameblo.jp/", - "order": 3, + "order": 4, "title": "Amebaブログ", "schema": 1, "exclude_locales": [], @@ -87,7 +105,7 @@ }, { "url": "https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8", - "order": 4, + "order": 5, "title": "ウィキペディア", "schema": 1, "exclude_locales": [], diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/DefaultTopSitesBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/DefaultTopSitesBindingTest.kt @@ -52,6 +52,8 @@ class DefaultTopSitesBindingTest { every { resources.openRawResource(R.raw.initial_shortcuts) } answers { this.javaClass.classLoader!!.getResourceAsStream("raw/test_initial_shortcuts.json")!! } + + every { settings.showFirefoxJpGuideDefaultSite } returns true } @Test @@ -137,7 +139,31 @@ class DefaultTopSitesBindingTest { val binding = createBinding() val topSites = binding.getTopSites(region = "US") - assertEquals(5, topSites.size) + assertEquals(7, topSites.size) + assertEquals("US Region Site", topSites[0].first) + assertEquals("https://www.example1.com/", topSites[0].second) + assertEquals("CA Excluded Region Site", topSites[1].first) + assertEquals("https://www.example2.com/", topSites[1].second) + assertEquals("All Region Site", topSites[2].first) + assertEquals("https://www.example3.com/", topSites[2].second) + assertEquals("www.example4.com", topSites[3].first) + assertEquals("https://www.example4.com/", topSites[3].second) + assertEquals("www.example5.com", topSites[4].first) + assertEquals("https://www.example5.com/", topSites[4].second) + assertEquals("www.example6.com", topSites[5].first) + assertEquals("https://www.example6.com/", topSites[5].second) + assertEquals("www.example7.com", topSites[6].first) + assertEquals("https://www.example7.com/", topSites[6].second) + } + + @Test + fun `GIVEN region is in an included region and Japan default site experiment is turned off WHEN getTopSites is called THEN the sites for that region are returned`() = runTest { + every { settings.showFirefoxJpGuideDefaultSite } returns false + + val binding = createBinding() + val topSites = binding.getTopSites(region = "US") + + assertEquals(6, topSites.size) assertEquals("US Region Site", topSites[0].first) assertEquals("https://www.example1.com/", topSites[0].second) assertEquals("CA Excluded Region Site", topSites[1].first) @@ -148,6 +174,8 @@ class DefaultTopSitesBindingTest { assertEquals("https://www.example4.com/", topSites[3].second) assertEquals("www.example5.com", topSites[4].first) assertEquals("https://www.example5.com/", topSites[4].second) + assertEquals("www.example7.com", topSites[5].first) + assertEquals("https://www.example7.com/", topSites[5].second) } @Test @@ -155,13 +183,17 @@ class DefaultTopSitesBindingTest { val binding = createBinding() val topSites = binding.getTopSites(region = "CA") - assertEquals(3, topSites.size) + assertEquals(5, topSites.size) assertEquals("All Region Site", topSites[0].first) assertEquals("https://www.example3.com/", topSites[0].second) assertEquals("www.example4.com", topSites[1].first) assertEquals("https://www.example4.com/", topSites[1].second) assertEquals("www.example5.com", topSites[2].first) assertEquals("https://www.example5.com/", topSites[2].second) + assertEquals("www.example6.com", topSites[3].first) + assertEquals("https://www.example6.com/", topSites[3].second) + assertEquals("www.example7.com", topSites[4].first) + assertEquals("https://www.example7.com/", topSites[4].second) } @Test @@ -169,7 +201,7 @@ class DefaultTopSitesBindingTest { val binding = createBinding() val topSites = binding.getTopSites(region = "XX") - assertEquals(4, topSites.size) + assertEquals(6, topSites.size) assertEquals("CA Excluded Region Site", topSites[0].first) assertEquals("https://www.example2.com/", topSites[0].second) assertEquals("All Region Site", topSites[1].first) @@ -178,6 +210,10 @@ class DefaultTopSitesBindingTest { assertEquals("https://www.example4.com/", topSites[2].second) assertEquals("www.example5.com", topSites[3].first) assertEquals("https://www.example5.com/", topSites[3].second) + assertEquals("www.example6.com", topSites[4].first) + assertEquals("https://www.example6.com/", topSites[4].second) + assertEquals("www.example7.com", topSites[5].first) + assertEquals("https://www.example7.com/", topSites[5].second) } @Test diff --git a/mobile/android/fenix/app/src/test/resources/raw/test_initial_shortcuts.json b/mobile/android/fenix/app/src/test/resources/raw/test_initial_shortcuts.json @@ -40,6 +40,22 @@ "order": 4, "schema": 1, "last_modified": 1 + }, + { + "id": "6", + "url": "https://www.example6.com/", + "order": 5, + "schema": 1, + "include_experiments": ["firefox-jp-guide-default-site"], + "last_modified": 1 + }, + { + "id": "7", + "url": "https://www.example7.com/", + "order": 6, + "schema": 1, + "include_experiments": ["test"], + "last_modified": 1 } ] }