diff --git a/app/src/androidTest/java/org/mozilla/tryfox/data/FakeDownloadFileRepository.kt b/app/src/androidTest/java/org/mozilla/tryfox/data/FakeDownloadFileRepository.kt index 4bb4a51..0ed5d71 100644 --- a/app/src/androidTest/java/org/mozilla/tryfox/data/FakeDownloadFileRepository.kt +++ b/app/src/androidTest/java/org/mozilla/tryfox/data/FakeDownloadFileRepository.kt @@ -1,6 +1,7 @@ package org.mozilla.tryfox.data import kotlinx.coroutines.delay +import org.mozilla.tryfox.data.repositories.DownloadFileRepository import java.io.File class FakeDownloadFileRepository( diff --git a/app/src/androidTest/java/org/mozilla/tryfox/data/FakeFenixRepository.kt b/app/src/androidTest/java/org/mozilla/tryfox/data/FakeTreeherderRepository.kt similarity index 95% rename from app/src/androidTest/java/org/mozilla/tryfox/data/FakeFenixRepository.kt rename to app/src/androidTest/java/org/mozilla/tryfox/data/FakeTreeherderRepository.kt index 4364399..e05097d 100644 --- a/app/src/androidTest/java/org/mozilla/tryfox/data/FakeFenixRepository.kt +++ b/app/src/androidTest/java/org/mozilla/tryfox/data/FakeTreeherderRepository.kt @@ -1,9 +1,11 @@ package org.mozilla.tryfox.data -class FakeFenixRepository( +import org.mozilla.tryfox.data.repositories.TreeherderRepository + +class FakeTreeherderRepository( private val simulateNetworkError: Boolean = false, private val networkErrorMessage: String = "Fake network error", -) : IFenixRepository { +) : TreeherderRepository { override suspend fun getPushByRevision( project: String, diff --git a/app/src/androidTest/java/org/mozilla/tryfox/data/FakeUserDataRepository.kt b/app/src/androidTest/java/org/mozilla/tryfox/data/FakeUserDataRepository.kt index ca4e6cd..7f5b93e 100644 --- a/app/src/androidTest/java/org/mozilla/tryfox/data/FakeUserDataRepository.kt +++ b/app/src/androidTest/java/org/mozilla/tryfox/data/FakeUserDataRepository.kt @@ -2,9 +2,10 @@ package org.mozilla.tryfox.data import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import org.mozilla.tryfox.data.repositories.UserDataRepository /** - * A fake implementation of [UserDataRepository] for testing purposes. + * A fake implementation of [org.mozilla.tryfox.data.repositories.UserDataRepository] for testing purposes. */ class FakeUserDataRepository : UserDataRepository { diff --git a/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/FirefoxReleaseAndBetaInstallationTest.kt b/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/FirefoxReleaseAndBetaInstallationTest.kt new file mode 100644 index 0000000..0b7ce44 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/FirefoxReleaseAndBetaInstallationTest.kt @@ -0,0 +1,179 @@ +package org.mozilla.tryfox.ui.screens + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.tryfox.MainActivity + +/** + * UI tests to verify that Firefox Release and Firefox Beta cards correctly detect + * installed apps and allow launching them. + * + * These tests assert that: + * - Firefox Release card (org.mozilla.firefox) is displayed on the home screen + * - Firefox Beta card (org.mozilla.fenix-beta) is displayed on the home screen + * - Both cards have proper download/install buttons + * - The cards are distinct from the Fenix Nightly card + */ +@RunWith(AndroidJUnit4::class) +class FirefoxReleaseAndBetaInstallationTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test + fun homeScreen_showsFirefoxReleaseCard() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Assert Firefox Release card title is displayed using testTag + // The testTag is: "app_title_text_fenix-release" + val firefoxReleaseTitleTag = "app_title_text_fenix-release" + composeTestRule.onNodeWithTag(firefoxReleaseTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun homeScreen_showsFirefoxBetaCard() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Assert Firefox Beta card title is displayed using testTag + // The testTag is: "app_title_text_fenix-beta" + val firefoxBetaTitleTag = "app_title_text_fenix-beta" + composeTestRule.onNodeWithTag(firefoxBetaTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun homeScreen_firefoxReleaseAndBetaCardsAreDistinct() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Verify that both Firefox Release and Beta cards are displayed separately + val firefoxReleaseTitleTag = "app_title_text_fenix-release" + val firefoxBetaTitleTag = "app_title_text_fenix-beta" + + composeTestRule.onNodeWithTag(firefoxReleaseTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + composeTestRule.onNodeWithTag(firefoxBetaTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun homeScreen_fenixNightlyCardStillDisplayed() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Verify that the original Fenix (Nightly) card is still displayed + val fenixNightlyTitleTag = "app_title_text_fenix" + composeTestRule.onNodeWithTag(fenixNightlyTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun homeScreen_allThreeFenixVariantsDisplayed() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Verify all three Fenix variants are displayed: + // 1. Fenix (Nightly) - org.mozilla.fenix + // 2. Firefox (Release) - org.mozilla.firefox + // 3. Firefox Beta - org.mozilla.fenix-beta + + val fenixNightlyTitleTag = "app_title_text_fenix" + val firefoxReleaseTitleTag = "app_title_text_fenix-release" + val firefoxBetaTitleTag = "app_title_text_fenix-beta" + + composeTestRule.onNodeWithTag(fenixNightlyTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + composeTestRule.onNodeWithTag(firefoxReleaseTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + composeTestRule.onNodeWithTag(firefoxBetaTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun homeScreen_firefoxReleaseCardCanBeLaunched() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Verify that the Firefox Release card is displayed and can be interacted with + val firefoxReleaseTitleTag = "app_title_text_fenix-release" + composeTestRule.onNodeWithTag(firefoxReleaseTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + + // The card exists and is displayed, which means it's ready for interaction + // (clicking on it would launch the app if installed) + } + + @Test + fun homeScreen_firefoxBetaCardCanBeLaunched() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Verify that the Firefox Beta card is displayed and can be interacted with + val firefoxBetaTitleTag = "app_title_text_fenix-beta" + composeTestRule.onNodeWithTag(firefoxBetaTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + + // The card exists and is displayed, which means it's ready for interaction + // (clicking on it would launch the app if installed) + } + + @Test + fun homeScreen_firefoxReleaseDetectsInstallationCorrectly() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Verify that the Firefox Release card is displayed + // This verifies that: + // 1. MozillaPackageManager.fenixRelease is being queried + // 2. The app correctly checks if org.mozilla.firefox is installed + val firefoxReleaseTitleTag = "app_title_text_fenix-release" + composeTestRule.onNodeWithTag(firefoxReleaseTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun homeScreen_firefoxBetaDetectsInstallationCorrectly() { + // Wait for the home screen to load + composeTestRule.waitForIdle() + + // Verify that the Firefox Beta card is displayed + // This verifies that: + // 1. MozillaPackageManager.fenixBeta is being queried + // 2. The app correctly checks if org.mozilla.fenix-beta is installed + val firefoxBetaTitleTag = "app_title_text_fenix-beta" + composeTestRule.onNodeWithTag(firefoxBetaTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun homeScreen_allFenixVariantsLoadWithoutErrors() { + // Wait for the home screen to load fully + composeTestRule.waitForIdle() + + // Verify that all three Fenix variants load successfully + // This is the core test that verifies the fix is working: + // - Fenix Nightly (org.mozilla.fenix) + // - Firefox Release (org.mozilla.firefox) + // - Firefox Beta (org.mozilla.fenix-beta) + + val fenixNightlyTitleTag = "app_title_text_fenix" + val firefoxReleaseTitleTag = "app_title_text_fenix-release" + val firefoxBetaTitleTag = "app_title_text_fenix-beta" + + // All should be displayed without errors + composeTestRule.onNodeWithTag(fenixNightlyTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + composeTestRule.onNodeWithTag(firefoxReleaseTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + composeTestRule.onNodeWithTag(firefoxBetaTitleTag, useUnmergedTree = true) + .assertIsDisplayed() + } +} diff --git a/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/HomeScreenTest.kt b/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/HomeScreenTest.kt index 50c1a31..767ad19 100644 --- a/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/HomeScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/HomeScreenTest.kt @@ -1,7 +1,6 @@ package org.mozilla.tryfox.ui.screens import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -9,7 +8,6 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.tryfox.MainActivity -import org.mozilla.tryfox.R @RunWith(AndroidJUnit4::class) class HomeScreenTest { @@ -18,29 +16,44 @@ class HomeScreenTest { val composeTestRule = createAndroidComposeRule() @Test - fun homeScreen_showsFenixAndFocusCards() { - val activity = composeTestRule.activity - + fun homeScreen_showsFenixNightlyCard() { // --- Fenix (Nightly) Card --- - val fenixIconDesc = activity.getString(R.string.app_icon_firefox_nightly_description) - val fenixIconMatcher = hasContentDescription(fenixIconDesc) val fenixTitleTag = "app_title_text_fenix" - // Assert Fenix icon is displayed - composeTestRule.onNode(fenixIconMatcher, useUnmergedTree = true).assertIsDisplayed() - // Assert Fenix text (found by tag) is displayed composeTestRule.onNodeWithTag(fenixTitleTag, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun homeScreen_showsFenixBetaCard() { + // --- Fenix Beta Card --- + val betaTitleTag = "app_title_text_fenix-beta" + + // Assert Beta text (found by tag) is displayed + composeTestRule.onNodeWithTag(betaTitleTag, useUnmergedTree = true).assertIsDisplayed() + } - // --- Focus Card --- - val focusIconDesc = activity.getString(R.string.app_icon_focus_description) - val focusIconMatcher = hasContentDescription(focusIconDesc) - val focusTitleTag = "app_title_text_focus" + @Test + fun homeScreen_showsFenixReleaseCard() { + // --- Fenix Release Card --- + val releaseTitleTag = "app_title_text_fenix-release" + + // Assert Release text (found by tag) is displayed + composeTestRule.onNodeWithTag(releaseTitleTag, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun homeScreen_showsAllThreeFenixVariants() { + // --- Fenix (Nightly) Card --- + val fenixTitleTag = "app_title_text_fenix" + composeTestRule.onNodeWithTag(fenixTitleTag, useUnmergedTree = true).assertIsDisplayed() - // Assert Focus icon is displayed - composeTestRule.onNode(focusIconMatcher, useUnmergedTree = true).assertIsDisplayed() + // --- Fenix Beta Card --- + val betaTitleTag = "app_title_text_fenix-beta" + composeTestRule.onNodeWithTag(betaTitleTag, useUnmergedTree = true).assertIsDisplayed() - // Assert Focus text (found by tag) is displayed - composeTestRule.onNodeWithTag(focusTitleTag, useUnmergedTree = true).assertIsDisplayed() + // --- Fenix Release Card --- + val releaseTitleTag = "app_title_text_fenix-release" + composeTestRule.onNodeWithTag(releaseTitleTag, useUnmergedTree = true).assertIsDisplayed() } } diff --git a/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/ProfileScreenTest.kt b/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/ProfileScreenTest.kt index f97e048..d6294ce 100644 --- a/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/ProfileScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/tryfox/ui/screens/ProfileScreenTest.kt @@ -15,11 +15,11 @@ import org.junit.Test import org.junit.runner.RunWith import org.mozilla.tryfox.data.FakeCacheManager import org.mozilla.tryfox.data.FakeDownloadFileRepository -import org.mozilla.tryfox.data.FakeFenixRepository import org.mozilla.tryfox.data.FakeIntentManager +import org.mozilla.tryfox.data.FakeTreeherderRepository import org.mozilla.tryfox.data.FakeUserDataRepository -import org.mozilla.tryfox.data.UserDataRepository import org.mozilla.tryfox.data.managers.CacheManager +import org.mozilla.tryfox.data.repositories.UserDataRepository @RunWith(AndroidJUnit4::class) class ProfileScreenTest { @@ -27,7 +27,7 @@ class ProfileScreenTest { @get:Rule val composeTestRule = createComposeRule() - private val fenixRepository = FakeFenixRepository() + private val fenixRepository = FakeTreeherderRepository() private val downloadFileRepository = FakeDownloadFileRepository() private val userDataRepository: UserDataRepository = FakeUserDataRepository() private val cacheManager: CacheManager = FakeCacheManager() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c783b4..fecc0a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,8 @@ + + diff --git a/app/src/main/java/org/mozilla/tryfox/TryFoxViewModel.kt b/app/src/main/java/org/mozilla/tryfox/TryFoxViewModel.kt index 790270b..6ebe4f6 100644 --- a/app/src/main/java/org/mozilla/tryfox/TryFoxViewModel.kt +++ b/app/src/main/java/org/mozilla/tryfox/TryFoxViewModel.kt @@ -17,12 +17,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.mozilla.tryfox.data.DownloadFileRepository import org.mozilla.tryfox.data.DownloadState -import org.mozilla.tryfox.data.IFenixRepository import org.mozilla.tryfox.data.NetworkResult import org.mozilla.tryfox.data.managers.CacheManager import org.mozilla.tryfox.data.managers.IntentManager +import org.mozilla.tryfox.data.repositories.DownloadFileRepository +import org.mozilla.tryfox.data.repositories.TreeherderRepository import org.mozilla.tryfox.model.CacheManagementState import org.mozilla.tryfox.ui.models.AbiUiModel import org.mozilla.tryfox.ui.models.ArtifactUiModel @@ -39,7 +39,7 @@ import java.io.File * @param project The initial repository to search in. */ class TryFoxViewModel( - private val fenixRepository: IFenixRepository, + private val fenixRepository: TreeherderRepository, private val downloadFileRepository: DownloadFileRepository, private val cacheManager: CacheManager, private val intentManager: IntentManager, diff --git a/app/src/main/java/org/mozilla/tryfox/data/DefaultMozillaPackageManager.kt b/app/src/main/java/org/mozilla/tryfox/data/DefaultMozillaPackageManager.kt index f218210..5d9a73b 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/DefaultMozillaPackageManager.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/DefaultMozillaPackageManager.kt @@ -12,9 +12,17 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import org.mozilla.tryfox.model.AppState -import org.mozilla.tryfox.util.FENIX_PACKAGE -import org.mozilla.tryfox.util.FOCUS_PACKAGE +import org.mozilla.tryfox.util.FENIX_BETA +import org.mozilla.tryfox.util.FENIX_BETA_PACKAGE +import org.mozilla.tryfox.util.FENIX_NIGHTLY +import org.mozilla.tryfox.util.FENIX_NIGHTLY_PACKAGE +import org.mozilla.tryfox.util.FENIX_RELEASE +import org.mozilla.tryfox.util.FENIX_RELEASE_PACKAGE +import org.mozilla.tryfox.util.FOCUS +import org.mozilla.tryfox.util.FOCUS_NIGHTLY_PACKAGE +import org.mozilla.tryfox.util.REFERENCE_BROWSER import org.mozilla.tryfox.util.REFERENCE_BROWSER_PACKAGE +import org.mozilla.tryfox.util.TRYFOX import org.mozilla.tryfox.util.TRYFOX_PACKAGE class DefaultMozillaPackageManager(private val context: Context) : MozillaPackageManager { @@ -25,10 +33,10 @@ class DefaultMozillaPackageManager(private val context: Context) : MozillaPackag return try { packageManager.getPackageInfo(packageName, 0) } catch (_: PackageManager.NameNotFoundException) { - Log.d("MozillaPackageManager", "Package not found: $packageName") + Log.d(TAG, "Package not found: $packageName") null } catch (e: Exception) { - Log.e("MozillaPackageManager", "Error getting package info for $packageName", e) + Log.e(TAG, "Error getting package info for $packageName", e) null } } @@ -45,17 +53,25 @@ class DefaultMozillaPackageManager(private val context: Context) : MozillaPackag } private val apps = mapOf( - FENIX_PACKAGE to "Fenix", - FOCUS_PACKAGE to "Focus", - REFERENCE_BROWSER_PACKAGE to "Reference Browser", - TRYFOX_PACKAGE to "TryFox", + FENIX_NIGHTLY_PACKAGE to FENIX_NIGHTLY, + FENIX_RELEASE_PACKAGE to FENIX_RELEASE, + FENIX_BETA_PACKAGE to FENIX_BETA, + FOCUS_NIGHTLY_PACKAGE to FOCUS, + REFERENCE_BROWSER_PACKAGE to REFERENCE_BROWSER, + TRYFOX_PACKAGE to TRYFOX, ) override val fenix: AppState - get() = getAppState(FENIX_PACKAGE) + get() = getAppState(FENIX_NIGHTLY_PACKAGE) + + override val fenixRelease: AppState + get() = getAppState(FENIX_RELEASE_PACKAGE) + + override val fenixBeta: AppState + get() = getAppState(FENIX_BETA_PACKAGE) override val focus: AppState - get() = getAppState(FOCUS_PACKAGE) + get() = getAppState(FOCUS_NIGHTLY_PACKAGE) override val referenceBrowser: AppState get() = getAppState(REFERENCE_BROWSER_PACKAGE) @@ -97,4 +113,8 @@ class DefaultMozillaPackageManager(private val context: Context) : MozillaPackag val intent = packageManager.getLaunchIntentForPackage(appName) intent?.let(context::startActivity) } + + companion object { + private const val TAG = "MozillaPackageManager" + } } diff --git a/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveHtmlParser.kt b/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveHtmlParser.kt new file mode 100644 index 0000000..5d4dc38 --- /dev/null +++ b/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveHtmlParser.kt @@ -0,0 +1,137 @@ +package org.mozilla.tryfox.data + +import kotlinx.datetime.LocalDate +import org.mozilla.tryfox.model.MozillaArchiveApk +import org.mozilla.tryfox.util.Version +import java.util.regex.Pattern + +class MozillaArchiveHtmlParser { + + fun parseNightlyBuildsFromHtml( + html: String, + archiveUrl: String, + date: LocalDate?, + ): List { + val htmlPattern = Regex("Dir\\s*([^<]+/)") + val rawBuildStrings = htmlPattern.findAll(html) + .mapNotNull { it.groups[1]?.value } + .filter { it != "../" } + .toList() + + val buildsForDate = if (date != null) { + val dateString = date.toString() + rawBuildStrings.filter { it.startsWith(dateString) } + } else { + val buildsByDay = rawBuildStrings.groupBy { it.substring(0, 10) } + if (buildsByDay.isEmpty()) return emptyList() + val latestDay = buildsByDay.keys.maxOrNull() ?: return emptyList() + buildsByDay[latestDay] ?: emptyList() + } + + return buildsForDate.mapNotNull { buildString -> + parseBuildString(buildString, archiveUrl) + } + } + + fun parseFenixReleasesFromHtml(html: String, releaseType: ReleaseType = ReleaseType.Beta): String { + val releasePattern = Regex("([0-9.]+[a-zA-Z0-9.-]*)/") + val rawReleaseStrings = releasePattern.findAll(html) + .mapNotNull { it.groups[1]?.value } + .toList() + + return when (releaseType) { + ReleaseType.Beta -> { + // Filter for beta versions only (containing 'b') + val betaReleases = rawReleaseStrings.filter { version -> + version.contains(Regex("[ab]\\d+")) + } + betaReleases.maxWithOrNull(::compareReleaseVersions) ?: "" + } + ReleaseType.Release -> { + // Filter for stable releases matching pattern: D+.D+(.D+)? + // Exclude beta versions (containing 'b', 'beta', 'a', 'alpha', etc.) + val stableReleases = rawReleaseStrings.filter { version -> + // Check if it matches the pattern D+.D+(.D+)? and has no pre-release identifier + val isBeta = version.contains(Regex("[ab]\\d+|beta|alpha|rc")) + !isBeta && version.matches(Regex("\\d+\\.\\d+(\\.\\d+)?")) + } + + // Use Version comparison to find the latest stable release + stableReleases.mapNotNull { Version.from(it) } + .maxOrNull()?.toString() ?: "" + } + } + } + + fun parseFenixReleaseAbisFromHtml(html: String, appName: String): List { + // Pattern: {appName}-D+.D+(.D+)?-android-ABI/ or {appName}-D+.D+(.D+)?-android/ + // Also supports beta/alpha markers: {appName}-D+.D+(.D+)?[ab]D+-android-ABI/ + val htmlPattern = Regex("Dir\\s*([^<]+/)") + val rawBuildStrings = htmlPattern.findAll(html) + .mapNotNull { it.groups[1]?.value } + .filter { it != "../" } + .toList() + + val abis = mutableListOf() + + for (buildString in rawBuildStrings) { + // Pattern: {appName}-D+.D+(.D+)?[ab]D+-android-ABI/ or {appName}-D+.D+(.D+)?-android/ + // Also supports: {appName}-D+.D+(.D+)?-android-ABI/ (stable releases) + // Examples: + // - fenix-145.0-android-arm64-v8a/ (stable) + // - fenix-145.0-android/ (stable universal) + // - fenix-146.0b5-android-arm64-v8a/ (beta) + // - fenix-146.0b5-android/ (beta universal) + val pattern = Regex("^$appName-\\d+\\.\\d+(?:\\.\\d+)?(?:[ab]\\d+)?-android(?:-(.+?))?/$") + val matchResult = pattern.find(buildString) + + if (matchResult != null) { + val abi = matchResult.groups[1]?.value + abis.add(abi ?: "universal") + } + } + return abis + } + + internal fun compareReleaseVersions(version1: String, version2: String): Int { + val parts1 = version1.split(Regex("[.b-]")).mapNotNull { it.toIntOrNull() } + val parts2 = version2.split(Regex("[.b-]")).mapNotNull { it.toIntOrNull() } + + val maxParts = maxOf(parts1.size, parts2.size) + for (i in 0 until maxParts) { + val part1 = parts1.getOrElse(i) { 0 } + val part2 = parts2.getOrElse(i) { 0 } + if (part1 != part2) { + return part1.compareTo(part2) + } + } + return 0 + } + + private fun parseBuildString(buildString: String, archiveUrl: String): MozillaArchiveApk? { + val apkPattern = + Pattern.compile("^(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})-(.*?)-([^-]+)-android-(.*?)/$") + val matcher = apkPattern.matcher(buildString) + if (matcher.matches()) { + val rawDate = matcher.group(1) ?: "" + val appNameResult = matcher.group(2) ?: "" + val version = matcher.group(3) ?: "" + val abi = matcher.group(4) ?: "" + + val fileName = "$appNameResult-$version.multi.android-$abi.apk" + val fullUrl = "${archiveUrl}${buildString}$fileName" + + return MozillaArchiveApk( + originalString = buildString, + rawDateString = rawDate, + appName = appNameResult, + version = version, + abiName = abi, + fullUrl = fullUrl, + fileName = fileName, + ) + } + + return null + } +} diff --git a/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveRepository.kt deleted file mode 100644 index e407187..0000000 --- a/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.mozilla.tryfox.data - -import kotlinx.datetime.LocalDate -import org.mozilla.tryfox.model.ParsedNightlyApk - -interface MozillaArchiveRepository { - /** - * Fetches and parses the list of Fenix nightly builds for the current month from the archive. - */ - suspend fun getFenixNightlyBuilds(date: LocalDate? = null): NetworkResult> - - /** - * Fetches and parses the list of Focus nightly builds for the current month from the archive. - */ - suspend fun getFocusNightlyBuilds(date: LocalDate? = null): NetworkResult> -} diff --git a/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveRepositoryImpl.kt b/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveRepositoryImpl.kt deleted file mode 100644 index 131beb1..0000000 --- a/app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveRepositoryImpl.kt +++ /dev/null @@ -1,114 +0,0 @@ -package org.mozilla.tryfox.data - -import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.minus -import kotlinx.datetime.todayIn -import org.mozilla.tryfox.model.ParsedNightlyApk -import org.mozilla.tryfox.network.MozillaArchivesApiService -import org.mozilla.tryfox.util.FENIX -import org.mozilla.tryfox.util.FOCUS -import retrofit2.HttpException -import java.util.regex.Pattern - -class MozillaArchiveRepositoryImpl( - private val mozillaArchivesApiService: MozillaArchivesApiService, - private val clock: Clock = Clock.System, -) : MozillaArchiveRepository { - - companion object { - const val ARCHIVE_MOZILLA_BASE_URL = "https://archive.mozilla.org/" - - internal fun archiveUrlForDate(appName: String, date: LocalDate): String { - val year = date.year.toString() - val month = date.monthNumber.toString().padStart(2, '0') - - return "${ARCHIVE_MOZILLA_BASE_URL}pub/$appName/nightly/$year/$month/" - } - } - - private suspend fun getNightlyBuilds(appName: String, date: LocalDate? = null): NetworkResult> { - if (date != null) { - val url = archiveUrlForDate(appName, date) - return fetchAndParseNightlyBuilds(url, appName, date) - } - - val today = clock.todayIn(TimeZone.currentSystemDefault()) - val currentMonthUrl = archiveUrlForDate(appName, today) - val result = fetchAndParseNightlyBuilds(currentMonthUrl, appName, null) - - if (result is NetworkResult.Error && (result.cause as? HttpException)?.code() == 404) { - val lastMonth = today.minus(1, DateTimeUnit.MONTH) - val lastMonthUrl = archiveUrlForDate(appName, lastMonth) - return fetchAndParseNightlyBuilds(lastMonthUrl, appName, null) - } - return result - } - - override suspend fun getFenixNightlyBuilds(date: LocalDate?): NetworkResult> = getNightlyBuilds(FENIX, date) - - override suspend fun getFocusNightlyBuilds(date: LocalDate?): NetworkResult> = getNightlyBuilds(FOCUS, date) - - private suspend fun fetchAndParseNightlyBuilds(archiveBaseUrl: String, appNameFilter: String, date: LocalDate?): NetworkResult> { - return try { - val htmlResult = mozillaArchivesApiService.getHtmlPage(archiveBaseUrl) - val parsedApks = parseNightlyBuildsFromHtml(htmlResult, archiveBaseUrl, appNameFilter, date) - NetworkResult.Success(parsedApks) - } catch (e: Exception) { - NetworkResult.Error("Failed to fetch or parse $appNameFilter builds: ${e.message}", e) - } - } - - private fun parseNightlyBuildsFromHtml( - html: String, - archiveUrl: String, - app: String, - date: LocalDate?, - ): List { - val htmlPattern = Regex("Dir\\s*([^<]+/)") - val rawBuildStrings = htmlPattern.findAll(html) - .mapNotNull { it.groups[1]?.value } - .filter { it != "../" } - .toList() - - val buildsForDate = if (date != null) { - val dateString = date.toString() - rawBuildStrings.filter { it.startsWith(dateString) } - } else { - val buildsByDay = rawBuildStrings.groupBy { it.substring(0, 10) } - if (buildsByDay.isEmpty()) return emptyList() - val latestDay = buildsByDay.keys.maxOrNull() ?: return emptyList() - buildsByDay[latestDay] ?: emptyList() - } - - val apkPattern = - Pattern.compile("^(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})-(.*?)-([^-]+)-android-(.*?)/$") - - return buildsForDate.mapNotNull { buildString -> - val matcher = apkPattern.matcher(buildString) - if (matcher.matches()) { - val rawDate = matcher.group(1) ?: "" - val appNameResult = matcher.group(2) ?: "" - val version = matcher.group(3) ?: "" - val abi = matcher.group(4) ?: "" - - val fileName = "$appNameResult-$version.multi.android-$abi.apk" - val fullUrl = "${archiveUrl}${buildString}$fileName" - - ParsedNightlyApk( - originalString = buildString, - rawDateString = rawDate, - appName = appNameResult, - version = version, - abiName = abi, - fullUrl = fullUrl, - fileName = fileName, - ) - } else { - null - } - } - } -} diff --git a/app/src/main/java/org/mozilla/tryfox/data/MozillaPackageManager.kt b/app/src/main/java/org/mozilla/tryfox/data/MozillaPackageManager.kt index be9e267..e79869a 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/MozillaPackageManager.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/MozillaPackageManager.kt @@ -9,10 +9,20 @@ import org.mozilla.tryfox.model.AppState */ interface MozillaPackageManager { /** - * The [AppState] for Fenix (Firefox for Android). + * The [AppState] for Fenix (Firefox for Android Nightly). */ val fenix: AppState + /** + * The [AppState] for Firefox Release. + */ + val fenixRelease: AppState + + /** + * The [AppState] for Firefox Beta. + */ + val fenixBeta: AppState + /** * The [AppState] for Focus Nightly. */ diff --git a/app/src/main/java/org/mozilla/tryfox/data/ReleaseType.kt b/app/src/main/java/org/mozilla/tryfox/data/ReleaseType.kt new file mode 100644 index 0000000..61fffbb --- /dev/null +++ b/app/src/main/java/org/mozilla/tryfox/data/ReleaseType.kt @@ -0,0 +1,6 @@ +package org.mozilla.tryfox.data + +enum class ReleaseType { + Beta, + Release, +} diff --git a/app/src/main/java/org/mozilla/tryfox/data/DefaultDownloadFileRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultDownloadFileRepository.kt similarity index 94% rename from app/src/main/java/org/mozilla/tryfox/data/DefaultDownloadFileRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultDownloadFileRepository.kt index dd0ac04..14fdc54 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/DefaultDownloadFileRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultDownloadFileRepository.kt @@ -1,8 +1,9 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.mozilla.tryfox.data.NetworkResult import org.mozilla.tryfox.network.DownloadApiService import java.io.File import java.io.FileOutputStream diff --git a/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultMozillaArchiveRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultMozillaArchiveRepository.kt new file mode 100644 index 0000000..6f96349 --- /dev/null +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultMozillaArchiveRepository.kt @@ -0,0 +1,128 @@ +package org.mozilla.tryfox.data.repositories + +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus +import kotlinx.datetime.todayIn +import org.mozilla.tryfox.data.MozillaArchiveHtmlParser +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.data.ReleaseType +import org.mozilla.tryfox.model.MozillaArchiveApk +import org.mozilla.tryfox.network.MozillaArchivesApiService +import org.mozilla.tryfox.util.FENIX +import org.mozilla.tryfox.util.FENIX_BETA +import org.mozilla.tryfox.util.FENIX_RELEASE +import org.mozilla.tryfox.util.FOCUS +import retrofit2.HttpException + +class DefaultMozillaArchiveRepository( + private val mozillaArchivesApiService: MozillaArchivesApiService, + private val clock: Clock = Clock.System, + private val mozillaArchiveHtmlParser: MozillaArchiveHtmlParser = MozillaArchiveHtmlParser(), +) : MozillaArchiveRepository { + + companion object { + const val ARCHIVE_MOZILLA_BASE_URL = "https://archive.mozilla.org/pub/" + const val RELEASES_FENIX_BASE_URL = "${ARCHIVE_MOZILLA_BASE_URL}fenix/releases/" + + internal fun archiveUrlForDate(appName: String, date: LocalDate): String { + val year = date.year.toString() + val month = date.monthNumber.toString().padStart(2, '0') + + return "${ARCHIVE_MOZILLA_BASE_URL}$appName/nightly/$year/$month/" + } + + internal fun archiveUrlForRelease(number: String): String { + return "${RELEASES_FENIX_BASE_URL}$number/android/" + } + } + + override suspend fun getFenixNightlyBuilds(date: LocalDate?): NetworkResult> = getNightlyBuilds(FENIX, date) + + override suspend fun getFocusNightlyBuilds(date: LocalDate?): NetworkResult> = getNightlyBuilds(FOCUS, date) + + override suspend fun getFenixReleaseBuilds(releaseType: ReleaseType): NetworkResult> { + return try { + // Get the latest release version + val releasesPageUrl = RELEASES_FENIX_BASE_URL + val releasesHtml = mozillaArchivesApiService.getHtmlPage(releasesPageUrl) + val latestReleaseVersion = mozillaArchiveHtmlParser.parseFenixReleasesFromHtml(releasesHtml, releaseType) + + if (latestReleaseVersion.isEmpty()) { + return NetworkResult.Error("No releases found for type $releaseType", null) + } + + // Get the release directory listing to find available ABIs + val releaseUrl = archiveUrlForRelease(latestReleaseVersion) + val releaseHtml = mozillaArchivesApiService.getHtmlPage(releaseUrl) + val abis = mozillaArchiveHtmlParser.parseFenixReleaseAbisFromHtml(releaseHtml, FENIX) + + if (abis.isEmpty()) { + return NetworkResult.Error("No ABIs found for release $latestReleaseVersion", null) + } + + val apks = abis.map { abi -> + constructReleaseApk(latestReleaseVersion, abi, releaseUrl, releaseType) + } + + if (apks.isEmpty()) { + return NetworkResult.Error("Failed to construct APKs for release $latestReleaseVersion", null) + } + + NetworkResult.Success(apks) + } catch (e: Exception) { + NetworkResult.Error("Failed to fetch or parse Fenix releases: ${e.message}", e) + } + } + + private fun constructReleaseApk(version: String, abi: String, releaseBaseUrl: String, releaseType: ReleaseType): MozillaArchiveApk { + val buildString = "fenix-$version-android${if (abi == "universal") "" else "-$abi"}/" + val fileName = "fenix-$version.multi.android-$abi.apk" + val fullUrl = "${releaseBaseUrl}${buildString}$fileName" + + val appName = when (releaseType) { + ReleaseType.Release -> FENIX_RELEASE + ReleaseType.Beta -> FENIX_BETA + } + + return MozillaArchiveApk( + originalString = buildString, + rawDateString = "", // Release builds don't have date strings + appName = appName, + version = version, + abiName = abi, + fullUrl = fullUrl, + fileName = fileName, + ) + } + + private suspend fun getNightlyBuilds(appName: String, date: LocalDate? = null): NetworkResult> { + if (date != null) { + val url = archiveUrlForDate(appName, date) + return fetchAndParseNightlyBuilds(url, appName, date) + } + + val today = clock.todayIn(TimeZone.currentSystemDefault()) + val currentMonthUrl = archiveUrlForDate(appName, today) + val result = fetchAndParseNightlyBuilds(currentMonthUrl, appName, null) + + if (result is NetworkResult.Error && (result.cause as? HttpException)?.code() == 404) { + val lastMonth = today.minus(1, DateTimeUnit.MONTH) + val lastMonthUrl = archiveUrlForDate(appName, lastMonth) + return fetchAndParseNightlyBuilds(lastMonthUrl, appName, null) + } + return result + } + + private suspend fun fetchAndParseNightlyBuilds(archiveBaseUrl: String, appNameFilter: String, date: LocalDate?): NetworkResult> { + return try { + val htmlResult = mozillaArchivesApiService.getHtmlPage(archiveBaseUrl) + val parsedApks = mozillaArchiveHtmlParser.parseNightlyBuildsFromHtml(htmlResult, archiveBaseUrl, date) + NetworkResult.Success(parsedApks) + } catch (e: Exception) { + NetworkResult.Error("Failed to fetch or parse $appNameFilter builds: ${e.message}", e) + } + } +} diff --git a/app/src/main/java/org/mozilla/tryfox/data/FenixRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultTreeherderRepository.kt similarity index 81% rename from app/src/main/java/org/mozilla/tryfox/data/FenixRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultTreeherderRepository.kt index 4277f91..31cee33 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/FenixRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultTreeherderRepository.kt @@ -1,10 +1,14 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories +import org.mozilla.tryfox.data.ArtifactsResponse +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.data.TreeherderJobsResponse +import org.mozilla.tryfox.data.TreeherderRevisionResponse import org.mozilla.tryfox.network.TreeherderApiService -class FenixRepository( +class DefaultTreeherderRepository( private val treeherderApiService: TreeherderApiService, -) : IFenixRepository { +) : TreeherderRepository { companion object { const val TASKCLUSTER_BASE_URL = "https://firefox-ci-tc.services.mozilla.com/api/queue/v1/" diff --git a/app/src/main/java/org/mozilla/tryfox/data/DefaultUserDataRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultUserDataRepository.kt similarity index 91% rename from app/src/main/java/org/mozilla/tryfox/data/DefaultUserDataRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultUserDataRepository.kt index 0601a86..264ec6b 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/DefaultUserDataRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/DefaultUserDataRepository.kt @@ -1,4 +1,4 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories import android.content.Context import androidx.datastore.core.DataStore @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import org.mozilla.tryfox.data.repositories.UserDataRepository /** * A repository that stores the last searched email in a DataStore. diff --git a/app/src/main/java/org/mozilla/tryfox/data/DownloadFileRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/DownloadFileRepository.kt similarity index 71% rename from app/src/main/java/org/mozilla/tryfox/data/DownloadFileRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/DownloadFileRepository.kt index 6c05c70..8340a14 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/DownloadFileRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/DownloadFileRepository.kt @@ -1,5 +1,6 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories +import org.mozilla.tryfox.data.NetworkResult import java.io.File /** @@ -12,7 +13,7 @@ interface DownloadFileRepository { * @param downloadUrl The URL of the file to download. * @param outputFile The file where the downloaded content will be saved. * @param onProgress A callback function to report download progress (bytesDownloaded, totalBytes). - * @return A [NetworkResult] indicating success with the downloaded [File] or an [NetworkResult.Error] on failure. + * @return A [org.mozilla.tryfox.data.NetworkResult] indicating success with the downloaded [File] or an [org.mozilla.tryfox.data.NetworkResult.Error] on failure. */ suspend fun downloadFile(downloadUrl: String, outputFile: File, onProgress: (bytesDownloaded: Long, totalBytes: Long) -> Unit): NetworkResult } diff --git a/app/src/main/java/org/mozilla/tryfox/data/repositories/FenixBetaReleaseRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/FenixBetaReleaseRepository.kt new file mode 100644 index 0000000..04e2bfd --- /dev/null +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/FenixBetaReleaseRepository.kt @@ -0,0 +1,19 @@ +package org.mozilla.tryfox.data.repositories + +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.data.ReleaseType +import org.mozilla.tryfox.model.MozillaArchiveApk +import org.mozilla.tryfox.util.FENIX_BETA + +/** + * A [ReleaseRepository] for Fenix builds. + */ +class FenixBetaReleaseRepository( + private val mozillaArchiveRepository: MozillaArchiveRepository, +) : ReleaseRepository { + override val appName: String = FENIX_BETA + + override suspend fun getLatestReleases(): NetworkResult> { + return mozillaArchiveRepository.getFenixReleaseBuilds(ReleaseType.Beta) + } +} diff --git a/app/src/main/java/org/mozilla/tryfox/data/repositories/FenixReleaseReleaseRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/FenixReleaseReleaseRepository.kt new file mode 100644 index 0000000..06bce63 --- /dev/null +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/FenixReleaseReleaseRepository.kt @@ -0,0 +1,19 @@ +package org.mozilla.tryfox.data.repositories + +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.data.ReleaseType +import org.mozilla.tryfox.model.MozillaArchiveApk +import org.mozilla.tryfox.util.FENIX_RELEASE + +/** + * A [ReleaseRepository] for Fenix builds. + */ +class FenixReleaseReleaseRepository( + private val mozillaArchiveRepository: MozillaArchiveRepository, +) : ReleaseRepository { + override val appName: String = FENIX_RELEASE + + override suspend fun getLatestReleases(): NetworkResult> { + return mozillaArchiveRepository.getFenixReleaseBuilds(ReleaseType.Release) + } +} diff --git a/app/src/main/java/org/mozilla/tryfox/data/FenixReleaseRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/FenixReleaseRepository.kt similarity index 74% rename from app/src/main/java/org/mozilla/tryfox/data/FenixReleaseRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/FenixReleaseRepository.kt index a311881..35fc1ed 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/FenixReleaseRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/FenixReleaseRepository.kt @@ -1,7 +1,8 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories import kotlinx.datetime.LocalDate -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.util.FENIX /** @@ -12,11 +13,11 @@ class FenixReleaseRepository( ) : DateAwareReleaseRepository { override val appName: String = FENIX - override suspend fun getLatestReleases(): NetworkResult> { + override suspend fun getLatestReleases(): NetworkResult> { return mozillaArchiveRepository.getFenixNightlyBuilds() } - override suspend fun getReleases(date: LocalDate?): NetworkResult> { + override suspend fun getReleases(date: LocalDate?): NetworkResult> { return mozillaArchiveRepository.getFenixNightlyBuilds(date) } } diff --git a/app/src/main/java/org/mozilla/tryfox/data/FocusReleaseRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/FocusReleaseRepository.kt similarity index 74% rename from app/src/main/java/org/mozilla/tryfox/data/FocusReleaseRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/FocusReleaseRepository.kt index 2ef6f7d..8010636 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/FocusReleaseRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/FocusReleaseRepository.kt @@ -1,7 +1,8 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories import kotlinx.datetime.LocalDate -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.util.FOCUS /** @@ -12,11 +13,11 @@ class FocusReleaseRepository( ) : DateAwareReleaseRepository { override val appName: String = FOCUS - override suspend fun getLatestReleases(): NetworkResult> { + override suspend fun getLatestReleases(): NetworkResult> { return mozillaArchiveRepository.getFocusNightlyBuilds() } - override suspend fun getReleases(date: LocalDate?): NetworkResult> { + override suspend fun getReleases(date: LocalDate?): NetworkResult> { return mozillaArchiveRepository.getFocusNightlyBuilds(date) } } diff --git a/app/src/main/java/org/mozilla/tryfox/data/repositories/MozillaArchiveRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/MozillaArchiveRepository.kt new file mode 100644 index 0000000..e0b751b --- /dev/null +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/MozillaArchiveRepository.kt @@ -0,0 +1,25 @@ +package org.mozilla.tryfox.data.repositories + +import kotlinx.datetime.LocalDate +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.data.ReleaseType +import org.mozilla.tryfox.model.MozillaArchiveApk + +interface MozillaArchiveRepository { + /** + * Fetches and parses the list of Fenix nightly builds for the current month from the archive. + */ + suspend fun getFenixNightlyBuilds(date: LocalDate? = null): NetworkResult> + + /** + * Fetches and parses the list of Focus nightly builds for the current month from the archive. + */ + suspend fun getFocusNightlyBuilds(date: LocalDate? = null): NetworkResult> + + /** + * Fetches and parses the latest Fenix release APKs from the archive. + * @param releaseType The type of release to fetch (Beta or Release) + * @return List of available MozillaArchiveApk for the release + */ + suspend fun getFenixReleaseBuilds(releaseType: ReleaseType = ReleaseType.Beta): NetworkResult> +} diff --git a/app/src/main/java/org/mozilla/tryfox/data/ReferenceBrowserReleaseRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/ReferenceBrowserReleaseRepository.kt similarity index 87% rename from app/src/main/java/org/mozilla/tryfox/data/ReferenceBrowserReleaseRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/ReferenceBrowserReleaseRepository.kt index d1a6623..1f8c63d 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/ReferenceBrowserReleaseRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/ReferenceBrowserReleaseRepository.kt @@ -1,6 +1,7 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.util.REFERENCE_BROWSER /** @@ -15,12 +16,12 @@ class ReferenceBrowserReleaseRepository : ReleaseRepository { override val appName: String = REFERENCE_BROWSER - override suspend fun getLatestReleases(): NetworkResult> { + override suspend fun getLatestReleases(): NetworkResult> { return try { val parsedApks = REFERENCE_BROWSER_ABIS.map { abi -> val fullUrl = "${REFERENCE_BROWSER_TASK_BASE_URL}$abi/artifacts/public/target.$abi.apk" val fileName = "target.$abi.apk" - ParsedNightlyApk( + MozillaArchiveApk( originalString = "reference-browser-latest-android-$abi/", rawDateString = null, appName = "reference-browser", diff --git a/app/src/main/java/org/mozilla/tryfox/data/ReleaseRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/ReleaseRepository.kt similarity index 65% rename from app/src/main/java/org/mozilla/tryfox/data/ReleaseRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/ReleaseRepository.kt index a8c5303..d88faec 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/ReleaseRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/ReleaseRepository.kt @@ -1,7 +1,8 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories import kotlinx.datetime.LocalDate -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.model.MozillaArchiveApk /** * Interface for a repository that provides release information for a specific application. @@ -15,9 +16,9 @@ interface ReleaseRepository { /** * Fetches the latest releases for the application. */ - suspend fun getLatestReleases(): NetworkResult> + suspend fun getLatestReleases(): NetworkResult> } interface DateAwareReleaseRepository : ReleaseRepository { - suspend fun getReleases(date: LocalDate? = null): NetworkResult> + suspend fun getReleases(date: LocalDate? = null): NetworkResult> } diff --git a/app/src/main/java/org/mozilla/tryfox/data/IFenixRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/TreeherderRepository.kt similarity index 56% rename from app/src/main/java/org/mozilla/tryfox/data/IFenixRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/TreeherderRepository.kt index a989ae4..be6cb5f 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/IFenixRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/TreeherderRepository.kt @@ -1,6 +1,11 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories -interface IFenixRepository { +import org.mozilla.tryfox.data.ArtifactsResponse +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.data.TreeherderJobsResponse +import org.mozilla.tryfox.data.TreeherderRevisionResponse + +interface TreeherderRepository { suspend fun getPushByRevision(project: String, revision: String): NetworkResult suspend fun getPushesByAuthor(author: String): NetworkResult suspend fun getJobsForPush(pushId: Int): NetworkResult diff --git a/app/src/main/java/org/mozilla/tryfox/data/TryFoxReleaseRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/TryFoxReleaseRepository.kt similarity index 84% rename from app/src/main/java/org/mozilla/tryfox/data/TryFoxReleaseRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/TryFoxReleaseRepository.kt index 8daee97..b912d4d 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/TryFoxReleaseRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/TryFoxReleaseRepository.kt @@ -1,6 +1,7 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.network.GithubApiService import org.mozilla.tryfox.util.TRYFOX @@ -12,11 +13,11 @@ class TryFoxReleaseRepository( ) : ReleaseRepository { override val appName: String = TRYFOX - override suspend fun getLatestReleases(): NetworkResult> { + override suspend fun getLatestReleases(): NetworkResult> { return try { val release = githubApiService.getLatestGitHubRelease("mozilla-mobile", "TryFox") val parsedApks = release.assets.map { asset -> - ParsedNightlyApk( + MozillaArchiveApk( originalString = asset.name, rawDateString = release.updatedAt, appName = TRYFOX, diff --git a/app/src/main/java/org/mozilla/tryfox/data/UserDataRepository.kt b/app/src/main/java/org/mozilla/tryfox/data/repositories/UserDataRepository.kt similarity index 89% rename from app/src/main/java/org/mozilla/tryfox/data/UserDataRepository.kt rename to app/src/main/java/org/mozilla/tryfox/data/repositories/UserDataRepository.kt index 787ecc7..dd857fd 100644 --- a/app/src/main/java/org/mozilla/tryfox/data/UserDataRepository.kt +++ b/app/src/main/java/org/mozilla/tryfox/data/repositories/UserDataRepository.kt @@ -1,4 +1,4 @@ -package org.mozilla.tryfox.data +package org.mozilla.tryfox.data.repositories import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/org/mozilla/tryfox/di/AppModule.kt b/app/src/main/java/org/mozilla/tryfox/di/AppModule.kt index d1a8687..ac2ffe4 100644 --- a/app/src/main/java/org/mozilla/tryfox/di/AppModule.kt +++ b/app/src/main/java/org/mozilla/tryfox/di/AppModule.kt @@ -13,25 +13,27 @@ import org.koin.core.qualifier.named import org.koin.dsl.module import org.mozilla.tryfox.BuildConfig import org.mozilla.tryfox.TryFoxViewModel -import org.mozilla.tryfox.data.DefaultDownloadFileRepository import org.mozilla.tryfox.data.DefaultMozillaPackageManager -import org.mozilla.tryfox.data.DefaultUserDataRepository -import org.mozilla.tryfox.data.DownloadFileRepository -import org.mozilla.tryfox.data.FenixReleaseRepository -import org.mozilla.tryfox.data.FenixRepository -import org.mozilla.tryfox.data.FocusReleaseRepository -import org.mozilla.tryfox.data.IFenixRepository -import org.mozilla.tryfox.data.MozillaArchiveRepository -import org.mozilla.tryfox.data.MozillaArchiveRepositoryImpl import org.mozilla.tryfox.data.MozillaPackageManager -import org.mozilla.tryfox.data.ReferenceBrowserReleaseRepository -import org.mozilla.tryfox.data.ReleaseRepository -import org.mozilla.tryfox.data.TryFoxReleaseRepository -import org.mozilla.tryfox.data.UserDataRepository import org.mozilla.tryfox.data.managers.CacheManager import org.mozilla.tryfox.data.managers.DefaultCacheManager import org.mozilla.tryfox.data.managers.DefaultIntentManager import org.mozilla.tryfox.data.managers.IntentManager +import org.mozilla.tryfox.data.repositories.DefaultDownloadFileRepository +import org.mozilla.tryfox.data.repositories.DefaultMozillaArchiveRepository +import org.mozilla.tryfox.data.repositories.DefaultTreeherderRepository +import org.mozilla.tryfox.data.repositories.DefaultUserDataRepository +import org.mozilla.tryfox.data.repositories.DownloadFileRepository +import org.mozilla.tryfox.data.repositories.FenixBetaReleaseRepository +import org.mozilla.tryfox.data.repositories.FenixReleaseReleaseRepository +import org.mozilla.tryfox.data.repositories.FenixReleaseRepository +import org.mozilla.tryfox.data.repositories.FocusReleaseRepository +import org.mozilla.tryfox.data.repositories.MozillaArchiveRepository +import org.mozilla.tryfox.data.repositories.ReferenceBrowserReleaseRepository +import org.mozilla.tryfox.data.repositories.ReleaseRepository +import org.mozilla.tryfox.data.repositories.TreeherderRepository +import org.mozilla.tryfox.data.repositories.TryFoxReleaseRepository +import org.mozilla.tryfox.data.repositories.UserDataRepository import org.mozilla.tryfox.network.DownloadApiService import org.mozilla.tryfox.network.GithubApiService import org.mozilla.tryfox.network.MozillaArchivesApiService @@ -39,6 +41,8 @@ import org.mozilla.tryfox.network.TreeherderApiService import org.mozilla.tryfox.ui.screens.HomeViewModel import org.mozilla.tryfox.ui.screens.ProfileViewModel import org.mozilla.tryfox.util.FENIX +import org.mozilla.tryfox.util.FENIX_BETA +import org.mozilla.tryfox.util.FENIX_RELEASE import org.mozilla.tryfox.util.FOCUS import org.mozilla.tryfox.util.REFERENCE_BROWSER import org.mozilla.tryfox.util.TRYFOX @@ -127,8 +131,8 @@ val repositoryModule = module { get(named("IODispatcher")), ) } - single { FenixRepository(get()) } - single { MozillaArchiveRepositoryImpl(get()) } + single { DefaultTreeherderRepository(get()) } + single { DefaultMozillaArchiveRepository(get()) } single { DefaultUserDataRepository(androidContext()) } single { DefaultMozillaPackageManager(androidContext()) } single { @@ -140,6 +144,8 @@ val repositoryModule = module { single { DefaultIntentManager(androidContext()) } single(named(FENIX)) { FenixReleaseRepository(get()) } + single(named(FENIX_RELEASE)) { FenixReleaseReleaseRepository(get()) } + single(named(FENIX_BETA)) { FenixBetaReleaseRepository(get()) } single(named(FOCUS)) { FocusReleaseRepository(get()) } single(named(REFERENCE_BROWSER)) { ReferenceBrowserReleaseRepository() } single(named(TRYFOX)) { TryFoxReleaseRepository(get()) } @@ -159,6 +165,8 @@ val viewModelModule = module { viewModel { val releaseRepositories = listOf( get(named(FENIX)), + get(named(FENIX_RELEASE)), + get(named(FENIX_BETA)), get(named(FOCUS)), get(named(REFERENCE_BROWSER)), get(named(TRYFOX)), diff --git a/app/src/main/java/org/mozilla/tryfox/model/ParsedNightlyApk.kt b/app/src/main/java/org/mozilla/tryfox/model/MozillaArchiveApk.kt similarity index 89% rename from app/src/main/java/org/mozilla/tryfox/model/ParsedNightlyApk.kt rename to app/src/main/java/org/mozilla/tryfox/model/MozillaArchiveApk.kt index 68bc4dc..b2014e8 100644 --- a/app/src/main/java/org/mozilla/tryfox/model/ParsedNightlyApk.kt +++ b/app/src/main/java/org/mozilla/tryfox/model/MozillaArchiveApk.kt @@ -1,6 +1,6 @@ package org.mozilla.tryfox.model -data class ParsedNightlyApk( +data class MozillaArchiveApk( val originalString: String, val rawDateString: String?, // Format: "yyyy-MM-dd-HH-mm-ss" val appName: String, diff --git a/app/src/main/java/org/mozilla/tryfox/ui/composables/AppCard.kt b/app/src/main/java/org/mozilla/tryfox/ui/composables/AppCard.kt index 72ffe9d..a86d796 100644 --- a/app/src/main/java/org/mozilla/tryfox/ui/composables/AppCard.kt +++ b/app/src/main/java/org/mozilla/tryfox/ui/composables/AppCard.kt @@ -27,7 +27,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -42,7 +41,6 @@ fun AppCard( job: JobDetailsUiModel, viewModel: TryFoxViewModel, ) { - val context = LocalContext.current val jobArtifacts = job.artifacts val (supportedArtifacts, unsupportedArtifacts) = remember(jobArtifacts) { jobArtifacts.partition { it.abi.isSupported } diff --git a/app/src/main/java/org/mozilla/tryfox/ui/composables/AppIcon.kt b/app/src/main/java/org/mozilla/tryfox/ui/composables/AppIcon.kt index 2b3c04f..40252d4 100644 --- a/app/src/main/java/org/mozilla/tryfox/ui/composables/AppIcon.kt +++ b/app/src/main/java/org/mozilla/tryfox/ui/composables/AppIcon.kt @@ -9,15 +9,24 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import org.mozilla.tryfox.R +import org.mozilla.tryfox.util.FENIX +import org.mozilla.tryfox.util.FENIX_BETA +import org.mozilla.tryfox.util.FENIX_RELEASE +import org.mozilla.tryfox.util.FOCUS +import org.mozilla.tryfox.util.REFERENCE_BROWSER @Composable fun AppIcon(appName: String, modifier: Modifier = Modifier) { val (iconResId, contentDescResId) = when { - appName.contains("reference", ignoreCase = true) -> R.drawable.ic_reference_browser to R.string.app_icon_reference_browser_description - appName.contains("fenix-nightly", ignoreCase = true) -> R.drawable.ic_fenix_nightly to R.string.app_icon_firefox_nightly_description - appName.contains("fenix", ignoreCase = true) -> R.drawable.ic_firefox to R.string.app_icon_firefox_description - appName.contains("focus", ignoreCase = true) -> R.drawable.ic_focus to R.string.app_icon_focus_description - else -> null to null + appName == REFERENCE_BROWSER -> R.drawable.ic_reference_browser to R.string.app_icon_reference_browser_description + appName == FENIX -> R.drawable.ic_fenix_nightly to R.string.app_icon_firefox_nightly_description + appName == FENIX_BETA -> R.drawable.ic_firefox_beta to R.string.app_icon_firefox_description + appName == FENIX_RELEASE -> R.drawable.ic_firefox to R.string.app_icon_firefox_description + appName == FOCUS -> R.drawable.ic_focus to R.string.app_icon_focus_description + else -> { + println("Titouan - Error - $appName") + null to null + } } if (iconResId != null && contentDescResId != null) { diff --git a/app/src/main/java/org/mozilla/tryfox/ui/composables/ArchiveGroupCard.kt b/app/src/main/java/org/mozilla/tryfox/ui/composables/ArchiveGroupCard.kt index b086521..451098f 100644 --- a/app/src/main/java/org/mozilla/tryfox/ui/composables/ArchiveGroupCard.kt +++ b/app/src/main/java/org/mozilla/tryfox/ui/composables/ArchiveGroupCard.kt @@ -54,7 +54,10 @@ import org.mozilla.tryfox.R import org.mozilla.tryfox.model.AppState import org.mozilla.tryfox.ui.models.ApkUiModel import org.mozilla.tryfox.util.FENIX +import org.mozilla.tryfox.util.FENIX_BETA +import org.mozilla.tryfox.util.FENIX_RELEASE import org.mozilla.tryfox.util.FOCUS +import org.mozilla.tryfox.util.FeatureFlags import org.mozilla.tryfox.util.REFERENCE_BROWSER import org.mozilla.tryfox.util.parseDateToLocalDate import java.io.File @@ -88,13 +91,12 @@ fun ArchiveGroupCard( ) { ElevatedCard( modifier = - modifier - .fillMaxWidth() - .padding(top = ArchiveGroupCardTokens.CardPaddingTop), + modifier + .fillMaxWidth() + .padding(top = ArchiveGroupCardTokens.CardPaddingTop), elevation = CardDefaults.cardElevation(defaultElevation = ArchiveGroupCardTokens.CardElevation), ) { val firstApk = apks.firstOrNull() - val friendlyAppName = getFriendlyAppName(appName) val version = firstApk?.version ?: "" val dateFromApk = firstApk?.date ?: "" val isDatePickerEnabled = appName != REFERENCE_BROWSER @@ -105,7 +107,7 @@ fun ArchiveGroupCard( ) ArchiveGroupHeader( - appName = friendlyAppName, + appName = appName, version = version, date = dateFromApk, onDateSelected = onDateSelected, @@ -128,6 +130,7 @@ fun ArchiveGroupCard( CircularProgressIndicator() } } + errorMessage != null -> { Text( text = errorMessage, @@ -136,9 +139,17 @@ fun ArchiveGroupCard( color = MaterialTheme.colorScheme.error, ) } + apks.isNotEmpty() -> { - ArchiveGroupAbiSelector(apks, onDownloadClick, onInstallClick, onUninstallClick, appState) + ArchiveGroupAbiSelector( + apks, + onDownloadClick, + onInstallClick, + onUninstallClick, + appState, + ) } + else -> { Text( stringResource(id = R.string.archive_group_card_no_apks_for_date), @@ -166,15 +177,17 @@ private fun ArchiveGroupHeader( ) { var showDatePicker by remember { mutableStateOf(false) } val displayDate = userPickedDate?.toString() ?: date + val friendlyAppName = getFriendlyAppName(appName) Row(verticalAlignment = Alignment.CenterVertically) { AppIcon( - appName = "$appName-nightly", - modifier = Modifier.size(ArchiveGroupCardTokens.AppIconSize) + appName = appName, + modifier = Modifier + .size(ArchiveGroupCardTokens.AppIconSize) .clickable { onOpenAppClick() }, ) Text( - text = "$appName $version", + text = "$friendlyAppName $version", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.testTag("app_title_text_${appName.lowercase()}"), @@ -195,7 +208,10 @@ private fun ArchiveGroupHeader( colors = chipColors, trailingIcon = { if (userPickedDate != null) { - IconButton(onClick = onClearDate, modifier = Modifier.size(AssistChipDefaults.IconSize)) { + IconButton( + onClick = onClearDate, + modifier = Modifier.size(AssistChipDefaults.IconSize), + ) { Icon( imageVector = Icons.Default.Clear, contentDescription = stringResource(R.string.clear_date_selection), @@ -208,12 +224,15 @@ private fun ArchiveGroupHeader( } if (showDatePicker) { - val initialDate = userPickedDate ?: parseDateToLocalDate(date) ?: Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + val initialDate = userPickedDate ?: parseDateToLocalDate(date) ?: Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()).date val datePickerState = rememberDatePickerState( - initialSelectedDateMillis = initialDate.atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds(), + initialSelectedDateMillis = initialDate.atStartOfDayIn(TimeZone.currentSystemDefault()) + .toEpochMilliseconds(), selectableDates = object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { - val localDate = Instant.fromEpochMilliseconds(utcTimeMillis).toLocalDateTime(TimeZone.UTC).date + val localDate = Instant.fromEpochMilliseconds(utcTimeMillis) + .toLocalDateTime(TimeZone.UTC).date return dateValidator(localDate) } }, @@ -226,7 +245,8 @@ private fun ArchiveGroupHeader( showDatePicker = false datePickerState.selectedDateMillis?.let { onDateSelected( - Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.currentSystemDefault()).date, + Instant.fromEpochMilliseconds(it) + .toLocalDateTime(TimeZone.currentSystemDefault()).date, ) } }, @@ -245,6 +265,7 @@ private fun ArchiveGroupHeader( } } +@Suppress("KotlinConstantConditions") @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ArchiveGroupAbiSelector( @@ -261,43 +282,47 @@ private fun ArchiveGroupAbiSelector( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth(), ) { - SingleChoiceSegmentedButtonRow { - apks.forEachIndexed { index, apk -> - val colors = - if (!apk.abi.isSupported) { - SegmentedButtonDefaults.colors( - inactiveContainerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.4f), - activeContainerColor = MaterialTheme.colorScheme.error, - inactiveContentColor = MaterialTheme.colorScheme.onSurface, - activeContentColor = MaterialTheme.colorScheme.onError, - ) - } else { - SegmentedButtonDefaults.colors() - } - SegmentedButton( - selected = selectedIndex == index, - onClick = { selectedIndex = index }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = apks.size), - colors = colors, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { + if (FeatureFlags.SHOW_ABI_SELECTOR) { + SingleChoiceSegmentedButtonRow { + apks.forEachIndexed { index, apk -> + val colors = if (!apk.abi.isSupported) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = stringResource(R.string.unsupported_abi), - modifier = Modifier.size(ButtonDefaults.IconSize).padding(end = 4.dp), + SegmentedButtonDefaults.colors( + inactiveContainerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.4f), + activeContainerColor = MaterialTheme.colorScheme.error, + inactiveContentColor = MaterialTheme.colorScheme.onSurface, + activeContentColor = MaterialTheme.colorScheme.onError, + ) + } else { + SegmentedButtonDefaults.colors() + } + SegmentedButton( + selected = selectedIndex == index, + onClick = { selectedIndex = index }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = apks.size), + colors = colors, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (!apk.abi.isSupported) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = stringResource(R.string.unsupported_abi), + modifier = Modifier + .size(ButtonDefaults.IconSize) + .padding(end = 4.dp), + ) + } + Text( + text = apk.abi.name ?: "", + style = MaterialTheme.typography.labelSmall, ) } - Text( - text = apk.abi.name ?: "", - style = MaterialTheme.typography.labelSmall, - ) } } } - } - Spacer(Modifier.height(ArchiveGroupCardTokens.SpacerHeight)) + Spacer(Modifier.height(ArchiveGroupCardTokens.SpacerHeight)) + } Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { if (appState?.isInstalled == true) { @@ -326,6 +351,8 @@ private fun ArchiveGroupAbiSelector( private fun getFriendlyAppName(appName: String): String = when (appName) { FENIX -> stringResource(id = R.string.app_name_fenix) + FENIX_RELEASE -> stringResource(R.string.app_name_fenix_release) + FENIX_BETA -> stringResource(R.string.app_name_fenix_beta) FOCUS -> stringResource(id = R.string.app_name_focus) REFERENCE_BROWSER -> stringResource(R.string.app_name_reference_browser) else -> appName diff --git a/app/src/main/java/org/mozilla/tryfox/ui/screens/HomeViewModel.kt b/app/src/main/java/org/mozilla/tryfox/ui/screens/HomeViewModel.kt index a6c8a5b..f6079f0 100644 --- a/app/src/main/java/org/mozilla/tryfox/ui/screens/HomeViewModel.kt +++ b/app/src/main/java/org/mozilla/tryfox/ui/screens/HomeViewModel.kt @@ -18,22 +18,24 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.format.FormatStringsInDatetimeFormats import kotlinx.datetime.format.byUnicodePattern import kotlinx.datetime.todayIn -import org.mozilla.tryfox.data.DateAwareReleaseRepository -import org.mozilla.tryfox.data.DownloadFileRepository import org.mozilla.tryfox.data.DownloadState import org.mozilla.tryfox.data.MozillaPackageManager import org.mozilla.tryfox.data.NetworkResult -import org.mozilla.tryfox.data.ReleaseRepository import org.mozilla.tryfox.data.managers.CacheManager import org.mozilla.tryfox.data.managers.IntentManager +import org.mozilla.tryfox.data.repositories.DateAwareReleaseRepository +import org.mozilla.tryfox.data.repositories.DownloadFileRepository +import org.mozilla.tryfox.data.repositories.ReleaseRepository import org.mozilla.tryfox.model.CacheManagementState -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.ui.models.AbiUiModel import org.mozilla.tryfox.ui.models.ApkUiModel import org.mozilla.tryfox.ui.models.ApksResult import org.mozilla.tryfox.ui.models.AppUiModel import org.mozilla.tryfox.ui.models.newVersionAvailable import org.mozilla.tryfox.util.FENIX +import org.mozilla.tryfox.util.FENIX_BETA +import org.mozilla.tryfox.util.FENIX_RELEASE import org.mozilla.tryfox.util.FOCUS import org.mozilla.tryfox.util.REFERENCE_BROWSER import org.mozilla.tryfox.util.TRYFOX @@ -154,6 +156,8 @@ class HomeViewModel( private suspend fun fetchData() { val appInfoMap = mapOf( FENIX to mozillaPackageManager.fenix, + FENIX_RELEASE to mozillaPackageManager.fenixRelease, + FENIX_BETA to mozillaPackageManager.fenixBeta, FOCUS to mozillaPackageManager.focus, REFERENCE_BROWSER to mozillaPackageManager.referenceBrowser, TRYFOX to mozillaPackageManager.tryfox, @@ -220,7 +224,7 @@ class HomeViewModel( } } - private fun getLatestApks(apks: List): List { + private fun getLatestApks(apks: List): List { if (apks.isEmpty()) { return emptyList() } else if (apks.none { it.rawDateString != null }) { @@ -230,7 +234,7 @@ class HomeViewModel( return apks.filter { it.rawDateString == latestDateString } } - private fun convertParsedApksToUiModels(parsedApks: List): List { + private fun convertParsedApksToUiModels(parsedApks: List): List { return parsedApks.map { parsedApk -> val date = parsedApk.rawDateString?.formatApkDate() val isCompatible = supportedAbis.any { deviceAbi -> @@ -415,7 +419,7 @@ class HomeViewModel( private fun updateDate( appName: String, date: LocalDate?, - getReleases: suspend (LocalDate?) -> NetworkResult>, + getReleases: suspend (LocalDate?) -> NetworkResult>, ) { viewModelScope.launch(ioDispatcher) { val currentState = _homeScreenState.value as? HomeScreenState.Loaded ?: return@launch diff --git a/app/src/main/java/org/mozilla/tryfox/ui/screens/ProfileViewModel.kt b/app/src/main/java/org/mozilla/tryfox/ui/screens/ProfileViewModel.kt index ee45036..a6195d2 100644 --- a/app/src/main/java/org/mozilla/tryfox/ui/screens/ProfileViewModel.kt +++ b/app/src/main/java/org/mozilla/tryfox/ui/screens/ProfileViewModel.kt @@ -14,13 +14,13 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import logcat.LogPriority import logcat.logcat -import org.mozilla.tryfox.data.DownloadFileRepository import org.mozilla.tryfox.data.DownloadState -import org.mozilla.tryfox.data.IFenixRepository import org.mozilla.tryfox.data.NetworkResult -import org.mozilla.tryfox.data.UserDataRepository import org.mozilla.tryfox.data.managers.CacheManager import org.mozilla.tryfox.data.managers.IntentManager +import org.mozilla.tryfox.data.repositories.DownloadFileRepository +import org.mozilla.tryfox.data.repositories.TreeherderRepository +import org.mozilla.tryfox.data.repositories.UserDataRepository import org.mozilla.tryfox.model.CacheManagementState import org.mozilla.tryfox.ui.models.AbiUiModel import org.mozilla.tryfox.ui.models.ArtifactUiModel @@ -38,7 +38,7 @@ import java.io.File * @param authorEmail The initial author email to search for, can be null. */ class ProfileViewModel( - private val fenixRepository: IFenixRepository, + private val fenixRepository: TreeherderRepository, private val downloadFileRepository: DownloadFileRepository, private val userDataRepository: UserDataRepository, private val cacheManager: CacheManager, diff --git a/app/src/main/java/org/mozilla/tryfox/util/Consts.kt b/app/src/main/java/org/mozilla/tryfox/util/Consts.kt index 99ac6fe..6586175 100644 --- a/app/src/main/java/org/mozilla/tryfox/util/Consts.kt +++ b/app/src/main/java/org/mozilla/tryfox/util/Consts.kt @@ -1,13 +1,17 @@ package org.mozilla.tryfox.util const val FENIX = "fenix" +const val FENIX_RELEASE = "fenix-release" +const val FENIX_BETA = "fenix-beta" const val FENIX_NIGHTLY = "fenix-nightly" const val FOCUS = "focus" const val REFERENCE_BROWSER = "reference-browser" const val TREEHERDER = "treeherder" const val TRYFOX = "TryFox" -const val FENIX_PACKAGE = "org.mozilla.fenix" -const val FOCUS_PACKAGE = "org.mozilla.focus.nightly" +const val FENIX_NIGHTLY_PACKAGE = "org.mozilla.fenix" +const val FENIX_RELEASE_PACKAGE = "org.mozilla.firefox" +const val FENIX_BETA_PACKAGE = "org.mozilla.firefox_beta" +const val FOCUS_NIGHTLY_PACKAGE = "org.mozilla.focus.nightly" const val REFERENCE_BROWSER_PACKAGE = "org.mozilla.reference.browser" const val TRYFOX_PACKAGE = "org.mozilla.tryfox" diff --git a/app/src/main/java/org/mozilla/tryfox/util/FeatureFlags.kt b/app/src/main/java/org/mozilla/tryfox/util/FeatureFlags.kt new file mode 100644 index 0000000..eae4db2 --- /dev/null +++ b/app/src/main/java/org/mozilla/tryfox/util/FeatureFlags.kt @@ -0,0 +1,5 @@ +package org.mozilla.tryfox.util + +object FeatureFlags { + const val SHOW_ABI_SELECTOR: Boolean = false +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index efeebad..f90fda7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ Download failed: %1$s Firefox Nightly Icon Firefox Icon + Firefox Beta Icon Focus Icon Reference Browser Icon App Icon @@ -53,6 +54,8 @@ Clear email field Fenix + Fenix Release + Fenix Beta Focus Reference Browser Unsupported ABI diff --git a/app/src/test/java/org/mozilla/tryfox/FenixReleaseTest.kt b/app/src/test/java/org/mozilla/tryfox/FenixReleaseTest.kt new file mode 100644 index 0000000..64eb0e5 --- /dev/null +++ b/app/src/test/java/org/mozilla/tryfox/FenixReleaseTest.kt @@ -0,0 +1,373 @@ +package org.mozilla.tryfox + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mozilla.tryfox.data.MozillaArchiveHtmlParser +import org.mozilla.tryfox.data.NetworkResult +import org.mozilla.tryfox.data.ReleaseType +import org.mozilla.tryfox.data.repositories.DefaultMozillaArchiveRepository +import org.mozilla.tryfox.network.MozillaArchivesApiService + +class FenixReleaseTest { + + private val parser = MozillaArchiveHtmlParser() + + // Tests for getFenixReleaseBuilds + + @Test + fun `test getFenixReleaseBuilds returns list of APKs for beta release`() = runBlocking { + val mockApiService: MozillaArchivesApiService = mock() + val releasesListHtml = loadHtmlResource("fenix-releases-page.html") + val releaseDetailsHtml = loadHtmlResource("fenix-releases-145.html") + + whenever(mockApiService.getHtmlPage(DefaultMozillaArchiveRepository.RELEASES_FENIX_BASE_URL)) + .thenReturn(releasesListHtml) + whenever(mockApiService.getHtmlPage(DefaultMozillaArchiveRepository.archiveUrlForRelease("146.0b5"))) + .thenReturn(releaseDetailsHtml) + + val repository = DefaultMozillaArchiveRepository(mockApiService) + val result = repository.getFenixReleaseBuilds(ReleaseType.Beta) + + assertTrue(result is NetworkResult.Success) + if (result is NetworkResult.Success) { + println("Fenix Release APKs: ${result.data}") + assertEquals(4, result.data.size, "Should have 4 ABIs") + assertTrue(result.data.any { it.abiName == "arm64-v8a" }) + assertTrue(result.data.any { it.abiName == "universal" }) + } + } + + @Test + fun `test getFenixReleaseBuilds returns list of APKs for stable release`() = runBlocking { + val mockApiService: MozillaArchivesApiService = mock() + val releasesListHtml = loadHtmlResource("fenix-releases-page.html") + val releaseDetailsHtml = loadHtmlResource("fenix-releases-145.html") + + whenever(mockApiService.getHtmlPage(DefaultMozillaArchiveRepository.RELEASES_FENIX_BASE_URL)) + .thenReturn(releasesListHtml) + whenever(mockApiService.getHtmlPage(DefaultMozillaArchiveRepository.archiveUrlForRelease("145.0.1"))) + .thenReturn(releaseDetailsHtml) + + val repository = DefaultMozillaArchiveRepository(mockApiService) + val result = repository.getFenixReleaseBuilds(ReleaseType.Release) + + assertTrue(result is NetworkResult.Success) + if (result is NetworkResult.Success) { + println("Fenix Stable Release APKs: ${result.data}") + assertEquals(4, result.data.size, "Should have 4 ABIs") + assertTrue(result.data.any { it.version == "145.0.1" }) + } + } + + @Test + fun `test constructed release APKs have correct URLs`() = runBlocking { + val mockApiService: MozillaArchivesApiService = mock() + val releasesListHtml = loadHtmlResource("fenix-releases-page.html") + val releaseDetailsHtml = loadHtmlResource("fenix-releases-145.html") + + whenever(mockApiService.getHtmlPage(DefaultMozillaArchiveRepository.RELEASES_FENIX_BASE_URL)) + .thenReturn(releasesListHtml) + whenever(mockApiService.getHtmlPage(DefaultMozillaArchiveRepository.archiveUrlForRelease("146.0b5"))) + .thenReturn(releaseDetailsHtml) + + val repository = DefaultMozillaArchiveRepository(mockApiService) + val result = repository.getFenixReleaseBuilds(ReleaseType.Beta) + + assertTrue(result is NetworkResult.Success) + if (result is NetworkResult.Success) { + val arm64Apk = result.data.find { it.abiName == "arm64-v8a" } + assertNotNull(arm64Apk) + assertTrue(arm64Apk!!.fullUrl.contains("fenix-146.0b5-android-arm64-v8a")) + assertTrue(arm64Apk.fullUrl.contains("fenix-146.0b5.multi.android-arm64-v8a.apk")) + } + } + + // Tests for parseFenixReleasesFromHtml + + @Test + fun `test parseFenixReleasesFromHtml with releases-page html returns latest version for both Beta and Release types`() { + val htmlContent = loadHtmlResource("fenix-releases-page.html") + + // Test with ReleaseType.Beta - should return latest beta only + val resultBeta = parser.parseFenixReleasesFromHtml(htmlContent, ReleaseType.Beta) + println("Latest Fenix Beta Release: $resultBeta") + assertEquals("146.0b5", resultBeta) + + // Test with ReleaseType.Release - should return latest stable release only + val resultRelease = parser.parseFenixReleasesFromHtml(htmlContent, ReleaseType.Release) + println("Latest stable Fenix Release: $resultRelease") + assertEquals("145.0.1", resultRelease) + } + + // Tests for parseFenixReleaseAbisFromHtml + + @Test + fun `test parseFenixReleaseAbisFromHtml extracts all ABIs from release HTML`() { + val htmlContent = loadHtmlResource("fenix-releases-145.html") + + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + + println("Extracted ABIs: $result") + assertEquals(4, result.size, "Should extract 4 ABIs") + assertTrue(result.contains("arm64-v8a"), "Should contain arm64-v8a") + assertTrue(result.contains("armeabi-v7a"), "Should contain armeabi-v7a") + assertTrue(result.contains("x86_64"), "Should contain x86_64") + assertTrue(result.contains("universal"), "Should contain universal (no ABI suffix)") + } + + @Test + fun `test parseFenixReleaseAbisFromHtml extracts in correct order`() { + val htmlContent = loadHtmlResource("fenix-releases-145.html") + + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + + // The order should match the order in the HTML + val expected = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "universal") + assertEquals(expected, result, "ABIs should be in the order they appear in HTML") + } + + @Test + fun `test parseFenixReleaseAbisFromHtml with different app name`() { + // This test demonstrates the function works with different app names + val htmlContent = loadHtmlResource("fenix-releases-145.html") + + // Using "fenix" app name (correct one) + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + assertEquals(4, result.size, "Should extract 4 ABIs with correct app name") + + // Using "focus" app name (shouldn't match fenix entries) + val resultFocus = parser.parseFenixReleaseAbisFromHtml(htmlContent, "focus") + assertEquals(0, resultFocus.size, "Should extract 0 ABIs with wrong app name") + } + + @Test + fun `test parseFenixReleaseAbisFromHtml empty HTML returns empty list`() { + val htmlContent = "" + + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + + assertEquals(0, result.size, "Should return empty list for HTML with no matching entries") + } + + @Test + fun `test parseFenixReleaseAbisFromHtml identifies universal ABI correctly`() { + val htmlContent = loadHtmlResource("fenix-releases-145.html") + + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + + // The last entry should be "universal" (fenix-145.0-android/) + assertTrue(result.last() == "universal", "Last ABI should be universal") + } + + @Test + fun `test parseFenixReleaseAbisFromHtml extracts all ABIs from beta release HTML`() { + val htmlContent = loadHtmlResource("fenix-releases-146b5.html") + + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + + println("Extracted ABIs from beta release: $result") + assertEquals(4, result.size, "Should extract 4 ABIs from beta release") + assertTrue(result.contains("arm64-v8a"), "Should contain arm64-v8a") + assertTrue(result.contains("armeabi-v7a"), "Should contain armeabi-v7a") + assertTrue(result.contains("x86_64"), "Should contain x86_64") + assertTrue(result.contains("universal"), "Should contain universal (no ABI suffix)") + } + + @Test + fun `test parseFenixReleaseAbisFromHtml handles beta markers correctly`() { + val htmlContent = loadHtmlResource("fenix-releases-146b5.html") + + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + + // Verify the order matches the HTML + val expected = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "universal") + assertEquals(expected, result, "ABIs should be extracted correctly from beta release with version markers like 146.0b5") + } + + @Test + fun `test parseFenixReleaseAbisFromHtml identifies universal ABI in beta release`() { + val htmlContent = loadHtmlResource("fenix-releases-146b5.html") + + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + + // The last entry should be "universal" (fenix-146.0b5-android/) + assertTrue(result.last() == "universal", "Last ABI in beta release should be universal") + } + + @Test + fun `test parseFenixReleaseAbisFromHtml extracts specific ABI from beta pattern`() { + val htmlContent = loadHtmlResource("fenix-releases-146b5.html") + + val result = parser.parseFenixReleaseAbisFromHtml(htmlContent, "fenix") + + // Verify specific ABIs are correctly extracted from beta version format + assertTrue(result.contains("arm64-v8a"), "fenix-146.0b5-android-arm64-v8a/ should extract arm64-v8a") + assertTrue(result.contains("armeabi-v7a"), "fenix-146.0b5-android-armeabi-v7a/ should extract armeabi-v7a") + assertTrue(result.contains("x86_64"), "fenix-146.0b5-android-x86_64/ should extract x86_64") + } + + // Tests for compareReleaseVersions + + @Test + fun `compareReleaseVersions - equal versions return 0`() { + val result = parser.compareReleaseVersions("145.0", "145.0") + assertEquals(0, result, "Equal versions should return 0") + } + + @Test + fun `compareReleaseVersions - first version greater by major version`() { + val result = parser.compareReleaseVersions("146.0", "145.0") + assertTrue(result > 0, "146.0 should be greater than 145.0") + } + + @Test + fun `compareReleaseVersions - first version smaller by major version`() { + val result = parser.compareReleaseVersions("145.0", "146.0") + assertTrue(result < 0, "145.0 should be smaller than 146.0") + } + + @Test + fun `compareReleaseVersions - first version greater by minor version`() { + val result = parser.compareReleaseVersions("145.1", "145.0") + assertTrue(result > 0, "145.1 should be greater than 145.0") + } + + @Test + fun `compareReleaseVersions - first version smaller by minor version`() { + val result = parser.compareReleaseVersions("145.0", "145.1") + assertTrue(result < 0, "145.0 should be smaller than 145.1") + } + + @Test + fun `compareReleaseVersions - first version greater by patch version`() { + val result = parser.compareReleaseVersions("145.0.1", "145.0") + assertTrue(result > 0, "145.0.1 should be greater than 145.0") + } + + @Test + fun `compareReleaseVersions - first version smaller by patch version`() { + val result = parser.compareReleaseVersions("145.0", "145.0.1") + assertTrue(result < 0, "145.0 should be smaller than 145.0.1") + } + + @Test + fun `compareReleaseVersions - beta versions - first greater`() { + val result = parser.compareReleaseVersions("145.0b5", "145.0b1") + assertTrue(result > 0, "145.0b5 should be greater than 145.0b1") + } + + @Test + fun `compareReleaseVersions - beta versions - first smaller`() { + val result = parser.compareReleaseVersions("145.0b1", "145.0b5") + assertTrue(result < 0, "145.0b1 should be smaller than 145.0b5") + } + + @Test + fun `compareReleaseVersions - beta versions equal`() { + val result = parser.compareReleaseVersions("145.0b3", "145.0b3") + assertEquals(0, result, "Equal beta versions should return 0") + } + + @Test + fun `compareReleaseVersions - same major minor, different beta numbers`() { + val result = parser.compareReleaseVersions("146.0b2", "146.0b10") + assertTrue(result < 0, "146.0b2 should be less than 146.0b10") + } + + @Test + fun `compareReleaseVersions - major version difference dominates beta vs beta`() { + val result = parser.compareReleaseVersions("146.0b1", "145.0b9") + assertTrue(result > 0, "146.0b1 should be greater than 145.0b9") + } + + @Test + fun `compareReleaseVersions - comparing beta versions with version dash separator`() { + val result = parser.compareReleaseVersions("145.0b5", "145.0-b1") + assertTrue(result > 0, "145.0b5 should be greater than 145.0-b1 (5 > 1)") + } + + @Test + fun `compareReleaseVersions - three part version comparison`() { + val result = parser.compareReleaseVersions("145.0.2", "145.0.1") + assertTrue(result > 0, "145.0.2 should be greater than 145.0.1") + } + + @Test + fun `compareReleaseVersions - complex version strings with multiple separators`() { + val result = parser.compareReleaseVersions("124.0b9", "126.0b5") + assertTrue(result < 0, "124.0b9 should be less than 126.0b5") + } + + @Test + fun `compareReleaseVersions - major version difference takes precedence`() { + val result = parser.compareReleaseVersions("146.0b1", "145.0.9") + assertTrue(result > 0, "146.0b1 should be greater than 145.0.9") + } + + @Test + fun `compareReleaseVersions - minor version difference when major equal`() { + val result = parser.compareReleaseVersions("145.1b5", "145.0.9") + assertTrue(result > 0, "145.1b5 should be greater than 145.0.9") + } + + @Test + fun `compareReleaseVersions - patch version comparison with beta`() { + val result = parser.compareReleaseVersions("145.0.2b5", "145.0.1b9") + assertTrue(result > 0, "145.0.2b5 should be greater than 145.0.1b9") + } + + @Test + fun `compareReleaseVersions - beta numbers compared when base versions equal`() { + val result = parser.compareReleaseVersions("145.0b5", "145.0b3") + assertTrue(result > 0, "145.0b5 should be greater than 145.0b3") + } + + @Test + fun `compareReleaseVersions - all parts equal returns 0`() { + val result = parser.compareReleaseVersions("145.0.1b5", "145.0.1b5") + assertEquals(0, result, "Identical complex versions should return 0") + } + + @Test + fun `compareReleaseVersions - higher beta minor number beats lower with zero`() { + val result = parser.compareReleaseVersions("145.0b10", "145.0b9") + assertTrue(result > 0, "145.0b10 should be greater than 145.0b9") + } + + @Test + fun `compareReleaseVersions - sequence preserves transitive ordering`() { + // Verify that comparison is transitive: if a > b and b > c, then a > c + val v1 = parser.compareReleaseVersions("146.0", "145.1") + val v2 = parser.compareReleaseVersions("145.1", "145.0") + val v3 = parser.compareReleaseVersions("146.0", "145.0") + + assertTrue(v1 > 0, "146.0 > 145.1") + assertTrue(v2 > 0, "145.1 > 145.0") + assertTrue(v3 > 0, "146.0 > 145.0") + } + + @Test + fun `compareReleaseVersions - versions with different structures`() { + val result = parser.compareReleaseVersions("145.0.1", "145.0b5") + // This comparison depends on how the function splits the string + // "145.0.1" splits to [145, 0, 1] + // "145.0b5" splits to [145, 0, 5] + // So 1 < 5, result should be negative + assertTrue(result < 0, "145.0.1 should be less than 145.0b5 based on numeric parts") + } + + // Helper method + + private fun loadHtmlResource(resourceName: String): String { + return this::class.java.classLoader?.getResource(resourceName)?.readText() + ?: throw IllegalArgumentException("Resource not found: $resourceName") + } + + private fun assertNotNull(value: Any?) { + assertTrue(value != null, "Value should not be null") + } +} diff --git a/app/src/test/java/org/mozilla/tryfox/data/FakeDownloadFileRepository.kt b/app/src/test/java/org/mozilla/tryfox/data/FakeDownloadFileRepository.kt index 4bb4a51..0ed5d71 100644 --- a/app/src/test/java/org/mozilla/tryfox/data/FakeDownloadFileRepository.kt +++ b/app/src/test/java/org/mozilla/tryfox/data/FakeDownloadFileRepository.kt @@ -1,6 +1,7 @@ package org.mozilla.tryfox.data import kotlinx.coroutines.delay +import org.mozilla.tryfox.data.repositories.DownloadFileRepository import java.io.File class FakeDownloadFileRepository( diff --git a/app/src/test/java/org/mozilla/tryfox/data/FakeMozillaArchiveRepository.kt b/app/src/test/java/org/mozilla/tryfox/data/FakeMozillaArchiveRepository.kt index 3e44526..fa32238 100644 --- a/app/src/test/java/org/mozilla/tryfox/data/FakeMozillaArchiveRepository.kt +++ b/app/src/test/java/org/mozilla/tryfox/data/FakeMozillaArchiveRepository.kt @@ -1,21 +1,27 @@ package org.mozilla.tryfox.data import kotlinx.datetime.LocalDate -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.repositories.MozillaArchiveRepository +import org.mozilla.tryfox.model.MozillaArchiveApk /** - * A fake implementation of [MozillaArchiveRepository] for use in unit tests. + * A fake implementation of [org.mozilla.tryfox.data.repositories.MozillaArchiveRepository] for use in unit tests. */ class FakeMozillaArchiveRepository( - private val fenixBuilds: NetworkResult> = NetworkResult.Success(emptyList()), - private val focusBuilds: NetworkResult> = NetworkResult.Success(emptyList()), + private val fenixBuilds: NetworkResult> = NetworkResult.Success(emptyList()), + private val focusBuilds: NetworkResult> = NetworkResult.Success(emptyList()), + private val fenixReleases: NetworkResult> = NetworkResult.Success(emptyList()), ) : MozillaArchiveRepository { - override suspend fun getFenixNightlyBuilds(date: LocalDate?): NetworkResult> { + override suspend fun getFenixNightlyBuilds(date: LocalDate?): NetworkResult> { return fenixBuilds } - override suspend fun getFocusNightlyBuilds(date: LocalDate?): NetworkResult> { + override suspend fun getFocusNightlyBuilds(date: LocalDate?): NetworkResult> { return focusBuilds } + + override suspend fun getFenixReleaseBuilds(releaseType: ReleaseType): NetworkResult> { + return fenixReleases + } } diff --git a/app/src/test/java/org/mozilla/tryfox/data/FakeReferenceBrowserReleaseRepository.kt b/app/src/test/java/org/mozilla/tryfox/data/FakeReferenceBrowserReleaseRepository.kt index 8befc82..e739a8d 100644 --- a/app/src/test/java/org/mozilla/tryfox/data/FakeReferenceBrowserReleaseRepository.kt +++ b/app/src/test/java/org/mozilla/tryfox/data/FakeReferenceBrowserReleaseRepository.kt @@ -1,18 +1,19 @@ package org.mozilla.tryfox.data -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.repositories.ReleaseRepository +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.util.REFERENCE_BROWSER /** - * A fake implementation of [ReferenceBrowserReleaseRepository] for use in unit tests. + * A fake implementation of [org.mozilla.tryfox.data.repositories.ReferenceBrowserReleaseRepository] for use in unit tests. */ class FakeReferenceBrowserReleaseRepository( - private val releases: NetworkResult> = NetworkResult.Success(emptyList()), + private val releases: NetworkResult> = NetworkResult.Success(emptyList()), ) : ReleaseRepository { override val appName: String = REFERENCE_BROWSER - override suspend fun getLatestReleases(): NetworkResult> { + override suspend fun getLatestReleases(): NetworkResult> { return releases } } diff --git a/app/src/test/java/org/mozilla/tryfox/data/FakeTryFoxReleaseRepository.kt b/app/src/test/java/org/mozilla/tryfox/data/FakeTryFoxReleaseRepository.kt index 5e4ab46..531181a 100644 --- a/app/src/test/java/org/mozilla/tryfox/data/FakeTryFoxReleaseRepository.kt +++ b/app/src/test/java/org/mozilla/tryfox/data/FakeTryFoxReleaseRepository.kt @@ -1,18 +1,19 @@ package org.mozilla.tryfox.data -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.repositories.ReleaseRepository +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.util.TRYFOX /** - * A fake implementation of [TryFoxReleaseRepository] for use in unit tests. + * A fake implementation of [org.mozilla.tryfox.data.repositories.TryFoxReleaseRepository] for use in unit tests. */ class FakeTryFoxReleaseRepository( - private val releases: NetworkResult> = NetworkResult.Success(emptyList()), + private val releases: NetworkResult> = NetworkResult.Success(emptyList()), ) : ReleaseRepository { override val appName: String = TRYFOX - override suspend fun getLatestReleases(): NetworkResult> { + override suspend fun getLatestReleases(): NetworkResult> { return releases } } diff --git a/app/src/test/java/org/mozilla/tryfox/data/MozillaArchiveRepositoryImplTest.kt b/app/src/test/java/org/mozilla/tryfox/data/MozillaArchiveRepositoryImplTest.kt index adeb8ac..51812bb 100644 --- a/app/src/test/java/org/mozilla/tryfox/data/MozillaArchiveRepositoryImplTest.kt +++ b/app/src/test/java/org/mozilla/tryfox/data/MozillaArchiveRepositoryImplTest.kt @@ -19,7 +19,8 @@ import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.eq import org.mockito.kotlin.whenever -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.data.repositories.DefaultMozillaArchiveRepository +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.network.MozillaArchivesApiService import retrofit2.HttpException import retrofit2.Response @@ -43,7 +44,7 @@ class MozillaArchiveRepositoryImplTest { @Mock private lateinit var mockMozillaArchivesApiService: MozillaArchivesApiService - private lateinit var repository: MozillaArchiveRepositoryImpl + private lateinit var repository: DefaultMozillaArchiveRepository // Test constants for Fenix private val fenixVersion = "125.0a1" @@ -66,7 +67,7 @@ class MozillaArchiveRepositoryImplTest { fun setUp() { val testDate = LocalDate(2023, 10, 1) val clock = FixedClock(testDate.atStartOfDayIn(TimeZone.UTC)) - repository = MozillaArchiveRepositoryImpl(mockMozillaArchivesApiService, clock) + repository = DefaultMozillaArchiveRepository(mockMozillaArchivesApiService, clock) } private fun createMockHtmlResponse(vararg dirStrings: String): String { @@ -110,8 +111,8 @@ class MozillaArchiveRepositoryImplTest { assertEquals(2, apks.size) val expectedApks = listOf( - ParsedNightlyApk(fenixDirString1, DATE, FENIX, fenixVersion, "arm64-v8a", fenixFullUrl1, fenixFileName1), - ParsedNightlyApk(fenixDirString2, DATE, FENIX, fenixVersion, "x86_64", fenixFullUrl2, fenixFileName2), + MozillaArchiveApk(fenixDirString1, DATE, FENIX, fenixVersion, "arm64-v8a", fenixFullUrl1, fenixFileName1), + MozillaArchiveApk(fenixDirString2, DATE, FENIX, fenixVersion, "x86_64", fenixFullUrl2, fenixFileName2), ) assertEquals(expectedApks.first(), apks.first()) assertTrue(apks.containsAll(expectedApks) && expectedApks.containsAll(apks)) @@ -163,12 +164,12 @@ class MozillaArchiveRepositoryImplTest { // Given val testDate = LocalDate(2024, 3, 15) val clock = FixedClock(testDate.atStartOfDayIn(TimeZone.UTC)) - val repository = MozillaArchiveRepositoryImpl(mockMozillaArchivesApiService, clock) + val repository = DefaultMozillaArchiveRepository(mockMozillaArchivesApiService, clock) - val currentMonthUrl = MozillaArchiveRepositoryImpl.archiveUrlForDate(FENIX, testDate) + val currentMonthUrl = DefaultMozillaArchiveRepository.archiveUrlForDate(FENIX, testDate) val previousMonthDate = testDate.minus(1, DateTimeUnit.MONTH) - val previousMonthUrl = MozillaArchiveRepositoryImpl.archiveUrlForDate(FENIX, previousMonthDate) + val previousMonthUrl = DefaultMozillaArchiveRepository.archiveUrlForDate(FENIX, previousMonthDate) val previousMonthDateString = "2024-02-28-10-00-00" val previousMonthDirString = "$previousMonthDateString-$FENIX-$fenixVersion-android-arm64-v8a/" diff --git a/app/src/test/java/org/mozilla/tryfox/data/managers/FakeUserDataRepository.kt b/app/src/test/java/org/mozilla/tryfox/data/managers/FakeUserDataRepository.kt index 165d69b..b87d9bd 100644 --- a/app/src/test/java/org/mozilla/tryfox/data/managers/FakeUserDataRepository.kt +++ b/app/src/test/java/org/mozilla/tryfox/data/managers/FakeUserDataRepository.kt @@ -2,7 +2,7 @@ package org.mozilla.tryfox.data.managers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import org.mozilla.tryfox.data.UserDataRepository +import org.mozilla.tryfox.data.repositories.UserDataRepository /** * A fake implementation of [UserDataRepository] for testing purposes. diff --git a/app/src/test/java/org/mozilla/tryfox/ui/screens/FakeMozillaPackageManager.kt b/app/src/test/java/org/mozilla/tryfox/ui/screens/FakeMozillaPackageManager.kt index a93fa22..d6c6cce 100644 --- a/app/src/test/java/org/mozilla/tryfox/ui/screens/FakeMozillaPackageManager.kt +++ b/app/src/test/java/org/mozilla/tryfox/ui/screens/FakeMozillaPackageManager.kt @@ -4,22 +4,34 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.mozilla.tryfox.data.MozillaPackageManager import org.mozilla.tryfox.model.AppState +import org.mozilla.tryfox.util.FENIX_BETA_PACKAGE +import org.mozilla.tryfox.util.FENIX_NIGHTLY_PACKAGE +import org.mozilla.tryfox.util.FENIX_RELEASE_PACKAGE +import org.mozilla.tryfox.util.FOCUS_NIGHTLY_PACKAGE +import org.mozilla.tryfox.util.REFERENCE_BROWSER_PACKAGE +import org.mozilla.tryfox.util.TRYFOX_PACKAGE class FakeMozillaPackageManager( private val apps: Map = emptyMap(), ) : MozillaPackageManager { override val fenix: AppState - get() = apps["org.mozilla.fenix"] ?: AppState("Fenix", "org.mozilla.fenix", null, null) + get() = apps[FENIX_NIGHTLY_PACKAGE] ?: AppState("Fenix", FENIX_NIGHTLY_PACKAGE, null, null) + + override val fenixRelease: AppState + get() = apps[FENIX_RELEASE_PACKAGE] ?: AppState("Firefox", FENIX_RELEASE_PACKAGE, null, null) + + override val fenixBeta: AppState + get() = apps[FENIX_BETA_PACKAGE] ?: AppState("Firefox Beta", FENIX_BETA_PACKAGE, null, null) override val focus: AppState - get() = apps["org.mozilla.focus.nightly"] ?: AppState("Focus", "org.mozilla.focus.nightly", null, null) + get() = apps[FOCUS_NIGHTLY_PACKAGE] ?: AppState("Focus", FOCUS_NIGHTLY_PACKAGE, null, null) override val referenceBrowser: AppState - get() = apps["org.mozilla.reference.browser"] ?: AppState("Reference Browser", "org.mozilla.reference.browser", null, null) + get() = apps[REFERENCE_BROWSER_PACKAGE] ?: AppState("Reference Browser", REFERENCE_BROWSER_PACKAGE, null, null) override val tryfox: AppState - get() = apps["org.mozilla.tryfox"] ?: AppState("TryFox", "org.mozilla.tryfox", null, null) + get() = apps[TRYFOX_PACKAGE] ?: AppState("TryFox", TRYFOX_PACKAGE, null, null) override val appStates: Flow = emptyFlow() diff --git a/app/src/test/java/org/mozilla/tryfox/ui/screens/HomeViewModelTest.kt b/app/src/test/java/org/mozilla/tryfox/ui/screens/HomeViewModelTest.kt index 976030d..9a09505 100644 --- a/app/src/test/java/org/mozilla/tryfox/ui/screens/HomeViewModelTest.kt +++ b/app/src/test/java/org/mozilla/tryfox/ui/screens/HomeViewModelTest.kt @@ -26,21 +26,21 @@ import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness -import org.mozilla.tryfox.data.DownloadFileRepository import org.mozilla.tryfox.data.DownloadState import org.mozilla.tryfox.data.FakeMozillaArchiveRepository import org.mozilla.tryfox.data.FakeReferenceBrowserReleaseRepository import org.mozilla.tryfox.data.FakeTryFoxReleaseRepository -import org.mozilla.tryfox.data.FenixReleaseRepository -import org.mozilla.tryfox.data.FocusReleaseRepository import org.mozilla.tryfox.data.MozillaPackageManager import org.mozilla.tryfox.data.NetworkResult -import org.mozilla.tryfox.data.ReleaseRepository import org.mozilla.tryfox.data.managers.FakeCacheManager import org.mozilla.tryfox.data.managers.FakeIntentManager +import org.mozilla.tryfox.data.repositories.DownloadFileRepository +import org.mozilla.tryfox.data.repositories.FenixReleaseRepository +import org.mozilla.tryfox.data.repositories.FocusReleaseRepository +import org.mozilla.tryfox.data.repositories.ReleaseRepository import org.mozilla.tryfox.model.AppState import org.mozilla.tryfox.model.CacheManagementState -import org.mozilla.tryfox.model.ParsedNightlyApk +import org.mozilla.tryfox.model.MozillaArchiveApk import org.mozilla.tryfox.ui.models.AbiUiModel import org.mozilla.tryfox.ui.models.ApkUiModel import org.mozilla.tryfox.ui.models.ApksResult @@ -83,7 +83,7 @@ class HomeViewModelTest { dateRaw: String?, version: String, abi: String, - ): ParsedNightlyApk { + ): MozillaArchiveApk { val fileName = if (appName == testReferenceBrowserAppName) { "target.$abi.apk" } else { @@ -100,7 +100,7 @@ class HomeViewModelTest { "http://fake.url/$dateRaw-$appName-$version-android-$abi/$fileName" } - return ParsedNightlyApk( + return MozillaArchiveApk( originalString = originalString, rawDateString = if (appName == testReferenceBrowserAppName) null else dateRaw, appName = appName, @@ -112,7 +112,7 @@ class HomeViewModelTest { } private fun createTestApkUiModel( - parsed: ParsedNightlyApk, + parsed: MozillaArchiveApk, downloadState: DownloadState = DownloadState.NotDownloaded, ): ApkUiModel { val dateFormatted = parsed.rawDateString?.formatApkDateForTest() ?: "" diff --git a/app/src/test/java/org/mozilla/tryfox/ui/screens/ProfileViewModelTest.kt b/app/src/test/java/org/mozilla/tryfox/ui/screens/ProfileViewModelTest.kt index d13b581..2682f1d 100644 --- a/app/src/test/java/org/mozilla/tryfox/ui/screens/ProfileViewModelTest.kt +++ b/app/src/test/java/org/mozilla/tryfox/ui/screens/ProfileViewModelTest.kt @@ -12,10 +12,10 @@ import org.junit.jupiter.api.io.TempDir import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mozilla.tryfox.data.FakeDownloadFileRepository -import org.mozilla.tryfox.data.IFenixRepository import org.mozilla.tryfox.data.managers.FakeCacheManager import org.mozilla.tryfox.data.managers.FakeIntentManager import org.mozilla.tryfox.data.managers.FakeUserDataRepository +import org.mozilla.tryfox.data.repositories.TreeherderRepository import java.io.File @ExperimentalCoroutinesApi @@ -26,7 +26,7 @@ class ProfileViewModelTest { private lateinit var cacheManager: FakeCacheManager @Mock - private lateinit var fenixRepository: IFenixRepository + private lateinit var fenixRepository: TreeherderRepository private val userDataRepository = FakeUserDataRepository() diff --git a/app/src/test/resources/fenix-releases-145.html b/app/src/test/resources/fenix-releases-145.html new file mode 100644 index 0000000..f35044b --- /dev/null +++ b/app/src/test/resources/fenix-releases-145.html @@ -0,0 +1,56 @@ + + + + + Directory Listing: /pub/fenix/releases/145.0/android/ + + +

Index of /pub/fenix/releases/145.0/android/

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeNameSizeLast Modified
Dir..
Dirfenix-145.0-android-arm64-v8a/
Dirfenix-145.0-android-armeabi-v7a/
Dirfenix-145.0-android-x86_64/
Dirfenix-145.0-android/
+ + \ No newline at end of file diff --git a/app/src/test/resources/fenix-releases-146b5.html b/app/src/test/resources/fenix-releases-146b5.html new file mode 100644 index 0000000..9cbce15 --- /dev/null +++ b/app/src/test/resources/fenix-releases-146b5.html @@ -0,0 +1,56 @@ + + + + + Directory Listing: /pub/fenix/releases/146.0b5/android/ + + +

Index of /pub/fenix/releases/146.0b5/android/

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeNameSizeLast Modified
Dir..
Dirfenix-146.0b5-android-arm64-v8a/
Dirfenix-146.0b5-android-armeabi-v7a/
Dirfenix-146.0b5-android-x86_64/
Dirfenix-146.0b5-android/
+ + diff --git a/app/src/test/resources/fenix-releases-page.html b/app/src/test/resources/fenix-releases-page.html new file mode 100644 index 0000000..6af061a --- /dev/null +++ b/app/src/test/resources/fenix-releases-page.html @@ -0,0 +1,676 @@ + + + + + Directory Listing: /pub/fenix/releases/ + + +

Index of /pub/fenix/releases/

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeNameSizeLast Modified
Dir..
Dir100.0.0-beta.1/
Dir100.0.0-beta.2/
Dir100.0.0-beta.3/
Dir102.0.0-beta.5/
Dir102.1.0/
Dir102.1.1/
Dir102.2.0/
Dir102.2.1/
Dir103.0.0-beta.5/
Dir103.1.0/
Dir126.0b2/
Dir139.0b9/
Dir140.0.3/
Dir140.0.4/
Dir140.0/
Dir140.0b1/
Dir140.0b10/
Dir140.0b2/
Dir140.0b3/
Dir140.0b4/
Dir140.0b5/
Dir140.0b6/
Dir140.0b7/
Dir140.0b8/
Dir140.0b9/
Dir141.0.1/
Dir141.0.2/
Dir141.0.3/
Dir141.0/
Dir141.0b1/
Dir141.0b2/
Dir141.0b3/
Dir141.0b4/
Dir141.0b5/
Dir141.0b6/
Dir141.0b7/
Dir141.0b8/
Dir141.0b9/
Dir142.0.1/
Dir142.0/
Dir142.0b1/
Dir142.0b2/
Dir142.0b3/
Dir142.0b4/
Dir142.0b5/
Dir142.0b6/
Dir142.0b7/
Dir142.0b8/
Dir142.0b9/
Dir143.0.2/
Dir143.0.3/
Dir143.0.4/
Dir143.0/
Dir143.0b1/
Dir143.0b2/
Dir143.0b3/
Dir143.0b4/
Dir143.0b5/
Dir143.0b6/
Dir143.0b7/
Dir143.0b8/
Dir143.0b9/
Dir144.0.1/
Dir144.0.2/
Dir144.0/
Dir144.0b1/
Dir144.0b10/
Dir144.0b11/
Dir144.0b2/
Dir144.0b3/
Dir144.0b4/
Dir144.0b5/
Dir144.0b6/
Dir144.0b7/
Dir144.0b8/
Dir144.0b9/
Dir145.0.1/
Dir145.0/
Dir145.0b1/
Dir145.0b2/
Dir145.0b3/
Dir145.0b4/
Dir145.0b5/
Dir145.0b6/
Dir145.0b7/
Dir145.0b8/
Dir145.0b9/
Dir146.0b1/
Dir146.0b2/
Dir146.0b3/
Dir146.0b4/
Dir146.0b5/
Dir75.0.0-beta.4/
+ \ No newline at end of file diff --git a/app/src/test/resources/fenix-releases-simple.html b/app/src/test/resources/fenix-releases-simple.html new file mode 100644 index 0000000..67a9b4c --- /dev/null +++ b/app/src/test/resources/fenix-releases-simple.html @@ -0,0 +1,28 @@ + + + + Index of /pub/fenix/releases/ + + +

Index of /pub/fenix/releases/

+
+
+        ../
+        123.0a1/                                           2024-01-01 10:00    -
+        123.0b1/                                           2024-01-05 10:00    -
+        124.0a1/                                           2024-02-01 10:00    -
+        124.0b1/                                           2024-02-05 10:00    -
+        124.0b2/                                           2024-02-06 10:00    -
+        124.0b5/                                           2024-02-09 10:00    -
+        125.0a1/                                           2024-03-01 10:00    -
+        125.0b1/                                           2024-03-05 10:00    -
+        125.0b2/                                           2024-03-06 10:00    -
+        125.0b5/                                           2024-03-09 10:00    -
+        126.0a1/                                           2024-04-01 10:00    -
+        126.0b1/                                           2024-04-05 10:00    -
+        126.0b2/                                           2024-04-06 10:00    -
+        126.0b5/                                           2024-04-09 10:00    -
+        146.0b5/                                           2024-04-09 10:00    -
+    
+ +