java.lang.Object
↳Service
↳androidx.car.app.CarAppService
Gradle dependencies
compile group: 'androidx.car.app', name: 'app', version: '1.2.0-rc01'
- groupId: androidx.car.app
- artifactId: app
- version: 1.2.0-rc01
Artifact androidx.car.app:app:1.2.0-rc01 it located at Google repository (https://maven.google.com/)
Overview
The base class for implementing a car app that runs in the car.
Service Declaration
The app must extend the
CarAppService to be bound by the car host. The service must also
respond to actions coming from the host, by adding an
intent-filter
to the service in the
AndroidManifest.xml
that handles
the
CarAppService.SERVICE_INTERFACE action. The app must also declare what category of application
it is (e.g.
CarAppService.CATEGORY_NAVIGATION_APP). For example:
For a list of all the supported categories see
Supported App Categories.
Accessing Location
When the app is running in the car display, the system will not consider it as being in the
foreground, and hence it will be considered in the background for the purpose of retrieving
location as described
here.
To reliably get location for your car app, we recommended that you use a foreground
service. If you have a service other than your CarAppService that accesses
location, run the service and your `CarAppService` in the same process. Also note that
accessing location may become unreliable when the phone is in the battery saver mode.
Summary
Fields |
---|
public static final java.lang.String | CATEGORY_CHARGING_APP Used to declare that this app is a charging app in the manifest. |
public static final java.lang.String | CATEGORY_NAVIGATION_APP Used to declare that this app is a navigation app in the manifest. |
public static final java.lang.String | CATEGORY_PARKING_APP Used to declare that this app is a parking app in the manifest. |
public static final java.lang.String | CATEGORY_SETTINGS_APP Used to declare that this app is a settings app in the manifest. |
public static final java.lang.String | SERVICE_INTERFACE The full qualified name of the CarAppService class. |
Methods |
---|
public abstract HostValidator | createHostValidator()
Returns the HostValidator this service will use to accept or reject host connections. |
public final void | dump(java.io.FileDescriptor fd, java.io.PrintWriter writer, java.lang.String args[])
|
public final Session | getCurrentSession()
Returns the current Session for this service. |
public final HostInfo | getHostInfo()
Returns information about the host attached to this service. |
public final IBinder | onBind(Intent intent)
Handles the host binding to this car app. |
public void | onCreate()
|
public abstract Session | onCreateSession()
Creates a new Session for the application. |
public void | onDestroy()
|
public final boolean | onUnbind(Intent intent)
Handles the host unbinding from this car app. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final java.lang.String
SERVICE_INTERFACEThe full qualified name of the CarAppService class.
This is the same name that must be used to declare the action of the intent filter for
the app's CarAppService in the app's manifest.
See also: CarAppService
public static final java.lang.String
CATEGORY_NAVIGATION_APPUsed to declare that this app is a navigation app in the manifest.
public static final java.lang.String
CATEGORY_PARKING_APPUsed to declare that this app is a parking app in the manifest.
public static final java.lang.String
CATEGORY_CHARGING_APPUsed to declare that this app is a charging app in the manifest.
public static final java.lang.String
CATEGORY_SETTINGS_APPUsed to declare that this app is a settings app in the manifest. This app can be used to
provide screens corresponding to the settings page and/or any error resolution screens e.g.
sign-in screen.
Constructors
Methods
public final IBinder
onBind(Intent intent)
Handles the host binding to this car app.
This method is final to ensure this car app's lifecycle is handled properly.
Use CarAppService.onCreateSession() and Session.onNewIntent(Intent) instead to handle incoming
s.
public final boolean
onUnbind(Intent intent)
Handles the host unbinding from this car app.
This method is final to ensure this car app's lifecycle is handled properly.
Returns the HostValidator this service will use to accept or reject host connections.
By default, the provided would produce a validator that
only accepts connections from hosts holding
HostValidator.TEMPLATE_RENDERER_PERMISSION permission.
Application developers are expected to also allow connections from known hosts which
don't hold the aforementioned permission (for example, Android Auto and Android
Automotive OS hosts below API level 31), by allow-listing the signatures of those hosts.
Refer to androidx.car.app.R.array.hosts_allowlist_sample to obtain a
list of package names and signatures that should be allow-listed by default.
It is also advised to allow connections from unknown hosts in debug builds to facilitate
debugging and testing.
Below is an example of this method implementation:
@Override
@NonNull
public HostValidator createHostValidator() {
if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR;
} else {
return new HostValidator.Builder(context)
.addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
.build();
}
}
public abstract
Session onCreateSession()
Creates a new Session for the application.
This method is invoked the first time the app is started, or if the previous
Session instance has been destroyed and the system has not yet destroyed
this service.
Once the method returns, Session.onCreateScreen(Intent) will be called on the
Session returned.
Called by the system, do not call this method directly.
See also: CarContext.startCarApp(Intent)
public final void
dump(java.io.FileDescriptor fd, java.io.PrintWriter writer, java.lang.String args[])
Returns information about the host attached to this service.
See also: HostInfo
public final
Session getCurrentSession()
Returns the current Session for this service.
Source
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.car.app;
import static androidx.car.app.utils.LogTags.TAG;
import static androidx.car.app.utils.ThreadUtils.runOnMain;
import static java.util.Objects.requireNonNull;
import android.app.Service;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.CallSuper;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.car.app.CarContext.CarServiceType;
import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.navigation.NavigationManager;
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.utils.RemoteUtils;
import androidx.car.app.utils.ThreadUtils;
import androidx.car.app.validation.HostValidator;
import androidx.car.app.versioning.CarAppApiLevels;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.Lifecycle.State;
import androidx.lifecycle.LifecycleRegistry;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.security.InvalidParameterException;
/**
* The base class for implementing a car app that runs in the car.
*
* <h4>Service Declaration</h4>
*
* The app must extend the {@link CarAppService} to be bound by the car host. The service must also
* respond to {@link Intent} actions coming from the host, by adding an
* <code>intent-filter</code> to the service in the <code>AndroidManifest.xml</code> that handles
* the {@link #SERVICE_INTERFACE} action. The app must also declare what category of application
* it is (e.g. {@link #CATEGORY_NAVIGATION_APP}). For example:
*
* <pre>{@code
* <service
* android:name=".YourAppService"
* android:exported="true">
* <intent-filter>
* <action android:name="androidx.car.app.CarAppService" />
* <category android:name="androidx.car.app.category.NAVIGATION"/>
* </intent-filter>
* </service>
* }</pre>
*
* <p>For a list of all the supported categories see
* <a href="https://developer.android.com/training/cars/apps#supported-app-categories">Supported App Categories</a>.
*
* <h4>Accessing Location</h4>
*
* When the app is running in the car display, the system will not consider it as being in the
* foreground, and hence it will be considered in the background for the purpose of retrieving
* location as described <a
* href="https://developer.android.com/about/versions/10/privacy/changes#app-access-device
* -location">here</a>.
*
* <p>To reliably get location for your car app, we recommended that you use a <a
* href="https://developer.android.com/guide/components/services?#Types-of-services">foreground
* service</a>. If you have a service other than your {@link CarAppService} that accesses
* location, run the service and your `CarAppService` in the same process. Also note that
* accessing location may become unreliable when the phone is in the battery saver mode.
*/
public abstract class CarAppService extends Service {
/**
* The full qualified name of the {@link CarAppService} class.
*
* <p>This is the same name that must be used to declare the action of the intent filter for
* the app's {@link CarAppService} in the app's manifest.
*
* @see CarAppService
*/
public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
/**
* Used to declare that this app is a navigation app in the manifest.
*/
public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
/**
* Used to declare that this app is a parking app in the manifest.
*/
public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
/**
* Used to declare that this app is a charging app in the manifest.
*/
public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
/**
* Used to declare that this app is a settings app in the manifest. This app can be used to
* provide screens corresponding to the settings page and/or any error resolution screens e.g.
* sign-in screen.
*/
@ExperimentalCarApi
public static final String CATEGORY_SETTINGS_APP = "androidx.car.app.category.SETTINGS";
private static final String AUTO_DRIVE = "AUTO_DRIVE";
@Nullable
private AppInfo mAppInfo;
@Nullable
private Session mCurrentSession;
@Nullable
private HostValidator mHostValidator;
@Nullable
private HostInfo mHostInfo;
@Nullable
private HandshakeInfo mHandshakeInfo;
@Nullable
private CarAppBinder mBinder;
@Override
@CallSuper
public void onCreate() {
mBinder = new CarAppBinder(this);
}
@Override
@CallSuper
public void onDestroy() {
if (mBinder != null) {
mBinder.destroy();
mBinder = null;
}
}
/**
* Handles the host binding to this car app.
*
* <p>This method is final to ensure this car app's lifecycle is handled properly.
*
* <p>Use {@link #onCreateSession()} and {@link Session#onNewIntent} instead to handle incoming
* {@link Intent}s.
*/
@Override
@CallSuper
@NonNull
public final IBinder onBind(@NonNull Intent intent) {
return requireNonNull(mBinder);
}
/**
* Handles the host unbinding from this car app.
*
* <p>This method is final to ensure this car app's lifecycle is handled properly.
*/
@Override
public final boolean onUnbind(@NonNull Intent intent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onUnbind intent: " + intent);
}
runOnMain(() -> {
if (mCurrentSession != null) {
// Destroy the session
// The session's lifecycle is observed by some of the manager and they will
// perform cleanup on destroy. For example, the ScreenManager can destroy all
// Screens it holds.
LifecycleRegistry lifecycleRegistry = getLifecycleIfValid();
if (lifecycleRegistry == null) {
Log.e(TAG, "Null Session when unbinding");
} else {
lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY);
}
}
mCurrentSession = null;
});
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onUnbind completed");
}
// Return true to request an onRebind call. This means that the process will cache this
// instance of the Service to return on future bind calls.
return true;
}
/**
* Returns the {@link HostValidator} this service will use to accept or reject host connections.
*
* <p>By default, the provided {@link HostValidator.Builder} would produce a validator that
* only accepts connections from hosts holding
* {@link HostValidator#TEMPLATE_RENDERER_PERMISSION} permission.
*
* <p>Application developers are expected to also allow connections from known hosts which
* don't hold the aforementioned permission (for example, Android Auto and Android
* Automotive OS hosts below API level 31), by allow-listing the signatures of those hosts.
*
* <p>Refer to {@code androidx.car.app.R.array.hosts_allowlist_sample} to obtain a
* list of package names and signatures that should be allow-listed by default.
*
* <p>It is also advised to allow connections from unknown hosts in debug builds to facilitate
* debugging and testing.
*
* <p>Below is an example of this method implementation:
*
* <pre>
* @Override
* @NonNull
* public HostValidator createHostValidator() {
* if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
* return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR;
* } else {
* return new HostValidator.Builder(context)
* .addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
* .build();
* }
* }
* </pre>
*/
@NonNull
public abstract HostValidator createHostValidator();
/**
* Creates a new {@link Session} for the application.
*
* <p>This method is invoked the first time the app is started, or if the previous
* {@link Session} instance has been destroyed and the system has not yet destroyed
* this service.
*
* <p>Once the method returns, {@link Session#onCreateScreen(Intent)} will be called on the
* {@link Session} returned.
*
* <p>Called by the system, do not call this method directly.
*
* @see CarContext#startCarApp(Intent)
*/
@NonNull
public abstract Session onCreateSession();
@Override
@CallSuper
public final void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
@Nullable String[] args) {
super.dump(fd, writer, args);
for (String arg : args) {
if (AUTO_DRIVE.equals(arg)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Executing onAutoDriveEnabled");
}
runOnMain(() -> {
if (mCurrentSession != null) {
mCurrentSession.getCarContext().getCarService(
NavigationManager.class).onAutoDriveEnabled();
}
});
}
}
}
/**
* Returns information about the host attached to this service.
*
* @see HostInfo
*/
@Nullable
public final HostInfo getHostInfo() {
return mHostInfo;
}
void setHostInfo(@Nullable HostInfo hostInfo) {
mHostInfo = hostInfo;
}
/**
* Returns the current {@link Session} for this service.
*/
@Nullable
public final Session getCurrentSession() {
return mCurrentSession;
}
// Strictly to avoid synthetic accessor.
void setCurrentSession(@Nullable Session session) {
mCurrentSession = session;
}
// Strictly to avoid synthetic accessor.
@NonNull
AppInfo getAppInfo() {
if (mAppInfo == null) {
// Lazy-initialized as the package manager is not available if this is created inlined.
mAppInfo = AppInfo.create(this);
}
return mAppInfo;
}
/**
* Used by tests to verify the different behaviors when the app has different api level than
* the host.
*/
@VisibleForTesting
void setAppInfo(@Nullable AppInfo appInfo) {
mAppInfo = appInfo;
}
@NonNull
HostValidator getHostValidator() {
if (mHostValidator == null) {
mHostValidator = createHostValidator();
}
return mHostValidator;
}
/**
* Used by tests to verify the different behaviors when the app has different api level than
* the host.
*/
@VisibleForTesting
void setHandshakeInfo(@NonNull HandshakeInfo handshakeInfo) {
int apiLevel = handshakeInfo.getHostCarAppApiLevel();
if (!CarAppApiLevels.isValid(apiLevel)) {
throw new IllegalArgumentException("Invalid Car App API level received: " + apiLevel);
}
mHandshakeInfo = handshakeInfo;
}
// Strictly to avoid synthetic accessor.
@Nullable
HandshakeInfo getHandshakeInfo() {
return mHandshakeInfo;
}
@Nullable
LifecycleRegistry getLifecycleIfValid() {
Session session = getCurrentSession();
return session == null ? null : (LifecycleRegistry) session.getLifecycleInternal();
}
@NonNull
LifecycleRegistry getLifecycle() {
return requireNonNull(getLifecycleIfValid());
}
private static final class CarAppBinder extends ICarApp.Stub {
@Nullable private CarAppService mService;
CarAppBinder(@NonNull CarAppService service) {
mService = service;
}
/**
* Explicitly mark the binder to be destroyed and remove the reference to the
* {@link CarAppService}, and any subsequent call from the host after this would be
* considered invalid and throws an exception.
*
* <p>This is needed because the binder object can outlive the service and will not be
* garbage collected until the car host cleans up its side of the binder reference,
* causing a leak. See https://github.com/square/leakcanary/issues/1906 for more context
* related to this issue.
*/
void destroy() {
mService = null;
}
// incompatible argument for parameter context of attachBaseContext.
// call to onCreateScreen(android.content.Intent) not allowed on the given receiver.
@SuppressWarnings({
"nullness:argument.type.incompatible",
"nullness:method.invocation.invalid"
})
@Override
public void onAppCreate(
ICarHost carHost,
Intent intent,
Configuration configuration,
IOnDoneCallback callback) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onAppCreate intent: " + intent);
}
RemoteUtils.dispatchCallFromHost(callback, "onAppCreate", () -> {
CarAppService service = requireNonNull(mService);
Session session = service.getCurrentSession();
if (session == null
|| service.getLifecycle().getCurrentState() == State.DESTROYED) {
session = service.onCreateSession();
service.setCurrentSession(session);
}
session.configure(service,
requireNonNull(service.getHandshakeInfo()),
requireNonNull(service.getHostInfo()),
carHost, configuration);
// Whenever the host unbinds, the screens in the stack are destroyed. If
// there is another bind, before the OS has destroyed this Service, then
// the stack will be empty, and we need to treat it as a new instance.
LifecycleRegistry registry = service.getLifecycle();
Lifecycle.State state = registry.getCurrentState();
int screenStackSize = session.getCarContext().getCarService(
ScreenManager.class).getScreenStack().size();
if (!state.isAtLeast(State.CREATED) || screenStackSize < 1) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onAppCreate the app was not yet created or the "
+ "screen stack was empty state: "
+ registry.getCurrentState()
+ ", stack size: " + screenStackSize);
}
registry.handleLifecycleEvent(Event.ON_CREATE);
session.getCarContext().getCarService(ScreenManager.class).push(
session.onCreateScreen(intent));
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onAppCreate the app was already created");
}
onNewIntentInternal(session, intent);
}
return null;
});
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onAppCreate completed");
}
}
@Override
public void onAppStart(IOnDoneCallback callback) {
CarAppService service = requireNonNull(mService);
RemoteUtils.dispatchCallFromHost(
service.getLifecycleIfValid(), callback,
"onAppStart", () -> {
service.getLifecycle().handleLifecycleEvent(Event.ON_START);
return null;
});
}
@Override
public void onAppResume(IOnDoneCallback callback) {
CarAppService service = requireNonNull(mService);
RemoteUtils.dispatchCallFromHost(
service.getLifecycleIfValid(), callback,
"onAppResume", () -> {
service.getLifecycle()
.handleLifecycleEvent(Event.ON_RESUME);
return null;
});
}
@Override
public void onAppPause(IOnDoneCallback callback) {
CarAppService service = requireNonNull(mService);
RemoteUtils.dispatchCallFromHost(
service.getLifecycleIfValid(), callback, "onAppPause",
() -> {
service.getLifecycle().handleLifecycleEvent(Event.ON_PAUSE);
return null;
});
}
@Override
public void onAppStop(IOnDoneCallback callback) {
CarAppService service = requireNonNull(mService);
RemoteUtils.dispatchCallFromHost(
service.getLifecycleIfValid(), callback, "onAppStop",
() -> {
service.getLifecycle().handleLifecycleEvent(Event.ON_STOP);
return null;
});
}
@Override
public void onNewIntent(Intent intent, IOnDoneCallback callback) {
CarAppService service = requireNonNull(mService);
RemoteUtils.dispatchCallFromHost(
service.getLifecycleIfValid(),
callback,
"onNewIntent",
() -> {
onNewIntentInternal(requireNonNull(service.getCurrentSession()), intent);
return null;
});
}
@Override
public void onConfigurationChanged(Configuration configuration,
IOnDoneCallback callback) {
CarAppService service = requireNonNull(mService);
RemoteUtils.dispatchCallFromHost(
service.getLifecycleIfValid(),
callback,
"onConfigurationChanged",
() -> {
onConfigurationChangedInternal(requireNonNull(service.getCurrentSession()),
configuration);
return null;
});
}
@Override
public void getManager(@CarServiceType @NonNull String type,
IOnDoneCallback callback) {
ThreadUtils.runOnMain(() -> {
CarAppService service = requireNonNull(mService);
Session session = requireNonNull(service.getCurrentSession());
switch (type) {
case CarContext.APP_SERVICE:
RemoteUtils.sendSuccessResponseToHost(
callback,
"getManager",
session.getCarContext().getCarService(
AppManager.class).getIInterface());
return;
case CarContext.NAVIGATION_SERVICE:
RemoteUtils.sendSuccessResponseToHost(
callback,
"getManager",
session.getCarContext().getCarService(
NavigationManager.class).getIInterface());
return;
default:
Log.e(TAG, type + "%s is not a valid manager");
RemoteUtils.sendFailureResponseToHost(callback, "getManager",
new InvalidParameterException(
type + " is not a valid manager type"));
}
});
}
@Override
public void getAppInfo(IOnDoneCallback callback) {
try {
CarAppService service = requireNonNull(mService);
RemoteUtils.sendSuccessResponseToHost(
callback, "getAppInfo", service.getAppInfo());
} catch (IllegalArgumentException e) {
// getAppInfo() could fail with the specified API version is invalid.
RemoteUtils.sendFailureResponseToHost(callback, "getAppInfo", e);
}
}
@Override
public void onHandshakeCompleted(Bundleable handshakeInfo,
IOnDoneCallback callback) {
CarAppService service = requireNonNull(mService);
try {
HandshakeInfo deserializedHandshakeInfo =
(HandshakeInfo) handshakeInfo.get();
String packageName = deserializedHandshakeInfo.getHostPackageName();
int uid = Binder.getCallingUid();
HostInfo hostInfo = new HostInfo(packageName, uid);
if (!service.getHostValidator().isValidHost(hostInfo)) {
RemoteUtils.sendFailureResponseToHost(callback, "onHandshakeCompleted",
new IllegalArgumentException("Unknown host '"
+ packageName + "', uid:" + uid));
return;
}
int appMinApiLevel = service.getAppInfo().getMinCarAppApiLevel();
int hostApiLevel = deserializedHandshakeInfo.getHostCarAppApiLevel();
if (appMinApiLevel > hostApiLevel) {
RemoteUtils.sendFailureResponseToHost(callback, "onHandshakeCompleted",
new IllegalArgumentException(
"Host API level (" + hostApiLevel + ") is "
+ "less than the app's min API level ("
+ appMinApiLevel + ")"));
return;
}
service.setHostInfo(hostInfo);
service.setHandshakeInfo(deserializedHandshakeInfo);
RemoteUtils.sendSuccessResponseToHost(callback, "onHandshakeCompleted",
null);
} catch (BundlerException | IllegalArgumentException e) {
service.setHostInfo(null);
RemoteUtils.sendFailureResponseToHost(callback, "onHandshakeCompleted", e);
}
}
// call to onNewIntent(android.content.Intent) not allowed on the given receiver.
@SuppressWarnings("nullness:method.invocation.invalid")
@MainThread
private void onNewIntentInternal(Session session, Intent intent) {
ThreadUtils.checkMainThread();
session.onNewIntent(intent);
}
// call to onCarConfigurationChanged(android.content.res.Configuration) not
// allowed on the given receiver.
@SuppressWarnings("nullness:method.invocation.invalid")
@MainThread
private void onConfigurationChangedInternal(Session session,
Configuration configuration) {
ThreadUtils.checkMainThread();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCarConfigurationChanged configuration: " + configuration);
}
session.onCarConfigurationChangedInternal(configuration);
}
}
}