Skip to content

Commit c6dd52a

Browse files
Titouan Thibaudtitooan
authored andcommitted
Refactors to improve code and architecture.
1 parent c514cc9 commit c6dd52a

File tree

14 files changed

+589
-224
lines changed

14 files changed

+589
-224
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.mozilla.tryfox.data
2+
3+
import org.mozilla.tryfox.data.managers.IntentManager
4+
import java.io.File
5+
6+
/**
7+
* A fake implementation of [IntentManager] for use in instrumented tests.
8+
* This class allows for verifying that the `installApk` method is called with the correct file.
9+
*/
10+
class FakeIntentManager() : IntentManager {
11+
12+
/**
13+
* A boolean flag to indicate whether the `installApk` method was called.
14+
*/
15+
val wasInstallApkCalled: Boolean
16+
get() = installedFile != null
17+
18+
/**
19+
* The file that was passed to the `installApk` method.
20+
*/
21+
var installedFile: File? = null
22+
private set
23+
24+
/**
25+
* Overrides the `installApk` method to capture the file and set the `wasInstallApkCalled` flag.
26+
*
27+
* @param file The file to be "installed".
28+
*/
29+
override fun installApk(file: File) {
30+
installedFile = file
31+
}
32+
}
Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
package org.mozilla.tryfox.ui.screens
22

33
import androidx.compose.ui.semantics.SemanticsProperties
4-
import androidx.compose.ui.test.assertIsDisplayed
4+
import androidx.compose.ui.test.assert
5+
import androidx.compose.ui.test.hasText
56
import androidx.compose.ui.test.junit4.createComposeRule
67
import androidx.compose.ui.test.onAllNodesWithTag
78
import androidx.compose.ui.test.onNodeWithTag
89
import androidx.compose.ui.test.performClick
910
import androidx.compose.ui.test.performTextInput
1011
import androidx.test.ext.junit.runners.AndroidJUnit4
11-
import org.junit.Assert.assertNotNull
12+
import org.junit.Assert.assertTrue
1213
import org.junit.Rule
1314
import org.junit.Test
1415
import org.junit.runner.RunWith
1516
import org.mozilla.tryfox.data.FakeCacheManager
1617
import org.mozilla.tryfox.data.FakeFenixRepository
18+
import org.mozilla.tryfox.data.FakeIntentManager
1719
import org.mozilla.tryfox.data.FakeUserDataRepository
1820
import org.mozilla.tryfox.data.UserDataRepository
1921
import org.mozilla.tryfox.data.managers.CacheManager
20-
import java.io.File
2122

