/* * 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. * *

Service Declaration

* * 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 * intent-filter to the service in the AndroidManifest.xml 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: * *
{@code
 * 
 *   
 *     
 *     
 *   
 * 
 * }
* *

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 {@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. * *

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. * *

This method is final to ensure this car app's lifecycle is handled properly. * *

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. * *

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. * *

By default, the provided {@link HostValidator.Builder} would produce a validator that * only accepts connections from hosts holding * {@link 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 {@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. * *

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();
     *     }
     * }
     * 
*/ @NonNull public abstract HostValidator createHostValidator(); /** * Creates a new {@link Session} for the application. * *

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. * *

Once the method returns, {@link Session#onCreateScreen(Intent)} will be called on the * {@link Session} returned. * *

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. * *

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); } } }