Skip to content

Commit

Permalink
Merge pull request #164 from SwEnt-Group13/feat/map-user-gps-location
Browse files Browse the repository at this point in the history
Feat/map: user gps location
  • Loading branch information
Zafouche authored Nov 15, 2024
2 parents 7e14419 + b4efe26 commit e9a5e0b
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 28 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ dependencies {
implementation(libs.androidx.ui.text.google.fonts)
implementation(libs.androidx.compose.material)
implementation(libs.core)
implementation(libs.play.services.location)

testImplementation(libs.test.core.ktx)
debugImplementation(libs.androidx.ui.tooling)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.android.unio.ui

import android.content.Context
import android.location.Location
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.rule.GrantPermissionRule
import com.android.unio.mocks.association.MockAssociation
import com.android.unio.mocks.event.MockEvent
import com.android.unio.mocks.user.MockUser
Expand All @@ -12,6 +15,7 @@ import com.android.unio.model.event.EventRepositoryFirestore
import com.android.unio.model.event.EventViewModel
import com.android.unio.model.follow.ConcurrentAssociationUserRepositoryFirestore
import com.android.unio.model.image.ImageRepositoryFirebaseStorage
import com.android.unio.model.map.MapViewModel
import com.android.unio.model.search.SearchRepository
import com.android.unio.model.search.SearchViewModel
import com.android.unio.model.strings.test_tags.AccountDetailsTestTags
Expand Down Expand Up @@ -45,6 +49,9 @@ import com.android.unio.ui.saved.SavedScreen
import com.android.unio.ui.settings.SettingsScreen
import com.android.unio.ui.user.SomeoneElseUserProfileScreen
import com.android.unio.ui.user.UserProfileScreenScaffold
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.tasks.OnSuccessListener
import com.google.android.gms.tasks.Task
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.auth
Expand All @@ -65,6 +72,8 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any

@HiltAndroidTest
class ScreenDisplayingTest {
Expand All @@ -81,6 +90,17 @@ class ScreenDisplayingTest {
@MockK private lateinit var eventRepository: EventRepositoryFirestore
private lateinit var eventViewModel: EventViewModel

// Mocking the mapViewModel and its dependencies
private lateinit var locationTask: Task<Location>
private lateinit var context: Context
private lateinit var mapViewModel: MapViewModel
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
private val location =
Location("mockProvider").apply {
latitude = 46.518831258
longitude = 6.559331096
}

@MockK private lateinit var imageRepositoryFirestore: ImageRepositoryFirebaseStorage

@MockK private lateinit var firebaseAuth: FirebaseAuth
Expand All @@ -91,6 +111,12 @@ class ScreenDisplayingTest {

@get:Rule val composeTestRule = createComposeRule()

@get:Rule
val permissionRule =
GrantPermissionRule.grant(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION)

@get:Rule val hiltRule = HiltAndroidRule(this)

private lateinit var searchViewModel: SearchViewModel
Expand Down Expand Up @@ -120,6 +146,22 @@ class ScreenDisplayingTest {
eventViewModel.loadEvents()
eventViewModel.selectEvent(events.first().uid)

// Mocking the mapViewModel and its dependencies
fusedLocationProviderClient = mock()
locationTask = mock()
context = mock()
`when`(fusedLocationProviderClient.lastLocation).thenReturn(locationTask)
`when`(locationTask.addOnSuccessListener(any())).thenAnswer {
(it.arguments[0] as OnSuccessListener<Location?>).onSuccess(location)
locationTask
}
mapViewModel =
spyk(MapViewModel(fusedLocationProviderClient)) {
every { hasLocationPermissions(any()) } returns true
}
mapViewModel = MapViewModel(fusedLocationProviderClient)
mapViewModel.fetchUserLocation(context)

every { userRepository.getUserWithId(any(), any(), any()) } answers
{
val onSuccess = args[1] as (User) -> Unit
Expand All @@ -138,7 +180,7 @@ class ScreenDisplayingTest {
onSuccess(emptyList())
}

// Mocking the Firebase.auth object and it's behaviour
// Mocking the Firebase.auth object and its behaviour
mockkStatic(FirebaseAuth::class)
every { Firebase.auth } returns firebaseAuth
every { firebaseAuth.currentUser } returns mockFirebaseUser
Expand Down Expand Up @@ -186,7 +228,9 @@ class ScreenDisplayingTest {

@Test
fun testMapDisplayed() {
composeTestRule.setContent { MapScreen(navigationAction, eventViewModel, userViewModel) }
composeTestRule.setContent {
MapScreen(navigationAction, eventViewModel, userViewModel, mapViewModel)
}
composeTestRule.onNodeWithTag(MapTestTags.SCREEN).assertIsDisplayed()
}

Expand Down
74 changes: 61 additions & 13 deletions app/src/androidTest/java/com/android/unio/ui/map/MapScreenTest.kt
Original file line number Diff line number Diff line change
@@ -1,43 +1,71 @@
package com.android.unio.ui.map

import android.content.Context
import android.location.Location
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.navigation.NavHostController
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import com.android.unio.mocks.user.MockUser
import com.android.unio.model.event.EventRepositoryFirestore
import com.android.unio.model.event.EventViewModel
import com.android.unio.model.image.ImageRepositoryFirebaseStorage
import com.android.unio.model.map.MapViewModel
import com.android.unio.model.strings.test_tags.MapTestTags
import com.android.unio.model.user.User
import com.android.unio.model.user.UserRepositoryFirestore
import com.android.unio.model.user.UserViewModel
import com.android.unio.ui.navigation.NavigationAction
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.tasks.OnSuccessListener
import com.google.android.gms.tasks.Task
import io.mockk.MockKAnnotations
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.spyk
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any

@RunWith(AndroidJUnit4::class)
class MapScreenTest {
private val user = MockUser.createMockUser()

@get:Rule val composeTestRule = createComposeRule()

@get:Rule
val permissionRule =
GrantPermissionRule.grant(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION)

private val user = MockUser.createMockUser()

@MockK private lateinit var eventRepository: EventRepositoryFirestore
@MockK private lateinit var imageRepository: ImageRepositoryFirebaseStorage
@MockK private lateinit var userRepository: UserRepositoryFirestore
@MockK private lateinit var navHostController: NavHostController

private lateinit var locationTask: Task<Location>
private lateinit var context: Context
private lateinit var mapViewModel: MapViewModel
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
private val location =
Location("mockProvider").apply {
latitude = 46.518831258
longitude = 6.559331096
}

private lateinit var navigationAction: NavigationAction
private lateinit var eventViewModel: EventViewModel
private lateinit var userViewModel: UserViewModel
Expand All @@ -59,17 +87,33 @@ class MapScreenTest {
}
userViewModel = UserViewModel(userRepository, false)
userViewModel.getUserByUid("123")
}

@Test
fun mapScreenComponentsAreDisplayed() {
fusedLocationProviderClient = mock()
locationTask = mock()
context = mock()
`when`(fusedLocationProviderClient.lastLocation).thenReturn(locationTask)
`when`(locationTask.addOnSuccessListener(any())).thenAnswer {
(it.arguments[0] as OnSuccessListener<Location?>).onSuccess(location)
locationTask
}
mapViewModel =
spyk(MapViewModel(fusedLocationProviderClient)) {
every { hasLocationPermissions(any()) } returns true
}
mapViewModel = MapViewModel(fusedLocationProviderClient)
mapViewModel.fetchUserLocation(context)

composeTestRule.setContent {
MapScreen(
navigationAction = navigationAction,
eventViewModel = eventViewModel,
userViewModel = userViewModel)
userViewModel = userViewModel,
mapViewModel = mapViewModel)
}
}

@Test
fun mapScreenComponentsAreDisplayed() {
composeTestRule.onNodeWithTag(MapTestTags.SCREEN).assertIsDisplayed()
composeTestRule.onNodeWithTag(MapTestTags.TITLE).assertIsDisplayed()
composeTestRule.onNodeWithTag(MapTestTags.GO_BACK_BUTTON).assertIsDisplayed()
Expand All @@ -78,18 +122,22 @@ class MapScreenTest {

@Test
fun mapScreenBackButtonNavigatesBack() {
composeTestRule.setContent {
MapScreen(
navigationAction = navigationAction,
eventViewModel = eventViewModel,
userViewModel = userViewModel)
}

composeTestRule.onNodeWithTag(MapTestTags.GO_BACK_BUTTON).assertHasClickAction()
composeTestRule.onNodeWithTag(MapTestTags.GO_BACK_BUTTON).performClick()
verify { navigationAction.goBack() }
}

@Test
fun centerOnUserFabCentersMap() {
composeTestRule.onNodeWithTag(MapTestTags.CENTER_ON_USER_FAB).assertIsDisplayed()
composeTestRule.onNodeWithTag(MapTestTags.CENTER_ON_USER_FAB).assertHasClickAction()
composeTestRule.onNodeWithTag(MapTestTags.CENTER_ON_USER_FAB).performClick()

assert(mapViewModel.userLocation.value != null)
assert(mapViewModel.userLocation.value!!.latitude == location.latitude)
assert(mapViewModel.userLocation.value!!.longitude == location.longitude)
}

@After
fun tearDown() {
clearAllMocks()
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/com/android/unio/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.android.unio.model.association.AssociationViewModel
import com.android.unio.model.authentication.AuthViewModel
import com.android.unio.model.event.EventViewModel
import com.android.unio.model.image.ImageRepositoryFirebaseStorage
import com.android.unio.model.map.MapViewModel
import com.android.unio.model.search.SearchViewModel
import com.android.unio.model.user.UserViewModel
import com.android.unio.ui.association.AssociationProfileScreen
Expand Down Expand Up @@ -80,6 +81,7 @@ fun UnioApp(imageRepository: ImageRepositoryFirebaseStorage) {
val searchViewModel = hiltViewModel<SearchViewModel>()
val authViewModel = hiltViewModel<AuthViewModel>()
val eventViewModel = hiltViewModel<EventViewModel>()
val mapViewModel = hiltViewModel<MapViewModel>()

// Observe the authentication state
val authState by authViewModel.authState.collectAsState()
Expand Down Expand Up @@ -115,7 +117,9 @@ fun UnioApp(imageRepository: ImageRepositoryFirebaseStorage) {
eventViewModel = eventViewModel,
userViewModel = userViewModel)
}
composable(Screen.MAP) { MapScreen(navigationActions, eventViewModel, userViewModel) }
composable(Screen.MAP) {
MapScreen(navigationActions, eventViewModel, userViewModel, mapViewModel)
}
}
navigation(startDestination = Screen.EXPLORE, route = Route.EXPLORE) {
composable(Screen.EXPLORE) {
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/com/android/unio/model/hilt/module/HiltModule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.android.unio.model.hilt.module

import android.content.Context
import com.android.unio.model.association.AssociationRepository
import com.android.unio.model.association.AssociationRepositoryFirestore
import com.android.unio.model.event.EventRepository
Expand All @@ -10,6 +11,8 @@ import com.android.unio.model.image.ImageRepository
import com.android.unio.model.image.ImageRepositoryFirebaseStorage
import com.android.unio.model.user.UserRepository
import com.android.unio.model.user.UserRepositoryFirestore
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
Expand All @@ -19,6 +22,7 @@ import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent

@Module
Expand Down Expand Up @@ -89,3 +93,15 @@ object FirebaseStorageModule {

@Provides fun provideFirebaseStorage(): FirebaseStorage = FirebaseStorage.getInstance()
}

@Module
@InstallIn(SingletonComponent::class)
object LocationModule {

@Provides
fun provideFusedLocationProviderClient(
@ApplicationContext context: Context
): FusedLocationProviderClient {
return LocationServices.getFusedLocationProviderClient(context)
}
}
48 changes: 48 additions & 0 deletions app/src/main/java/com/android/unio/model/map/MapViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.android.unio.model.map

import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.maps.model.LatLng
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

@HiltViewModel
class MapViewModel
@Inject
constructor(private val fusedLocationClient: FusedLocationProviderClient) : ViewModel() {

/** State flow that holds the user's location. */
private val _userLocation = MutableStateFlow<LatLng?>(null)
val userLocation: StateFlow<LatLng?> = _userLocation.asStateFlow()

/** Fetches the user's location and updates the [_userLocation] state flow. */
fun fetchUserLocation(context: Context) {
if (hasLocationPermissions(context)) {
try {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
location?.let { _userLocation.value = LatLng(it.latitude, it.longitude) }
}
} catch (e: SecurityException) {
Log.e("MapViewModel", "Permission for location access was revoked: ${e.localizedMessage}")
}
} else {
Log.e("MapViewModel", "Location permission is not granted.")
}
}

fun hasLocationPermissions(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context, android.Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
context, android.Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ object MapTestTags {
const val TITLE = "mapTitle"
const val GO_BACK_BUTTON = "goBackButton"
const val GOOGLE_MAPS = "googleMaps"
const val CENTER_ON_USER_FAB = "centerOnUserFab"
const val LOCATION_APPROXIMATE_CIRCLE = "locationApproximateCircle"
}
Loading

0 comments on commit e9a5e0b

Please sign in to comment.