Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement Google Map Feature with Location Handling #76

Merged
merged 10 commits into from
Nov 3, 2024
Merged
12 changes: 11 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -34,6 +34,8 @@ android {
vectorDrawables {
useSupportLibrary = true
}
manifestPlaceholders["MAPS_API_KEY"] = mapsApiKey

}

signingConfigs {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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)
}
}
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>

<application
android:allowBackup="true"
Expand All @@ -24,6 +27,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
</application>


</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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) }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Location?>(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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ class NavigationActionsTest {
verify(navHostController).navigate(eq(Route.MENU), any<NavOptionsBuilder.() -> 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)
Expand Down
15 changes: 15 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down
Loading