Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add carplay and android auto support #209

Merged
merged 28 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f53f7f4
feat: create separate ios build target for carplay
illuminati1911 Oct 21, 2024
ab02104
chore: separate ios appdelegates to own files
illuminati1911 Oct 21, 2024
2cd7c5c
chore: split ios info plist for carplay target
illuminati1911 Oct 21, 2024
b9a0e98
feat: initial carplay setup
illuminati1911 Oct 21, 2024
f753f17
feat: initial basecarscenedelegate wip
illuminati1911 Oct 22, 2024
67d6ba1
feat: improve carplay functionality on basescenedelegate
illuminati1911 Oct 24, 2024
1028f31
feat: add separate view handling for carplay views
illuminati1911 Oct 24, 2024
c331da0
feature: add initial android auto implementation wip
illuminati1911 Oct 30, 2024
cd8549d
feat: initial android auto and carlay communication from flutter sdk
illuminati1911 Nov 1, 2024
0498442
feat: implement remaining auto vc functions
illuminati1911 Nov 1, 2024
42aa210
feat: enable android auto turn by turn navigation info
illuminati1911 Nov 4, 2024
13c16b4
feat: add custom auto navigation events
illuminati1911 Nov 5, 2024
cb0d5e3
feat: make viewId and viewEventAPI optional for carplay and aa
illuminati1911 Nov 6, 2024
5d2ee38
chore: formatting
illuminati1911 Nov 6, 2024
edbcc59
docs: add aa and carplay docs
illuminati1911 Nov 7, 2024
c3d8f48
feat: add function and event to see if auto screen is available
illuminati1911 Nov 8, 2024
5d80444
chore: formatting
illuminati1911 Nov 8, 2024
9e9440a
chore: update android compile sdk to 34
illuminati1911 Nov 8, 2024
21164c8
refactor: general refactoring
illuminati1911 Nov 13, 2024
6ad7d0b
refactor: carplay view init refactoring
illuminati1911 Nov 14, 2024
09c3f94
refactor: carplay pigeon api refactoring
illuminati1911 Nov 14, 2024
d8560dc
test: ios tests fix
illuminati1911 Nov 19, 2024
6ef2861
feat: add recenter funtionality to carplay example app
illuminati1911 Nov 21, 2024
f921bf4
feat: add navigation session detection to android auto example
illuminati1911 Nov 22, 2024
0b45676
fix: add navigation listener removal to android auto base screen
illuminati1911 Nov 22, 2024
72b6add
fix: android auto service binding fix
illuminati1911 Nov 25, 2024
de9932b
regen pigeon for iOS
jokerttu Nov 28, 2024
c372c6c
feat: expose mapview for carplay implementation
jokerttu Nov 28, 2024
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
81 changes: 81 additions & 0 deletions ANDROIDAUTO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Navigation for Android Auto

This guide explains how to enable and integrate Android Auto with the Flutter Navigation SDK.

## Requirements

- Android device
- Android Auto test device or Android Automotive OS emulator

## Setup

