Tutorial: Integrate a HERE Map into Android Auto
This tutorial shows how a MapView
can be rendered in your car's infotainment display with Android Auto. Android Auto is an app on its own that is installed on the actual hardware of a car. It can be simulated using a Desktop Head Unit (DHU) app running on your development machine.
The integration with proprietary Android Automotive solutions is not covered here, but follows the same principles as shown in these Android example apps.
Setting up a DHU can be a time-consuming task, so it is recommended to test your local setup with the official Android example apps from the aforementioned car-app-library.
The resulting app of this tutorial will show the MapView
instance on a DHU with a zoom-in button (+) and a zoom-out (-) button. Another MapView
instance will be shown on the screen of the mobile device that is connected to the DHU. The finished HelloMapAndroidAuto example app can be found on GitHub.
Note
Android Auto is only compatible with phones running Android 6.0 (API level 23) or higher.
Step 1 - Set up Android Auto and the Desktop Head Unit (DHU)
For this step, please follow the official documentation from Android Auto.
In essence:
- Install Google Play Services on your mobile test device or make sure you have the latest version.
- On your mobile device, go to the Play Store and make sure you have the latest Android Auto app installed. On Samsung devices - or when you don't find Android Auto on your device: Go to the
Settings
app and search for "Android Auto". - On your mobile device, enable the Android Developer Settings for both device and the Android Auto app: https://developer.android.com/studio/debug/dev-options.
- On your mobile device, enable Unknown Sources in the Android Auto app: https://developer.android.com/training/cars/testing#step1
- Install DHU on your development machine. We have used DHU 1.1 for testing.
- Run the the DHU and install the Android Auto example apps to see if your setup is working.
Make sure to use a real mobile device for testing and run the DHU on your computer. Do not use an emulator for testing.
How to start testing:
- Attach your device to your computer via USB.
- In the Android Auto app make sure the Android Head Unit Server is started: Search in Settings app for "Android Auto" and click "Start head unit server". A toast message appears: "Starting head unit server ... ".
- Open a command line window and change the directory to the installation of Android Auto, for example:
cd /Users/yourname/Library/Android/sdk/extras/google/auto
- Type
adb kill-server
to reset any ongoing session. - Type
adb forward tcp:5277 tcp:5277
to connect your device with ADB. You should see a message that the "daemon started successfully at tcp:5037" or similar. - Now start the DHU by typing:
./desktop-head-unit
. - Wait a few seconds. The screen may stay black. Watch permission prompts on your device and DHU - if shown. Repeat all above steps if it does not work.
- A separate window appears showing the DHU with all installed Android Auto apps.
- You can quit a session by typing
quit
. Don't forget to also call "Stop head unit server" on your device.
Note
When you do this for the first time, make sure to go through the setup process on device and DHU. A lot of licenses and permissions need to be accepted. If still something does not work, look at the Troubleshooting section below.
When you are successful, you can see a few Android Auto apps running in the DHU. To test your own app, deploy it onto the device via USB/ADB - and the device's screen and the DHU's screen should be updated automatically.
Note
You cannot test your Android Auto apps in-car on a real head unit, unless you have them already distributed through the Google Play Store. See also this note from Google.
Step 2 - Integrate Android Auto into an existing app
For this tutorial, we use the HelloMap app you can find on GitHub. By default, it shows a MapView
on the device. Now we want to extend this app to show a second instance of the MapView
on a DHU. For this, we need to integrate Android Auto.
Note
The resulting app is only meant for testing purposes. For a production-ready app, make sure to follow the Android Auto guidelines to see how to design an app that is relevant to the car environment.
Make sure the minSdkVersion
Version is set to 23 or higher in the app's build.gradle
file as Android Auto is only supported on Android 6.0 or higher:
minSdkVersion 23
In the same file, integrate Android Auto in the dependencies
closure:
dependencies {
...
implementation "androidx.car.app:app:1.2.0-rc01"
}
In the AndroidManifest
file, add the following required permissions:
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
<uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
In the same file, add the following to the application
tag:
<service
android:name="com.here.hellomapandroidauto.HelloMapCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService"/>
<category android:name="androidx.car.app.category.NAVIGATION"/>
</intent-filter>
</service>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1"/>
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"
tools:ignore="MetadataTagInsideApplicationTag"/>
Adapt the path and class name (com.here.hellomapandroidauto.HelloMapCarAppService
) according to your own app. We will create the HelloMapCarAppService
class below - as well as the required automotive_app_desc.xml
template. For our example, this file has only this content:
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="template" />
</automotiveApp>
Create a new "xml" folder and add the file: app/src/main/res/xml/automotive_app_desc.xml
.
Now create the HelloMapCarAppService
Java class / file. Note, you can also use Kotlin, if you prefer. Here we use Java:
package com.here.hellomapandroidauto;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarAppService;
import androidx.car.app.Screen;
import androidx.car.app.Session;
import androidx.car.app.validation.HostValidator;
public final class HelloMapCarAppService extends CarAppService {
public HelloMapCarAppService() {
}
@Override
@NonNull
public Session onCreateSession() {
return new Session() {
@Override
@NonNull
public Screen onCreateScreen(@Nullable Intent intent) {
return new HelloMapScreen(getCarContext());
}
};
}
@NonNull
@Override
public HostValidator createHostValidator() {
if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR;
} else {
return new HostValidator.Builder(getApplicationContext())
.addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
.build();
}
}
}
Every Android Auto app needs to extend at least one CarAppService
. We use it to show the HelloMapScreen
. This is the only screen we want to show on the DHU for this tutorial. For the other parts of this class, refer to the Android Auto documentation: It shows the typical boiler plate code that every CarAppService
requires.
As a next step, we create the HelloMapScreen
like this:
package com.here.hellomapandroidauto;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.CarIcon;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.NavigationTemplate;
import androidx.core.graphics.drawable.IconCompat;
import com.here.sdk.core.GeoCoordinates;
import com.here.sdk.core.Point2D;
import com.here.sdk.core.Size2D;
import com.here.sdk.mapview.MapError;
import com.here.sdk.mapview.MapMeasure;
import com.here.sdk.mapview.MapScene;
import com.here.sdk.mapview.MapScheme;
import com.here.sdk.mapview.MapSurface;
public class HelloMapScreen extends Screen implements SurfaceCallback {
private MapSurface mapSurface;
private CarContext carContext;
public HelloMapScreen(@NonNull CarContext carContext) {
super(carContext);
this.carContext = carContext;
Log.d("HelloMapScreen", "Register surface callback");
carContext.getCarService(AppManager.class).setSurfaceCallback(this);
mapSurface = new MapSurface();
}
@NonNull
@Override
public Template onGetTemplate() {
CarIcon zoomInIcon = new CarIcon.Builder(
IconCompat.createWithResource(carContext, R.drawable.plus)).build();
CarIcon zoomOutIcon = new CarIcon.Builder(
IconCompat.createWithResource(carContext, R.drawable.minus)).build();
ActionStrip.Builder actionStripBuilder = new ActionStrip.Builder();
actionStripBuilder.addAction(
new Action.Builder()
.setIcon(zoomInIcon)
.setOnClickListener(this::zoomIn)
.build());
actionStripBuilder.addAction(
new Action.Builder()
.setIcon(zoomOutIcon)
.setOnClickListener(this::zoomOut)
.build());
actionStripBuilder.addAction(
new Action.Builder()
.setTitle("Exit")
.setOnClickListener(this::exit)
.build());
NavigationTemplate.Builder builder = new NavigationTemplate.Builder();
builder.setActionStrip(actionStripBuilder.build());
return builder.build();
}
@Override
public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer) {
Log.d("HelloMapScreen", "Received a surface.");
mapSurface.setSurface(
carContext,
surfaceContainer.getSurface(),
surfaceContainer.getWidth(),
surfaceContainer.getHeight());
mapSurface.getMapScene().loadScene(MapScheme.NORMAL_DAY, new MapScene.LoadSceneCallback() {
@Override
public void onLoadScene(@Nullable MapError mapError) {
if (mapError == null) {
double distanceInMeters = 1000 * 10;
MapMeasure mapMeasureZoom = new MapMeasure(MapMeasure.Kind.DISTANCE, distanceInMeters);
mapSurface.getCamera().lookAt(new GeoCoordinates(52.530932, 13.384915), mapMeasureZoom);
} else {
Log.d("HelloMapScreen", "Loading map failed: mapError: " + mapError.name());
}
}
});
}
@Override
public void onSurfaceDestroyed(@NonNull SurfaceContainer surfaceContainer) {
mapSurface.destroySurface();
}
private void zoomIn() {
double zoomFactor = 2;
mapSurface.getCamera().zoomBy(zoomFactor, getCenterPoint());
}
private void zoomOut() {
double zoomFactor = 0.5;
mapSurface.getCamera().zoomBy(zoomFactor, getCenterPoint());
}
private void exit() {
carContext.finishCarApp();
}
private Point2D getCenterPoint() {
Size2D viewport = mapSurface.getViewportSize();
return new Point2D(viewport.width * 0.5, viewport.height * 0.5);
}
}
This is the most interesting part of our app. Since we declared our app as service to support Android Auto in our AndroidManifest
, it will automatically show the HelloMapScreen
on a DHU when installing - or when launching it later via the Android Auto app directly.
In order to render content, an app needs to render to a Surface
. For this, every Android Auto screen implements the SurfaceCallback
to get notified when the surface is available. For convenience, the HERE SDK provides a MapSurface
class that accepts a SurfaceContainer
as parameter:
mapSurface.setSurface(
carContext,
surfaceContainer.getSurface(),
surfaceContainer.getWidth(),
surfaceContainer.getHeight());
Once the app receives a SurfaceContainer
from Android Auto, you are ready to go. The HERE SDK's MapSurface
behaves like a normal MapView
as it implements the MapViewBase
interface. In this example, we use it to load a map scene, just like we do in the MainActivity
of the same app - basically, we use a MapSurface
instance instead of a MapView
instance.
Note
The HERE SDK MapSurface
does not yet support gestures. Therefore, buttons can be used to interact with the map. On top, not all cars support touch gestures such as pan movements.
In this example, we use two buttons to interact with the map. We create these buttons with Android Auto's ActionStrip.Builder
. In the callback, we perform a basic map interaction to zoom the map:
double zoomFactor = 2;
mapSurface.getCamera().zoomBy(zoomFactor, getCenterPoint());
You can add more buttons to interact with the map, e.g. for panning. However, make sure to follow the Android Auto guidelines to not distract the driver's attention. Note that as an alternative to Android's CarIcon
you can also pin images as an overlay to a MapScene
with the class MapImageOverlay
that allows to show bitmap graphics on top of the map view that do not move, scale or tilt together with the map.
The resulting app looks like this:
Screenshot: Showing the HERE Map running on a DHU.
Troubleshooting
Setting up Android Auto (or Android tools in general) for testing can be cumbersome. Here are a few tips that may help you.
- When I touch the DHU to pan, nothing happens: The DHU does not support instant touch gestures (like known from a mobile device). This is a design decision by Android to not distract the driver too much. You can allow interaction via dedicated buttons. On top, the
MapSurface
class provided by the HERE SDK does not yet support gestures. - When executing ADB I get "command not found: adb": This means that you have not yet set up Android's ADB in your path variables. ADB is a tool to help you to connect your device from a console terminal with your computer. Below we show an example solution when using Zsh shell on a Mac. First, launch Android's SDK Manager via Tools -> Android -> SDK Manager and check that you have installed the latest Android SDK Platform-Tools. If it is not installed, please install it. The ADB tool is a part of the Android SDK Platform-Tools. Now you need to make ADB known in your shell environment: Run the following command to update your PATH variable using, for example, the
nano
editor via nano $HOME/.zshrc
. Add the below lines and press "control + x" and then "y" to save it. Adjust the example below to your environment. Afterwards, run source ~/.zshrc
.
export PATH=$PATH:~/Library/Android/sdk/platform-tools
export ANDROID_HOME=~/Library/Android/sdk
export PATH="$HOME/.bin:$PATH"
export PATH="~/Library/Android/sdk/platform-tools":$PATH
Where to go next?
In this tutorial, we showed you how to add support for Android Auto to an existing app. Although we run two separate MapView
instances - one on the device and one on the head unit, you can handover functionality between the device and the head unit - since both MapView
instances run within the same app lifecycle. For example, a user can start route planning on a mobile device at home - then connect the device via USB in-car and start driving (if your app uses the Navigate Edition). The navigation progress can be shown on the car's head unit, while the device can show supportive information like maneuver details.
Make sure to not distract the driver with too much information: So, why not implementing voice controls on top to interact with the app? Some more ideas:
- Add a speed warner assistant, that shows the current valid speed limits on the head's unit display and warns acoustically when you exceed it.
- Show supporting road attribute information for the current road you are driving on.
- Implement an app that shows places information of POIs nearby, like fuel stations, restaurants or sightseeing spots.