Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Maps3DSamples/advanced/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
alias(libs.plugins.secrets.gradle.plugin)
alias(libs.plugins.kotlinx.serialization)
}

android {
Expand Down Expand Up @@ -154,6 +155,12 @@ dependencies {
// Google Maps Utils for the polyline decoder
implementation(libs.maps.utils.ktx)

implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json)

implementation(libs.androidx.material.icons.extended)
}

Expand Down
4 changes: 4 additions & 0 deletions Maps3DSamples/advanced/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
android:name=".scenarios.ScenariosActivity"
android:exported="true"
/>
<activity
android:name=".scenarios.RouteSampleActivity"
android:exported="true"
/>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.advancedmaps3dsamples.scenarios.RouteSampleActivity
import com.example.advancedmaps3dsamples.scenarios.ScenariosActivity
import com.example.advancedmaps3dsamples.ui.theme.AdvancedMaps3DSamplesTheme
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -48,6 +49,7 @@ data class MapSample(@StringRes val label: Int, val clazz: Class<*>)
private val samples =
listOf(
MapSample(R.string.map_sample_scenarios, ScenariosActivity::class.java),
MapSample(R.string.map_sample_route, RouteSampleActivity::class.java),
)

@OptIn(ExperimentalMaterial3Api::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2025 Google LLC
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have the header check in this repo, but being a new file, it could be worth adding the 2026 year.

//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.example.advancedmaps3dsamples.common

import android.util.Log
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.http.isSuccess
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json

/**
* Exception thrown when the Routes API returns an error, such as a 403 Forbidden
* if the API is not enabled for the provided key.
*/
class DirectionsErrorException(message: String) : Exception(message)

/**
* A simple network service to fetch routes from the Google Maps Routes API.
*
* Note: In a production application, making direct API calls to Google Maps Platform
* services from a client device requires embedding the API key in the app, which
* poses a security risk. Best practice is to proxy these requests through a secure
* backend server. This client implementation is provided for demonstration purposes.
*/
object RoutesApiService {

private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
encodeDefaults = true
})
}
}

/**
* Fetches a route between the origin and destination coordinates.
*
* @param apiKey The Google Maps API key (requires Routes API enabled).
* @param originLat The latitude of the starting point.
* @param originLng The longitude of the starting point.
* @param destLat The latitude of the destination point.
* @param destLng The longitude of the destination point.
* @return [RoutesResponse] containing the computed route.
* @throws [DirectionsErrorException] if the API returns a non-success HTTP status.
*/
suspend fun fetchRoute(
apiKey: String,
originLat: Double,
originLng: Double,
destLat: Double,
destLng: Double
): RoutesResponse {
val requestBody = RoutesRequest(
origin = Waypoint(Location(RequestLatLng(originLat, originLng))),
destination = Waypoint(Location(RequestLatLng(destLat, destLng)))
)

val response: HttpResponse = client.post("https://routes.googleapis.com/directions/v2:computeRoutes") {
contentType(ContentType.Application.Json)
header("X-Goog-Api-Key", apiKey)
// Requesting only the most relevant fields to optimize payload size
header("X-Goog-FieldMask", "routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline,routes.legs.steps.startLocation")
setBody(requestBody)
}

if (response.status.isSuccess()) {
return response.body()
} else {
val errorBody = response.bodyAsText()
Log.e("RoutesApiService", "Failed to fetch route: ${response.status.value}\n$errorBody")

// Provide a localized, user-friendly message based on typical API errors
val userMsg = if (response.status.value == 403) {
"API Error (HTTP 403). Ensure the Routes API is enabled in the Google Cloud Console for the provided API key."
} else {
"Failed to fetch route (HTTP ${response.status.value})."
}
throw DirectionsErrorException(userMsg)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2025 Google LLC
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright 2025 Google LLC
// Copyright 2026 Google LLC

//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.example.advancedmaps3dsamples.common

import kotlinx.serialization.Serializable

@Serializable
data class RoutesRequest(
val origin: Waypoint,
val destination: Waypoint,
val travelMode: String = "DRIVE",
val routingPreference: String = "TRAFFIC_AWARE",
val computeAlternativeRoutes: Boolean = false,
val routeModifiers: RouteModifiers = RouteModifiers(),
val languageCode: String = "en-US",
val units: String = "METRIC"
)

@Serializable
data class Waypoint(
val location: Location
)

@Serializable
data class Location(
val latLng: RequestLatLng
)

@Serializable
data class RequestLatLng(
val latitude: Double,
val longitude: Double
)

@Serializable
data class RouteModifiers(
val avoidTolls: Boolean = false,
val avoidHighways: Boolean = false,
val avoidFerries: Boolean = false
)

@Serializable
data class RoutesResponse(
val routes: List<Route> = emptyList()
)

@Serializable
data class Route(
val distanceMeters: Int? = null,
val duration: String? = null,
val polyline: Polyline? = null,
val legs: List<RouteLeg> = emptyList()
)

@Serializable
data class RouteLeg(
val steps: List<RouteStep> = emptyList()
)

@Serializable
data class RouteStep(
val startLocation: Location? = null
)

@Serializable
data class Polyline(
val encodedPolyline: String
)
Loading
Loading