Skip to content

Commit b170d6e

Browse files
authored
Merge pull request #8 from titooan/date-picker
Allow user to install Fenix and Focus from a specific date
2 parents f49e7fb + 411cb25 commit b170d6e

File tree

15 files changed

+847
-454
lines changed

15 files changed

+847
-454
lines changed

app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveRepository.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package org.mozilla.tryfox.data
22

3+
import kotlinx.datetime.LocalDate
34
import org.mozilla.tryfox.model.ParsedNightlyApk
45

56
interface MozillaArchiveRepository {
67
/**
78
* Fetches and parses the list of Fenix nightly builds for the current month from the archive.
89
*/
9-
suspend fun getFenixNightlyBuilds(): NetworkResult<List<ParsedNightlyApk>>
10+
suspend fun getFenixNightlyBuilds(date: LocalDate? = null): NetworkResult<List<ParsedNightlyApk>>
1011

1112
/**
1213
* Fetches and parses the list of Focus nightly builds for the current month from the archive.
1314
*/
14-
suspend fun getFocusNightlyBuilds(): NetworkResult<List<ParsedNightlyApk>>
15+
suspend fun getFocusNightlyBuilds(date: LocalDate? = null): NetworkResult<List<ParsedNightlyApk>>
1516

1617
/**
1718
* Fetches and parses the list of Reference Browser nightly builds from TaskCluster storage.

app/src/main/java/org/mozilla/tryfox/data/MozillaArchiveRepositoryImpl.kt

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ package org.mozilla.tryfox.data
33
import kotlinx.datetime.Clock
44
import kotlinx.datetime.DateTimeUnit
55
import kotlinx.datetime.LocalDate
6-
import kotlinx.datetime.LocalDateTime
76
import kotlinx.datetime.TimeZone
8-
import kotlinx.datetime.format.char
97
import kotlinx.datetime.minus
108
import kotlinx.datetime.todayIn
119
import org.mozilla.tryfox.model.ParsedNightlyApk
@@ -33,22 +31,27 @@ class MozillaArchiveRepositoryImpl(
3331
}
3432
}
3533

36-
private suspend fun getNightlyBuilds(appName: String): NetworkResult<List<ParsedNightlyApk>> {
34+
private suspend fun getNightlyBuilds(appName: String, date: LocalDate? = null): NetworkResult<List<ParsedNightlyApk>> {
35+
if (date != null) {
36+
val url = archiveUrlForDate(appName, date)
37+
return fetchAndParseNightlyBuilds(url, appName, date)
38+
}
39+
3740
val today = clock.todayIn(TimeZone.currentSystemDefault())
3841
val currentMonthUrl = archiveUrlForDate(appName, today)
39-
val result = fetchAndParseNightlyBuilds(currentMonthUrl, appName)
42+
val result = fetchAndParseNightlyBuilds(currentMonthUrl, appName, null)
4043

4144
if (result is NetworkResult.Error && (result.cause as? HttpException)?.code() == 404) {
4245
val lastMonth = today.minus(1, DateTimeUnit.MONTH)
4346
val lastMonthUrl = archiveUrlForDate(appName, lastMonth)
44-
return fetchAndParseNightlyBuilds(lastMonthUrl, appName)
47+
return fetchAndParseNightlyBuilds(lastMonthUrl, appName, null)
4548
}
4649
return result
4750
}
4851

49-
override suspend fun getFenixNightlyBuilds(): NetworkResult<List<ParsedNightlyApk>> = getNightlyBuilds(FENIX)
52+
override suspend fun getFenixNightlyBuilds(date: LocalDate?): NetworkResult<List<ParsedNightlyApk>> = getNightlyBuilds(FENIX, date)
5053

51-
override suspend fun getFocusNightlyBuilds(): NetworkResult<List<ParsedNightlyApk>> = getNightlyBuilds(FOCUS)
54+
override suspend fun getFocusNightlyBuilds(date: LocalDate?): NetworkResult<List<ParsedNightlyApk>> = getNightlyBuilds(FOCUS, date)
5255

5356
override suspend fun getReferenceBrowserNightlyBuilds(): NetworkResult<List<ParsedNightlyApk>> {
5457
return try {
@@ -71,10 +74,10 @@ class MozillaArchiveRepositoryImpl(
7174
}
7275
}
7376

74-
private suspend fun fetchAndParseNightlyBuilds(archiveBaseUrl: String, appNameFilter: String): NetworkResult<List<ParsedNightlyApk>> {
77+
private suspend fun fetchAndParseNightlyBuilds(archiveBaseUrl: String, appNameFilter: String, date: LocalDate?): NetworkResult<List<ParsedNightlyApk>> {
7578
return try {
7679
val htmlResult = archiveApiService.getHtmlPage(archiveBaseUrl)
77-
val parsedApks = parseNightlyBuildsFromHtml(htmlResult, archiveBaseUrl, appNameFilter)
80+
val parsedApks = parseNightlyBuildsFromHtml(htmlResult, archiveBaseUrl, appNameFilter, date)
7881
NetworkResult.Success(parsedApks)
7982
} catch (e: Exception) {
8083
NetworkResult.Error("Failed to fetch or parse $appNameFilter builds: ${e.message}", e)
@@ -85,24 +88,28 @@ class MozillaArchiveRepositoryImpl(
8588
html: String,
8689
archiveUrl: String,
8790
app: String,
91+
date: LocalDate?,
8892
): List<ParsedNightlyApk> {
89-
val htmlPattern = Regex("""<td>Dir</td>\s*<td><a href="[^"]*">([^<]+/)</a></td>""")
93+
val htmlPattern = Regex("<td>Dir</td>\\s*<td><a href=\"[^\"]*\">([^<]+/)</a></td>")
9094
val rawBuildStrings = htmlPattern.findAll(html)
9195
.mapNotNull { it.groups[1]?.value }
9296
.filter { it != "../" }
9397
.toList()
9498

95-
val buildsByDate = rawBuildStrings.groupBy { it.substringBefore("-$app") }
96-
if (buildsByDate.isEmpty()) return emptyList()
97-
98-
val lastBuildDateStr = buildsByDate.keys.maxByOrNull(::parseDateString) ?: return emptyList()
99-
100-
val lastBuildsForDate = buildsByDate[lastBuildDateStr] ?: return emptyList()
99+
val buildsForDate = if (date != null) {
100+
val dateString = date.toString()
101+
rawBuildStrings.filter { it.startsWith(dateString) }
102+
} else {
103+
val buildsByDay = rawBuildStrings.groupBy { it.substring(0, 10) }
104+
if (buildsByDay.isEmpty()) return emptyList()
105+
val latestDay = buildsByDay.keys.maxOrNull() ?: return emptyList()
106+
buildsByDay[latestDay] ?: emptyList()
107+
}
101108

102109
val apkPattern =
103-
Pattern.compile("""^(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2})-(.*?)-([^-]+)-android-(.*?)/$""")
110+
Pattern.compile("^(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})-(.*?)-([^-]+)-android-(.*?)/$")
104111

105-
return lastBuildsForDate.mapNotNull { buildString ->
112+
return buildsForDate.mapNotNull { buildString ->
106113
val matcher = apkPattern.matcher(buildString)
107114
if (matcher.matches()) {
108115
val rawDate = matcher.group(1) ?: ""
@@ -127,17 +134,4 @@ class MozillaArchiveRepositoryImpl(
127134
}
128135
}
129136
}
130-
131-
private fun parseDateString(dateStr: String): LocalDateTime {
132-
return try {
133-
val format = LocalDateTime.Format {
134-
year(); char('-'); monthNumber(); char('-'); dayOfMonth()
135-
char('-'); hour(); char('-'); minute(); char('-'); second()
136-
}
137-
LocalDateTime.parse(dateStr, format)
138-
} catch (_: Exception) {
139-
// Return a very old date so that it's never chosen as the max
140-
LocalDateTime(1, 1, 1, 0, 0)
141-
}
142-
}
143137
}

app/src/main/java/org/mozilla/tryfox/data/MozillaPackageManager.kt

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
package org.mozilla.tryfox.data
22

3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.content.IntentFilter
37
import android.content.pm.PackageInfo
48
import android.content.pm.PackageManager
59
import android.util.Log
10+
import kotlinx.coroutines.channels.awaitClose
11+
import kotlinx.coroutines.flow.Flow
12+
import kotlinx.coroutines.flow.callbackFlow
613
import org.mozilla.tryfox.model.AppState
14+
import org.mozilla.tryfox.util.FENIX_PACKAGE
15+
import org.mozilla.tryfox.util.FOCUS_PACKAGE
16+
import org.mozilla.tryfox.util.REFERENCE_BROWSER_PACKAGE
717

818
/**
919
* Manages interactions with the Android [PackageManager] to retrieve information
1020
* about Mozilla applications installed on the device.
1121
*
12-
* @property packageManager The Android [PackageManager] instance used to query package information.
22+
* @property context The application context.
1323
*/
14-
class MozillaPackageManager(private val packageManager: PackageManager) {
24+
class MozillaPackageManager(private val context: Context) {
25+
26+
private val packageManager: PackageManager = context.packageManager
1527

1628
/**
1729
* Retrieves [PackageInfo] for a given package name.
@@ -35,38 +47,67 @@ class MozillaPackageManager(private val packageManager: PackageManager) {
3547
* Constructs an [AppState] object for a given package name and friendly name.
3648
*
3749
* @param packageName The package name of the application.
38-
* @param friendlyName A user-friendly name for the application.
3950
* @return An [AppState] object representing the application's state.
4051
*/
41-
private fun getAppState(packageName: String, friendlyName: String): AppState {
52+
private fun getAppState(packageName: String): AppState {
4253
val packageInfo = getPackageInfo(packageName)
4354

4455
return AppState(
45-
name = friendlyName,
56+
name = apps[packageName] ?: "",
4657
packageName = packageName,
4758
version = packageInfo?.versionName,
4859
installDateMillis = packageInfo?.lastUpdateTime,
4960
)
5061
}
5162

63+
private val apps = mapOf(
64+
FENIX_PACKAGE to "Fenix",
65+
FOCUS_PACKAGE to "Focus",
66+
REFERENCE_BROWSER_PACKAGE to "Reference Browser",
67+
)
68+
5269
/**
5370
* The [AppState] for Fenix (Firefox for Android).
5471
*/
55-
val fenix: AppState by lazy {
56-
getAppState("org.mozilla.fenix", "Fenix")
57-
}
72+
fun fenix(): AppState = getAppState("org.mozilla.fenix")
5873

5974
/**
6075
* The [AppState] for Focus Nightly.
6176
*/
62-
val focus: AppState by lazy {
63-
getAppState("org.mozilla.focus.nightly", "Focus Nightly")
64-
}
77+
fun focus(): AppState = getAppState("org.mozilla.focus.nightly")
6578

6679
/**
6780
* The [AppState] for Reference Browser.
6881
*/
69-
val referenceBrowser: AppState by lazy {
70-
getAppState("org.mozilla.reference.browser", "Reference Browser")
82+
fun referenceBrowser(): AppState = getAppState("org.mozilla.reference.browser")
83+
84+
val appStates: Flow<AppState> = callbackFlow {
85+
val receiver = object : BroadcastReceiver() {
86+
override fun onReceive(context: Context, intent: Intent) {
87+
if (intent.action == Intent.ACTION_PACKAGE_ADDED || intent.action == Intent.ACTION_PACKAGE_REMOVED) {
88+
val packageName = intent.data?.schemeSpecificPart
89+
if (packageName != null && packageName in apps.keys) {
90+
trySend(getAppState(packageName))
91+
}
92+
}
93+
}
94+
}
95+
96+
val intentFilter = IntentFilter().apply {
97+
addAction(Intent.ACTION_PACKAGE_ADDED)
98+
addAction(Intent.ACTION_PACKAGE_REMOVED)
99+
addDataScheme("package")
100+
}
101+
102+
context.registerReceiver(receiver, intentFilter)
103+
104+
awaitClose {
105+
context.unregisterReceiver(receiver)
106+
}
107+
}
108+
109+
fun launchApp(appName: String) {
110+
val intent = packageManager.getLaunchIntentForPackage(appName)
111+
intent?.let(context::startActivity)
71112
}
72113
}

app/src/main/java/org/mozilla/tryfox/di/AppModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ val repositoryModule = module {
6868
single<IFenixRepository> { FenixRepository(get()) }
6969
single<UserDataRepository> { DefaultUserDataRepository(androidContext()) }
7070
single<MozillaArchiveRepository> { MozillaArchiveRepositoryImpl(get()) }
71-
single { MozillaPackageManager(androidContext().packageManager) }
71+
single { MozillaPackageManager(androidContext()) }
7272
single<CacheManager> { DefaultCacheManager(androidContext().cacheDir, get(named("IODispatcher"))) }
7373
}
7474

0 commit comments

Comments
 (0)