/* * Copyright (C) 2015 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 android.car; import static android.car.CarLibLog.TAG_CAR; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.Activity; import android.app.Service; import android.car.admin.CarDevicePolicyManager; import android.car.annotation.MandatoryFeature; import android.car.annotation.OptionalFeature; import android.car.cluster.CarInstrumentClusterManager; import android.car.cluster.ClusterActivityState; import android.car.cluster.ClusterHomeManager; import android.car.content.pm.CarPackageManager; import android.car.diagnostic.CarDiagnosticManager; import android.car.drivingstate.CarDrivingStateManager; import android.car.drivingstate.CarUxRestrictionsManager; import android.car.evs.CarEvsManager; import android.car.hardware.CarSensorManager; import android.car.hardware.CarVendorExtensionManager; import android.car.hardware.cabin.CarCabinManager; import android.car.hardware.hvac.CarHvacManager; import android.car.hardware.power.CarPowerManager; import android.car.hardware.property.CarPropertyManager; import android.car.hardware.property.ICarProperty; import android.car.input.CarInputManager; import android.car.media.CarAudioManager; import android.car.media.CarMediaIntents; import android.car.media.CarMediaManager; import android.car.navigation.CarNavigationStatusManager; import android.car.occupantawareness.OccupantAwarenessManager; import android.car.storagemonitoring.CarStorageMonitoringManager; import android.car.telemetry.CarTelemetryManager; import android.car.test.CarTestManagerBinderWrapper; import android.car.user.CarUserManager; import android.car.vms.VmsClientManager; import android.car.vms.VmsSubscriberManager; import android.car.watchdog.CarWatchdogManager; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.TransactionTooLargeException; import android.os.UserHandle; import android.util.Log; import com.android.car.internal.common.CommonConstants; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Objects; /** * Top level car API for embedded Android Auto deployments. * This API works only for devices with {@link PackageManager#FEATURE_AUTOMOTIVE} * Calling this API on a device with no such feature will lead to an exception. */ public final class Car { /** * Binder service name of car service registered to service manager. * * @hide */ public static final String CAR_SERVICE_BINDER_SERVICE_NAME = "car_service"; /** * This represents AndroidManifest meta-data to tell that {@code Activity} is optimized for * driving distraction. * *
Activities without this meta-data can be blocked while car is in moving / driving state. * *
Note that having this flag does not guarantee that the {@code Activity} will be always * allowed for all driving states. * *
For this meta-data, android:value can be {@code true} (=optimized) or {@code false}. * *
Example usage:
*
Apps like launcher or installer app can use this information to filter out apps * not usable in a specific car. This meta-data is not necessary for mandatory features. * *
For this meta-data, android:value should contain the feature name string defined by * (@link android.car.annotation.OptionalFeature} or * {@link android.car.annotation.ExperimentalFeature} annotations. * *
Example usage:
*
Access to car service should happen * after {@link CarServiceLifecycleListener#onLifecycleChanged(Car, boolean)} call with * {@code ready} set {@code true}.
* *When {@link CarServiceLifecycleListener#onLifecycleChanged(Car, boolean)} is * called with ready set to false, access to car service should stop until car service is ready * again from {@link CarServiceLifecycleListener#onLifecycleChanged(Car, boolean)} call * with {@code ready} set to {@code true}.
*/ public interface CarServiceLifecycleListener { /** * Car service has gone through status change. * *This is always called in the main thread context.
* * @param car {@code Car} object that was originally associated with this lister from * {@link #createCar(Context, Handler, long, Car.CarServiceLifecycleListener)} * call. * @param ready When {@code true, car service is ready and all accesses are ok. * Otherwise car service has crashed or killed and will be restarted. */ void onLifecycleChanged(@NonNull Car car, boolean ready); } /** * {@link #createCar(Context, Handler, long, CarServiceLifecycleListener)}'s * waitTimeoutMs value to use to wait forever inside the call until car service is ready. */ public static final long CAR_WAIT_TIMEOUT_WAIT_FOREVER = -1; /** * {@link #createCar(Context, Handler, long, CarServiceLifecycleListener)}'s * waitTimeoutMs value to use to skip any waiting inside the call. */ public static final long CAR_WAIT_TIMEOUT_DO_NOT_WAIT = 0; private static final long CAR_SERVICE_BIND_RETRY_INTERVAL_MS = 500; private static final long CAR_SERVICE_BIND_MAX_RETRY = 20; private static final long CAR_SERVICE_BINDER_POLLING_INTERVAL_MS = 50; private static final long CAR_SERVICE_BINDER_POLLING_MAX_RETRY = 100; private static final int STATE_DISCONNECTED = 0; private static final int STATE_CONNECTING = 1; private static final int STATE_CONNECTED = 2; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "STATE_", value = { STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, }) @Target({ElementType.TYPE_USE}) public @interface StateTypeEnum {} /** * The enabling request was successful and requires reboot to take effect. * @hide */ @SystemApi public static final int FEATURE_REQUEST_SUCCESS = 0; /** * The requested feature is already enabled or disabled as requested. No need to reboot the * system. * @hide */ @SystemApi public static final int FEATURE_REQUEST_ALREADY_IN_THE_STATE = 1; /** * The requested feature is mandatory cannot be enabled or disabled. It is always enabled. * @hide */ @SystemApi public static final int FEATURE_REQUEST_MANDATORY = 2; /** * The requested feature is not available and cannot be enabled or disabled. * @hide */ @SystemApi public static final int FEATURE_REQUEST_NOT_EXISTING = 3; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "FEATURE_REQUEST_", value = { FEATURE_REQUEST_SUCCESS, FEATURE_REQUEST_ALREADY_IN_THE_STATE, FEATURE_REQUEST_MANDATORY, FEATURE_REQUEST_NOT_EXISTING, }) @Target({ElementType.TYPE_USE}) public @interface FeaturerRequestEnum {} private static final boolean DBG = false; private final Context mContext; private final Exception mConstructionStack; private final Object mLock = new Object(); @GuardedBy("mLock") private ICar mService; @GuardedBy("mLock") private boolean mServiceBound; @GuardedBy("mLock") @StateTypeEnum private int mConnectionState; @GuardedBy("mLock") private int mConnectionRetryCount; private final Runnable mConnectionRetryRunnable = new Runnable() { @Override public void run() { startCarService(); } }; private final Runnable mConnectionRetryFailedRunnable = new Runnable() { @Override public void run() { mServiceConnectionListener.onServiceDisconnected(new ComponentName(CAR_SERVICE_PACKAGE, CAR_SERVICE_CLASS)); } }; private final ServiceConnection mServiceConnectionListener = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mLock) { ICar newService = ICar.Stub.asInterface(service); if (newService == null) { Log.wtf(TAG_CAR, "null binder service", new RuntimeException()); return; // should not happen. } if (mService != null && mService.asBinder().equals(newService.asBinder())) { // already connected. return; } mConnectionState = STATE_CONNECTED; mService = newService; } if (mStatusChangeCallback != null) { mStatusChangeCallback.onLifecycleChanged(Car.this, true); } else if (mServiceConnectionListenerClient != null) { mServiceConnectionListenerClient.onServiceConnected(name, service); } } @Override public void onServiceDisconnected(ComponentName name) { // Car service can pick up feature changes after restart. mFeatures.resetCache(); synchronized (mLock) { if (mConnectionState == STATE_DISCONNECTED) { // can happen when client calls disconnect before onServiceDisconnected call. return; } handleCarDisconnectLocked(); } if (mStatusChangeCallback != null) { mStatusChangeCallback.onLifecycleChanged(Car.this, false); } else if (mServiceConnectionListenerClient != null) { mServiceConnectionListenerClient.onServiceDisconnected(name); } else { // This client does not handle car service restart, so should be terminated. finishClient(); } } }; @Nullable private final ServiceConnection mServiceConnectionListenerClient; /** Can be added after ServiceManager.getService call */ @Nullable private final CarServiceLifecycleListener mStatusChangeCallback; @GuardedBy("mLock") private final HashMapInstance created with this should be disconnected from car service by calling * {@link #disconnect()} before the passed {code Context} is released. * * @param context App's Context. This should not be null. If you are passing * {@link ContextWrapper}, make sure that its base Context is non-null as well. * Otherwise it will throw {@link java.lang.NullPointerException}. * @param serviceConnectionListener listener for monitoring service connection. * @param handler the handler on which the callback should execute, or null to execute on the * service's main thread. Note: the service connection listener will be always on the main * thread regardless of the handler given. * @return Car instance if system is in car environment and returns {@code null} otherwise. * * @deprecated use {@link #createCar(Context, Handler)} instead. */ @Deprecated public static Car createCar(Context context, ServiceConnection serviceConnectionListener, @Nullable Handler handler) { assertNonNullContext(context); if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { Log.e(TAG_CAR, "FEATURE_AUTOMOTIVE not declared while android.car is used"); return null; } try { return new Car(context, /* service= */ null , serviceConnectionListener, /* statusChangeListener= */ null, handler); } catch (IllegalArgumentException e) { // Expected when car service loader is not available. } return null; } /** * A factory method that creates Car instance for all Car API access using main thread {@code * Looper}. * *
Instance created with this should be disconnected from car service by calling * {@link #disconnect()} before the passed {code Context} is released. * * @see #createCar(Context, ServiceConnection, Handler) * * @deprecated use {@link #createCar(Context, Handler)} instead. */ @Deprecated public static Car createCar(Context context, ServiceConnection serviceConnectionListener) { return createCar(context, serviceConnectionListener, null); } /** * Creates new {@link Car} object which connected synchronously to Car Service and ready to use. * *
Instance created with this should be disconnected from car service by calling * {@link #disconnect()} before the passed {code Context} is released. * * @param context application's context * * @return Car object if operation succeeded, otherwise null. */ @Nullable public static Car createCar(Context context) { return createCar(context, (Handler) null); } /** * Creates new {@link Car} object which connected synchronously to Car Service and ready to use. * *
Instance created with this should be disconnected from car service by calling * {@link #disconnect()} before the passed {code Context} is released. * * @param context App's Context. This should not be null. If you are passing * {@link ContextWrapper}, make sure that its base Context is non-null as well. * Otherwise it will throw {@link java.lang.NullPointerException}. * @param handler the handler on which the manager's callbacks will be executed, or null to * execute on the application's main thread. * * @return Car object if operation succeeded, otherwise null. */ @Nullable public static Car createCar(Context context, @Nullable Handler handler) { assertNonNullContext(context); Car car = null; IBinder service = null; boolean started = false; int retryCount = 0; while (true) { service = ServiceManager.getService(CAR_SERVICE_BINDER_SERVICE_NAME); if (car == null) { // service can be still null. The constructor is safe for null service. car = new Car(context, ICar.Stub.asInterface(service), null /*serviceConnectionListener*/, null /*statusChangeListener*/, handler); } if (service != null) { if (!started) { // specialization for most common case. // Do this to crash client when car service crashes. car.startCarService(); return car; } break; } if (!started) { car.startCarService(); started = true; } retryCount++; if (retryCount > CAR_SERVICE_BINDER_POLLING_MAX_RETRY) { Log.e(TAG_CAR, "cannot get car_service, waited for car service (ms):" + CAR_SERVICE_BINDER_POLLING_INTERVAL_MS * CAR_SERVICE_BINDER_POLLING_MAX_RETRY, new RuntimeException()); return null; } try { Thread.sleep(CAR_SERVICE_BINDER_POLLING_INTERVAL_MS); } catch (InterruptedException e) { Log.e(CarLibLog.TAG_CAR, "interrupted while waiting for car_service", new RuntimeException()); return null; } } // Can be accessed from mServiceConnectionListener in main thread. synchronized (car) { if (car.mService == null) { car.mService = ICar.Stub.asInterface(service); Log.w(TAG_CAR, "waited for car_service (ms):" + CAR_SERVICE_BINDER_POLLING_INTERVAL_MS * retryCount, new RuntimeException()); } car.mConnectionState = STATE_CONNECTED; } return car; } /** * Creates new {@link Car} object with {@link CarServiceLifecycleListener}. * *
Instance created with this should be disconnected from car service by calling * {@link #disconnect()} before the passed {code Context} is released. * *
If car service is ready inside this call and if the caller is running in the main thread, * {@link CarServiceLifecycleListener#onLifecycleChanged(Car, boolean)} will be called * with ready set to be true. Otherwise, * {@link CarServiceLifecycleListener#onLifecycleChanged(Car, boolean)} will be called * from the main thread later.
* *This call can block up to specified waitTimeoutMs to wait for car service to be ready. * If car service is not ready within the given time, it will return a Car instance in * disconnected state. Blocking main thread forever can lead into getting ANR (Application Not * Responding) killing from system and should not be used if the app is supposed to survive * across the crash / restart of car service. It can be still useful in case the app cannot do * anything without car service being ready. In any waiting, if the thread is getting * interrupted, it will return immediately. *
* *Note that returned {@link Car} object is not guaranteed to be connected when there is * a limited timeout. Regardless of returned car being connected or not, it is recommended to * implement all car related initialization inside * {@link CarServiceLifecycleListener#onLifecycleChanged(Car, boolean)} and avoid the * needs to check if returned {@link Car} is connected or not from returned {@link Car}.
* * @param context App's Context. This should not be null. If you are passing * {@link ContextWrapper}, make sure that its base Context is non-null as well. * Otherwise it will throw {@link java.lang.NullPointerException}. * @param handler dispatches all Car*Manager events to this Handler. Exception is * {@link CarServiceLifecycleListener} which will be always dispatched to main * thread. Passing null leads into dispatching all Car*Manager callbacks to main * thread as well. * @param waitTimeoutMs Setting this to {@link #CAR_WAIT_TIMEOUT_DO_NOT_WAIT} will guarantee * that the API does not wait for the car service at all. Setting this to * to {@link #CAR_WAIT_TIMEOUT_WAIT_FOREVER} will block the call forever * until the car service is ready. Setting any positive value will be * interpreted as timeout value. */ @NonNull public static Car createCar(@NonNull Context context, @Nullable Handler handler, long waitTimeoutMs, @NonNull CarServiceLifecycleListener statusChangeListener) { assertNonNullContext(context); Objects.requireNonNull(statusChangeListener); Car car = null; IBinder service = null; boolean started = false; int retryCount = 0; long maxRetryCount = 0; if (waitTimeoutMs > 0) { maxRetryCount = waitTimeoutMs / CAR_SERVICE_BINDER_POLLING_INTERVAL_MS; // at least wait once if it is positive value. if (maxRetryCount == 0) { maxRetryCount = 1; } } boolean isMainThread = Looper.myLooper() == Looper.getMainLooper(); while (true) { service = ServiceManager.getService(CAR_SERVICE_BINDER_SERVICE_NAME); if (car == null) { // service can be still null. The constructor is safe for null service. car = new Car(context, ICar.Stub.asInterface(service), null, statusChangeListener, handler); } if (service != null) { if (!started) { // specialization for most common case : car service already ready car.dispatchCarReadyToMainThread(isMainThread); // Needs this for CarServiceLifecycleListener. Note that ServiceConnection // will skip the callback as valid mService is set already. car.startCarService(); return car; } // service available after starting. break; } if (!started) { car.startCarService(); started = true; } retryCount++; if (waitTimeoutMs < 0 && retryCount >= CAR_SERVICE_BINDER_POLLING_MAX_RETRY && retryCount % CAR_SERVICE_BINDER_POLLING_MAX_RETRY == 0) { // Log warning if car service is not alive even for waiting forever case. Log.w(TAG_CAR, "car_service not ready, waited for car service (ms):" + retryCount * CAR_SERVICE_BINDER_POLLING_INTERVAL_MS, new RuntimeException()); } else if (waitTimeoutMs >= 0 && retryCount > maxRetryCount) { if (waitTimeoutMs > 0) { Log.w(TAG_CAR, "car_service not ready, waited for car service (ms):" + waitTimeoutMs, new RuntimeException()); } return car; } try { Thread.sleep(CAR_SERVICE_BINDER_POLLING_INTERVAL_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.w(TAG_CAR, "interrupted", new RuntimeException()); return car; } } // Can be accessed from mServiceConnectionListener in main thread. synchronized (car.mLock) { Log.w(TAG_CAR, "waited for car_service (ms):" + retryCount * CAR_SERVICE_BINDER_POLLING_INTERVAL_MS, new RuntimeException()); // ServiceConnection has handled everything. if (car.mService != null) { return car; } // mService check in ServiceConnection prevents calling // onLifecycleChanged. So onLifecycleChanged should be called explicitly // but do it outside lock. car.mService = ICar.Stub.asInterface(service); car.mConnectionState = STATE_CONNECTED; } car.dispatchCarReadyToMainThread(isMainThread); return car; } private static void assertNonNullContext(Context context) { Objects.requireNonNull(context); if (context instanceof ContextWrapper && ((ContextWrapper) context).getBaseContext() == null) { throw new NullPointerException( "ContextWrapper with null base passed as Context, forgot to set base Context?"); } } private void dispatchCarReadyToMainThread(boolean isMainThread) { if (isMainThread) { mStatusChangeCallback.onLifecycleChanged(this, true); } else { // should dispatch to main thread. mMainThreadEventHandler.post( () -> mStatusChangeCallback.onLifecycleChanged(this, true)); } } private Car(Context context, @Nullable ICar service, @Nullable ServiceConnection serviceConnectionListener, @Nullable CarServiceLifecycleListener statusChangeListener, @Nullable Handler handler) { mContext = context; mEventHandler = determineEventHandler(handler); mMainThreadEventHandler = determineMainThreadEventHandler(mEventHandler); mService = service; if (service != null) { mConnectionState = STATE_CONNECTED; } else { mConnectionState = STATE_DISCONNECTED; } mServiceConnectionListenerClient = serviceConnectionListener; mStatusChangeCallback = statusChangeListener; // Store construction stack so that client can get help when it crashes when car service // crashes. if (serviceConnectionListener == null && statusChangeListener == null) { mConstructionStack = new RuntimeException(); } else { mConstructionStack = null; } } /** * Car constructor when ICar binder is already available. The binder can be null. * @hide */ public Car(Context context, @Nullable ICar service, @Nullable Handler handler) { this(context, service, null /*serviceConnectionListener*/, null /*statusChangeListener*/, handler); } private static Handler determineMainThreadEventHandler(Handler eventHandler) { Looper mainLooper = Looper.getMainLooper(); return (eventHandler.getLooper() == mainLooper) ? eventHandler : new Handler(mainLooper); } private static Handler determineEventHandler(@Nullable Handler handler) { if (handler == null) { Looper looper = Looper.getMainLooper(); handler = new Handler(looper); } return handler; } /** * Connect to car service. This can be called while it is disconnected. * @throws IllegalStateException If connection is still on-going from previous * connect call or it is already connected * * @deprecated this method is not need if this object is created via * {@link #createCar(Context, Handler)}. */ @Deprecated public void connect() throws IllegalStateException { synchronized (mLock) { if (mConnectionState != STATE_DISCONNECTED) { throw new IllegalStateException("already connected or connecting"); } mConnectionState = STATE_CONNECTING; startCarService(); } } private void handleCarDisconnectLocked() { if (mConnectionState == STATE_DISCONNECTED) { // can happen when client calls disconnect with onServiceDisconnected already called. return; } mEventHandler.removeCallbacks(mConnectionRetryRunnable); mMainThreadEventHandler.removeCallbacks(mConnectionRetryFailedRunnable); mConnectionRetryCount = 0; tearDownCarManagersLocked(); mService = null; mConnectionState = STATE_DISCONNECTED; } /** * Disconnect from car service. This can be called while disconnected. Once disconnect is * called, all Car*Managers from this instance becomes invalid, and * {@link Car#getCarManager(String)} will return different instance if it is connected again. */ public void disconnect() { synchronized (mLock) { handleCarDisconnectLocked(); if (mServiceBound) { mContext.unbindService(mServiceConnectionListener); mServiceBound = false; } } } /** * Tells if it is connected to the service or not. This will return false if it is still * connecting. * @return */ public boolean isConnected() { synchronized (mLock) { return mService != null; } } /** * Tells if this instance is already connecting to car service or not. * @return */ public boolean isConnecting() { synchronized (mLock) { return mConnectionState == STATE_CONNECTING; } } /** @hide */ @VisibleForTesting public ServiceConnection getServiceConnectionListener() { return mServiceConnectionListener; } /** * Get car specific service as in {@link Context#getSystemService(String)}. Returned * {@link Object} should be type-casted to the desired service. * For example, to get sensor service, * SensorManagerService sensorManagerService = car.getCarManager(Car.SENSOR_SERVICE); * @param serviceName Name of service that should be created like {@link #SENSOR_SERVICE}. * @return Matching service manager or null if there is no such service. */ @Nullable public Object getCarManager(String serviceName) { CarManagerBase manager; synchronized (mLock) { if (mService == null) { Log.w(TAG_CAR, "getCarManager not working while car service not ready"); return null; } manager = mServiceMap.get(serviceName); if (manager == null) { try { IBinder binder = mService.getCarService(serviceName); if (binder == null) { Log.w(TAG_CAR, "getCarManager could not get binder for service:" + serviceName); return null; } manager = createCarManagerLocked(serviceName, binder); if (manager == null) { Log.w(TAG_CAR, "getCarManager could not create manager for service:" + serviceName); return null; } mServiceMap.put(serviceName, manager); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } } return manager; } /** * @return the type of currently connected car. * * @deprecated connection type will be always {@link CONNECTION_TYPE_EMBEDDED} */ @ConnectionType @Deprecated public int getCarConnectionType() { return CONNECTION_TYPE_EMBEDDED; } /** * Checks if {code featureName} is enabled in this car. * *For optional features, this can return false if the car cannot support it. Optional * features should be used only when they are supported.
* *For mandatory features, this will always return true.
*/
public boolean isFeatureEnabled(@NonNull String featureName) {
ICar service;
synchronized (mLock) {
if (mService == null) {
return false;
}
service = mService;
}
return mFeatures.isFeatureEnabled(service, featureName);
}
/**
* Enables the requested car feature. It becomes no-op if the feature is already enabled. The
* change take effects after reboot.
*
* @return true if the feature is enabled or was enabled before.
*
* @hide
*/
@SystemApi
@RequiresPermission(PERMISSION_CONTROL_CAR_FEATURES)
@FeaturerRequestEnum
public int enableFeature(@NonNull String featureName) {
ICar service;
synchronized (mLock) {
if (mService == null) {
return FEATURE_REQUEST_NOT_EXISTING;
}
service = mService;
}
try {
return service.enableFeature(featureName);
} catch (RemoteException e) {
return handleRemoteExceptionFromCarService(e, FEATURE_REQUEST_NOT_EXISTING);
}
}
/**
* Disables the requested car feature. It becomes no-op if the feature is already disabled. The
* change take effects after reboot.
*
* @return true if the request succeeds or if it was already disabled.
*
* @hide
*/
@SystemApi
@RequiresPermission(PERMISSION_CONTROL_CAR_FEATURES)
@FeaturerRequestEnum
public int disableFeature(@NonNull String featureName) {
ICar service;
synchronized (mLock) {
if (mService == null) {
return FEATURE_REQUEST_NOT_EXISTING;
}
service = mService;
}
try {
return service.disableFeature(featureName);
} catch (RemoteException e) {
return handleRemoteExceptionFromCarService(e, FEATURE_REQUEST_NOT_EXISTING);
}
}
/**
* Returns all =enabled features at the moment including mandatory, optional, and
* experimental features.
*
* @hide
*/
@SystemApi
@RequiresPermission(PERMISSION_CONTROL_CAR_FEATURES)
@NonNull public List