/* * 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.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. * *
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. * *
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"; 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; /** * 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 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 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) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onAppCreate intent: " + intent); } RemoteUtils.dispatchCallFromHost(callback, "onAppCreate", () -> { Session session = getCurrentSession(); if (session == null || getLifecycle().getCurrentState() == State.DESTROYED) { session = onCreateSession(); setCurrentSession(session); } session.configure(CarAppService.this, requireNonNull(getHandshakeInfo()), requireNonNull(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 = 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) { RemoteUtils.dispatchCallFromHost( getLifecycleIfValid(), callback, "onAppStart", () -> { getLifecycle().handleLifecycleEvent(Event.ON_START); return null; }); } @Override public void onAppResume(IOnDoneCallback callback) { RemoteUtils.dispatchCallFromHost( getLifecycleIfValid(), callback, "onAppResume", () -> { getLifecycle() .handleLifecycleEvent(Event.ON_RESUME); return null; }); } @Override public void onAppPause(IOnDoneCallback callback) { RemoteUtils.dispatchCallFromHost( getLifecycleIfValid(), callback, "onAppPause", () -> { getLifecycle().handleLifecycleEvent(Event.ON_PAUSE); return null; }); } @Override public void onAppStop(IOnDoneCallback callback) { RemoteUtils.dispatchCallFromHost( getLifecycleIfValid(), callback, "onAppStop", () -> { getLifecycle().handleLifecycleEvent(Event.ON_STOP); return null; }); } @Override public void onNewIntent(Intent intent, IOnDoneCallback callback) { RemoteUtils.dispatchCallFromHost( getLifecycleIfValid(), callback, "onNewIntent", () -> { onNewIntentInternal(throwIfInvalid(getCurrentSession()), intent); return null; }); } @Override public void onConfigurationChanged(Configuration configuration, IOnDoneCallback callback) { RemoteUtils.dispatchCallFromHost( getLifecycleIfValid(), callback, "onConfigurationChanged", () -> { onConfigurationChangedInternal( throwIfInvalid(getCurrentSession()), configuration); return null; }); } @Override public void getManager(@CarServiceType @NonNull String type, IOnDoneCallback callback) { ThreadUtils.runOnMain(() -> { Session session = throwIfInvalid(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 { RemoteUtils.sendSuccessResponseToHost( callback, "getAppInfo", CarAppService.this.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) { 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.sendFailureResponseToHost(callback, "onHandshakeCompleted", new IllegalArgumentException("Unknown host '" + packageName + "', uid:" + uid)); return; } int appMinApiLevel = CarAppService.this.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; } setHostInfo(hostInfo); setHandshakeInfo(deserializedHandshakeInfo); RemoteUtils.sendSuccessResponseToHost(callback, "onHandshakeCompleted", null); } catch (BundlerException | IllegalArgumentException e) { 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); } }; Session throwIfInvalid(@Nullable Session session) { if (session == null) { throw new IllegalStateException("Null session found when non-null expected"); } return session; } }