2223
@RunWith(AndroidJUnit4::class)
2324
class ProfileScreenTest {
@@ -28,11 +29,7 @@ class ProfileScreenTest {
2829
private val fenixRepository = FakeFenixRepository(downloadProgressDelayMillis = 100L)
2930
private val userDataRepository: UserDataRepository = FakeUserDataRepository()
3031
private val cacheManager: CacheManager = FakeCacheManager()
31-
private val profileViewModel = ProfileViewModel(
32-
fenixRepository = fenixRepository,
33-
userDataRepository = userDataRepository,
34-
cacheManager = cacheManager,
35-
)
32+
private val intentManager = FakeIntentManager()
3633
private val emailInputTag = "profile_email_input"
3734
private val emailClearButtonTag = "profile_email_clear_button"
3835
private val searchButtonTag = "profile_search_button"
@@ -45,11 +42,13 @@ class ProfileScreenTest {
4542

4643
@Test
4744
fun searchPushesAndCheckDownloadAndInstallStates() {
48-
var capturedApkFile: File? = null
49-
50-
profileViewModel.onInstallApk = { apkFile ->
51-
capturedApkFile = apkFile
52-
}
45+
val profileViewModel = ProfileViewModel(
46+
fenixRepository = fenixRepository,
47+
userDataRepository = userDataRepository,
48+
cacheManager = cacheManager,
49+
intentManager = intentManager,
50+
authorEmail = null,
51+
)
5352

5453
composeTestRule.setContent {
5554
ProfileScreen(
@@ -75,25 +74,39 @@ class ProfileScreenTest {
7574
.performClick()
7675

7776
composeTestRule.waitUntil("Download button enters loading state", longTimeoutMillis) {
78-
tryOrFalse {
79-
composeTestRule.onNodeWithTag(downloadButtonLoadingTag, useUnmergedTree = true).assertIsDisplayed()
80-
}
77+
composeTestRule.onAllNodesWithTag(downloadButtonLoadingTag, useUnmergedTree = true)
78+
.fetchSemanticsNodes().isNotEmpty()
8179
}
8280

8381
composeTestRule.waitUntil("Download button enters install state", longTimeoutMillis) {
84-
tryOrFalse {
85-
composeTestRule.onNodeWithTag(downloadButtonInstallTag, useUnmergedTree = true)
86-
.assertIsDisplayed()
87-
}
82+
composeTestRule.onAllNodesWithTag(downloadButtonInstallTag, useUnmergedTree = true)
83+
.fetchSemanticsNodes().isNotEmpty()
8884
}
8985

90-
assertNotNull("APK file should have been captured by onInstallApk callback", capturedApkFile)
86+
assertTrue(
87+
"APK file should have been captured by onInstallApk callback",
88+
intentManager.wasInstallApkCalled,
89+
)
9190
}
9291

93-
private fun tryOrFalse(block: () -> Unit): Boolean = try {
94-
block()
95-
true
96-
} catch (_: AssertionError) {
97-
false
92+
@Test
93+
fun test_profileScreen_displays_initial_authorEmail_in_searchField() {
94+
val initialEmail = "initial@example.com"
95+
val profileViewModelWithEmail = ProfileViewModel(
96+
fenixRepository = fenixRepository,
97+
userDataRepository = userDataRepository,
98+
cacheManager = cacheManager,
99+
intentManager = intentManager,
100+
authorEmail = initialEmail,
101+
)
102+
103+
composeTestRule.setContent {
104+
ProfileScreen(
105+
profileViewModel = profileViewModelWithEmail,
106+
onNavigateUp = { },
107+
)
108+
}
109+
110+
composeTestRule.onNodeWithTag(emailInputTag).assert(hasText(initialEmail))
98111
}
99112
}

app/src/main/java/org/mozilla/tryfox/MainActivity.kt

Lines changed: 47 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
package org.mozilla.tryfox
22

3-
import android.content.ActivityNotFoundException
43
import android.content.Intent
5-
import android.net.Uri
64
import android.os.Bundle
75
import android.util.Log
8-
import android.widget.Toast
96
import androidx.activity.ComponentActivity
107
import androidx.activity.compose.setContent
118
import androidx.activity.enableEdgeToEdge
129
import androidx.compose.runtime.Composable
13-
import androidx.compose.runtime.LaunchedEffect
14-
import androidx.core.content.FileProvider
1510
import androidx.navigation.NavHostController
1611
import androidx.navigation.NavType
1712
import androidx.navigation.compose.NavHost
@@ -20,30 +15,58 @@ import androidx.navigation.compose.rememberNavController
2015
import androidx.navigation.navArgument
2116
import androidx.navigation.navDeepLink
2217
import org.koin.androidx.compose.koinViewModel
23-
import org.koin.androidx.viewmodel.ext.android.viewModel
18+
import org.koin.core.parameter.parametersOf
2419
import org.mozilla.tryfox.ui.screens.HomeScreen
25-
import org.mozilla.tryfox.ui.screens.HomeViewModel
2620
import org.mozilla.tryfox.ui.screens.ProfileScreen
27-
import org.mozilla.tryfox.ui.screens.ProfileViewModel
2821
import org.mozilla.tryfox.ui.screens.TryFoxMainScreen
2922
import org.mozilla.tryfox.ui.theme.TryFoxTheme
30-
import java.io.File
3123
import java.net.URLDecoder
3224

25+
/**
26+
* Sealed class representing the navigation screens in the application.
27+
* Each object corresponds to a specific route in the navigation graph.
28+
*/
3329
sealed class NavScreen(val route: String) {
30+
/**
31+
* Represents the Home screen.
32+
*/
3433
data object Home : NavScreen("home")
34+
35+
/**
36+
* Represents the Treeherder search screen without arguments.
37+
*/
3538
data object TreeherderSearch : NavScreen("treeherder_search")
39+
40+
/**
41+
* Represents the Treeherder search screen with project and revision arguments.
42+
*/
3643
data object TreeherderSearchWithArgs : NavScreen("treeherder_search/{project}/{revision}") {
44+
/**
45+
* Creates a route for the Treeherder search screen with the given project and revision.
46+
* @param project The project name.
47+
* @param revision The revision hash.
48+
* @return The formatted route string.
49+
*/
3750
fun createRoute(project: String, revision: String) = "treeherder_search/$project/$revision"
3851
}
52+
53+
/**
54+
* Represents the Profile screen.
55+
*/
3956
data object Profile : NavScreen("profile")
57+
58+
/**
59+
* Represents the Profile screen filtered by email.
60+
*/
4061
data object ProfileByEmail : NavScreen("profile_by_email?email={email}")
4162
}
4263

64+
/**
65+
* The main activity of the TryFox application.
66+
* This activity sets up the navigation host and handles deep links.
67+
*/
4368
class MainActivity : ComponentActivity() {
4469

45-
// Inject FenixInstallerViewModel using Koin
46-
private val tryFoxViewModel: TryFoxViewModel by viewModel()
4770
private lateinit var navController: NavHostController
4871

4972
override fun onCreate(savedInstanceState: Bundle?) {
@@ -53,7 +76,7 @@ class MainActivity : ComponentActivity() {
5376
setContent {
5477
TryFoxTheme {
5578
// Pass the Koin-injected ViewModel
56-
AppNavigation(tryFoxViewModel)
79+
AppNavigation()
5780
}
5881
}
5982
}
@@ -67,47 +90,29 @@ class MainActivity : ComponentActivity() {
6790
}
6891
}
6992

70-
private fun installApk(file: File) {
71-
val fileUri: Uri = FileProvider.getUriForFile(
72-
this,
73-
"${BuildConfig.APPLICATION_ID}.provider",
74-
file,
75-
)
76-
val intent = Intent(Intent.ACTION_VIEW).apply {
77-
setDataAndType(fileUri, "application/vnd.android.package-archive")
78-
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
79-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
80-
}
81-
try {
82-
startActivity(intent)
83-
} catch (e: ActivityNotFoundException) {
84-
Toast.makeText(this, "No application found to install APK", Toast.LENGTH_LONG).show()
85-
Log.e("MainActivity", "Error installing APK", e)
86-
}
87-
}
88-
93+
/**
94+
* Composable function that sets up the application's navigation.
95+
* It defines the navigation graph and handles different routes and deep links.
96+
*/
8997
@Composable
90-
fun AppNavigation(mainActivityViewModel: TryFoxViewModel) {
98+
fun AppNavigation() {
9199
val localNavController = rememberNavController()
92100
this@MainActivity.navController = localNavController
93101
Log.d("MainActivity", "AppNavigation: NavController instance assigned: $localNavController")
94102

95103
NavHost(navController = localNavController, startDestination = NavScreen.Home.route) {
96104
composable(NavScreen.Home.route) {
97105
// Inject HomeViewModel using Koin in Composable
98-
val homeViewModel: HomeViewModel = koinViewModel()
99-
homeViewModel.onInstallApk = ::installApk
100106
HomeScreen(
101107
onNavigateToTreeherder = { localNavController.navigate(NavScreen.TreeherderSearch.route) },
102108
onNavigateToProfile = { localNavController.navigate(NavScreen.Profile.route) },
103-
homeViewModel = homeViewModel,
109+
homeViewModel = koinViewModel(),
104110
)
105111
}
106112
composable(NavScreen.TreeherderSearch.route) {
107113
// mainActivityViewModel is already injected and passed as a parameter
108-
mainActivityViewModel.onInstallApk = ::installApk
109114
TryFoxMainScreen(
110-
tryFoxViewModel = mainActivityViewModel,
115+
tryFoxViewModel = koinViewModel(),
111116
onNavigateUp = { localNavController.popBackStack() },
112117
)
113118
}
@@ -134,52 +139,29 @@ class MainActivity : ComponentActivity() {
134139
"MainActivity",
135140
"TreeherderSearchWithArgs composable: project='$project', revision='$revision' from NavBackStackEntry. ID: ${backStackEntry.id}",
136141
)
137-
138-
LaunchedEffect(project, revision) {
139-
Log.d(
140-
"MainActivity",
141-
"TreeherderSearchWithArgs LaunchedEffect: project='$project', revision='$revision'",
142-
)
143-
mainActivityViewModel.setRevisionFromDeepLinkAndSearch(
144-
project,
145-
revision,
146-
)
147-
}
148-
mainActivityViewModel.onInstallApk = ::installApk
149142
TryFoxMainScreen(
150-
tryFoxViewModel = mainActivityViewModel,
143+
tryFoxViewModel = koinViewModel { parametersOf(project, revision) },
151144
onNavigateUp = { localNavController.popBackStack() },
152145
)
153146
}
154147
composable(NavScreen.Profile.route) {
155-
// Inject ProfileViewModel using Koin in Composable
156-
val profileViewModel: ProfileViewModel = koinViewModel()
157-
profileViewModel.onInstallApk = ::installApk // Assuming ProfileViewModel also needs this
158148
ProfileScreen(
159149
onNavigateUp = { localNavController.popBackStack() },
160-
profileViewModel = profileViewModel,
150+
profileViewModel = koinViewModel(),
161151
)
162152
}
163153
composable(
164154
route = NavScreen.ProfileByEmail.route,
165155
arguments = listOf(navArgument("email") { type = NavType.StringType }),
166156
deepLinks = listOf(navDeepLink { uriPattern = "https://treeherder.mozilla.org/jobs?repo={repo}&author={email}" }),
167157
) { backStackEntry ->
168-
val profileViewModel: ProfileViewModel = koinViewModel()
169-
val encodedEmail = backStackEntry.arguments?.getString("email")
170-
171-
LaunchedEffect(encodedEmail) {
172-
encodedEmail?.let {
173-
val email = URLDecoder.decode(it, "UTF-8")
174-
profileViewModel.updateAuthorEmail(email)
175-
profileViewModel.searchByAuthor()
176-
}
158+
val email = backStackEntry.arguments?.getString("email")?.let {
159+
URLDecoder.decode(it, "UTF-8")
177160
}
178161

179-
profileViewModel.onInstallApk = ::installApk
180162
ProfileScreen(
181163
onNavigateUp = { localNavController.popBackStack() },
182-
profileViewModel = profileViewModel,
164+
profileViewModel = koinViewModel { parametersOf(email) },
183165
)
184166
}
185167
}

app/src/main/java/org/mozilla/tryfox/TryFoxViewModel.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,25 @@ import org.mozilla.tryfox.ui.models.ArtifactUiModel
2727
import org.mozilla.tryfox.ui.models.JobDetailsUiModel
2828
import java.io.File
2929

30+
/**
31+
* ViewModel for the TryFox feature, responsible for fetching job and artifact data from the repository,
32+
* managing the download and caching of artifacts, and exposing the UI state to the composable screens.
33+
*
34+
* @param repository The repository for fetching data from the network.
35+
* @param cacheManager The manager for handling application cache.
36+
* @param revision The initial revision to search for.
37+
* @param repo The initial repository to search in.
38+
*/
3039
class TryFoxViewModel(
3140
private val repository: IFenixRepository,
3241
private val cacheManager: CacheManager,
42+
revision: String?,
43+
repo: String?,
3344
) : ViewModel() {
34-
var revision by mutableStateOf("")
45+
var revision by mutableStateOf(revision ?: "")
3546
private set
3647

37-
var selectedProject by mutableStateOf("try")
48+
var selectedProject by mutableStateOf(repo ?: "try")
3849
private set
3950

4051
var relevantPushComment by mutableStateOf<String?>(null)

0 commit comments

Comments
 (0)