/*
* 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.ThreadUtils.runOnMain;
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.car.app.CarContext.CarServiceType;
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>
*
* <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>. Also note that accessing location may become unreliable when the phone is in the
* battery saver mode.
*/
public abstract class CarAppService extends Service {
/**
* The {@link Intent} that must be declared as handled by the service.
*/
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";
private static final String TAG = "CarAppService";
private static final String AUTO_DRIVE = "AUTO_DRIVE";
private AppInfo mAppInfo;
@Nullable
private Session mCurrentSession;
@Nullable
private HostValidator mHostValidator;
@Nullable
private HostInfo mHostInfo;
@Nullable
private HandshakeInfo mHandshakeInfo;
/**
* 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
@Nullable
public final IBinder onBind(@NonNull Intent intent) {
return 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) {
Log.d(TAG, "onUnbind intent: " + intent);
runOnMain(() -> {
// Destroy the session
if (mCurrentSession != null) {
CarContext carContext = mCurrentSession.getCarContext();
// Stop any active navigation
carContext.getCarService(NavigationManager.class).onStopNavigation();
// Destroy all screens in the stack
carContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
// Remove binders to the host
carContext.resetHosts();
((LifecycleRegistry) mCurrentSession.getLifecycle()).handleLifecycleEvent(
Event.ON_DESTROY);
}
mCurrentSession = null;
});
// 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.
Log.d(TAG, "onUnbind completed");
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 (e.g.:
* Android Auto and Android Automotive OS hosts) which currently don't hold the above mentioned
* permission by allow listing these hosts signatures.
*
* <p>It is also advised to allow connections from unknown hosts in debug builds to facilitate
* debugging and testing, or if the information exposed through this service has no privacy
* concerns (e.g.: no user credentials and no personal user information (PII) is exchanged with
* the hosts).
*
* <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()
* .addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
* .build();
* }
* }
* </pre>
*
* <p>Please, refer to <a href="https://developer.android.com/training/cars/navigation">Build
* navigation, parking, and charging apps for Android Auto</a> and
* <a href="https://github.com/android/car-samples/tree/main/Auto/car_app_library">Android for
* Cars App Library Samples</a> to obtain a list of package names and signatures that should
* be allow-listed by default.
*/
@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
*/
@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)) {
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;
}
@NonNull
HostValidator getHostValidator() {
if (mHostValidator == null) {
mHostValidator = createHostValidator();
}
return mHostValidator;
}
// Strictly to avoid synthetic accessor.
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;
}
private final ICarApp.Stub mBinder =
new ICarApp.Stub() {
// 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) {
Log.d(TAG, "onAppCreate intent: " + intent);
RemoteUtils.dispatchHostCall(() -> {
Session session = getCurrentSession();
if (session == null
|| session.getLifecycle().getCurrentState() == State.DESTROYED) {
session = onCreateSession();
session.getCarContext().updateHandshakeInfo(getHandshakeInfo());
setCurrentSession(session);
}
// CarContext is not set up until the base Context is attached. First
// thing we need to do here is attach the base Context, so that any usage of
// it works after this point.
CarContext carContext = session.getCarContext();
carContext.attachBaseContext(CarAppService.this, configuration);
carContext.setCarHost(carHost);
// 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 = (LifecycleRegistry) session.getLifecycle();
Lifecycle.State state = registry.getCurrentState();
int screenStackSize = carContext.getCarService(
ScreenManager.class).getScreenStack().size();
if (!state.isAtLeast(State.CREATED) || screenStackSize < 1) {
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);
carContext.getCarService(ScreenManager.class).push(
session.onCreateScreen(intent));
} else {
Log.d(TAG, "onAppCreate the app was already created");
onNewIntentInternal(session, intent);
}
}, callback, "onAppCreate");
Log.d(TAG, "onAppCreate completed");
}
@Override
public void onAppStart(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
() -> {
((LifecycleRegistry) throwIfInvalid(
getCurrentSession()).getLifecycle())
.handleLifecycleEvent(Event.ON_START);
}, callback,
"onAppStart");
}
@Override
public void onAppResume(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
() -> {
((LifecycleRegistry) throwIfInvalid(
getCurrentSession()).getLifecycle())
.handleLifecycleEvent(Event.ON_RESUME);
}, callback,
"onAppResume");
}
@Override
public void onAppPause(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
() -> {
((LifecycleRegistry) throwIfInvalid(
getCurrentSession()).getLifecycle())
.handleLifecycleEvent(Event.ON_PAUSE);
}, callback, "onAppPause");
}
@Override
public void onAppStop(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
() -> {
((LifecycleRegistry) throwIfInvalid(
getCurrentSession()).getLifecycle())
.handleLifecycleEvent(Event.ON_STOP);
}, callback, "onAppStop");
}
@Override
public void onNewIntent(Intent intent, IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
() -> onNewIntentInternal(throwIfInvalid(getCurrentSession()), intent),
callback,
"onNewIntent");
}
@Override
public void onConfigurationChanged(Configuration configuration,
IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
() -> onConfigurationChangedInternal(
throwIfInvalid(getCurrentSession()), configuration),
callback,
"onConfigurationChanged");
}
@Override
public void getManager(@CarServiceType @NonNull String type,
IOnDoneCallback callback) {
Session session = throwIfInvalid(getCurrentSession());
switch (type) {
case CarContext.APP_SERVICE:
RemoteUtils.sendSuccessResponse(
callback,
"getManager",
session.getCarContext().getCarService(
AppManager.class).getIInterface());
return;
case CarContext.NAVIGATION_SERVICE:
RemoteUtils.sendSuccessResponse(
callback,
"getManager",
session.getCarContext().getCarService(
NavigationManager.class).getIInterface());
return;
default:
Log.e(TAG, type + "%s is not a valid manager");
RemoteUtils.sendFailureResponse(callback, "getManager",
new InvalidParameterException(
type + " is not a valid manager type"));
}
}
@Override
public void getAppInfo(IOnDoneCallback callback) {
RemoteUtils.sendSuccessResponse(
callback, "getAppInfo", CarAppService.this.getAppInfo());
}
@Override
public void onHandshakeCompleted(Bundleable handshakeInfo,
IOnDoneCallback callback) {
try {
HandshakeInfo deserializedHandshakeInfo =
(HandshakeInfo) handshakeInfo.get();
String packageName = deserializedHandshakeInfo.getHostPackageName();
int uid = Binder.getCallingUid();
HostInfo hostInfo = new HostInfo(packageName, uid);
if (!getHostValidator().isValidHost(hostInfo)) {
RemoteUtils.sendFailureResponse(callback, "onHandshakeCompleted",
new IllegalArgumentException("Unknown host '"
+ packageName + "', uid:" + uid));
return;
}
setHostInfo(hostInfo);
setHandshakeInfo(deserializedHandshakeInfo);
RemoteUtils.sendSuccessResponse(callback, "onHandshakeCompleted", null);
} catch (BundlerException | IllegalArgumentException e) {
setHostInfo(null);
RemoteUtils.sendFailureResponse(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();
Log.d(TAG, "onCarConfigurationChanged configuration: " + configuration);
session.getCarContext().onCarConfigurationChanged(configuration);
session.onCarConfigurationChanged(
session.getCarContext().getResources().getConfiguration());
}
};
Session throwIfInvalid(Session session) {
if (session == null) {
throw new IllegalStateException("Null session found when non-null expected");
}
return session;
}
}