diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1993aaac3..ddbf6b5ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,7 +21,7 @@ android { localProperties.load(FileInputStream(localPropertiesFile)) } - //val mapsApiKey: String = localProperties.getProperty("MAPS_API_KEY") ?: "" + val mapsApiKey: String = localProperties.getProperty("MAPS_API_KEY") ?: "" defaultConfig { applicationId = "com.github.lookupgroup27.lookup" @@ -34,6 +34,8 @@ android { vectorDrawables { useSupportLibrary = true } + manifestPlaceholders["MAPS_API_KEY"] = mapsApiKey + } signingConfigs { @@ -188,6 +190,14 @@ dependencies { implementation(libs.androidx.navigation.fragment.ktx) implementation(libs.androidx.navigation.ui.ktx) + // Google Service and Maps + implementation(libs.play.services.maps) + implementation(libs.maps.compose) + implementation(libs.maps.compose.utils) + implementation(libs.play.services.auth) + implementation(libs.play.services.location) + + // Unit Testing testImplementation(libs.junit) androidTestImplementation(libs.mockk) diff --git a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMapKtTest.kt b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMapKtTest.kt new file mode 100644 index 000000000..100aee5a8 --- /dev/null +++ b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMapKtTest.kt @@ -0,0 +1,77 @@ +package com.github.lookupgroup27.lookup.ui.googlemap + +import android.Manifest +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.github.lookupgroup27.lookup.ui.navigation.NavigationActions +import com.github.lookupgroup27.lookup.ui.navigation.Screen +import com.google.android.gms.maps.model.LatLng +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` + +class GoogleMapScreenTest { + + private lateinit var navigationActions: NavigationActions + + @get:Rule val composeTestRule = createComposeRule() + + @get:Rule + val permissionRule: GrantPermissionRule = + GrantPermissionRule.grant(Manifest.permission.ACCESS_FINE_LOCATION) + + @Before + fun setUp() { + // Mock NavigationActions + navigationActions = mock(NavigationActions::class.java) + // Setup to return the map route as current + `when`(navigationActions.currentRoute()).thenReturn(Screen.GOOGLE_MAP) + + // Set the Compose content to GoogleMapScreen + composeTestRule.setContent { GoogleMapScreen(navigationActions) } + } + + @Test + fun mapScreenDisplaysCorrectly() { + + // Verify that the GoogleMapScreen is displayed + composeTestRule.onNodeWithTag("googleMapScreen").assertIsDisplayed() + + // Ensure the bottom navigation is set up correctly + composeTestRule.onNodeWithTag("bottomNavigationMenu").assertIsDisplayed() + } + + @Test + fun mapIsCenteredOnCurrentLocation() { + val fakeLocation = LatLng(37.7749, -122.4194) // Example coordinates for San Francisco + + // Simulate location update + composeTestRule.runOnIdle { + // Update the locationProvider's currentLocation value + // This part depends on how you can access and update the locationProvider in your test + } + + // Verify if the map is centered on the current location + composeTestRule.onNodeWithTag("googleMapScreen").assertIsDisplayed() + // Add more assertions to verify the map's camera position if possible + } + + @Test + fun markerIsDisplayedOnCurrentLocation() { + val fakeLocation = LatLng(37.7749, -122.4194) // Example coordinates for San Francisco + + // Simulate location update + composeTestRule.runOnIdle { + // Update the locationProvider's currentLocation value + // This part depends on how you can access and update the locationProvider in your test + } + + // Verify if the marker is displayed on the current location + composeTestRule.onNodeWithTag("googleMapScreen").assertIsDisplayed() + // Add more assertions to verify the marker's position if possible + } +} diff --git a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/overview/MenuKtTest.kt b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/overview/MenuKtTest.kt index 81827f454..d566e69a4 100644 --- a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/overview/MenuKtTest.kt +++ b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/overview/MenuKtTest.kt @@ -65,7 +65,7 @@ class MenuKtTest { // Check that all buttons are displayed composeTestRule.onNodeWithText("Quizzes").assertIsDisplayed() composeTestRule.onNodeWithText("Calendar").assertIsDisplayed() - composeTestRule.onNodeWithText("Sky Tracker").assertIsDisplayed() + composeTestRule.onNodeWithText("Google Map").assertIsDisplayed() composeTestRule.onNodeWithTag("profile_button").assertIsDisplayed() } @@ -115,9 +115,9 @@ class MenuKtTest { composeTestRule.setContent { MenuScreen(navigationActions = mockNavigationActions) } // Perform click on "Sky Tracker" button - composeTestRule.onNodeWithText("Sky Tracker").performClick() + composeTestRule.onNodeWithText("Google Map").performClick() // Verify navigation to Sky Tracker screen is triggered - verify(mockNavigationActions).navigateTo(Screen.SKY_TRACKER) + verify(mockNavigationActions).navigateTo(Screen.GOOGLE_MAP) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad038ba8a..0e88e87f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/MainActivity.kt b/app/src/main/java/com/github/lookupgroup27/lookup/MainActivity.kt index 94d26ec79..5d9be0139 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/MainActivity.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/MainActivity.kt @@ -17,6 +17,7 @@ import com.github.lookupgroup27.lookup.model.profile.ProfileViewModel import com.github.lookupgroup27.lookup.model.quiz.QuizViewModel import com.github.lookupgroup27.lookup.ui.authentication.SignInScreen import com.github.lookupgroup27.lookup.ui.calendar.CalendarScreen +import com.github.lookupgroup27.lookup.ui.googlemap.GoogleMapScreen import com.github.lookupgroup27.lookup.ui.map.MapScreen import com.github.lookupgroup27.lookup.ui.navigation.NavigationActions import com.github.lookupgroup27.lookup.ui.navigation.Route @@ -28,7 +29,6 @@ import com.github.lookupgroup27.lookup.ui.profile.ProfileInformationScreen import com.github.lookupgroup27.lookup.ui.profile.ProfileScreen import com.github.lookupgroup27.lookup.ui.quiz.QuizPlayScreen import com.github.lookupgroup27.lookup.ui.quiz.QuizScreen -import com.github.lookupgroup27.lookup.ui.skytracker.SkyTrackerScreen import com.github.lookupgroup27.lookup.ui.theme.LookUpTheme import com.google.firebase.auth.FirebaseAuth @@ -79,7 +79,7 @@ fun LookUpApp() { composable(Screen.MENU) { MenuScreen(navigationActions) } composable(Screen.PROFILE) { ProfileScreen(navigationActions) } composable(Screen.CALENDAR) { CalendarScreen(calendarViewModel, navigationActions) } - composable(Screen.SKY_TRACKER) { SkyTrackerScreen(navigationActions) } + composable(Screen.GOOGLE_MAP) { GoogleMapScreen(navigationActions) } composable(Screen.QUIZ) { QuizScreen(quizViewModel, navigationActions) } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/model/location/LocationProvider.kt b/app/src/main/java/com/github/lookupgroup27/lookup/model/location/LocationProvider.kt new file mode 100644 index 000000000..775b19d23 --- /dev/null +++ b/app/src/main/java/com/github/lookupgroup27/lookup/model/location/LocationProvider.kt @@ -0,0 +1,25 @@ +package com.github.lookupgroup27.lookup.model.location + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import androidx.compose.runtime.mutableStateOf +import androidx.core.app.ActivityCompat +import com.google.android.gms.location.* + +class LocationProvider(private val context: Context) { + private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) + var currentLocation = mutableStateOf(null) + + fun requestLocationUpdates() { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != + PackageManager.PERMISSION_GRANTED) { + // Handle permission request + return + } + fusedLocationClient.lastLocation.addOnSuccessListener { location: Location? -> + currentLocation.value = location + } + } +} diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt new file mode 100644 index 000000000..7ed4768f2 --- /dev/null +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/googlemap/GoogleMap.kt @@ -0,0 +1,104 @@ +package com.github.lookupgroup27.lookup.ui.googlemap + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.location.Location +import android.widget.Toast +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.github.lookupgroup27.lookup.model.location.LocationProvider +import com.github.lookupgroup27.lookup.ui.navigation.BottomNavigationMenu +import com.github.lookupgroup27.lookup.ui.navigation.LIST_TOP_LEVEL_DESTINATION +import com.github.lookupgroup27.lookup.ui.navigation.NavigationActions +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapProperties +import com.google.maps.android.compose.MapType +import com.google.maps.android.compose.MapUiSettings +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.rememberCameraPositionState + +private const val LOCATION_PERMISSION_REQUEST_CODE = 1001 + +@Composable +fun GoogleMapScreen(navigationActions: NavigationActions) { + val context = LocalContext.current + var hasLocationPermission by remember { mutableStateOf(false) } + val locationProvider = remember { LocationProvider(context) } + + LaunchedEffect(Unit) { + hasLocationPermission = + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED + if (hasLocationPermission) { + locationProvider.requestLocationUpdates() + } else { + // Request permission + ActivityCompat.requestPermissions( + context as Activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_PERMISSION_REQUEST_CODE) + Toast.makeText( + context, "Location permission is required to access the map.", Toast.LENGTH_LONG) + .show() + } + } + + Scaffold( + modifier = Modifier.testTag("googleMapScreen"), + bottomBar = { + BottomNavigationMenu( + onTabSelect = { route -> navigationActions.navigateTo(route) }, + tabList = LIST_TOP_LEVEL_DESTINATION, + selectedItem = navigationActions.currentRoute()) + }, + content = { padding -> + MapView( + padding, + hasLocationPermission, + locationProvider.currentLocation.value) // Pass current location to MapView + }) +} + +@Composable +fun MapView(padding: PaddingValues, hasLocationPermission: Boolean, location: Location?) { + var mapProperties by remember { mutableStateOf(MapProperties(mapType = MapType.NORMAL)) } + var mapUiSettings by remember { mutableStateOf(MapUiSettings(zoomControlsEnabled = false)) } + val cameraPositionState = rememberCameraPositionState() + + LaunchedEffect(location) { + if (hasLocationPermission && location != null) { + val latLng = LatLng(location.latitude, location.longitude) + cameraPositionState.position = CameraPosition.fromLatLngZoom(latLng, 5f) + } + } + + GoogleMap( + modifier = Modifier.fillMaxSize().padding(padding), + properties = mapProperties, + uiSettings = mapUiSettings, + cameraPositionState = cameraPositionState) { + if (hasLocationPermission && location != null) { + val latLng = LatLng(location.latitude, location.longitude) + Marker(state = MarkerState(position = latLng), title = "You are here") + } else { + // case where location is not available + } + } +} diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/map/Map.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/map/Map.kt index 8f6815ae2..bc1fb434e 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/map/Map.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/map/Map.kt @@ -21,7 +21,7 @@ fun MapScreen(navigationActions: NavigationActions) { BottomNavigationMenu( onTabSelect = { destination -> navigationActions.navigateTo(destination) }, tabList = LIST_TOP_LEVEL_DESTINATION, - selectedItem = Route.MENU) + selectedItem = Route.MAP) }) { innerPadding -> Box(modifier = Modifier.fillMaxSize().padding(innerPadding).testTag("map_screen")) { Image( diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActions.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActions.kt index b1dd3c86f..901382bba 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActions.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActions.kt @@ -12,7 +12,7 @@ object Route { const val LANDING = "Landing" const val MAP = "Map" const val CALENDAR = "Calendar" - const val SKY_TRACKER = "SkyTracker" + const val GOOGLE_MAP = "Google Map" const val QUIZ = "Quiz" const val QUIZ_PLAY = "QuizPlay" const val PROFILE = "Profile" @@ -26,7 +26,7 @@ object Screen { const val LANDING = "Landing Screen" const val MAP = "Map Screen" const val CALENDAR = "Calendar Screen" - const val SKY_TRACKER = "Sky Tracker Screen" + const val GOOGLE_MAP = "Google Map Screen" const val QUIZ = "Quiz Screen" const val QUIZ_PLAY = "Quiz Play Screen" const val PROFILE = "Profile Screen" diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/overview/Menu.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/overview/Menu.kt index 37e249a69..c8ce2dc3d 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/overview/Menu.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/overview/Menu.kt @@ -82,8 +82,8 @@ fun MenuScreen(navigationActions: NavigationActions) { Button(onClick = { navigationActions.navigateTo(Screen.CALENDAR) }) { Text(text = "Calendar", style = MaterialTheme.typography.headlineSmall) } - Button(onClick = { navigationActions.navigateTo(Screen.SKY_TRACKER) }) { - Text(text = "Sky Tracker", style = MaterialTheme.typography.headlineSmall) + Button(onClick = { navigationActions.navigateTo(Screen.GOOGLE_MAP) }) { + Text(text = "Google Map", style = MaterialTheme.typography.headlineSmall) } } } diff --git a/app/src/test/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActionsTest.kt b/app/src/test/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActionsTest.kt index 2d8c4fa17..919c276d6 100644 --- a/app/src/test/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActionsTest.kt +++ b/app/src/test/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActionsTest.kt @@ -44,8 +44,8 @@ class NavigationActionsTest { verify(navHostController).navigate(eq(Route.MENU), any Unit>()) // Test navigating to specific screens - navigationActions.navigateTo(Screen.SKY_TRACKER) - verify(navHostController).navigate(Screen.SKY_TRACKER) + navigationActions.navigateTo(Screen.GOOGLE_MAP) + verify(navHostController).navigate(Screen.GOOGLE_MAP) navigationActions.navigateTo(Screen.QUIZ) verify(navHostController).navigate(Screen.QUIZ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1ce711cb..b9a893c6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,6 +65,13 @@ firebaseFirestore = "25.1.0" firebaseUiAuth = "8.0.0" navigationRuntimeKtx = "2.8.2" +# Google Service and Maps +playServicesAuth = "21.2.0" +playServicesMaps = "19.0.0" +playServicesLocation = "21.3.0" +mapsCompose = "4.3.3" +mapsComposeUtils = "4.3.0" + # Calendar Libraries ical4j = "3.0.21" compose = "1.5.1" @@ -150,6 +157,14 @@ ical4j = { module = "org.mnode.ical4j:ical4j", version.ref = "ical4j" } compose = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "compose" } androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "navigationTesting" } +maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" } +maps-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "mapsComposeUtils" } +play-services-auth = { module = "com.google.android.gms:play-services-auth", version.ref = "playServicesAuth" } +play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" } + +play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" } + + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }