From 6a6dc06be2dc109bb31c33fa5c958d3ed6d612ee Mon Sep 17 00:00:00 2001 From: Marina Coelho Date: Tue, 16 Jun 2026 12:06:04 +0100 Subject: [PATCH 1/3] Add Store Localizer bottomsheet and Grounding with Maps logic --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 2 + .../data/datasource/AIRemoteDataSource.kt | 74 ++- .../data/repository/AIRepository.kt | 17 + .../data/schema/LocalizerSchema.kt | 20 + .../ui/groceryList/GroceryListScreen.kt | 440 +++++++++++++++++- .../ui/groceryList/GroceryListViewModel.kt | 60 ++- app/src/main/res/values/strings.xml | 12 + gradle/libs.versions.toml | 3 + 9 files changed, 626 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/LocalizerSchema.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04a0a2a..4c4c9e3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,6 +70,8 @@ dependencies { implementation(libs.firebase.storage) implementation(libs.firebase.firestore) implementation(libs.firebase.perf) + implementation(libs.play.services.location) + implementation(libs.compose.material.icons.core) //Library to handle Markdown in Compose implementation(libs.richtext.commonmark) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ab4840..7402536 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools"> + + , + latitude: Double, + longitude: Double, + currentTime: String, + dayOfWeek: String + ): List { + val retrievalConfig = retrievalConfig { + latLng = LatLng(latitude = latitude, longitude = longitude) + languageCode = "en_US" + } + val toolConfig = ToolConfig( + retrievalConfig = retrievalConfig + ) + val model = aiModel.generativeModel( + modelName = "gemini-2.5-flash", + tools = listOf(Tool.googleMaps()), + toolConfig = toolConfig + ) + + val prompt = """ + What are the nearest grocery stores or markets near me that stock these ingredients: ${ingredients.joinToString(", ")}? + For each place, tell me their business hours, if it's open right now on $dayOfWeek at $currentTime, and if it's closing in less than 30 minutes. + Tell me what the parking situation is like at each place: is there a dedicated lot or should I look for street parking? + + Format your response strictly as a JSON object with a "stores" array containing store objects. + Each store object must have these exact keys: + - "name": string + - "address": string + - "distance": string + - "openNow": boolean + - "closingSoon": boolean + - "hasParking": boolean + - "parkingDetails": string + + Do not include markdown code block formatting (like ```json). Output only the raw JSON string. + """.trimIndent() + + return try { + val response = model.generateContent(prompt) + val rawText = response.text ?: return emptyList() + + val cleanJson = rawText + .replace("```json", "") + .replace("```", "") + .trim() + + val result = json.decodeFromString(cleanJson) + val groundingChunks = response.candidates.firstOrNull()?.groundingMetadata?.groundingChunks + + result.stores.map { store -> + val matchingChunk = groundingChunks?.find { chunk -> + val chunkTitle = chunk.maps?.title.orEmpty().lowercase() + val storeName = store.name.lowercase() + chunkTitle.contains(storeName) || storeName.contains(chunkTitle) + } + store.copy(mapUrl = matchingChunk?.maps?.uri.orEmpty()) + } + } catch (e: Exception) { + Log.e("AIRemoteDataSource", "Error localizing ingredients", e) + emptyList() + } + } + suspend fun generateIngredients(image: Bitmap): String { // Adding a Performance Monitoring trace is completely optional. Traces can help you // measure how long it takes to generate ingredients on device and in cloud. diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt index 5a1a37e..0871dae 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt @@ -4,6 +4,7 @@ import android.graphics.Bitmap import com.google.firebase.example.friendlymeals.data.datasource.AIRemoteDataSource import com.google.firebase.example.friendlymeals.data.schema.MealSchema import com.google.firebase.example.friendlymeals.data.schema.RecipeSchema +import com.google.firebase.example.friendlymeals.data.schema.LocalStore import javax.inject.Inject class AIRepository @Inject constructor( @@ -13,6 +14,22 @@ class AIRepository @Inject constructor( return aiRemoteDataSource.generateIngredients(image) } + suspend fun localizeIngredients( + ingredients: List, + latitude: Double, + longitude: Double, + currentTime: String, + dayOfWeek: String + ): List { + return aiRemoteDataSource.localizeIngredients( + ingredients, + latitude, + longitude, + currentTime, + dayOfWeek + ) + } + suspend fun generateRecipe(ingredients: String, notes: String): RecipeSchema? { return aiRemoteDataSource.generateRecipe(ingredients, notes) } diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/LocalizerSchema.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/LocalizerSchema.kt new file mode 100644 index 0000000..2bdfe66 --- /dev/null +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/LocalizerSchema.kt @@ -0,0 +1,20 @@ +package com.google.firebase.example.friendlymeals.data.schema + +import kotlinx.serialization.Serializable + +@Serializable +data class LocalStore( + val name: String = "", + val address: String = "", + val distance: String = "", + val openNow: Boolean = false, + val closingSoon: Boolean = false, + val hasParking: Boolean = false, + val parkingDetails: String = "", + val mapUrl: String = "" +) + +@Serializable +data class StoreLocalizerResult( + val stores: List = emptyList() +) diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt index fd56772..64ef93e 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt @@ -1,5 +1,12 @@ package com.google.firebase.example.friendlymeals.ui.groceryList +import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -20,16 +27,22 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Place import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -39,6 +52,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -47,10 +62,16 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import com.google.android.gms.tasks.CancellationTokenSource import com.google.firebase.example.friendlymeals.R import com.google.firebase.example.friendlymeals.data.model.GroceryItem +import com.google.firebase.example.friendlymeals.data.schema.LocalStore import com.google.firebase.example.friendlymeals.ui.theme.BorderColor import com.google.firebase.example.friendlymeals.ui.theme.FriendlyMealsTheme import com.google.firebase.example.friendlymeals.ui.theme.LightTeal @@ -66,23 +87,69 @@ fun GroceryListScreen( viewModel: GroceryListViewModel = hiltViewModel() ) { val groceries = viewModel.groceries.collectAsStateWithLifecycle() + val localizerState = viewModel.localizerState.collectAsStateWithLifecycle() GroceryListScreenContent( groceries = groceries.value, + localizerState = localizerState.value, onAddItem = viewModel::addItem, onToggleItem = viewModel::toggleItem, - onDeleteItem = viewModel::deleteItem + onDeleteItem = viewModel::deleteItem, + onLocalize = viewModel::localizeGroceryList, + onResetLocalizer = viewModel::resetLocalizer ) } @Composable fun GroceryListScreenContent( groceries: List, + localizerState: LocalizerUiState = LocalizerUiState.Idle, onAddItem: (String) -> Unit = {}, onToggleItem: (GroceryItem) -> Unit = {}, - onDeleteItem: (GroceryItem) -> Unit = {} + onDeleteItem: (GroceryItem) -> Unit = {}, + onLocalize: (Double, Double) -> Unit = { _, _ -> }, + onResetLocalizer: () -> Unit = {} ) { var inputText by remember { mutableStateOf("") } + val context = LocalContext.current + val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } + var showBottomSheet by remember { mutableStateOf(false) } + + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val fineLocationGranted = permissions[ACCESS_FINE_LOCATION] ?: false + val coarseLocationGranted = permissions[ACCESS_COARSE_LOCATION] ?: false + if (fineLocationGranted || coarseLocationGranted) { + showBottomSheet = true + getCurrentLocation(fusedLocationClient, + onSuccess = { lat, lng -> + onLocalize(lat, lng) + }, + onFailure = { + //TODO: handle failure + } + ) + } + } + + fun startLocalizer() { + if (hasLocationPermission(context)) { + showBottomSheet = true + getCurrentLocation(fusedLocationClient, + onSuccess = { lat, lng -> + onLocalize(lat, lng) + }, + onFailure = { + //TODO: handle failure + } + ) + } else { + permissionLauncher.launch( + arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION) + ) + } + } fun handleAdd() { if (inputText.isNotBlank()) { @@ -104,6 +171,20 @@ fun GroceryListScreenContent( fontSize = 28.sp, fontWeight = FontWeight.Bold ) + + Spacer(modifier = Modifier.weight(1f)) + + IconButton( + onClick = { startLocalizer() }, + modifier = Modifier.size(48.dp) + ) { + Icon( + imageVector = Icons.Default.Place, + contentDescription = stringResource(R.string.store_localizer_button_content_description), + tint = Teal, + modifier = Modifier.size(28.dp) + ) + } } } ) { innerPadding -> @@ -182,6 +263,25 @@ fun GroceryListScreenContent( } } } + if (showBottomSheet) { + LocalizerBottomSheet( + uiState = localizerState, + onDismiss = { + showBottomSheet = false + onResetLocalizer() + }, + onRetry = { + getCurrentLocation(fusedLocationClient, + onSuccess = { lat, lng -> + onLocalize(lat, lng) + }, + onFailure = { + //TODO: handle failure + } + ) + } + ) + } } } } @@ -261,6 +361,342 @@ fun GroceryCard( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LocalizerBottomSheet( + uiState: LocalizerUiState, + onDismiss: () -> Unit, + onRetry: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = Color(0xFFF7F9FB), + shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp) + ) { + Text( + text = stringResource(R.string.store_localizer_title), + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = TextColor, + modifier = Modifier.padding(bottom = 8.dp) + ) + Text( + text = stringResource(R.string.store_localizer_subtitle), + fontSize = 14.sp, + color = Color.Gray, + modifier = Modifier.padding(bottom = 20.dp) + ) + + when (uiState) { + is LocalizerUiState.Idle -> { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + color = Teal, + modifier = Modifier.size(48.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.store_localizer_determining_location), + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = TextColor + ) + } + } + is LocalizerUiState.Loading -> { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + color = Teal, + modifier = Modifier.size(48.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.store_localizer_locating_stores), + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = TextColor + ) + } + } + is LocalizerUiState.Error -> { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = uiState.message, + fontSize = 16.sp, + color = Color.Red, + fontWeight = FontWeight.Medium + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onRetry, + colors = ButtonDefaults.buttonColors(containerColor = Teal) + ) { + Text(stringResource(R.string.store_localizer_retry)) + } + } + } + is LocalizerUiState.Success -> { + if (uiState.stores.isEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 40.dp), + contentAlignment = Alignment.Center + ) { + Text( + stringResource(R.string.store_localizer_no_stores), + color = Color.Gray + ) + } + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(uiState.stores) { store -> + StoreCard(store = store) + } + } + } + } + } + } + } +} + +@Composable +fun StoreCard(store: LocalStore) { + val uriHandler = LocalUriHandler.current + + Card( + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = store.mapUrl.isNotBlank()) { + uriHandler.openUri(store.mapUrl) + } + ) { + Column( + modifier = Modifier + .padding(18.dp) + .fillMaxWidth() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = store.name, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = TextColor + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = store.address, + fontSize = 14.sp, + color = Color.Gray + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(Color(0xFFE8F5E9), RoundedCornerShape(8.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Icon( + imageVector = Icons.Default.Place, + contentDescription = "Place icon", + tint = Color(0xFF2E7D32), + modifier = Modifier.size(14.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = store.distance, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF2E7D32) + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (store.openNow) { + Row( + modifier = Modifier + .background(Color(0xFFE8F5E9), RoundedCornerShape(8.dp)) + .padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(Color(0xFF4CAF50), CircleShape) + ) + + Spacer(modifier = Modifier.width(6.dp)) + + Text( + text = stringResource(R.string.store_localizer_open_now), + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + color = Color(0xFF2E7D32) + ) + } + } else { + Row( + modifier = Modifier + .background(Color(0xFFFFEBEE), RoundedCornerShape(8.dp)) + .padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(Color(0xFFE57373), CircleShape) + ) + + Spacer(modifier = Modifier.width(6.dp)) + + Text( + text = stringResource(R.string.store_localizer_closed), + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + color = Color(0xFFC62828) + ) + } + } + + if (store.closingSoon) { + Row( + modifier = Modifier + .background(Color(0xFFFFF3E0), RoundedCornerShape(8.dp)) + .padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(Color(0xFFFF9800), CircleShape) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.store_localizer_closing_soon), + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + color = Color(0xFFEF6C00) + ) + } + } + + val parkingColor = if (store.hasParking) Color(0xFFE3F2FD) else Color(0xFFECEFF1) + val parkingTextColor = if (store.hasParking) Color(0xFF1565C0) else Color(0xFF455A64) + val text = if (store.hasParking) R.string.store_localizer_parking else R.string.store_localizer_no_parking + + Text( + text = stringResource(text), + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + color = parkingTextColor, + modifier = Modifier + .background(parkingColor, RoundedCornerShape(8.dp)) + .padding(horizontal = 10.dp, vertical = 6.dp) + ) + } + + if (store.parkingDetails.isNotBlank()) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = store.parkingDetails, + fontSize = 12.sp, + color = Color.Gray, + lineHeight = 16.sp + ) + } + } + } +} + +fun hasLocationPermission(context: Context): Boolean { + return ContextCompat.checkSelfPermission( + context, + ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission( + context, + ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED +} + +@SuppressLint("MissingPermission") +fun getCurrentLocation( + fusedLocationClient: FusedLocationProviderClient, + onSuccess: (Double, Double) -> Unit, + onFailure: (Exception) -> Unit +) { + val priority = Priority.PRIORITY_HIGH_ACCURACY + val cancellationTokenSource = CancellationTokenSource() + + fusedLocationClient.getCurrentLocation(priority, cancellationTokenSource.token) + .addOnSuccessListener { location -> + if (location != null) { + onSuccess(location.latitude, location.longitude) + } else { + onFailure(Exception("Location is null")) + //TODO: Fix OnFailure logic + } + } + .addOnFailureListener { exception -> + onFailure(exception) + } +} + @Preview @Composable fun GroceryListScreenPreview() { diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt index 48f7d9f..b361f5c 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt @@ -4,21 +4,37 @@ import com.google.firebase.example.friendlymeals.MainViewModel import com.google.firebase.example.friendlymeals.data.model.GroceryItem import com.google.firebase.example.friendlymeals.data.repository.AuthRepository import com.google.firebase.example.friendlymeals.data.repository.DatabaseRepository +import com.google.firebase.example.friendlymeals.data.repository.AIRepository +import com.google.firebase.example.friendlymeals.data.schema.LocalStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch +import java.time.LocalDateTime +import java.time.format.TextStyle +import java.util.Locale import javax.inject.Inject +sealed interface LocalizerUiState { + object Idle : LocalizerUiState + object Loading : LocalizerUiState + data class Success(val stores: List) : LocalizerUiState + data class Error(val message: String) : LocalizerUiState +} + @HiltViewModel class GroceryListViewModel @Inject constructor( private val authRepository: AuthRepository, - private val databaseRepository: DatabaseRepository + private val databaseRepository: DatabaseRepository, + private val aiRepository: AIRepository ) : MainViewModel() { private val _groceries = MutableStateFlow>(emptyList()) val groceries: StateFlow> = _groceries.asStateFlow() + private val _localizerState = MutableStateFlow(LocalizerUiState.Idle) + val localizerState: StateFlow = _localizerState.asStateFlow() + val userId: String get() = authRepository.currentUser?.uid.orEmpty() init { @@ -61,4 +77,46 @@ class GroceryListViewModel @Inject constructor( databaseRepository.deleteGroceryItem(item.id) } } + + fun resetLocalizer() { + _localizerState.value = LocalizerUiState.Idle + } + + fun localizeGroceryList(latitude: Double, longitude: Double) { + val uncheckedIngredients = _groceries.value + .filter { !it.checked } + .map { it.name } + + if (uncheckedIngredients.isEmpty()) { + _localizerState.value = LocalizerUiState.Error(EMPTY_ITEMS_ERROR) + return + } + + _localizerState.value = LocalizerUiState.Loading + + launchCatching { + val now = LocalDateTime.now() + val dayOfWeek = now.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.US) + val currentTime = String.format(Locale.US, "%02d:%02d", now.hour, now.minute) + + val stores = aiRepository.localizeIngredients( + ingredients = uncheckedIngredients, + latitude = latitude, + longitude = longitude, + currentTime = currentTime, + dayOfWeek = dayOfWeek + ) + + if (stores.isEmpty()) { + _localizerState.value = LocalizerUiState.Error(EMPTY_STORE_ERROR) + } else { + _localizerState.value = LocalizerUiState.Success(stores) + } + } + } + + companion object { + private const val EMPTY_ITEMS_ERROR = "Your grocery list is empty or all items are checked." + private const val EMPTY_STORE_ERROR = "Failed to fetch local stores." + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c9ca124..2f9ab68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,4 +64,16 @@ Add ingredients to Grocery List Added to Grocery List! Your grocery list is empty + Ingredient-to-Store Localizer + Find stores nearby with the ingredients you need. + Determining your location… + Locating nearest stores with Google Maps… + No stores found matching your ingredients nearby. + Open Now + Closed + Closing Soon + Retry + 🅿️ Parking Available + No Parking + Store Localizer \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a3479a..42b219d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ kotlinxSerializationJson = "1.11.0" richtextCommonmark = "1.0.0-alpha04" firebasePerf = "2.0.2" cameraView = "1.6.1" +playServicesLocation = "21.3.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -63,6 +64,8 @@ androidx-camera-core = { group = "androidx.camera", name = "camera-core", versio androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraView" } androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraView" } androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraView" } +play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" } +compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 6489d1bec8a0057c9bed889eebd5baeed7bce3f7 Mon Sep 17 00:00:00 2001 From: Marina Coelho Date: Wed, 17 Jun 2026 11:56:19 +0100 Subject: [PATCH 2/3] Fixing dark theme, improving Data Source and View Model code --- .../example/friendlymeals/MainActivity.kt | 7 ++- .../data/datasource/AIRemoteDataSource.kt | 55 +++++++---------- .../data/repository/AIRepository.kt | 4 +- .../{LocalizerSchema.kt => StoreSchema.kt} | 4 +- .../ui/groceryList/GroceryListScreen.kt | 60 +++++++++---------- .../ui/groceryList/GroceryListViewModel.kt | 22 +++---- .../ui/groceryList/StoreLocalizerUiState.kt | 10 ++++ .../main/res/xml/remote_config_defaults.xml | 8 +++ 8 files changed, 84 insertions(+), 86 deletions(-) rename app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/{LocalizerSchema.kt => StoreSchema.kt} (85%) create mode 100644 app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreLocalizerUiState.kt diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/MainActivity.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/MainActivity.kt index 91ccc36..5b2aff3 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/MainActivity.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/MainActivity.kt @@ -143,7 +143,12 @@ class MainActivity : ComponentActivity() { ) } composable { - GroceryListScreen() + GroceryListScreen( + showError = { + val message = this@MainActivity.getString(R.string.error_message) + scope.launch { snackbarHostState.showSnackbar(message) } + } + ) } } } diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt index 85fb734..ef2d70f 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt @@ -20,7 +20,6 @@ import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.content import com.google.firebase.example.friendlymeals.data.schema.MealSchema import com.google.firebase.example.friendlymeals.data.schema.RecipeSchema -import com.google.firebase.example.friendlymeals.data.schema.LocalStore import com.google.firebase.example.friendlymeals.data.schema.StoreLocalizerResult import com.google.firebase.perf.performance import com.google.firebase.perf.trace @@ -28,11 +27,10 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig import kotlinx.serialization.json.Json import javax.inject.Inject import com.google.firebase.ai.type.LatLng -import com.google.firebase.ai.type.Schema import com.google.firebase.ai.type.Tool import com.google.firebase.ai.type.ToolConfig -import com.google.firebase.ai.type.generationConfig import com.google.firebase.ai.type.retrievalConfig +import com.google.firebase.example.friendlymeals.data.schema.StoreSchema @OptIn(PublicPreviewAPI::class) class AIRemoteDataSource @Inject constructor( @@ -40,6 +38,7 @@ class AIRemoteDataSource @Inject constructor( private val remoteConfig: FirebaseRemoteConfig ) { private val json = Json { ignoreUnknownKeys = true } + private val hybridGenerativeModel = aiModel.generativeModel( modelName = remoteConfig.getString(HYBRID_CLOUD_MODEL_KEY), onDeviceConfig = OnDeviceConfig(mode = InferenceMode.PREFER_IN_CLOUD) @@ -53,40 +52,25 @@ class AIRemoteDataSource @Inject constructor( longitude: Double, currentTime: String, dayOfWeek: String - ): List { - val retrievalConfig = retrievalConfig { - latLng = LatLng(latitude = latitude, longitude = longitude) - languageCode = "en_US" - } - val toolConfig = ToolConfig( - retrievalConfig = retrievalConfig - ) - val model = aiModel.generativeModel( - modelName = "gemini-2.5-flash", + ): List { + val groundingModel = aiModel.generativeModel( + modelName = remoteConfig.getString(GROUNDING_MODEL_KEY), tools = listOf(Tool.googleMaps()), - toolConfig = toolConfig + toolConfig = ToolConfig( + retrievalConfig = retrievalConfig { + latLng = LatLng(latitude = latitude, longitude = longitude) + languageCode = LANGUAGE + } + ) ) - val prompt = """ - What are the nearest grocery stores or markets near me that stock these ingredients: ${ingredients.joinToString(", ")}? - For each place, tell me their business hours, if it's open right now on $dayOfWeek at $currentTime, and if it's closing in less than 30 minutes. - Tell me what the parking situation is like at each place: is there a dedicated lot or should I look for street parking? - - Format your response strictly as a JSON object with a "stores" array containing store objects. - Each store object must have these exact keys: - - "name": string - - "address": string - - "distance": string - - "openNow": boolean - - "closingSoon": boolean - - "hasParking": boolean - - "parkingDetails": string - - Do not include markdown code block formatting (like ```json). Output only the raw JSON string. - """.trimIndent() + val groundingPrompt = remoteConfig.getString(GROUNDING_PROMPT_KEY) + .replace("{{ingredients}}", ingredients.joinToString(", ")) + .replace("{{dayOfWeek}}", dayOfWeek) + .replace("{{currentTime}}", currentTime) return try { - val response = model.generateContent(prompt) + val response = groundingModel.generateContent(groundingPrompt) val rawText = response.text ?: return emptyList() val cleanJson = rawText @@ -106,7 +90,7 @@ class AIRemoteDataSource @Inject constructor( store.copy(mapUrl = matchingChunk?.maps?.uri.orEmpty()) } } catch (e: Exception) { - Log.e("AIRemoteDataSource", "Error localizing ingredients", e) + Log.e(TAG, "Error localizing ingredients", e) emptyList() } } @@ -214,6 +198,8 @@ class AIRemoteDataSource @Inject constructor( private const val SCAN_MEAL_KEY = "scan_meal" private const val HYBRID_CLOUD_MODEL_KEY = "hybrid_cloud_model" private const val HYBRID_INGREDIENTS_PROMPT_KEY = "hybrid_ingredients_prompt" + private const val GROUNDING_MODEL_KEY = "grounding_model" + private const val GROUNDING_PROMPT_KEY = "grounding_prompt" //Template input fields private const val IMAGE_DATA_FIELD = "imageData" @@ -225,6 +211,9 @@ class AIRemoteDataSource @Inject constructor( //Template input values private const val MIME_TYPE_VALUE = "image/jpeg" + //Grounding with Maps config + private const val LANGUAGE = "en_US" + //Class TAG private const val TAG = "AIRemoteDataSource" } diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt index 0871dae..a19f6dc 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt @@ -4,7 +4,7 @@ import android.graphics.Bitmap import com.google.firebase.example.friendlymeals.data.datasource.AIRemoteDataSource import com.google.firebase.example.friendlymeals.data.schema.MealSchema import com.google.firebase.example.friendlymeals.data.schema.RecipeSchema -import com.google.firebase.example.friendlymeals.data.schema.LocalStore +import com.google.firebase.example.friendlymeals.data.schema.StoreSchema import javax.inject.Inject class AIRepository @Inject constructor( @@ -20,7 +20,7 @@ class AIRepository @Inject constructor( longitude: Double, currentTime: String, dayOfWeek: String - ): List { + ): List { return aiRemoteDataSource.localizeIngredients( ingredients, latitude, diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/LocalizerSchema.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/StoreSchema.kt similarity index 85% rename from app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/LocalizerSchema.kt rename to app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/StoreSchema.kt index 2bdfe66..c057384 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/LocalizerSchema.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/schema/StoreSchema.kt @@ -3,7 +3,7 @@ package com.google.firebase.example.friendlymeals.data.schema import kotlinx.serialization.Serializable @Serializable -data class LocalStore( +data class StoreSchema( val name: String = "", val address: String = "", val distance: String = "", @@ -16,5 +16,5 @@ data class LocalStore( @Serializable data class StoreLocalizerResult( - val stores: List = emptyList() + val stores: List = emptyList() ) diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt index 64ef93e..1f8c394 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListScreen.kt @@ -71,7 +71,7 @@ import com.google.android.gms.location.Priority import com.google.android.gms.tasks.CancellationTokenSource import com.google.firebase.example.friendlymeals.R import com.google.firebase.example.friendlymeals.data.model.GroceryItem -import com.google.firebase.example.friendlymeals.data.schema.LocalStore +import com.google.firebase.example.friendlymeals.data.schema.StoreSchema import com.google.firebase.example.friendlymeals.ui.theme.BorderColor import com.google.firebase.example.friendlymeals.ui.theme.FriendlyMealsTheme import com.google.firebase.example.friendlymeals.ui.theme.LightTeal @@ -84,14 +84,16 @@ object GroceryListRoute @Composable fun GroceryListScreen( - viewModel: GroceryListViewModel = hiltViewModel() + viewModel: GroceryListViewModel = hiltViewModel(), + showError: () -> Unit ) { val groceries = viewModel.groceries.collectAsStateWithLifecycle() - val localizerState = viewModel.localizerState.collectAsStateWithLifecycle() + val uiState = viewModel.uiState.collectAsStateWithLifecycle() GroceryListScreenContent( groceries = groceries.value, - localizerState = localizerState.value, + uiState = uiState.value, + showError = showError, onAddItem = viewModel::addItem, onToggleItem = viewModel::toggleItem, onDeleteItem = viewModel::deleteItem, @@ -103,7 +105,8 @@ fun GroceryListScreen( @Composable fun GroceryListScreenContent( groceries: List, - localizerState: LocalizerUiState = LocalizerUiState.Idle, + uiState: StoreLocalizerUiState = StoreLocalizerUiState.Idle, + showError: () -> Unit = {}, onAddItem: (String) -> Unit = {}, onToggleItem: (GroceryItem) -> Unit = {}, onDeleteItem: (GroceryItem) -> Unit = {}, @@ -127,7 +130,7 @@ fun GroceryListScreenContent( onLocalize(lat, lng) }, onFailure = { - //TODO: handle failure + showError() } ) } @@ -141,7 +144,7 @@ fun GroceryListScreenContent( onLocalize(lat, lng) }, onFailure = { - //TODO: handle failure + showError() } ) } else { @@ -264,8 +267,8 @@ fun GroceryListScreenContent( } } if (showBottomSheet) { - LocalizerBottomSheet( - uiState = localizerState, + StoreLocalizerBottomSheet( + uiState = uiState, onDismiss = { showBottomSheet = false onResetLocalizer() @@ -276,7 +279,7 @@ fun GroceryListScreenContent( onLocalize(lat, lng) }, onFailure = { - //TODO: handle failure + showError() } ) } @@ -363,8 +366,8 @@ fun GroceryCard( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LocalizerBottomSheet( - uiState: LocalizerUiState, +fun StoreLocalizerBottomSheet( + uiState: StoreLocalizerUiState, onDismiss: () -> Unit, onRetry: () -> Unit ) { @@ -373,7 +376,6 @@ fun LocalizerBottomSheet( ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = Color(0xFFF7F9FB), shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp) ) { Column( @@ -386,18 +388,16 @@ fun LocalizerBottomSheet( text = stringResource(R.string.store_localizer_title), fontSize = 22.sp, fontWeight = FontWeight.Bold, - color = TextColor, modifier = Modifier.padding(bottom = 8.dp) ) Text( text = stringResource(R.string.store_localizer_subtitle), fontSize = 14.sp, - color = Color.Gray, modifier = Modifier.padding(bottom = 20.dp) ) when (uiState) { - is LocalizerUiState.Idle -> { + is StoreLocalizerUiState.Idle -> { Column( modifier = Modifier .fillMaxWidth() @@ -414,12 +414,11 @@ fun LocalizerBottomSheet( Text( text = stringResource(R.string.store_localizer_determining_location), fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = TextColor + fontWeight = FontWeight.Medium ) } } - is LocalizerUiState.Loading -> { + is StoreLocalizerUiState.Loading -> { Column( modifier = Modifier .fillMaxWidth() @@ -436,12 +435,11 @@ fun LocalizerBottomSheet( Text( text = stringResource(R.string.store_localizer_locating_stores), fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = TextColor + fontWeight = FontWeight.Medium ) } } - is LocalizerUiState.Error -> { + is StoreLocalizerUiState.Error -> { Column( modifier = Modifier .fillMaxWidth() @@ -465,7 +463,7 @@ fun LocalizerBottomSheet( } } } - is LocalizerUiState.Success -> { + is StoreLocalizerUiState.Success -> { if (uiState.stores.isEmpty()) { Box( modifier = Modifier @@ -473,10 +471,7 @@ fun LocalizerBottomSheet( .padding(vertical = 40.dp), contentAlignment = Alignment.Center ) { - Text( - stringResource(R.string.store_localizer_no_stores), - color = Color.Gray - ) + Text(stringResource(R.string.store_localizer_no_stores)) } } else { LazyColumn( @@ -495,7 +490,7 @@ fun LocalizerBottomSheet( } @Composable -fun StoreCard(store: LocalStore) { +fun StoreCard(store: StoreSchema) { val uriHandler = LocalUriHandler.current Card( @@ -678,7 +673,7 @@ fun hasLocationPermission(context: Context): Boolean { fun getCurrentLocation( fusedLocationClient: FusedLocationProviderClient, onSuccess: (Double, Double) -> Unit, - onFailure: (Exception) -> Unit + onFailure: () -> Unit ) { val priority = Priority.PRIORITY_HIGH_ACCURACY val cancellationTokenSource = CancellationTokenSource() @@ -688,12 +683,11 @@ fun getCurrentLocation( if (location != null) { onSuccess(location.latitude, location.longitude) } else { - onFailure(Exception("Location is null")) - //TODO: Fix OnFailure logic + onFailure() } } - .addOnFailureListener { exception -> - onFailure(exception) + .addOnFailureListener { + onFailure() } } diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt index b361f5c..c60eb5d 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/GroceryListViewModel.kt @@ -5,7 +5,6 @@ import com.google.firebase.example.friendlymeals.data.model.GroceryItem import com.google.firebase.example.friendlymeals.data.repository.AuthRepository import com.google.firebase.example.friendlymeals.data.repository.DatabaseRepository import com.google.firebase.example.friendlymeals.data.repository.AIRepository -import com.google.firebase.example.friendlymeals.data.schema.LocalStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,13 +15,6 @@ import java.time.format.TextStyle import java.util.Locale import javax.inject.Inject -sealed interface LocalizerUiState { - object Idle : LocalizerUiState - object Loading : LocalizerUiState - data class Success(val stores: List) : LocalizerUiState - data class Error(val message: String) : LocalizerUiState -} - @HiltViewModel class GroceryListViewModel @Inject constructor( private val authRepository: AuthRepository, @@ -32,8 +24,8 @@ class GroceryListViewModel @Inject constructor( private val _groceries = MutableStateFlow>(emptyList()) val groceries: StateFlow> = _groceries.asStateFlow() - private val _localizerState = MutableStateFlow(LocalizerUiState.Idle) - val localizerState: StateFlow = _localizerState.asStateFlow() + private val _uiState = MutableStateFlow(StoreLocalizerUiState.Idle) + val uiState: StateFlow = _uiState.asStateFlow() val userId: String get() = authRepository.currentUser?.uid.orEmpty() @@ -79,7 +71,7 @@ class GroceryListViewModel @Inject constructor( } fun resetLocalizer() { - _localizerState.value = LocalizerUiState.Idle + _uiState.value = StoreLocalizerUiState.Idle } fun localizeGroceryList(latitude: Double, longitude: Double) { @@ -88,11 +80,11 @@ class GroceryListViewModel @Inject constructor( .map { it.name } if (uncheckedIngredients.isEmpty()) { - _localizerState.value = LocalizerUiState.Error(EMPTY_ITEMS_ERROR) + _uiState.value = StoreLocalizerUiState.Error(EMPTY_ITEMS_ERROR) return } - _localizerState.value = LocalizerUiState.Loading + _uiState.value = StoreLocalizerUiState.Loading launchCatching { val now = LocalDateTime.now() @@ -108,9 +100,9 @@ class GroceryListViewModel @Inject constructor( ) if (stores.isEmpty()) { - _localizerState.value = LocalizerUiState.Error(EMPTY_STORE_ERROR) + _uiState.value = StoreLocalizerUiState.Error(EMPTY_STORE_ERROR) } else { - _localizerState.value = LocalizerUiState.Success(stores) + _uiState.value = StoreLocalizerUiState.Success(stores) } } } diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreLocalizerUiState.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreLocalizerUiState.kt new file mode 100644 index 0000000..e6cd463 --- /dev/null +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/ui/groceryList/StoreLocalizerUiState.kt @@ -0,0 +1,10 @@ +package com.google.firebase.example.friendlymeals.ui.groceryList + +import com.google.firebase.example.friendlymeals.data.schema.StoreSchema + +sealed interface StoreLocalizerUiState { + object Idle : StoreLocalizerUiState + object Loading : StoreLocalizerUiState + data class Success(val stores: List) : StoreLocalizerUiState + data class Error(val message: String) : StoreLocalizerUiState +} \ No newline at end of file diff --git a/app/src/main/res/xml/remote_config_defaults.xml b/app/src/main/res/xml/remote_config_defaults.xml index 91c3b0f..e4d0920 100644 --- a/app/src/main/res/xml/remote_config_defaults.xml +++ b/app/src/main/res/xml/remote_config_defaults.xml @@ -1,4 +1,12 @@ + + grounding_model + gemini-3.1-flash-lite + + + grounding_prompt + What are the nearest grocery stores or markets near me that stock these ingredients: {{ingredients}}? For each place, tell me their business hours, if it's open right now on {{dayOfWeek}} at {{currentTime}}, and if it's closing in less than 30 minutes. Tell me what the parking situation is like at each place: is there a dedicated lot or should I look for street parking? Tell the Map URL so I can open it in Google Maps. Format your response strictly as a JSON object with a "stores" array containing store objects. Each store object must have these EXACT keys: "name": string, "address": string, "distance": string, "openNow": boolean, "closingSoon": boolean, "hasParking": boolean, "parkingDetails": string, "mapUrl": string. Do NOT include markdown code block formatting (like ```json). Output only the raw JSON string. + hybrid_cloud_model gemini-3.1-flash-lite From 577f8f49540de1a63fc69f0c8631435330d14c83 Mon Sep 17 00:00:00 2001 From: Marina Coelho Date: Wed, 17 Jun 2026 12:03:48 +0100 Subject: [PATCH 3/3] Removing unnecessary mapping --- .../data/datasource/AIRemoteDataSource.kt | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt b/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt index ef2d70f..d185cc9 100644 --- a/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt +++ b/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt @@ -78,17 +78,7 @@ class AIRemoteDataSource @Inject constructor( .replace("```", "") .trim() - val result = json.decodeFromString(cleanJson) - val groundingChunks = response.candidates.firstOrNull()?.groundingMetadata?.groundingChunks - - result.stores.map { store -> - val matchingChunk = groundingChunks?.find { chunk -> - val chunkTitle = chunk.maps?.title.orEmpty().lowercase() - val storeName = store.name.lowercase() - chunkTitle.contains(storeName) || storeName.contains(chunkTitle) - } - store.copy(mapUrl = matchingChunk?.maps?.uri.orEmpty()) - } + json.decodeFromString(cleanJson).stores } catch (e: Exception) { Log.e(TAG, "Error localizing ingredients", e) emptyList()