diff --git a/.github/workflows/lint-report.yml b/.github/workflows/lint-report.yml index 989f5959..f0527b70 100644 --- a/.github/workflows/lint-report.yml +++ b/.github/workflows/lint-report.yml @@ -41,7 +41,7 @@ jobs: - name: Merge SARIF files run: | - jq -s '{ "$schema": "https://json.schemastore.org/sarif-2.1.0", "version": "2.1.0", "runs": map(.runs) | add }' maps-compose/build/reports/lint-results.sarif maps-compose-utils/build/reports/lint-results.sarif maps-compose-widgets/build/reports/lint-results.sarif app/build/reports/lint-results.sarif > merged.sarif + jq -s '{ "$schema": "https://json.schemastore.org/sarif-2.1.0", "version": "2.1.0", "runs": map(.runs) | add }' maps-compose/build/reports/lint-results.sarif maps-compose-utils/build/reports/lint-results.sarif maps-compose-widgets/build/reports/lint-results.sarif maps-app/build/reports/lint-results.sarif navigation-app/build/reports/lint-results.sarif > merged.sarif - name: Upload SARIF file uses: github/codeql-action/upload-sarif@v3 diff --git a/README.md b/README.md index 7a2479bd..e17b2798 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ -![Tests](https://github.com/googlemaps/android-maps-compose/actions/workflows/test.yml/badge.svg) +[![Maven Central](https://img.shields.io/maven-central/v/com.google.maps.android/maps-compose)](https://maven-badges.herokuapp.com/maven-central/com.google.maps.android/maps-compose) +![Release](https://github.com/googlemaps/android-maps-compose/workflows/Release/badge.svg) ![Stable](https://img.shields.io/badge/stability-stable-green) -[![Discord](https://img.shields.io/discord/676948200904589322)][Discord server] -![Apache-2.0](https://img.shields.io/badge/license-Apache-blue) +[![Tests/Build](https://github.com/googlemaps/android-maps-compose/actions/workflows/test.yml/badge.svg)](https://github.com/googlemaps/android-maps-compose/actions/workflows/test.yml) + +![Contributors](https://img.shields.io/github/contributors/googlemaps/android-maps-compose?color=green) +[![License](https://img.shields.io/github/license/googlemaps/android-maps-compose?color=blue)][license] +[![StackOverflow](https://img.shields.io/stackexchange/stackoverflow/t/google-maps?color=orange&label=google-maps&logo=stackoverflow)](https://stackoverflow.com/questions/tagged/google-maps) +[![Discord](https://img.shields.io/discord/676948200904589322?color=6A7EC2&logo=discord&logoColor=ffffff)][Discord server] # Maps Compose 🗺 @@ -11,10 +16,12 @@ This repository contains [Jetpack Compose][jetpack-compose] components for the [ ## Requirements +* Android API level 21+ * Kotlin-enabled project * Jetpack Compose-enabled project (see [releases](https://github.com/googlemaps/android-maps-compose/releases) for the required version of Jetpack Compose) -* An [API key][api-key] -* API level 21+ +* [Sign up with Google Maps Platform] +* A Google Maps Platform [project] with the **Maps SDK for Android** enabled +- An [API key] associated with the project above ... follow the [API key instructions] if you're new to the process ## Installation @@ -22,35 +29,39 @@ You no longer need to specify the Maps SDK for Android or its Utility Library as ```groovy dependencies { - implementation 'com.google.maps.android:maps-compose:6.4.4' + implementation 'com.google.maps.android:maps-compose:6.5.0' // Optionally, you can include the Compose utils library for Clustering, // Street View metadata checks, etc. - implementation 'com.google.maps.android:maps-compose-utils:6.4.4' + implementation 'com.google.maps.android:maps-compose-utils:6.5.0' // Optionally, you can include the widgets library for ScaleBar, etc. - implementation 'com.google.maps.android:maps-compose-widgets:6.4.4' + implementation 'com.google.maps.android:maps-compose-widgets:6.5.0' } ``` ## Sample App -This repository includes a [sample app](app). +Currently, there are two sample apps in the repository: [maps-app](app) and [navigation-app](navigation-app). Each of them run a different version of the Android Maps Compose SDK, where either the maps or the navigation SDK are run, respectively. -To run it: +To run the maps demo app, ensure you've met the requirements above then: -1. Get a [Maps API key][api-key] -2. Open the `secrets.properties` file in your top-level directory, and then add the following code. Replace YOUR_API_KEY with your API key. Store your key in this file because secrets.properties is excluded from being checked into a version control system. +1. Open the `secrets.properties` file in your top-level directory, and then add the following code. Replace YOUR_API_KEY with your API key. Store your key in this file because secrets.properties is excluded from being checked into a version control system. If the `secrets.properties` file does not exist, create it in the same folder as the `local.default.properties` file. - ``` MAPS_API_KEY=YOUR_API_KEY ``` -3. Build and run + + If you want to use also the Navigation SDK, make sure the PLACES_API_KEY also contains an entry. + ``` + PLACES_API_KEY=DEFAULT_API_KEY + ``` + +1. Build and run ## Documentation -You can learn more about all the extensions provided by this library by reading the [reference documents][Javadoc]. +See the [documentation] for a full list of classes and their methods. ## Usage @@ -386,6 +397,18 @@ GoogleMap( } ``` +## Using the Navigation SDK + +In order to use the Navigation SDK, make sure to include the Composable `NavigationScreen` as follows: + +```kotlin +NavigationScreen( + modifier = Modifier.padding(innerPadding), + deviceLocation = location +) +``` + + ## Maps Compose Utility Library @@ -474,24 +497,42 @@ The colors of the text, line, and shadow are also all configurable (e.g., based ## Contributing -Contributions are welcome and encouraged! See [contributing] for more info. +Contributions are welcome and encouraged! If you'd like to contribute, send us a [pull request] and refer to our [code of conduct] and [contributing guide]. + +## Terms of Service + +This library uses Google Maps Platform services. Use of Google Maps Platform services through this library is subject to the Google Maps Platform [Terms of Service]. + +This library is not a Google Maps Platform Core Service. Therefore, the Google Maps Platform Terms of Service (e.g. Technical Support Services, Service Level Agreements, and Deprecation Policy) do not apply to the code in this library. ## Support -This library is offered via an open source [license](LICENSE). It is not governed by the Google Maps Platform [Technical Support Services Guidelines](https://cloud.google.com/maps-platform/terms/tssg?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=android_oss), the [SLA](https://cloud.google.com/maps-platform/terms/sla?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=android_oss), or the [Deprecation Policy](https://cloud.google.com/maps-platform/terms?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=android_oss) (however, any Google Maps Platform services used by the library remain subject to the Google Maps Platform Terms of Service). +This library is offered via an open source [license]. It is not governed by the Google Maps Platform Support [Technical Support Services Guidelines, the SLA, or the [Deprecation Policy]. However, any Google Maps Platform services used by the library remain subject to the Google Maps Platform Terms of Service. -This library adheres to [semantic versioning](https://semver.org/) to indicate when backwards-incompatible changes are introduced. +This library adheres to [semantic versioning] to indicate when backwards-incompatible changes are introduced. Accordingly, while the library is in version 0.x, backwards-incompatible changes may be introduced at any time. -If you find a bug, or have a feature request, please [file an issue] on GitHub. +If you find a bug, or have a feature request, please [file an issue] on GitHub. If you would like to get answers to technical questions from other Google Maps Platform developers, ask through one of our [developer community channels]. If you'd like to contribute, please check the [contributing guide]. -If you would like to get answers to technical questions from other Google Maps Platform developers, ask through one of our [developer community channels](https://developers.google.com/maps/developer-community?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=android_oss) including the Google Maps Platform [Discord server]. +You can also discuss this library on our [Discord server]. +[API key]: https://developers.google.com/maps/documentation/android-sdk/get-api-key +[API key instructions]: https://developers.google.com/maps/documentation/android-sdk/config#step_3_add_your_api_key_to_the_project [maps-sdk]: https://developers.google.com/maps/documentation/android-sdk -[api-key]: https://developers.google.com/maps/documentation/android-sdk/get-api-key +[documentation]: https://googlemaps.github.io/android-maps-compose +[jetpack-compose]: https://developer.android.com/jetpack/compose + +[code of conduct]: ?tab=coc-ov-file#readme +[contributing guide]: CONTRIBUTING.md +[Deprecation Policy]: https://cloud.google.com/maps-platform/terms +[developer community channels]: https://developers.google.com/maps/developer-community [Discord server]: https://discord.gg/hYsWbmk -[Javadoc]: https://googlemaps.github.io/android-maps-compose -[contributing]: CONTRIBUTING.md -[code of conduct]: CODE_OF_CONDUCT.md [file an issue]: https://github.com/googlemaps/android-maps-compose/issues/new/choose +[license]: LICENSE +[project]: https://developers.google.com/maps/documentation/android-sdk/cloud-setup [pull request]: https://github.com/googlemaps/android-maps-compose/compare -[jetpack-compose]: https://developer.android.com/jetpack/compose +[semantic versioning]: https://semver.org +[Sign up with Google Maps Platform]: https://console.cloud.google.com/google/maps-apis/start +[similar inquiry]: https://github.com/googlemaps/android-maps-compose/issues +[SLA]: https://cloud.google.com/maps-platform/terms/sla +[Technical Support Services Guidelines]: https://cloud.google.com/maps-platform/terms/tssg +[Terms of Service]: https://cloud.google.com/maps-platform/terms diff --git a/build.gradle.kts b/build.gradle.kts index 86407696..f7a8642d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,8 @@ plugins { alias(libs.plugins.dokka) apply true alias(libs.plugins.compose.compiler) apply false id("com.autonomousapps.dependency-analysis") version "2.0.0" + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false } @@ -32,7 +34,7 @@ val projectArtifactId by extra { project: Project -> allprojects { group = "com.google.maps.android" - version = "6.4.4" + version = "6.5.0" val projectArtifactId by extra { project.name } } diff --git a/gradle.properties b/gradle.properties index 0ed08c6d..42dfc0bb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,9 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX # (not needed, since no requested libraries are pre-AndroidX) -android.enableJetifier=false +#android.enableJetifier=false +# This is needed for the navigation SDK library. It needs the recycler view. +android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 027597b8..ae27c157 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,35 @@ [versions] +accompanistPermissions = "0.37.0" activitycompose = "1.9.3" agp = "8.7.2" -androidxtest = "1.6.2" androidCore = "1.6.1" +androidx-core = "1.15.0" +androidxtest = "1.6.2" compose-bom = "2024.11.00" dokka = "1.9.20" espresso = "3.6.1" jacoco-plugin = "0.2.1" -junitktx = "1.2.1" junit = "4.13.2" +junitVersion = "1.2.1" +junitktx = "1.2.1" kotlin = "2.0.21" kotlinxCoroutines = "1.9.0" -mapsktx = "5.1.1" +leakcanaryAndroid = "2.12" +lifecycleRuntimeKtx = "2.8.7" mapsecrets = "2.0.1" +mapsktx = "5.1.1" +navigation = "6.1.0" org-jacoco-core = "0.8.11" -androidx-core = "1.15.0" +places = "4.1.0" +playServicesLocation = "21.3.0" +robolectric = "4.14.1" screenshot = "0.0.1-alpha08" +secretsGradlePlugin = "2.0.1" +truth = "1.4.4" [libraries] +# robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } androidx-compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activitycompose" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } @@ -27,25 +39,40 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-test-compose-ui = { module = "androidx.compose.ui:ui-test-junit4" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidCore" } androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } androidx-test-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitktx" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidCore" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxtest" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } jacoco-android-plugin = { module = "com.mxalbert.gradle:jacoco-android", version.ref = "jacoco-plugin", version.require = "0.2.1" } kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } +leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" } maps-ktx-std = { module = "com.google.maps.android:maps-ktx", version.ref = "mapsktx" } maps-ktx-utils = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "mapsktx" } maps-secrets-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "mapsecrets" } +navigation = { module = "com.google.android.libraries.navigation:navigation", version.ref = "navigation" } org-jacoco-core = { module = "org.jacoco:org.jacoco.core", version.ref = "org-jacoco-core" } +places = { group = "com.google.android.libraries.places", name = "places", version.ref = "places" } +play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } test-junit = { module = "junit:junit", version.ref = "junit" } +truth = { module = "com.google.truth:truth", version.ref = "truth" } [plugins] -dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +android-application = { id = "com.android.application", version.ref = "agp" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"} \ No newline at end of file +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"} +secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } diff --git a/local.defaults.properties b/local.defaults.properties index e56e0bf3..3c792a96 100644 --- a/local.defaults.properties +++ b/local.defaults.properties @@ -7,4 +7,5 @@ # Location of the SDK. This is only used by Gradle. # For customization when using a Version Control System, please read the # header note. -MAPS_API_KEY=YOUR_API_KEY \ No newline at end of file +MAPS_API_KEY=YOUR_API_KEY +PLACES_API_KEY=DEFAULT_API_KEY \ No newline at end of file diff --git a/app/.gitignore b/maps-app/.gitignore similarity index 100% rename from app/.gitignore rename to maps-app/.gitignore diff --git a/app/build.gradle.kts b/maps-app/build.gradle.kts similarity index 98% rename from app/build.gradle.kts rename to maps-app/build.gradle.kts index 030af4ca..7af6545d 100644 --- a/app/build.gradle.kts +++ b/maps-app/build.gradle.kts @@ -66,6 +66,8 @@ dependencies { implementation(libs.kotlinx.coroutines.android) implementation(libs.androidx.compose.ui.preview.tooling) debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.leakcanary.android) + androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.test.core) diff --git a/app/proguard-rules.pro b/maps-app/proguard-rules.pro similarity index 100% rename from app/proguard-rules.pro rename to maps-app/proguard-rules.pro diff --git a/app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt similarity index 100% rename from app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt rename to maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt diff --git a/app/src/androidTest/java/com/google/maps/android/compose/MapInColumnTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/MapInColumnTests.kt similarity index 100% rename from app/src/androidTest/java/com/google/maps/android/compose/MapInColumnTests.kt rename to maps-app/src/androidTest/java/com/google/maps/android/compose/MapInColumnTests.kt diff --git a/app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt similarity index 100% rename from app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt rename to maps-app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt diff --git a/app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt similarity index 100% rename from app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt rename to maps-app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt diff --git a/app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewDisappearingScaleBar_0.png b/maps-app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewDisappearingScaleBar_0.png similarity index 100% rename from app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewDisappearingScaleBar_0.png rename to maps-app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewDisappearingScaleBar_0.png diff --git a/app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewScaleBar_0.png b/maps-app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewScaleBar_0.png similarity index 100% rename from app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewScaleBar_0.png rename to maps-app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewScaleBar_0.png diff --git a/app/src/main/AndroidManifest.xml b/maps-app/src/main/AndroidManifest.xml similarity index 100% rename from app/src/main/AndroidManifest.xml rename to maps-app/src/main/AndroidManifest.xml diff --git a/app/src/main/java/com/google/maps/android/compose/AccessibilityActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/AccessibilityActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/AccessibilityActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/AccessibilityActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/MainActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/RecompositionActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/RecompositionActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/RecompositionActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/RecompositionActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt rename to maps-app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt diff --git a/app/src/main/java/com/google/maps/android/compose/theme/Theme.kt b/maps-app/src/main/java/com/google/maps/android/compose/theme/Theme.kt similarity index 100% rename from app/src/main/java/com/google/maps/android/compose/theme/Theme.kt rename to maps-app/src/main/java/com/google/maps/android/compose/theme/Theme.kt diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/maps-app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to maps-app/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/maps-app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_background.xml rename to maps-app/src/main/res/drawable/ic_launcher_background.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/maps-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to maps-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/maps-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to maps-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/maps-app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to maps-app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/maps-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to maps-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/maps-app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to maps-app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/maps-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to maps-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/maps-app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to maps-app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/maps-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to maps-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/maps-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to maps-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/maps-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to maps-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/maps-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to maps-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/maps-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to maps-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/values/strings.xml b/maps-app/src/main/res/values/strings.xml similarity index 100% rename from app/src/main/res/values/strings.xml rename to maps-app/src/main/res/values/strings.xml diff --git a/app/src/main/res/values/themes.xml b/maps-app/src/main/res/values/themes.xml similarity index 100% rename from app/src/main/res/values/themes.xml rename to maps-app/src/main/res/values/themes.xml diff --git a/app/src/screenshotTest/java/com/google/maps/android/compose/ScaleBarTest.kt b/maps-app/src/screenshotTest/java/com/google/maps/android/compose/ScaleBarTest.kt similarity index 100% rename from app/src/screenshotTest/java/com/google/maps/android/compose/ScaleBarTest.kt rename to maps-app/src/screenshotTest/java/com/google/maps/android/compose/ScaleBarTest.kt diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/ComposeInfoWindowAdapter.kt b/maps-compose/src/main/java/com/google/maps/android/compose/ComposeInfoWindowAdapter.kt index cfaf370b..bb8a7754 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/ComposeInfoWindowAdapter.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/ComposeInfoWindowAdapter.kt @@ -17,7 +17,6 @@ package com.google.maps.android.compose import android.view.View import androidx.compose.ui.platform.ComposeView import com.google.android.gms.maps.GoogleMap -import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.Marker /** @@ -36,7 +35,7 @@ import com.google.android.gms.maps.model.Marker * implementation should be updated. */ internal class ComposeInfoWindowAdapter( - private val mapView: MapView, + private val mapViewDelegate: AbstractMapViewDelegate<*>, private val markerNodeFinder: (Marker) -> MarkerNode? ) : GoogleMap.InfoWindowAdapter { @@ -46,10 +45,10 @@ internal class ComposeInfoWindowAdapter( if (content == null) { return null } - val view = ComposeView(mapView.context).apply { + val view = ComposeView(mapViewDelegate.mapView.context).apply { setContent { content(marker) } } - mapView.renderComposeViewOnce(view, parentContext = markerNode.compositionContext) + mapViewDelegate.renderComposeViewOnce(view, parentContext = markerNode.compositionContext) return view } @@ -59,11 +58,10 @@ internal class ComposeInfoWindowAdapter( if (infoWindow == null) { return null } - val view = ComposeView(mapView.context).apply { + val view = ComposeView(mapViewDelegate.mapView.context).apply { setContent { infoWindow(marker) } } - mapView.renderComposeViewOnce(view, parentContext = markerNode.compositionContext) + mapViewDelegate.renderComposeViewOnce(view, parentContext = markerNode.compositionContext) return view } - } diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index b7595b23..70d5a90f 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -16,6 +16,7 @@ package com.google.maps.android.compose import android.content.ComponentCallbacks import android.content.ComponentCallbacks2 +import android.content.Context import android.content.res.Configuration import android.location.Location import android.os.Bundle @@ -35,12 +36,15 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner +import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.LocationSource import com.google.android.gms.maps.MapView @@ -98,6 +102,7 @@ public fun GoogleMap( onPOIClick: ((PointOfInterest) -> Unit)? = null, contentPadding: PaddingValues = DefaultMapContentPadding, mapColorScheme: ComposeMapColorScheme? = null, + mapViewCreator: ((Context, GoogleMapOptions) -> AbstractMapViewDelegate<*>)? = null, content: @Composable @GoogleMapComposable () -> Unit = {}, ) { // When in preview, early return a Box with the received modifier preserving layout @@ -145,19 +150,30 @@ public fun GoogleMap( var subcompositionJob by remember { mutableStateOf(null) } val parentCompositionScope = rememberCoroutineScope() + var delegate by remember { + mutableStateOf?>(null) + } + AndroidView( modifier = modifier, factory = { context -> - MapView(context, googleMapOptionsFactory()).also { mapView -> + if (mapViewCreator != null) { + mapViewCreator(context, googleMapOptionsFactory()) + } else { + MapViewDelegate(MapView(context, googleMapOptionsFactory())) + }.also { mapViewDelegate: AbstractMapViewDelegate<*> -> + delegate = mapViewDelegate + val mapView = mapViewDelegate.mapView + val componentCallbacks = object : ComponentCallbacks2 { override fun onConfigurationChanged(newConfig: Configuration) {} @Deprecated("Deprecated in Java", ReplaceWith("onTrimMemory(level)")) - override fun onLowMemory() { mapView.onLowMemory() } - override fun onTrimMemory(level: Int) { mapView.onLowMemory() } + override fun onLowMemory() { mapViewDelegate.onLowMemory() } + override fun onTrimMemory(level: Int) { mapViewDelegate.onLowMemory() } } context.registerComponentCallbacks(componentCallbacks) - val lifecycleObserver = MapLifecycleEventObserver(mapView) + val lifecycleObserver = MapLifecycleEventObserver(mapViewDelegate) mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) @@ -179,11 +195,11 @@ public fun GoogleMap( } mapView.addOnAttachStateChangeListener(onAttachStateListener) - } + }.mapView }, onReset = { /* View is detached. */ }, onRelease = { mapView -> - val (componentCallbacks, lifecycleObserver) = mapView.tagData + val (componentCallbacks, lifecycleObserver) = delegate!!.tagData mapView.context.unregisterComponentCallbacks(componentCallbacks) lifecycleObserver.moveToDestroyedState() mapView.tag = null @@ -193,7 +209,7 @@ public fun GoogleMap( subcompositionJob = parentCompositionScope.launchSubcomposition( mapUpdaterState, parentComposition, - mapView, + delegate!!, mapClickListeners, currentContent, ) @@ -209,15 +225,15 @@ public fun GoogleMap( private fun CoroutineScope.launchSubcomposition( mapUpdaterState: MapUpdaterState, parentComposition: CompositionContext, - mapView: MapView, + mapViewDelegate: AbstractMapViewDelegate<*>, mapClickListeners: MapClickListeners, content: @Composable @GoogleMapComposable () -> Unit, ): Job { // Use [CoroutineStart.UNDISPATCHED] to kick off GoogleMap loading immediately return launch(start = CoroutineStart.UNDISPATCHED) { - val map = mapView.awaitMap() + val map = mapViewDelegate.awaitMap() val composition = Composition( - applier = MapApplier(map, mapView, mapClickListeners), + applier = MapApplier(map, mapViewDelegate, mapClickListeners), parent = parentComposition ) @@ -308,7 +324,65 @@ public fun googleMapFactory( } } -private class MapLifecycleEventObserver(private val mapView: MapView) : LifecycleEventObserver { +public interface AbstractMapViewDelegate { + public fun onCreate(savedInstanceState: Bundle?) + public fun onStart() + public fun onResume() + public fun onPause() + public fun onStop() + public fun onLowMemory() + public fun onDestroy() + public suspend fun awaitMap(): GoogleMap + public fun renderComposeViewOnce( + view: AbstractComposeView, + parentContext: CompositionContext, + onAddedToWindow: ((View) -> Unit)? = null, + ) + + public fun startRenderingComposeView( + view: AbstractComposeView, + parentContext: CompositionContext, + ): ComposeUiViewRenderer.RenderHandle + + public val mapView: T +} + +private val AbstractMapViewDelegate.tagData: MapTagData + get() = mapView.tag as MapTagData + +public class MapViewDelegate(override val mapView: MapView) : AbstractMapViewDelegate { + override fun onCreate(savedInstanceState: Bundle?): Unit = mapView.onCreate(savedInstanceState) + override fun onStart(): Unit = mapView.onStart() + override fun onResume(): Unit = mapView.onResume() + override fun onPause(): Unit = mapView.onPause() + override fun onStop(): Unit = mapView.onStop() + override fun onLowMemory(): Unit = mapView.onLowMemory() + override fun onDestroy(): Unit = mapView.onDestroy() + override suspend fun awaitMap(): GoogleMap = mapView.awaitMap() + override fun renderComposeViewOnce( + view: AbstractComposeView, + parentContext: CompositionContext, + onAddedToWindow: ((View) -> Unit)? + ) { + mapView.renderComposeViewOnce( + view = view, + parentContext = parentContext, + onAddedToWindow = onAddedToWindow + ) + } + + override fun startRenderingComposeView( + view: AbstractComposeView, + parentContext: CompositionContext + ): ComposeUiViewRenderer.RenderHandle { + return mapView.startRenderingComposeView( + view = view, + parentContext = parentContext, + ) + } +} + +private class MapLifecycleEventObserver(private val mapView: AbstractMapViewDelegate<*>) : LifecycleEventObserver { private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt index b887c716..5a265a31 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt @@ -16,10 +16,8 @@ package com.google.maps.android.compose import androidx.compose.runtime.AbstractApplier import com.google.android.gms.maps.GoogleMap -import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.GroundOverlay -import com.google.android.gms.maps.model.MapColorScheme import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.Polyline @@ -39,7 +37,7 @@ private object MapNodeRoot : MapNode // for that particular listener; yet MapClickListeners never actually changes. internal class MapApplier( val map: GoogleMap, - internal val mapView: MapView, + internal val mapViewDelegate: AbstractMapViewDelegate<*>, val mapClickListeners: MapClickListeners, ) : AbstractApplier(MapNodeRoot) { @@ -212,7 +210,7 @@ internal class MapApplier( }) map.setInfoWindowAdapter( ComposeInfoWindowAdapter( - mapView, + mapViewDelegate, markerNodeFinder = { marker -> decorations.firstOrNull { it is MarkerNode && it.marker == marker } as MarkerNode? diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt index 8e1fa5ad..36e0876a 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt @@ -22,7 +22,7 @@ import java.io.Closeable * to a window. [onAddedToWindow] is called in place, and then [view] is removed from the window * before returning. */ -internal fun MapView.renderComposeViewOnce( +public fun MapView.renderComposeViewOnce( view: AbstractComposeView, onAddedToWindow: ((View) -> Unit)? = null, parentContext: CompositionContext, @@ -38,7 +38,7 @@ internal fun MapView.renderComposeViewOnce( * to a window. A [ComposeUiViewRenderer.RenderHandle] is returned, which must be disposed after * this view no longer needs to render. Disposing removes [view] from the [MapView]. */ -internal fun MapView.startRenderingComposeView( +public fun MapView.startRenderingComposeView( view: AbstractComposeView, parentContext: CompositionContext, ): ComposeUiViewRenderer.RenderHandle { @@ -69,7 +69,7 @@ private fun MapView.ensureContainerView(): NoDrawContainerView { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @Composable public fun rememberComposeUiViewRenderer(): ComposeUiViewRenderer { - val mapView = (currentComposer.applier as MapApplier).mapView + val mapView = (currentComposer.applier as MapApplier).mapViewDelegate val compositionContext = rememberCompositionContext() return remember(compositionContext) { diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt index 930ecf09..513a00f2 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt @@ -100,7 +100,7 @@ public val DefaultMapContentPadding: PaddingValues = PaddingValues() @Composable internal inline fun MapUpdater(mapUpdaterState: MapUpdaterState) = with(mapUpdaterState) { val map = (currentComposer.applier as MapApplier).map - val mapView = (currentComposer.applier as MapApplier).mapView + val mapView = (currentComposer.applier as MapApplier).mapViewDelegate.mapView if (mergeDescendants) { mapView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS } diff --git a/navigation-app/.gitignore b/navigation-app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/navigation-app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/navigation-app/build.gradle.kts b/navigation-app/build.gradle.kts new file mode 100644 index 00000000..b28891dd --- /dev/null +++ b/navigation-app/build.gradle.kts @@ -0,0 +1,114 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") +} + +android { + lint { + sarifOutput = file("$buildDir/reports/lint-results.sarif") + } + + namespace = "com.google.maps.android.compose.navigation" + compileSdk = 35 + + defaultConfig { + applicationId = "com.google.maps.android.compose.navigation" + minSdk = 24 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + } + + buildFeatures { + buildConfig = true + compose = true + } +} + +configurations.all { + resolutionStrategy { + // IMPORTANT: This actually is done to exclude all the packages from the play-services-maps, since they will + // conflict with the version from the navigation SDK. + exclude(group = "com.google.android.gms", module = "play-services-maps") + } +} + +dependencies { + + implementation(libs.androidx.core) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.compose.activity) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.compose.ui.preview.tooling) + implementation(libs.androidx.material3) + testImplementation(libs.test.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.test.espresso) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.test.compose.ui) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + // Instead of the lines below, regular apps would load these libraries from Maven according to + // the README installation instructions + implementation(project(":maps-compose")) + implementation(project(":maps-compose-widgets")) + implementation(project(":maps-compose-utils")) + + implementation(libs.maps.ktx.std) + implementation(libs.maps.ktx.utils) + + // Use the navigation SDK which includes the maps SDK + implementation(libs.navigation) + + implementation(libs.play.services.location) + + testImplementation(libs.androidx.core) + testImplementation(libs.truth) + + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + + implementation(libs.places) + + // Accompanist permission helper + implementation(libs.accompanist.permissions) +} + +secrets { + // To add your Maps API key to this project: + // 1. If the secrets.properties file does not exist, create it in the same folder as the local.properties file. + // 2. Add this line, where YOUR_API_KEY is your API key: + // MAPS_API_KEY=YOUR_API_KEY + propertiesFileName = "secrets.properties" + + // A properties file containing default secret values. This file can be + // checked in version control. + defaultPropertiesFileName = "local.defaults.properties" +} diff --git a/navigation-app/proguard-rules.pro b/navigation-app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/navigation-app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/navigation-app/src/androidTest/java/com/google/maps/android/compose/navigation/ExampleInstrumentedTest.kt b/navigation-app/src/androidTest/java/com/google/maps/android/compose/navigation/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..a106d153 --- /dev/null +++ b/navigation-app/src/androidTest/java/com/google/maps/android/compose/navigation/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.google.maps.android.compose.navigation + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.google.maps.android.compose.navigation", appContext.packageName) + } +} \ No newline at end of file diff --git a/navigation-app/src/main/AndroidManifest.xml b/navigation-app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..93f321ef --- /dev/null +++ b/navigation-app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/MainActivity.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/MainActivity.kt new file mode 100644 index 00000000..1bbb6ae0 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/MainActivity.kt @@ -0,0 +1,90 @@ +package com.google.maps.android.compose.navigation + +import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.android.gms.maps.model.LatLng +import com.google.android.libraries.navigation.NavigationApi +import com.google.maps.android.compose.navigation.ui.theme.AndroidmapscomposeTheme +import kotlinx.coroutines.launch + +val defaultLocation = LatLng(39.9828503662161, -105.71835147137016) + +@OptIn(ExperimentalPermissionsApi::class) +class MainActivity : ComponentActivity() { + private val myViewModel: NavigationViewModel by viewModels { NavigationViewModel.Factory } + + override fun onResume() { + super.onResume() + myViewModel.checkLocationPermission() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + NavigationApi.getNavigator(this, myViewModel) + + setContent { + val context = LocalContext.current + val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) } + val scope = rememberCoroutineScope() + val locationPermissionsState = rememberMultiplePermissionsState( + listOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION,) + ) + + val location by myViewModel.location.collectAsStateWithLifecycle() + + fun onShowSnackbar(message: String) { + scope.launch { + val duration = if (message.length > 40) + SnackbarDuration.Long else SnackbarDuration.Short + snackbarHostState.showSnackbar( + message = message, + duration = duration + ) + } + } + + LaunchedEffect(myViewModel, context) { + myViewModel.uiEvent.collect { event -> + when (event) { + is UiEvent.ShowSnackbar -> onShowSnackbar(event.message) + is UiEvent.RequestLocationPermission -> { + locationPermissionsState.launchMultiplePermissionRequest() + } + } + } + } + + AndroidmapscomposeTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + NavigationScreen( + modifier = Modifier.padding(innerPadding), + deviceLocation = location + ) + } + } + } + } +} + diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationApplication.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationApplication.kt new file mode 100644 index 00000000..c0d0d874 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationApplication.kt @@ -0,0 +1,18 @@ +package com.google.maps.android.compose.navigation + +import android.app.Application +import com.google.android.libraries.places.api.Places +import com.google.android.libraries.places.api.net.PlacesClient +import com.google.maps.android.compose.navigation.repositories.ApiKeyProvider + +class NavigationApplication : Application() { + val placesClient: PlacesClient by lazy { + Places.createClient(this) + } + + override fun onCreate() { + super.onCreate() + val keyProvider = ApiKeyProvider(this) + Places.initializeWithNewPlacesApiEnabled(applicationContext, keyProvider.placesApiKey) + } +} diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationScreen.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationScreen.kt new file mode 100644 index 00000000..ec0b9fbc --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationScreen.kt @@ -0,0 +1,91 @@ +package com.google.maps.android.compose.navigation + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.res.painterResource +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.android.libraries.navigation.NavigationView +import com.google.maps.android.compose.ComposeMapColorScheme +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MarkerComposable +import com.google.maps.android.compose.navigation.components.MovableMarker +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.rememberMarkerState + +@Composable +fun NavigationScreen( + deviceLocation: LatLng?, + modifier: Modifier = Modifier +) { + val cameraPositionState = rememberCameraPositionState { + position = CameraPosition.fromLatLngZoom( + deviceLocation ?: defaultLocation, + 15f + ) + } + + LaunchedEffect(deviceLocation) { + cameraPositionState.animate( + update = CameraUpdateFactory.newCameraPosition( + CameraPosition.fromLatLngZoom( + deviceLocation ?: defaultLocation, + 15f + ) + ) + ) + } + + Column( + modifier = modifier + ) { + GoogleMap( + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState, + mapColorScheme = ComposeMapColorScheme.FOLLOW_SYSTEM, + mapViewCreator = { context, options -> + NavigationViewDelegate(NavigationView(context, options)) + } + ) { + if (deviceLocation != null) { + MovableMarker( + position = deviceLocation, + title = "User location", + ) + } + + MarkerComposable( + title = "Bigfoot", + state = rememberMarkerState(position = LatLng(39.99932703674056, -105.28152457787887)), + ) { + Box( + modifier = Modifier + .width(48.dp) + .height(48.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color.Transparent), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(R.drawable.bigfoot), + contentDescription = "" + ) + } + } + } + } +} diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewDelegate.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewDelegate.kt new file mode 100644 index 00000000..78d2c922 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewDelegate.kt @@ -0,0 +1,104 @@ +package com.google.maps.android.compose.navigation + +import android.content.ComponentCallbacks2 +import android.content.Context +import android.graphics.Canvas +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.CompositionContext +import androidx.compose.ui.platform.AbstractComposeView +import com.google.android.gms.maps.GoogleMap +import com.google.android.libraries.navigation.NavigationView +import com.google.maps.android.compose.AbstractMapViewDelegate +import com.google.maps.android.compose.ComposeUiViewRenderer +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class NavigationViewDelegate(override val mapView: NavigationView) : + AbstractMapViewDelegate { + override fun onCreate(savedInstanceState: Bundle?): Unit = mapView.onCreate(savedInstanceState) + override fun onStart(): Unit = mapView.onStart() + override fun onResume(): Unit = mapView.onResume() + override fun onPause(): Unit = mapView.onPause() + override fun onStop(): Unit = mapView.onStop() + + override fun onLowMemory() { + mapView.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE) + } + + override fun onDestroy(): Unit = mapView.onDestroy() + + override suspend fun awaitMap(): GoogleMap = mapView.awaitMap() + + override fun renderComposeViewOnce( + view: AbstractComposeView, + parentContext: CompositionContext, + onAddedToWindow: ((View) -> Unit)? + ) { + mapView.renderComposeViewOnce( + view = view, + parentContext = parentContext, + onAddedToWindow = onAddedToWindow + ) + } + + override fun startRenderingComposeView( + view: AbstractComposeView, + parentContext: CompositionContext + ): ComposeUiViewRenderer.RenderHandle { + return mapView.startRenderingComposeView( + view = view, + parentContext = parentContext, + ) + } +} + +suspend inline fun NavigationView.awaitMap(): GoogleMap = + suspendCoroutine { continuation -> + getMapAsync { + continuation.resume(it) + } + } + +private fun NavigationView.renderComposeViewOnce( + view: AbstractComposeView, + onAddedToWindow: ((View) -> Unit)? = null, + parentContext: CompositionContext, +) { + startRenderingComposeView(view, parentContext).use { + onAddedToWindow?.invoke(view) + } +} + +private fun NavigationView.startRenderingComposeView( + view: AbstractComposeView, + parentContext: CompositionContext, +): ComposeUiViewRenderer.RenderHandle { + val containerView = ensureContainerView() + containerView.addView(view) + view.apply { + setParentCompositionContext(parentContext) + } + return object : ComposeUiViewRenderer.RenderHandle { + override fun dispose() { + containerView.removeView(view) + } + + } +} + +private fun NavigationView.ensureContainerView(): com.google.maps.android.compose.navigation.NoDrawContainerView { + return findViewById(R.id.maps_compose_nodraw_container_view) + ?: NoDrawContainerView(context) + .apply { id = R.id.maps_compose_nodraw_container_view } + .also(::addView) +} + +private class NoDrawContainerView(context: Context) : ViewGroup(context) { + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + } + + override fun dispatchDraw(canvas: Canvas) { + } +} diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewModel.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewModel.kt new file mode 100644 index 00000000..0d644a09 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewModel.kt @@ -0,0 +1,239 @@ +package com.google.maps.android.compose.navigation + + +import android.Manifest +import android.annotation.SuppressLint +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import com.google.android.gms.maps.model.LatLng +import com.google.android.libraries.navigation.NavigationApi +import com.google.android.libraries.navigation.NavigationApi.NavigatorListener +import com.google.android.libraries.navigation.Navigator +import com.google.android.libraries.navigation.RoutingOptions +import com.google.android.libraries.navigation.SimulationOptions +import com.google.android.libraries.navigation.Waypoint +import com.google.android.libraries.places.api.net.PlacesClient +import com.google.maps.android.compose.navigation.repositories.LocationProvider +import com.google.maps.android.compose.navigation.repositories.PermissionChecker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import com.google.android.gms.tasks.CancellationTokenSource +import com.google.android.libraries.navigation.ListenableResultFuture +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import com.google.maps.android.compose.navigation.BuildConfig + +class NavigationViewModel( + private val placesClient: PlacesClient, + private val locationProvider: LocationProvider, + private val permissionChecker: PermissionChecker, +) : ViewModel(), NavigatorListener { + + private fun String.isPermissionGranted() = permissionChecker.isGranted(this) + + private val _location = MutableStateFlow(null) + val location = _location.asStateFlow().onStart { + requestLocation() + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = null + ) + + private val _uiEvent = MutableSharedFlow() + val uiEvent: SharedFlow = _uiEvent.asSharedFlow() + + private val _hasLocationPermission = MutableStateFlow(false) + + private var navigator: Navigator? = null + + init { + viewModelScope.launch { + _hasLocationPermission.collect() { + if (it) { + requestLocation() + } + } + } + } + + @SuppressLint("MissingPermission") + fun requestLocation() { + viewModelScope.launch { + if (Manifest.permission.ACCESS_FINE_LOCATION.isPermissionGranted() || Manifest.permission.ACCESS_COARSE_LOCATION.isPermissionGranted()) { + val location = locationProvider.getLastLocation()?.toLatLng() + if (location != null) { + _location.value = location + } + } else { + _uiEvent.emit(UiEvent.RequestLocationPermission) + } + } + } + + fun checkLocationPermission() { + viewModelScope.launch { + _hasLocationPermission.value = Manifest.permission.ACCESS_FINE_LOCATION.isPermissionGranted() || Manifest.permission.ACCESS_COARSE_LOCATION.isPermissionGranted() + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + extras: CreationExtras + ): T { + val application = + checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) as NavigationApplication + + return NavigationViewModel( + placesClient = application.placesClient, + locationProvider = LocationProvider(application.applicationContext), + permissionChecker = PermissionChecker(application.applicationContext) + ) as T + } + } + } + + private fun navigateToPlace(placeId: String, routingOptions: RoutingOptions) { + val localNavigator = checkNotNull(navigator) + + viewModelScope.launch { + try { + val destination = withContext(Dispatchers.IO) { + Waypoint.builder().setPlaceIdString(placeId).build() + } + + val cancellationTokenSource = CancellationTokenSource() + val routeStatusFuture = localNavigator.setDestination(destination, routingOptions) + + suspendCoroutine { continuation -> + val callback = + ListenableResultFuture.OnResultListener { status -> + fun onFailure(message: String) { + continuation.resumeWithException(Exception(message)) + } + + when (status) { + Navigator.RouteStatus.OK -> { + // // Hide the toolbar to maximize the navigation UI. + // if (getActionBar() != null) { + // getActionBar().hide() + // } + // // Enable voice audio guidance (through the device speaker). + // navigator.setAudioGuidance( + // Navigator.AudioGuidance.VOICE_ALERTS_AND_GUIDANCE + // ) + // Simulate vehicle progress along the route for demo/debug builds. + if (BuildConfig.DEBUG) { + localNavigator.simulator.simulateLocationsAlongExistingRoute( + SimulationOptions().speedMultiplier(5f) + ) + } + + // Start turn-by-turn guidance along the current route. + localNavigator.startGuidance() + + continuation.resume(Unit) + } + + Navigator.RouteStatus.NO_ROUTE_FOUND -> onFailure("Error starting navigation: No route found") + Navigator.RouteStatus.NETWORK_ERROR -> onFailure("Error starting navigation: Network error") + Navigator.RouteStatus.QUOTA_CHECK_FAILED -> onFailure("Error starting navigation: Quota check failed") + Navigator.RouteStatus.ROUTE_CANCELED -> onFailure("Error starting navigation: Route canceled") + Navigator.RouteStatus.LOCATION_DISABLED -> onFailure("Error starting navigation: Location disabled") + Navigator.RouteStatus.LOCATION_UNKNOWN -> onFailure("Error starting navigation: Location unknown") + Navigator.RouteStatus.WAYPOINT_ERROR -> onFailure("Error starting navigation: Waypoint error") + + else -> onFailure("Error starting navigation: $status") + } + } + + routeStatusFuture.setOnResultListener(callback) + } + } catch (e: Waypoint.UnsupportedPlaceIdException) { + withContext(Dispatchers.Main) { + displayMessage("Error starting navigation: Place ID is not supported.") + } + } + } + } + + override fun onNavigatorReady(navigator: Navigator?) { + displayMessage("navigator ready") + + val chautauquaDinningHall = "ChIJ9zb1-0bsa4cRcpW_h34lLBU" + + this.navigator = navigator ?: error("Navigator is null") + + /* + // Optional. Disable the guidance notifications and shut down the app + // and background service when the user closes the app. + // mNavigator.setTaskRemovedBehavior(Navigator.TaskRemovedBehavior.QUIT_SERVICE) + + // Optional. Set the last digit of the car's license plate to get + // route restrictions for supported countries. + // mNavigator.setLicensePlateRestrictionInfo(getLastDigit(), "BZ"); + + // Set the camera to follow the device location with 'TILTED' driving view. + mNavFragment.getCamera().followMyLocation(Camera.Perspective.TILTED); + + // Set the travel mode (DRIVING, WALKING, CYCLING, TWO_WHEELER, or TAXI). + mRoutingOptions = new RoutingOptions(); + mRoutingOptions.travelMode(RoutingOptions.TravelMode.DRIVING); + + // Navigate to a place, specified by Place ID. + navigateToPlace(SYDNEY_OPERA_HOUSE, mRoutingOptions); + */ + + val routingOptions = RoutingOptions().apply { + travelMode(RoutingOptions.TravelMode.DRIVING) + } + + navigateToPlace(chautauquaDinningHall, routingOptions) + } + + override fun onError(@NavigationApi.ErrorCode errorCode: Int) { + when (errorCode) { + NavigationApi.ErrorCode.NOT_AUTHORIZED -> displayMessage( + "Error loading Navigation SDK: Your API key is " + + "invalid or not authorized to use the Navigation SDK." + ) + + NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED -> displayMessage( + "Error loading Navigation SDK: User did not accept " + + "the Navigation Terms of Use." + ) + + NavigationApi.ErrorCode.NETWORK_ERROR -> displayMessage("Error loading Navigation SDK: Network error.") + NavigationApi.ErrorCode.LOCATION_PERMISSION_MISSING -> displayMessage( + "Error loading Navigation SDK: Location permission " + + "is missing." + ) + + else -> displayMessage("Error loading Navigation SDK: $errorCode") + } + } + + private fun displayMessage(message: String) { + Log.w("NavigationViewModel", message) + viewModelScope.launch { + _uiEvent.emit(UiEvent.ShowSnackbar(message)) + } + } +} + diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/UiEvent.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/UiEvent.kt new file mode 100644 index 00000000..979b39e1 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/UiEvent.kt @@ -0,0 +1,9 @@ +package com.google.maps.android.compose.navigation + +/** + * Represents UI events that can be triggered within the UI from a view model. + */ +sealed interface UiEvent { + data class ShowSnackbar(val message: String) : UiEvent + data object RequestLocationPermission : UiEvent +} \ No newline at end of file diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/Utils.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/Utils.kt new file mode 100644 index 00000000..b5b99e19 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/Utils.kt @@ -0,0 +1,6 @@ +package com.google.maps.android.compose.navigation + +import android.location.Location +import com.google.android.gms.maps.model.LatLng + +fun Location.toLatLng() = LatLng(this.latitude, this.longitude) \ No newline at end of file diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/components/MovableMarker.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/components/MovableMarker.kt new file mode 100644 index 00000000..203e4364 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/components/MovableMarker.kt @@ -0,0 +1,21 @@ +package com.google.maps.android.compose.navigation.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerState + +@Composable +fun MovableMarker(position: LatLng, title: String? = null, snippet: String? = null) { + val state = rememberUpdatedMarkerState(position) + Marker( + state = state, + title = title, + snippet = snippet, + ) +} + +@Composable +fun rememberUpdatedMarkerState(newPosition: LatLng) = + remember { MarkerState(position = newPosition) }.apply { position = newPosition } diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/ApiKeyProvider.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/ApiKeyProvider.kt new file mode 100644 index 00000000..3af86d84 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/ApiKeyProvider.kt @@ -0,0 +1,48 @@ +package com.google.maps.android.compose.navigation.repositories + +import android.content.Context +import android.content.pm.PackageManager +import android.widget.Toast +import com.google.maps.android.compose.navigation.BuildConfig + +/** + * Provides the Google Maps API key from the AndroidManifest.xml file. + * + * @param context The application context. + */ +class ApiKeyProvider(private val context: Context) { + val mapsApiKey: String by lazy { + getMapsApiKeyFromManifest() + } + + val placesApiKey: String = BuildConfig.PLACES_API_KEY + + private fun getMapsApiKeyFromManifest(): String { + val mapsApiKey = + try { + val applicationInfo = + context.packageManager.getApplicationInfo( + context.packageName, + PackageManager.GET_META_DATA, + ) + applicationInfo.metaData?.getString("com.google.android.geo.API_KEY") ?: "" + } catch (e: PackageManager.NameNotFoundException) { + error("Unable to find package: ${e.message}") + } + if (mapsApiKey.isBlank()) { + error("MapsApiKey missing from AndroidManifest.") + } + return mapsApiKey + } + + init { + if (placesApiKey == "DEFAULT_API_KEY" || mapsApiKey == "DEFAULT_API_KEY") { + Toast.makeText( + context, + "One or more API keys have not been set. Please see the README.md file.", + Toast.LENGTH_LONG + ).show() + error("One or more API keys have not been set. Please see the README.md file.") + } + } +} \ No newline at end of file diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/LocationProvider.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/LocationProvider.kt new file mode 100644 index 00000000..e91f4be7 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/LocationProvider.kt @@ -0,0 +1,18 @@ +package com.google.maps.android.compose.navigation.repositories + +import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.content.Context +import android.location.Location +import androidx.annotation.RequiresPermission +import com.google.android.gms.location.LocationServices +import kotlinx.coroutines.tasks.await + +class LocationProvider(private val applicationContext: Context) { + + @RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION]) + suspend fun getLastLocation(): Location? { + val fusedLocationClient = LocationServices.getFusedLocationProviderClient(applicationContext) + return fusedLocationClient.lastLocation.await() + } +} diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/PermissionChecker.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/PermissionChecker.kt new file mode 100644 index 00000000..ac8f8d97 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/repositories/PermissionChecker.kt @@ -0,0 +1,11 @@ +package com.google.maps.android.compose.navigation.repositories + +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat + +class PermissionChecker(private val applicationContext: Context) { + fun isGranted(permission: String): Boolean { + return ContextCompat.checkSelfPermission(applicationContext, permission) == PackageManager.PERMISSION_GRANTED + } +} diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Color.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Color.kt new file mode 100644 index 00000000..2b171124 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.google.maps.android.compose.navigation.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Theme.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Theme.kt new file mode 100644 index 00000000..f318bb95 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.google.maps.android.compose.navigation.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun AndroidmapscomposeTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Type.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Type.kt new file mode 100644 index 00000000..5670f974 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.google.maps.android.compose.navigation.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/navigation-app/src/main/res/drawable/bigfoot.png b/navigation-app/src/main/res/drawable/bigfoot.png new file mode 100644 index 00000000..1f7cc629 Binary files /dev/null and b/navigation-app/src/main/res/drawable/bigfoot.png differ diff --git a/navigation-app/src/main/res/drawable/ic_launcher_background.xml b/navigation-app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/navigation-app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/navigation-app/src/main/res/drawable/ic_launcher_foreground.xml b/navigation-app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/navigation-app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/navigation-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/navigation-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/navigation-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/navigation-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/navigation-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/navigation-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/navigation-app/src/main/res/mipmap-hdpi/ic_launcher.webp b/navigation-app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/navigation-app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/navigation-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/navigation-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b2dfe3d1 Binary files /dev/null and b/navigation-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/navigation-app/src/main/res/mipmap-mdpi/ic_launcher.webp b/navigation-app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/navigation-app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/navigation-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/navigation-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..62b611da Binary files /dev/null and b/navigation-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/navigation-app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/navigation-app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/navigation-app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/navigation-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/navigation-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1b9a6956 Binary files /dev/null and b/navigation-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/navigation-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/navigation-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/navigation-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/navigation-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/navigation-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9287f508 Binary files /dev/null and b/navigation-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/navigation-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/navigation-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/navigation-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/navigation-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/navigation-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9126ae37 Binary files /dev/null and b/navigation-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/navigation-app/src/main/res/values/colors.xml b/navigation-app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/navigation-app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/navigation-app/src/main/res/values/strings.xml b/navigation-app/src/main/res/values/strings.xml new file mode 100644 index 00000000..5dc6ab09 --- /dev/null +++ b/navigation-app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + navigation-app + \ No newline at end of file diff --git a/navigation-app/src/main/res/values/themes.xml b/navigation-app/src/main/res/values/themes.xml new file mode 100644 index 00000000..ef7248be --- /dev/null +++ b/navigation-app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +