/* * 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.os.IBinder; import android.util.Log; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.car.app.annotations.ExperimentalCarApi; import androidx.car.app.annotations.RequiresCarApi; import androidx.car.app.validation.HostValidator; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; /** * 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 supports cluster in the manifest. */ @RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER"; /** * 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. * * @deprecated use {@link #CATEGORY_POI_APP} instead */ @Deprecated 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. * * @deprecated use {@link #CATEGORY_POI_APP} instead */ @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING"; /** * Used in the app manifest. It declares that this app finds points of interests (POI). */ public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI"; /** * Used in the app manifest. It declares that this app declares physical objects with sensors, * that connect and exchange data with other devices and systems. */ @ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT"; /** * 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"; /** * Used to declare that this app is a messaging app in the manifest. * *

This app can be used to send and receive short-form chat messages (IM/SMS). */ @ExperimentalCarApi public static final String CATEGORY_MESSAGING_APP = "androidx.car.app.category.MESSAGING"; private static final String AUTO_DRIVE = "AUTO_DRIVE"; @NonNull private final Map mBinders = new HashMap<>(); @Nullable private AppInfo mAppInfo; @Nullable private HostInfo mHostInfo; @Override @CallSuper public void onDestroy() { synchronized (mBinders) { for (CarAppBinder binder : mBinders.values()) { binder.destroy(); } mBinders.clear(); } } /** * 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(SessionInfo)} and {@link Session#onNewIntent} instead to * handle incoming {@link Intent}s. */ @Override @CallSuper @NonNull public final IBinder onBind(@NonNull Intent intent) { SessionInfo sessionInfo = SessionInfoIntentEncoder.containsSessionInfo(intent) ? SessionInfoIntentEncoder.decode(intent) : SessionInfo.DEFAULT_SESSION_INFO; synchronized (mBinders) { if (!mBinders.containsKey(sessionInfo)) { mBinders.put(sessionInfo, new CarAppBinder(this, sessionInfo)); } return requireNonNull(mBinders.get(sessionInfo)); } } /** * 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); } SessionInfo sessionInfo = SessionInfoIntentEncoder.containsSessionInfo(intent) ? SessionInfoIntentEncoder.decode(intent) : SessionInfo.DEFAULT_SESSION_INFO; runOnMain(() -> { synchronized (mBinders) { CarAppBinder binder = mBinders.remove(sessionInfo); if (binder != null) { binder.onDestroyLifecycle(); } } }); 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 Session onCreateSession() { throw new RuntimeException( "Please override and implement CarAppService#onCreateSession(SessionInfo)."); } // TODO(b/236140507): Link AndroidManifest.xml documentation or equivalent in this javadoc /** * Creates a new {@link Session}. * *

This method is invoked once per app-supported physical display with a unique * {@link SessionInfo} identifying the type of display. Support for displays is declared within * the AndroidManifest.xml. This method can also be invoked if the previous instance has been * destroyed (ie. due to memory pressure) and the system has not yet destroyed this service. * *

This method is called by the system and should not be called directly. * * @see CarContext#startCarApp(Intent) */ @NonNull @SuppressWarnings("deprecation") @RequiresCarApi(6) public Session onCreateSession(@NonNull SessionInfo sessionInfo) { return 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)) { runOnMain(() -> { synchronized (mBinders) { for (CarAppBinder binder : mBinders.values()) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Executing onAutoDriveEnabled for " + binder.getCurrentSessionInfo()); } binder.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 the main display if one exists, otherwise returns * {@code null}. * * @deprecated use {@link #getSession(SessionInfo)} */ @Nullable @Deprecated public final Session getCurrentSession() { synchronized (mBinders) { for (Map.Entry entry : mBinders.entrySet()) { if (entry.getKey().getDisplayType() == SessionInfo.DISPLAY_TYPE_MAIN) { return entry.getValue().getCurrentSession(); } } } return null; } /** * Returns the {@link Session} associated to the given {@link SessionInfo}, or {@code null} * if one doesn't exist. */ @Nullable public final Session getSession(@NonNull SessionInfo sessionInfo) { synchronized (mBinders) { CarAppBinder binder = mBinders.get(sessionInfo); if (binder == null) { return null; } return binder.getCurrentSession(); } } // 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; } @VisibleForTesting void setBinder(@NonNull SessionInfo sessionInfo, @Nullable CarAppBinder binder) { if (binder == null) { mBinders.remove(sessionInfo); return; } mBinders.put(sessionInfo, binder); } }