Refer to the [Android for Cars developer documentation](https://developer.android.com/training/cars) to understand how the Android Auto works and to complete the initial setup. Key steps include:

- Installing Android for Cars App Library.
- Configuring your app's manifest file to include Android Auto.
- Declaring a minimum car-app level in your manifest.
- Creating 'CarAppService' and session

For all the steps above, you can refer to the Android example application for guidance.

### Screen for Android Auto

Once your project is configured accordingly, and you are ready to build the screen for Android Auto, you can leverage the `AndroidAutoBaseScreen` provided by the SDK. This base class simplifies the setup by handling initialization, teardown, and rendering the map on the Android Auto display.

Please refer to the `SampleAndroidAutoScreen.kt` file in the Android example app for guidance.

To customize the Android Auto experience, override the `onGetTemplate` method in your custom AndroidAutoScreen class, providing your own `Template`:

```kotlin
override fun onGetTemplate(): Template {
/** ... */
@SuppressLint("MissingPermission")
val navigationTemplateBuilder =
NavigationTemplate.Builder()
.setActionStrip(
ActionStrip.Builder()
.addAction(
Action.Builder()
.setTitle("Re-center")
.setOnClickListener {
if (mGoogleMap == null) return@setOnClickListener
jokerttu marked this conversation as resolved.
Show resolved Hide resolved
mGoogleMap!!.followMyLocation(GoogleMap.CameraPerspective.TILTED)
}
.build())
.addAction(
Action.Builder()
.setTitle("Custom event")
.setOnClickListener {
sendCustomNavigationAutoEvent("CustomAndroidAutoEvent", mapOf("sampleDataKey" to "sampleDataContent"))
}
.build())
.build())
.setMapActionStrip(ActionStrip.Builder().addAction(Action.PAN).build())
/** ... */
}
```

For advanced customization, you can bypass the base class and implement your own screen by inheriting `Screen`. You can use the provided `AndroidAutoBaseScreen` base class as a reference on how to do that.

### Flutter Setup

On the Flutter side, you can use the `GoogleMapsAutoViewController` to interface with the CarPlay instance. The `GoogleMapsAutoViewController` allows you to call map functions on the CarPlay map view, and you can manage listeners using the provided functions.

```dart
final GoogleMapsAutoViewController _autoViewController =
GoogleMapsAutoViewController();

_autoViewController.listenForCustomNavigationAutoEvents((event) {
showMessage("Received event: ${event.event}");
});

Future<void> _setMapTypeForAutoToSatellite() async {
await _autoViewController.setMapType(mapType: MapType.satellite);
}
```

For a more detailed example, refer to the `lib/pages/navigation.dart` file in the Flutter example application.

## Example Project

For a fully functional Android Auto implementation, check out the [Android Studio example app](./example/android/).
63 changes: 63 additions & 0 deletions CARPLAY.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Navigation for Apple CarPlay

This guide explains how to enable and integrate Apple CarPlay with the Flutter SDK.

## Requirements

- iOS device or iOS simulator
- CarPlay Simulator
- CarPlay entitlement for your application (provided by Apple)

## Setup

Refer to the [Apple CarPlay Developer Guide](https://developer.apple.com/carplay/) to understand how CarPlay works and to complete the initial setup. Key steps include:

- Adding the CarPlay entitlement to your Xcode project.
- Creating a separate scene for the CarPlay map and enabling support for multiple scenes.

### SceneDelegate for CarPlay

Once your project is configured to support multiple scenes, and you are setting up a dedicated scene for CarPlay, you can leverage the `BaseCarSceneDelegate` provided by the SDK. This base class simplifies the setup by handling initialization, teardown, and rendering the map on the CarPlay display.

Please refer to the `CarSceneDelegate.swift` file in the iOS example app for guidance.

To customize the CarPlay experience, override the `getTemplate` method in your custom `CarSceneDelegate` class, providing your own `CPMapTemplate`:

```swift
override func getTemplate() -> CPMapTemplate {
let template = CPMapTemplate()
template.showPanningInterface(animated: true)

let button = CPBarButton(title: "Custom Event") { [weak self] _ in
let data = ["sampleDataKey": "sampleDataContent"]
self?.sendCustomNavigationAutoEvent(event: "CustomCarPlayEvent", data: data)
}
template.leadingNavigationBarButtons = [button]
return template
}
```

For advanced customization, you can bypass the base class and implement your own delegate inheriting `CPTemplateApplicationSceneDelegate`. You can use the provided `BaseCarSceneDelegate` base class as a reference on how to do that.

### Flutter Setup

On the Flutter side, you can use the `GoogleMapsAutoViewController` to interface with the CarPlay instance. The `GoogleMapsAutoViewController` allows you to call map functions on the CarPlay map view, and you can manage listeners using the provided functions.

```dart
final GoogleMapsAutoViewController _autoViewController =
GoogleMapsAutoViewController();

_autoViewController.listenForCustomNavigationAutoEvents((event) {
showMessage("Received event: ${event.event}");
});

Future<void> _setMapTypeForAutoToSatellite() async {
await _autoViewController.setMapType(mapType: MapType.satellite);
}
```

For a more detailed example, refer to the `lib/pages/navigation.dart` file in the Flutter example application.

## Example Project

For a fully functional CarPlay implementation, check out the [Runner](./example/ios/) Xcode project, which includes the `RunnerCarPlay` build target. The sample already contains test entitlement so you don't need to request one from Apple to run it.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ Widget build(BuildContext context) {
}
```

## Support for Android Auto and Apple CarPlay
This plugin is compatible with both Android Auto and Apple CarPlay infotainment systems. For more details, please refer to the respective platform documentation:

- [Android Auto documentation](./ANDROIDAUTO.md)
- [CarPlay documentation](./CARPLAY.md)

## Known issues

### Compatibility with other libraries
Expand Down
4 changes: 3 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ android {
namespace 'com.google.maps.flutter.navigation'
}

compileSdk 33
compileSdk 34

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
Expand All @@ -77,6 +77,8 @@ android {
}

dependencies {
implementation "androidx.car.app:app:1.4.0"
implementation "androidx.car.app:app-projected:1.4.0"
implementation 'com.google.android.libraries.navigation:navigation:6.0.0'
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'io.mockk:mockk:1.13.8'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright 2024 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
*
* https://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.google.maps.flutter.navigation

import android.app.Presentation
import android.graphics.Point
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import androidx.car.app.AppManager
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.GoogleMapOptions
import com.google.android.libraries.navigation.NavigationViewForAuto

open class AndroidAutoBaseScreen(carContext: CarContext) :
Screen(carContext), SurfaceCallback, NavigationReadyListener {
private val VIRTUAL_DISPLAY_NAME = "AndroidAutoNavScreen"
private var mVirtualDisplay: VirtualDisplay? = null
private var mPresentation: Presentation? = null
private var mNavigationView: NavigationViewForAuto? = null
private var mAutoMapView: GoogleMapsAutoMapView? = null
private var mViewRegistry: GoogleMapsViewRegistry? = null
protected var mIsNavigationReady: Boolean = false
var mGoogleMap: GoogleMap? = null

init {
initializeSurfaceCallback()
initializeNavigationListener()
}

private fun initializeNavigationListener() {
GoogleMapsNavigationSessionManager.navigationReadyListener = this
try {
mIsNavigationReady = GoogleMapsNavigationSessionManager.getInstance().isInitialized()
} catch (exception: RuntimeException) {
// If GoogleMapsNavigationSessionManager is not initialized navigation is not ready.
mIsNavigationReady = false
}
}

private fun initializeSurfaceCallback() {
carContext.getCarService(AppManager::class.java).setSurfaceCallback(this)
}

private val mLifeCycleObserver: LifecycleObserver =
object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
GoogleMapsNavigationSessionManager.navigationReadyListener = null
mIsNavigationReady = false
}
}

private fun isSurfaceReady(surfaceContainer: SurfaceContainer): Boolean {
return surfaceContainer.surface != null &&
surfaceContainer.dpi != 0 &&
surfaceContainer.height != 0 &&
surfaceContainer.width != 0
}

override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
super.onSurfaceAvailable(surfaceContainer)
lifecycle.addObserver(mLifeCycleObserver)
if (!isSurfaceReady(surfaceContainer)) {
return
}
mVirtualDisplay =
carContext
.getSystemService(DisplayManager::class.java)
.createVirtualDisplay(
VIRTUAL_DISPLAY_NAME,
surfaceContainer.width,
surfaceContainer.height,
surfaceContainer.dpi,
surfaceContainer.surface,
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY,
)
val virtualDisplay = mVirtualDisplay ?: return

mPresentation = Presentation(carContext, virtualDisplay.display)
val presentation = mPresentation ?: return

mNavigationView = NavigationViewForAuto(carContext)
val navigationView = mNavigationView ?: return
navigationView.onCreate(null)
navigationView.onStart()
navigationView.onResume()

presentation.setContentView(navigationView)
presentation.show()

navigationView.getMapAsync { googleMap: GoogleMap ->
val viewRegistry = GoogleMapsNavigationPlugin.getInstance()?.viewRegistry
val imageRegistry = GoogleMapsNavigationPlugin.getInstance()?.imageRegistry
if (viewRegistry != null && imageRegistry != null) {
mGoogleMap = googleMap
mViewRegistry = viewRegistry
mAutoMapView =
GoogleMapsAutoMapView(
GoogleMapOptions(),
viewRegistry,
imageRegistry,
navigationView,
googleMap,
)
sendAutoScreenAvailabilityChangedEvent(true)
invalidate()
}
}
}

override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
super.onSurfaceDestroyed(surfaceContainer)
sendAutoScreenAvailabilityChangedEvent(false)
mViewRegistry?.unregisterAndroidAutoView()
mNavigationView?.onPause()
mNavigationView?.onStop()
mNavigationView?.onDestroy()
mGoogleMap = null

mPresentation?.dismiss()
mVirtualDisplay?.release()
}

override fun onScroll(distanceX: Float, distanceY: Float) {
mGoogleMap?.moveCamera(CameraUpdateFactory.scrollBy(distanceX, distanceY))
}

override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
val update =
CameraUpdateFactory.zoomBy((scaleFactor - 1), Point(focusX.toInt(), focusY.toInt()))
mGoogleMap?.animateCamera(update) // map is set in onSurfaceAvailable.
}

override fun onGetTemplate(): Template {
return NavigationTemplate.Builder()
.setMapActionStrip(ActionStrip.Builder().addAction(Action.PAN).build())
.build()
}

fun sendCustomNavigationAutoEvent(event: String, data: Any) {
GoogleMapsNavigationPlugin.getInstance()?.autoViewEventApi?.onCustomNavigationAutoEvent(
event,
data,
) {}
}

private fun sendAutoScreenAvailabilityChangedEvent(isAvailable: Boolean) {
GoogleMapsNavigationPlugin.getInstance()?.autoViewEventApi?.onAutoScreenAvailabilityChanged(
isAvailable
) {}
}

override fun onNavigationReady(ready: Boolean) {
mIsNavigationReady = ready
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ internal constructor(
_mapView.onStop()
_mapView.onDestroy()

viewRegistry.unregisterMapView(viewId)
viewRegistry.unregisterMapView(getViewId())
}

override fun onStart() {
Expand Down
Loading
Loading