Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ android {
minSdk = 26
targetSdk = 36
versionCode = (findProperty("versionCode") as String?)?.toInt() ?: 1
versionName = (findProperty("versionName") as String?) ?: "0.0.1"
versionName = (findProperty("versionName") as String?) ?: "0.0.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.mozilla.tryfox.data

import kotlinx.coroutines.delay
import java.io.File

class FakeDownloadFileRepository(
private val simulateNetworkError: Boolean = false,
private val networkErrorMessage: String = "Fake network error",
private val downloadProgressDelayMillis: Long = 100L,
) : DownloadFileRepository {

var downloadFileCalled = false
var downloadFileResult: NetworkResult<File> = NetworkResult.Success(File("fake_path"))

override suspend fun downloadFile(
downloadUrl: String,
outputFile: File,
onProgress: (bytesDownloaded: Long, totalBytes: Long) -> Unit,
): NetworkResult<File> {
downloadFileCalled = true

if (simulateNetworkError) {
return NetworkResult.Error(networkErrorMessage, null)
}

val totalBytes = 10_000_000L

onProgress(0, totalBytes)
delay(downloadProgressDelayMillis)

onProgress(totalBytes / 2, totalBytes)
delay(downloadProgressDelayMillis)

outputFile.parentFile?.mkdirs()
try {
if (!outputFile.exists()) {
outputFile.createNewFile()
}
outputFile.writeText("This is a fake downloaded artifact: ${outputFile.name} from $downloadUrl")
} catch (e: Exception) {
return NetworkResult.Error("Failed to create fake artifact file: ${e.message}", e)
}

onProgress(totalBytes, totalBytes)
delay(downloadProgressDelayMillis)

return NetworkResult.Success(outputFile)
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package org.mozilla.tryfox.data

import kotlinx.coroutines.delay
import java.io.File

class FakeFenixRepository(
private val simulateNetworkError: Boolean = false,
private val networkErrorMessage: String = "Fake network error",
private val downloadProgressDelayMillis: Long = 100L,
) : IFenixRepository {

override suspend fun getPushByRevision(
Expand Down Expand Up @@ -81,37 +77,4 @@ class FakeFenixRepository(
NetworkResult.Success(ArtifactsResponse(artifacts = listOf(dummyArtifact)))
}
}

override suspend fun downloadArtifact(
downloadUrl: String,
outputFile: File,
onProgress: (bytesDownloaded: Long, totalBytes: Long) -> Unit,
): NetworkResult<File> {
if (simulateNetworkError) {
return NetworkResult.Error(networkErrorMessage, null)
}

val totalBytes = 10_000_000L

onProgress(0, totalBytes)
delay(downloadProgressDelayMillis)

onProgress(totalBytes / 2, totalBytes)
delay(downloadProgressDelayMillis)

outputFile.parentFile?.mkdirs()
try {
if (!outputFile.exists()) {
outputFile.createNewFile()
}
outputFile.writeText("This is a fake downloaded artifact: ${outputFile.name} from $downloadUrl")
} catch (e: Exception) {
return NetworkResult.Error("Failed to create fake artifact file: ${e.message}", e)
}

onProgress(totalBytes, totalBytes)
delay(downloadProgressDelayMillis)

return NetworkResult.Success(outputFile)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import java.io.File
*/
class FakeIntentManager() : IntentManager {

var wasUninstallApkCalled: Boolean = false
private set

/**
* A boolean flag to indicate whether the `installApk` method was called.
*/
Expand All @@ -29,4 +32,8 @@ class FakeIntentManager() : IntentManager {
override fun installApk(file: File) {
installedFile = file
}

override fun uninstallApk(packageName: String) {
wasUninstallApkCalled = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.junit.Rule
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.FakeUserDataRepository
Expand All @@ -26,7 +27,8 @@ class ProfileScreenTest {
@get:Rule
val composeTestRule = createComposeRule()

private val fenixRepository = FakeFenixRepository(downloadProgressDelayMillis = 100L)
private val fenixRepository = FakeFenixRepository()
private val downloadFileRepository = FakeDownloadFileRepository()
private val userDataRepository: UserDataRepository = FakeUserDataRepository()
private val cacheManager: CacheManager = FakeCacheManager()
private val intentManager = FakeIntentManager()
Expand All @@ -44,6 +46,7 @@ class ProfileScreenTest {
fun searchPushesAndCheckDownloadAndInstallStates() {
val profileViewModel = ProfileViewModel(
fenixRepository = fenixRepository,
downloadFileRepository = downloadFileRepository,
userDataRepository = userDataRepository,
cacheManager = cacheManager,
intentManager = intentManager,
Expand Down Expand Up @@ -94,6 +97,7 @@ class ProfileScreenTest {
val initialEmail = "[email protected]"
val profileViewModelWithEmail = ProfileViewModel(
fenixRepository = fenixRepository,
downloadFileRepository = downloadFileRepository,
userDataRepository = userDataRepository,
cacheManager = cacheManager,
intentManager = intentManager,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />

<queries>
<package android:name="org.mozilla.fenix" />
Expand Down
14 changes: 8 additions & 6 deletions app/src/main/java/org/mozilla/tryfox/TryFoxViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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
Expand All @@ -31,13 +32,14 @@ import java.io.File
* ViewModel for the TryFox feature, responsible for fetching job and artifact data from the repository,
* managing the download and caching of artifacts, and exposing the UI state to the composable screens.
*
* @param repository The repository for fetching data from the network.
* @param fenixRepository The repository for fetching data from the network.
* @param cacheManager The manager for handling application cache.
* @param revision The initial revision to search for.
* @param repo The initial repository to search in.
*/
class TryFoxViewModel(
private val repository: IFenixRepository,
private val fenixRepository: IFenixRepository,
private val downloadFileRepository: DownloadFileRepository,
private val cacheManager: CacheManager,
revision: String?,
repo: String?,
Expand Down Expand Up @@ -157,7 +159,7 @@ class TryFoxViewModel(
cacheManager.checkCacheStatus() // Use CacheManager
checkAndUpdateDownloadingStatus() // Initial check before fetching

when (val revisionResult = repository.getPushByRevision(selectedProject, revision)) {
when (val revisionResult = fenixRepository.getPushByRevision(selectedProject, revision)) {
is NetworkResult.Success -> {
val pushData = revisionResult.data
var foundComment: String? = null
Expand Down Expand Up @@ -195,7 +197,7 @@ class TryFoxViewModel(

private suspend fun fetchJobs(pushId: Int) {
Log.d("FenixInstallerViewModel", "Fetching jobs for push ID: $pushId")
when (val jobsResult = repository.getJobsForPush(pushId)) {
when (val jobsResult = fenixRepository.getJobsForPush(pushId)) {
is NetworkResult.Success -> {
val networkJobDetailsList = jobsResult.data.results
.filter { it.isSignedBuild && !it.isTest }
Expand Down Expand Up @@ -245,7 +247,7 @@ class TryFoxViewModel(

private suspend fun fetchArtifacts(taskId: String): List<ArtifactUiModel> {
Log.d("FenixInstallerViewModel", "Fetching artifacts for task ID: $taskId")
return when (val artifactsResult = repository.getArtifactsForTask(taskId)) {
return when (val artifactsResult = fenixRepository.getArtifactsForTask(taskId)) {
is NetworkResult.Success -> {
val filteredApks = artifactsResult.data.artifacts.filter {
it.name.endsWith(".apk", ignoreCase = true)
Expand Down Expand Up @@ -318,7 +320,7 @@ class TryFoxViewModel(
}
val outputFile = File(outputDir, artifactFileName)

val result = repository.downloadArtifact(
val result = downloadFileRepository.downloadFile(
downloadUrl = downloadUrl,
outputFile = outputFile,
onProgress = { bytesDownloaded, totalBytes ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.mozilla.tryfox.data

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.mozilla.tryfox.network.DownloadApiService
import java.io.File
import java.io.FileOutputStream

/**
* Default implementation of [DownloadFileRepository] for downloading files.
*/
class DefaultDownloadFileRepository(
private val downloadApiService: DownloadApiService,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : DownloadFileRepository {
override suspend fun downloadFile(downloadUrl: String, outputFile: File, onProgress: (Long, Long) -> Unit): NetworkResult<File> {
return withContext(ioDispatcher) {
try {
val response = downloadApiService.downloadFile(downloadUrl)
val body = response.byteStream()
val totalBytes = response.contentLength()
var bytesCopied: Long = 0

body.use { inputStream ->
FileOutputStream(outputFile).use { outputStream ->
val buffer = ByteArray(4 * 1024)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
bytesCopied += read
onProgress(bytesCopied, totalBytes)
}
}
}
NetworkResult.Success(outputFile)
} catch (e: Exception) {
NetworkResult.Error("Failed to download file: ${e.message}", e)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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.REFERENCE_BROWSER_PACKAGE
import org.mozilla.tryfox.util.TRYFOX_PACKAGE

class DefaultMozillaPackageManager(private val context: Context) : MozillaPackageManager {

Expand Down Expand Up @@ -47,16 +48,20 @@ class DefaultMozillaPackageManager(private val context: Context) : MozillaPackag
FENIX_PACKAGE to "Fenix",
FOCUS_PACKAGE to "Focus",
REFERENCE_BROWSER_PACKAGE to "Reference Browser",
TRYFOX_PACKAGE to "TryFox",
)

override val fenix: AppState
get() = getAppState("org.mozilla.fenix")
get() = getAppState(FENIX_PACKAGE)

override val focus: AppState
get() = getAppState("org.mozilla.focus.nightly")
get() = getAppState(FOCUS_PACKAGE)

override val referenceBrowser: AppState
get() = getAppState("org.mozilla.reference.browser")
get() = getAppState(REFERENCE_BROWSER_PACKAGE)

override val tryfox: AppState
get() = getAppState(TRYFOX_PACKAGE)

override val appStates: Flow<AppState> = callbackFlow {
val receiver = object : BroadcastReceiver() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.mozilla.tryfox.data

import java.io.File

/**
* Interface for a repository responsible for downloading files.
*/
interface DownloadFileRepository {
/**
* Downloads a file from the given URL to the specified output file, reporting progress.
*
* @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.
*/
suspend fun downloadFile(downloadUrl: String, outputFile: File, onProgress: (bytesDownloaded: Long, totalBytes: Long) -> Unit): NetworkResult<File>
}
27 changes: 3 additions & 24 deletions app/src/main/java/org/mozilla/tryfox/data/DownloadState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,9 @@ package org.mozilla.tryfox.data

import java.io.File

/**
* Represents the various states of a file download operation.
*/
sealed class DownloadState {
/**
* Indicates that the download has not yet started.
*/
data object NotDownloaded : DownloadState()

/**
* Indicates that the download is currently in progress.
* @property progress A float value between 0.0 and 1.0 representing the download progress.
*/
data class InProgress(val progress: Float) : DownloadState()

/**
* Indicates that the download has completed successfully.
* @property file The [File] object representing the downloaded file.
*/
object NotDownloaded : DownloadState()
data class InProgress(val progress: Float, val isIndeterminate: Boolean = false) : DownloadState()
data class Downloaded(val file: File) : DownloadState()

/**
* Indicates that the download has failed.
* @property errorMessage An optional string containing a message describing the reason for the failure.
*/
data class DownloadFailed(val errorMessage: String?) : DownloadState()
data class DownloadFailed(val message: String?) : DownloadState()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.mozilla.tryfox.data

import kotlinx.datetime.LocalDate
import org.mozilla.tryfox.model.ParsedNightlyApk
import org.mozilla.tryfox.util.FENIX

/**
* A [ReleaseRepository] for Fenix builds.
*/
class FenixReleaseRepository(
private val mozillaArchiveRepository: MozillaArchiveRepository,
) : DateAwareReleaseRepository {
override val appName: String = FENIX

override suspend fun getLatestReleases(): NetworkResult<List<ParsedNightlyApk>> {
return mozillaArchiveRepository.getFenixNightlyBuilds()
}

override suspend fun getReleases(date: LocalDate?): NetworkResult<List<ParsedNightlyApk>> {
return mozillaArchiveRepository.getFenixNightlyBuilds(date)
}
}
Loading