aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--car-lib/Android.bp2
-rw-r--r--car-lib/api/current.txt1
-rw-r--r--car-lib/api/system-current.txt10
-rw-r--r--car-lib/src/android/car/Car.java251
-rw-r--r--car-lib/src/android/car/CarFeatures.java83
-rw-r--r--car-lib/src/android/car/CarInfoManager.java1
-rw-r--r--car-lib/src/android/car/ICar.aidl12
-rw-r--r--car-lib/src/android/car/IExperimentalCar.aidl31
-rw-r--r--car-lib/src/android/car/IExperimentalCarHelper.aidl33
-rw-r--r--car-lib/src/android/car/annotation/ExperimentalFeature.java38
-rw-r--r--car-lib/src/android/car/annotation/MandatoryFeature.java (renamed from car-lib/src/android/car/annotation/FutureFeature.java)21
-rw-r--r--car-lib/src/android/car/annotation/OptionalFeature.java35
-rw-r--r--car-lib/src/android/car/annotation/RequiredFeature.java38
-rw-r--r--car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java2
-rw-r--r--car-lib/src/android/car/vms/VmsPublisherClientService.java2
-rw-r--r--car-lib/src/android/car/vms/VmsSubscriberManager.java2
-rw-r--r--car-test-lib/src/android/car/testapi/FakeCar.java38
-rw-r--r--car_product/build/car.mk2
-rw-r--r--car_product/sepolicy/test/experimentalcarservice_app.te46
-rw-r--r--car_product/sepolicy/test/seapp_contexts1
-rw-r--r--experimental/experimental_api/Android.bp37
-rw-r--r--experimental/experimental_api/src/android/car/experimental/CarTestDemoExperimentalFeatureManager.java59
-rw-r--r--experimental/experimental_api/src/android/car/experimental/ExperimentalCar.java (renamed from car-lib/src_feature_future/com/android/car/internal/FeatureConfiguration.java)25
-rw-r--r--experimental/experimental_api/src/android/car/experimental/ITestDemoExperimental.aidl22
-rw-r--r--experimental/service/Android.bp55
-rw-r--r--experimental/service/AndroidManifest.xml33
-rw-r--r--experimental/service/proguard.flags3
-rw-r--r--experimental/service/res/values/strings.xml18
-rw-r--r--experimental/service/src/com/android/experimentalcar/ExperimentalCarService.java63
-rw-r--r--experimental/service/src/com/android/experimentalcar/IExperimentalCarImpl.java188
-rw-r--r--experimental/service/src/com/android/experimentalcar/TestDemoExperimentalFeatureService.java50
-rw-r--r--service/Android.bp17
-rw-r--r--service/AndroidManifest.xml9
-rw-r--r--service/res/values/config.xml19
-rw-r--r--service/res/values/strings.xml5
-rw-r--r--service/src/com/android/car/CarExperimentalFeatureServiceController.java237
-rw-r--r--service/src/com/android/car/CarFeatureController.java437
-rw-r--r--service/src/com/android/car/ICarImpl.java171
-rw-r--r--tests/EmbeddedKitchenSinkApp/Android.mk3
-rw-r--r--tests/EmbeddedKitchenSinkApp/AndroidManifest.xml3
-rw-r--r--tests/EmbeddedKitchenSinkApp/res/layout/experimental_feature_test.xml45
-rw-r--r--tests/EmbeddedKitchenSinkApp/res/values/strings.xml3
-rw-r--r--tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java2
-rw-r--r--tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/experimental/ExperimentalFeatureTestFragment.java84
-rw-r--r--tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java8
-rw-r--r--tests/android_car_api_test/src/android/car/apitest/CarFeatureTest.java143
-rw-r--r--tests/carservice_unit_test/src/android/car/CarTest.java37
47 files changed, 2386 insertions, 39 deletions
diff --git a/car-lib/Android.bp b/car-lib/Android.bp
index 7005260318..9e0d16610f 100644
--- a/car-lib/Android.bp
+++ b/car-lib/Android.bp
@@ -69,7 +69,6 @@ java_library {
name: "android.car",
srcs: [
"src/**/*.java",
- "src_feature_future/**/*.java",
"src/**/I*.aidl",
],
aidl: {
@@ -93,7 +92,6 @@ stubs_defaults {
name: "android.car-docs-default",
srcs: [
"src/**/*.java",
- "src_feature_future/**/*.java",
],
libs: [
"android.car",
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index cfddac0277..c1c0e63d37 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -13,6 +13,7 @@ package android.car {
method @Nullable public Object getCarManager(String);
method public boolean isConnected();
method public boolean isConnecting();
+ method public boolean isFeatureEnabled(@NonNull String);
field public static final String APP_FOCUS_SERVICE = "app_focus";
field public static final String AUDIO_SERVICE = "audio";
field public static final String CAR_CONFIGURATION_SERVICE = "configuration";
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index 584d527b21..0c1084eaf6 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -12,12 +12,21 @@ package android.car {
}
public final class Car {
+ method @RequiresPermission(android.car.Car.PERMISSION_CONTROL_CAR_FEATURES) public int disableFeature(@NonNull String);
+ method @RequiresPermission(android.car.Car.PERMISSION_CONTROL_CAR_FEATURES) public int enableFeature(@NonNull String);
+ method @NonNull @RequiresPermission(android.car.Car.PERMISSION_CONTROL_CAR_FEATURES) public java.util.List<java.lang.String> getAllEnabledFeatures();
+ method @NonNull @RequiresPermission(android.car.Car.PERMISSION_CONTROL_CAR_FEATURES) public java.util.List<java.lang.String> getAllPendingDisabledFeatures();
+ method @NonNull @RequiresPermission(android.car.Car.PERMISSION_CONTROL_CAR_FEATURES) public java.util.List<java.lang.String> getAllPendingEnabledFeatures();
field @Deprecated public static final String CABIN_SERVICE = "cabin";
field public static final String CAR_DRIVING_STATE_SERVICE = "drivingstate";
field public static final String CAR_EXTRA_CLUSTER_ACTIVITY_STATE = "android.car.cluster.ClusterActivityState";
field public static final String CAR_TRUST_AGENT_ENROLLMENT_SERVICE = "trust_enroll";
field public static final String CAR_USER_SERVICE = "car_user_service";
field public static final String DIAGNOSTIC_SERVICE = "diagnostic";
+ field public static final int FEATURE_REQUEST_ALREADY_IN_THE_STATE = 1; // 0x1
+ field public static final int FEATURE_REQUEST_MANDATORY = 2; // 0x2
+ field public static final int FEATURE_REQUEST_NOT_EXISTING = 3; // 0x3
+ field public static final int FEATURE_REQUEST_SUCCESS = 0; // 0x0
field @Deprecated public static final String HVAC_SERVICE = "hvac";
field public static final String PERMISSION_ADJUST_RANGE_REMAINING = "android.car.permission.ADJUST_RANGE_REMAINING";
field public static final String PERMISSION_CAR_DIAGNOSTIC_CLEAR = "android.car.permission.CLEAR_CAR_DIAGNOSTICS";
@@ -34,6 +43,7 @@ package android.car {
field public static final String PERMISSION_CONTROL_APP_BLOCKING = "android.car.permission.CONTROL_APP_BLOCKING";
field public static final String PERMISSION_CONTROL_CAR_CLIMATE = "android.car.permission.CONTROL_CAR_CLIMATE";
field public static final String PERMISSION_CONTROL_CAR_DOORS = "android.car.permission.CONTROL_CAR_DOORS";
+ field public static final String PERMISSION_CONTROL_CAR_FEATURES = "android.car.permission.CONTROL_CAR_FEATURES";
field public static final String PERMISSION_CONTROL_CAR_MIRRORS = "android.car.permission.CONTROL_CAR_MIRRORS";
field public static final String PERMISSION_CONTROL_CAR_SEATS = "android.car.permission.CONTROL_CAR_SEATS";
field public static final String PERMISSION_CONTROL_CAR_WINDOWS = "android.car.permission.CONTROL_CAR_WINDOWS";
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index b83e7c53b1..f3fb100214 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -21,11 +21,14 @@ 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.SystemApi;
import android.app.Activity;
import android.app.Service;
+import android.car.annotation.MandatoryFeature;
+import android.car.annotation.OptionalFeature;
import android.car.cluster.CarInstrumentClusterManager;
import android.car.cluster.ClusterActivityState;
import android.car.content.pm.CarPackageManager;
@@ -69,7 +72,10 @@ 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.util.Collections;
import java.util.HashMap;
+import java.util.List;
/**
* Top level car API for embedded Android Auto deployments.
@@ -89,25 +95,32 @@ public final class Car {
*
* @deprecated {@link CarSensorManager} is deprecated. Use {@link CarPropertyManager} instead.
*/
+ @MandatoryFeature
@Deprecated
public static final String SENSOR_SERVICE = "sensor";
/** Service name for {@link CarInfoManager}, to be used in {@link #getCarManager(String)}. */
+ @MandatoryFeature
public static final String INFO_SERVICE = "info";
/** Service name for {@link CarAppFocusManager}. */
+ @MandatoryFeature
public static final String APP_FOCUS_SERVICE = "app_focus";
/** Service name for {@link CarPackageManager} */
+ @MandatoryFeature
public static final String PACKAGE_SERVICE = "package";
/** Service name for {@link CarAudioManager} */
+ @MandatoryFeature
public static final String AUDIO_SERVICE = "audio";
/** Service name for {@link CarNavigationStatusManager} */
+ @MandatoryFeature
public static final String CAR_NAVIGATION_SERVICE = "car_navigation_service";
/** Service name for {@link CarOccupantZoneManager} */
+ @MandatoryFeature
public static final String CAR_OCCUPANT_ZONE_SERVICE = "car_occupant_zone_service";
/**
@@ -115,6 +128,7 @@ public final class Car {
*
* @hide
*/
+ @MandatoryFeature
@SystemApi
public static final String CAR_USER_SERVICE = "car_user_service";
@@ -124,6 +138,7 @@ public final class Car {
* @deprecated CarInstrumentClusterManager is being deprecated
* @hide
*/
+ @MandatoryFeature
@Deprecated
public static final String CAR_INSTRUMENT_CLUSTER_SERVICE = "cluster_service";
@@ -133,6 +148,7 @@ public final class Car {
* @deprecated {@link CarCabinManager} is deprecated. Use {@link CarPropertyManager} instead.
* @hide
*/
+ @MandatoryFeature
@Deprecated
@SystemApi
public static final String CABIN_SERVICE = "cabin";
@@ -148,6 +164,7 @@ public final class Car {
* @deprecated {@link CarHvacManager} is deprecated. Use {@link CarPropertyManager} instead.
* @hide
*/
+ @MandatoryFeature
@Deprecated
@SystemApi
public static final String HVAC_SERVICE = "hvac";
@@ -155,18 +172,21 @@ public final class Car {
/**
* @hide
*/
+ @MandatoryFeature
@SystemApi
public static final String POWER_SERVICE = "power";
/**
* @hide
*/
+ @MandatoryFeature
@SystemApi
public static final String PROJECTION_SERVICE = "projection";
/**
* Service name for {@link CarPropertyManager}
*/
+ @MandatoryFeature
public static final String PROPERTY_SERVICE = "property";
/**
@@ -176,6 +196,7 @@ public final class Car {
* Use {@link CarPropertyManager} instead.
* @hide
*/
+ @MandatoryFeature
@Deprecated
@SystemApi
public static final String VENDOR_EXTENSION_SERVICE = "vendor_extension";
@@ -183,11 +204,13 @@ public final class Car {
/**
* @hide
*/
+ @MandatoryFeature
public static final String BLUETOOTH_SERVICE = "car_bluetooth";
/**
* @hide
*/
+ @OptionalFeature
@SystemApi
public static final String VMS_SUBSCRIBER_SERVICE = "vehicle_map_subscriber_service";
@@ -195,6 +218,7 @@ public final class Car {
* Service name for {@link CarDrivingStateManager}
* @hide
*/
+ @MandatoryFeature
@SystemApi
public static final String CAR_DRIVING_STATE_SERVICE = "drivingstate";
@@ -212,6 +236,7 @@ public final class Car {
* Service name for {@link android.car.media.CarMediaManager}
* @hide
*/
+ @MandatoryFeature
public static final String CAR_MEDIA_SERVICE = "car_media";
/**
@@ -219,11 +244,13 @@ public final class Car {
* Service name for {@link android.car.CarBugreportManager}
* @hide
*/
+ @MandatoryFeature
public static final String CAR_BUGREPORT_SERVICE = "car_bugreport";
/**
* @hide
*/
+ @OptionalFeature
@SystemApi
public static final String STORAGE_MONITORING_SERVICE = "storage_monitoring";
@@ -239,6 +266,7 @@ public final class Car {
* Service name for {@link CarTestManager}, to be used in {@link #getCarManager(String)}.
* @hide
*/
+ @MandatoryFeature
@SystemApi
public static final String TEST_SERVICE = "car-service-test";
@@ -570,6 +598,15 @@ public final class Car {
public static final String PERMISSION_CAR_ENROLL_TRUST =
"android.car.permission.CAR_ENROLL_TRUST";
+ /**
+ * Permission necessary to dynamically enable / disable optional car features.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String PERMISSION_CONTROL_CAR_FEATURES =
+ "android.car.permission.CONTROL_CAR_FEATURES";
+
/** Type of car connection: platform runs directly in car. */
public static final int CONNECTION_TYPE_EMBEDDED = 5;
@@ -700,6 +737,43 @@ public final class Car {
@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;
@@ -760,6 +834,8 @@ public final class Car {
@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.
@@ -793,6 +869,8 @@ public final class Car {
private final Handler mMainThreadEventHandler;
+ private final CarFeatures mFeatures = new CarFeatures();
+
/**
* A factory method that creates Car instance for all Car API access.
* @param context App's Context. This should not be null. If you are passing
@@ -1202,7 +1280,7 @@ public final class Car {
+ serviceName);
return null;
}
- manager = createCarManager(serviceName, binder);
+ manager = createCarManagerLocked(serviceName, binder);
if (manager == null) {
Log.w(TAG_CAR, "getCarManager could not create manager for service:"
+ serviceName);
@@ -1226,6 +1304,146 @@ public final class Car {
return CONNECTION_TYPE_EMBEDDED;
}
+ /**
+ * Checks if {code featureName} is enabled in this car.
+ *
+ * <p>For optional features, this can return false if the car cannot support it. Optional
+ * features should be used only when they are supported.</p>
+ *
+ * <p>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<String> getAllEnabledFeatures() {
+ ICar service;
+ synchronized (mLock) {
+ if (mService == null) {
+ return Collections.EMPTY_LIST;
+ }
+ service = mService;
+ }
+ try {
+ return service.getAllEnabledFeatures();
+ } catch (RemoteException e) {
+ return handleRemoteExceptionFromCarService(e, Collections.EMPTY_LIST);
+ }
+ }
+
+ /**
+ * Returns the list of disabled features which are not effective yet. Those features will be
+ * disabled when system restarts later.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(PERMISSION_CONTROL_CAR_FEATURES)
+ @NonNull public List<String> getAllPendingDisabledFeatures() {
+ ICar service;
+ synchronized (mLock) {
+ if (mService == null) {
+ return Collections.EMPTY_LIST;
+ }
+ service = mService;
+ }
+ try {
+ return service.getAllPendingDisabledFeatures();
+ } catch (RemoteException e) {
+ return handleRemoteExceptionFromCarService(e, Collections.EMPTY_LIST);
+ }
+ }
+
+ /**
+ * Returns the list of enabled features which are not effective yet. Those features will be
+ * enabled when system restarts later.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(PERMISSION_CONTROL_CAR_FEATURES)
+ @NonNull public List<String> getAllPendingEnabledFeatures() {
+ ICar service;
+ synchronized (mLock) {
+ if (mService == null) {
+ return Collections.EMPTY_LIST;
+ }
+ service = mService;
+ }
+ try {
+ return service.getAllPendingEnabledFeatures();
+ } catch (RemoteException e) {
+ return handleRemoteExceptionFromCarService(e, Collections.EMPTY_LIST);
+ }
+ }
+
/** @hide */
Context getContext() {
return mContext;
@@ -1287,7 +1505,7 @@ public final class Car {
}
@Nullable
- private CarManagerBase createCarManager(String serviceName, IBinder binder) {
+ private CarManagerBase createCarManagerLocked(String serviceName, IBinder binder) {
CarManagerBase manager = null;
switch (serviceName) {
case AUDIO_SERVICE:
@@ -1370,11 +1588,40 @@ public final class Car {
case CAR_USER_SERVICE:
manager = new CarUserManager(this, binder);
default:
+ // Experimental or non-existing
+ String className = null;
+ try {
+ className = mService.getCarManagerClassForFeature(serviceName);
+ } catch (RemoteException e) {
+ handleRemoteExceptionFromCarService(e);
+ return null;
+ }
+ if (className == null) {
+ Log.e(TAG_CAR, "Cannot construct CarManager for service:" + serviceName
+ + " : no class defined");
+ return null;
+ }
+ manager = constructCarManager(className, binder);
break;
}
return manager;
}
+ private CarManagerBase constructCarManager(String className, IBinder binder) {
+ try {
+ // Should use class loader for the Context as class loader for car api does not
+ // see the class.
+ ClassLoader loader = mContext.getClassLoader();
+ Class managerClass = loader.loadClass(className);
+ Constructor constructor = managerClass.getConstructor(Car.class, IBinder.class);
+ CarManagerBase manager = (CarManagerBase) constructor.newInstance(this, binder);
+ return manager;
+ } catch (Exception e) {
+ Log.e(TAG_CAR, "Cannot construct CarManager, class:" + className, e);
+ return null;
+ }
+ }
+
private void startCarService() {
Intent intent = new Intent();
intent.setPackage(CAR_SERVICE_PACKAGE);
diff --git a/car-lib/src/android/car/CarFeatures.java b/car-lib/src/android/car/CarFeatures.java
new file mode 100644
index 0000000000..3fe0f1bd2e
--- /dev/null
+++ b/car-lib/src/android/car/CarFeatures.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 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 android.annotation.NonNull;
+import android.car.annotation.OptionalFeature;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * This class declares all car features which does not have API as {@code String}.
+ *
+ * <p>Note that all {@code Car*Managers'} feature string is their service name
+ * {@code Car.*_SERVICE.}
+ * For features with APIs, subfeature {@code String} will be also defined inside the API.
+ *
+ * <p>To prevent potential conflict in feature / subfeature name, all feature name should use
+ * implementation package name space like {@code com.android.car.user.FeatureA} unless it is
+ * {@code Car.*_SERVICE}.
+ *
+ * <p>To define a subfeature, main feature should be already declared and sub feature name should
+ * have the format of "main_feature/sub_feature_name". Note that feature name cannot use '/' and
+ * should use only alphabet, digit, '_', '-' and '.'.
+ *
+ * @hide
+ */
+public final class CarFeatures {
+ /**
+ * Service to show initial user notice screen. This feature has no API and thus defined here.
+ * @hide */
+ @OptionalFeature
+ public static String FEATURE_CAR_USER_NOTICE_SERVICE =
+ "com.android.car.user.CarUserNoticeService";
+
+ // Local cache for making feature query fast.
+ // Key: feature name, value: supported or not.
+ @GuardedBy("mCachedFeatures")
+ private final ArrayMap<String, Boolean> mCachedFeatures = new ArrayMap<>();
+
+ /** @hide */
+ boolean isFeatureEnabled(@NonNull ICar service, @NonNull String featureName) {
+ synchronized (mCachedFeatures) {
+ Boolean supported = mCachedFeatures.get(featureName);
+ if (supported != null) {
+ return supported;
+ }
+ }
+ // Need to fetch from car service. This should happen only once
+ try {
+ boolean supported = service.isFeatureEnabled(featureName);
+ synchronized (mCachedFeatures) {
+ mCachedFeatures.put(featureName, Boolean.valueOf(supported));
+ }
+ return supported;
+ } catch (RemoteException e) {
+ // car service has crashed. return false.
+ }
+ return false;
+ }
+
+ /** @hide */
+ void resetCache() {
+ synchronized (mCachedFeatures) {
+ mCachedFeatures.clear();
+ }
+ }
+}
diff --git a/car-lib/src/android/car/CarInfoManager.java b/car-lib/src/android/car/CarInfoManager.java
index c78523bd3f..6b2ddd71b9 100644
--- a/car-lib/src/android/car/CarInfoManager.java
+++ b/car-lib/src/android/car/CarInfoManager.java
@@ -61,7 +61,6 @@ public final class CarInfoManager extends CarManagerBase {
public static final String BASIC_INFO_KEY_VEHICLE_ID = "android.car.vehicle-id";
/**
* Key for product configuration info.
- * @FutureFeature Cannot drop due to usage in non-flag protected place.
* @hide
*/
@ValueTypeDef(type = String.class)
diff --git a/car-lib/src/android/car/ICar.aidl b/car-lib/src/android/car/ICar.aidl
index 671542ce39..1382db9ce2 100644
--- a/car-lib/src/android/car/ICar.aidl
+++ b/car-lib/src/android/car/ICar.aidl
@@ -48,5 +48,15 @@ interface ICar {
IBinder getCarService(in String serviceName) = 3;
int getCarConnectionType() = 4;
-
+ boolean isFeatureEnabled(in String featureName) = 5;
+ int enableFeature(in String featureName) = 6;
+ int disableFeature(in String featureName) = 7;
+ List<String> getAllEnabledFeatures() = 8;
+ List<String> getAllPendingDisabledFeatures() = 9;
+ List<String> getAllPendingEnabledFeatures() = 10;
+ /**
+ * Get class name for experimental feature. Class should have constructor taking (Car, IBinder)
+ * and should inherit CarManagerBase.
+ */
+ String getCarManagerClassForFeature(in String featureName) = 11;
}
diff --git a/car-lib/src/android/car/IExperimentalCar.aidl b/car-lib/src/android/car/IExperimentalCar.aidl
new file mode 100644
index 0000000000..18cbb0d167
--- /dev/null
+++ b/car-lib/src/android/car/IExperimentalCar.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 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 android.car.IExperimentalCarHelper;
+
+/** @hide */
+oneway interface IExperimentalCar {
+ /**
+ * Initialize the experimental car service.
+ * @param helper After init, experimental car service should return binder, class names for
+ * enabled features with all features available through helper.onInitComplete().
+ * @param enabledFeatures Currently enabled features. If this feature is not available any more,
+ * helper.onInitComplete should drop it from started features.
+ */
+ void init(in IExperimentalCarHelper helper, in List<String> enabledFeatures);
+}
diff --git a/car-lib/src/android/car/IExperimentalCarHelper.aidl b/car-lib/src/android/car/IExperimentalCarHelper.aidl
new file mode 100644
index 0000000000..4275c512cb
--- /dev/null
+++ b/car-lib/src/android/car/IExperimentalCarHelper.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 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;
+
+/** @hide */
+interface IExperimentalCarHelper {
+ /**
+ * Notify the completion of init to car service.
+ * @param allAvailableFeatures All features available in the package.
+ * @param startedFeatures Started features. This is a subset of enabledFeatures passed through
+ * IExperimentalCar.init(..). Only available features should be started.
+ * @param classNames Car*Manager class names for all started experimental features. Class name
+ * can be null if the feature does not have Car*Manager (=internal feature).
+ * @param binders Car*Manager binders for all started experimental features. Binder can be null
+ * if the feature does not have Car*Manager.
+ */
+ void onInitComplete(in List<String> allAvailableFeatures, in List<String> startedFeatures,
+ in List<String> classNames, in List<IBinder> binders);
+}
diff --git a/car-lib/src/android/car/annotation/ExperimentalFeature.java b/car-lib/src/android/car/annotation/ExperimentalFeature.java
new file mode 100644
index 0000000000..76e6b088b6
--- /dev/null
+++ b/car-lib/src/android/car/annotation/ExperimentalFeature.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 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.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * This is for experimental features. Note that experimental feature will not be allowed for user
+ * build and experiental features will not be part of car API. But this annotation is provided
+ * to mark it in separate library, which will be typically static library.
+ *
+ * <p>Note that experimental feature can become official feature later.
+ *
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({ANNOTATION_TYPE, FIELD})
+public @interface ExperimentalFeature {
+}
diff --git a/car-lib/src/android/car/annotation/FutureFeature.java b/car-lib/src/android/car/annotation/MandatoryFeature.java
index 1854771ab4..3aca13d96f 100644
--- a/car-lib/src/android/car/annotation/FutureFeature.java
+++ b/car-lib/src/android/car/annotation/MandatoryFeature.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2019 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.
@@ -13,22 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package android.car.annotation;
-import java.lang.annotation.ElementType;
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Annotation to represent future feature which is not ready for the current platform release.
- * Any API marked with this is for future development and should not be used for product.
+ * This is for mandatory features. Features marked with this will be always available in all car
+ * products.
*
* @hide
*/
-@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR,
- ElementType.LOCAL_VARIABLE})
-@Retention(RetentionPolicy.CLASS)
-public @interface FutureFeature {
- Class type() default Object.class;
+@Retention(SOURCE)
+@Target({ANNOTATION_TYPE, FIELD})
+public @interface MandatoryFeature {
}
diff --git a/car-lib/src/android/car/annotation/OptionalFeature.java b/car-lib/src/android/car/annotation/OptionalFeature.java
new file mode 100644
index 0000000000..342f696763
--- /dev/null
+++ b/car-lib/src/android/car/annotation/OptionalFeature.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 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.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * This is for optional features. Features marked with this should be first checked if it is
+ * supported using {@link android.car.Car#isFeatureSupported(featureName)}.
+ *
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({ANNOTATION_TYPE, FIELD})
+public @interface OptionalFeature {
+}
diff --git a/car-lib/src/android/car/annotation/RequiredFeature.java b/car-lib/src/android/car/annotation/RequiredFeature.java
new file mode 100644
index 0000000000..a5a3d14490
--- /dev/null
+++ b/car-lib/src/android/car/annotation/RequiredFeature.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 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.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to represent a feature required to use the specified method.
+ * Ex: @RequiredFeature(Car.STORAGE_MONITORING_SERVICE)
+ *
+ * @hide
+ */
+@Target({ANNOTATION_TYPE, CONSTRUCTOR, METHOD, TYPE})
+@Retention(SOURCE)
+public @interface RequiredFeature {
+ String value();
+}
diff --git a/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java b/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
index 69c092b9a4..ac59bac708 100644
--- a/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
+++ b/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
@@ -19,6 +19,7 @@ import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.car.Car;
import android.car.CarManagerBase;
+import android.car.annotation.RequiredFeature;
import android.os.IBinder;
import android.os.RemoteException;
@@ -36,6 +37,7 @@ import java.util.Set;
* @hide
*/
@SystemApi
+@RequiredFeature(Car.STORAGE_MONITORING_SERVICE)
public final class CarStorageMonitoringManager extends CarManagerBase {
private static final String TAG = CarStorageMonitoringManager.class.getSimpleName();
private static final int MSG_IO_STATS_EVENT = 0;
diff --git a/car-lib/src/android/car/vms/VmsPublisherClientService.java b/car-lib/src/android/car/vms/VmsPublisherClientService.java
index ea75707b4b..30ef727ba0 100644
--- a/car-lib/src/android/car/vms/VmsPublisherClientService.java
+++ b/car-lib/src/android/car/vms/VmsPublisherClientService.java
@@ -21,6 +21,7 @@ import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.app.Service;
import android.car.Car;
+import android.car.annotation.RequiredFeature;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
@@ -52,6 +53,7 @@ import java.lang.ref.WeakReference;
*
* @hide
*/
+@RequiredFeature(Car.VMS_SUBSCRIBER_SERVICE)
@SystemApi
public abstract class VmsPublisherClientService extends Service {
private static final boolean DBG = false;
diff --git a/car-lib/src/android/car/vms/VmsSubscriberManager.java b/car-lib/src/android/car/vms/VmsSubscriberManager.java
index edde9820f1..4e53bf8edf 100644
--- a/car-lib/src/android/car/vms/VmsSubscriberManager.java
+++ b/car-lib/src/android/car/vms/VmsSubscriberManager.java
@@ -21,6 +21,7 @@ import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.car.Car;
import android.car.CarManagerBase;
+import android.car.annotation.RequiredFeature;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
@@ -39,6 +40,7 @@ import java.util.concurrent.Executor;
*
* @hide
*/
+@RequiredFeature(Car.VMS_SUBSCRIBER_SERVICE)
@SystemApi
public final class VmsSubscriberManager extends CarManagerBase {
private static final String TAG = "VmsSubscriberManager";
diff --git a/car-test-lib/src/android/car/testapi/FakeCar.java b/car-test-lib/src/android/car/testapi/FakeCar.java
index dbd2b0f114..79ea973f49 100644
--- a/car-test-lib/src/android/car/testapi/FakeCar.java
+++ b/car-test-lib/src/android/car/testapi/FakeCar.java
@@ -36,6 +36,9 @@ import android.util.Log;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Collections;
+import java.util.List;
+
/*
The idea behind this class is that we can fake-out interfaces between Car*Manager and
Car Service. Effectively creating a fake version of Car Service that can run under Robolectric
@@ -217,6 +220,41 @@ public class FakeCar {
public int getCarConnectionType() throws RemoteException {
return Car.CONNECTION_TYPE_EMBEDDED;
}
+
+ @Override
+ public boolean isFeatureEnabled(String featureName) {
+ return false;
+ }
+
+ @Override
+ public int enableFeature(String featureName) {
+ return Car.FEATURE_REQUEST_SUCCESS;
+ }
+
+ @Override
+ public int disableFeature(String featureName) {
+ return Car.FEATURE_REQUEST_SUCCESS;
+ }
+
+ @Override
+ public List<String> getAllEnabledFeatures() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<String> getAllPendingDisabledFeatures() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<String> getAllPendingEnabledFeatures() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public String getCarManagerClassForFeature(String featureName) {
+ return null;
+ }
}
}
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index 8246d37e67..e192401e85 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -41,6 +41,8 @@ PRODUCT_PACKAGES += \
VmsSubscriberClientSample \
DirectRenderingCluster \
GarageModeTestApp \
+ ExperimentalCarService \
+
# SEPolicy for test apps / services
BOARD_SEPOLICY_DIRS += packages/services/Car/car_product/sepolicy/test
diff --git a/car_product/sepolicy/test/experimentalcarservice_app.te b/car_product/sepolicy/test/experimentalcarservice_app.te
new file mode 100644
index 0000000000..082f32645e
--- /dev/null
+++ b/car_product/sepolicy/test/experimentalcarservice_app.te
@@ -0,0 +1,46 @@
+# Domain to run ExperimentalCarService (com.android.experimentalcar)
+type experimentalcarservice_app, domain, coredomain;
+app_domain(experimentalcarservice_app);
+
+allow experimentalcarservice_app wifi_service:service_manager find;
+
+# Allow access certain to system services.
+# Keep alphabetically sorted.
+allow experimentalcarservice_app {
+ accessibility_service
+ activity_service
+ activity_task_service
+ audio_service
+ audioserver_service
+ autofill_service
+ bluetooth_manager_service
+ carservice_service
+ connectivity_service
+ content_service
+ deviceidle_service
+ display_service
+ graphicsstats_service
+ input_method_service
+ input_service
+ location_service
+ media_session_service
+ network_management_service
+ power_service
+ procfsinspector_service
+ sensorservice_service
+ surfaceflinger_service
+ telecom_service
+ uimode_service
+ voiceinteraction_service
+}:service_manager find;
+
+# Read and write /data/data subdirectory.
+allow experimentalcarservice_app system_app_data_file:dir create_dir_perms;
+allow experimentalcarservice_app system_app_data_file:{ file lnk_file } create_file_perms;
+# R/W /data/system/car
+allow experimentalcarservice_app system_car_data_file:dir create_dir_perms;
+allow experimentalcarservice_app system_car_data_file:{ file lnk_file } create_file_perms;
+
+net_domain(experimentalcarservice_app)
+
+allow experimentalcarservice_app cgroup:file rw_file_perms;
diff --git a/car_product/sepolicy/test/seapp_contexts b/car_product/sepolicy/test/seapp_contexts
new file mode 100644
index 0000000000..a818d73cb6
--- /dev/null
+++ b/car_product/sepolicy/test/seapp_contexts
@@ -0,0 +1 @@
+user=system seinfo=platform name=com.android.experimentalcar domain=experimentalcarservice_app type=system_app_data_file
diff --git a/experimental/experimental_api/Android.bp b/experimental/experimental_api/Android.bp
new file mode 100644
index 0000000000..24e9b994cf
--- /dev/null
+++ b/experimental/experimental_api/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 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.
+//
+//
+
+// Experimental API backed by Experimental Car service.
+
+java_library {
+
+ name: "car-experimental-api-static-lib",
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.aidl"
+ ],
+
+ libs: ["android.car"],
+
+ platform_apis: true,
+
+ product_variables: {
+ pdk: {
+ enabled: false,
+ },
+ },
+}
diff --git a/experimental/experimental_api/src/android/car/experimental/CarTestDemoExperimentalFeatureManager.java b/experimental/experimental_api/src/android/car/experimental/CarTestDemoExperimentalFeatureManager.java
new file mode 100644
index 0000000000..517165ed4a
--- /dev/null
+++ b/experimental/experimental_api/src/android/car/experimental/CarTestDemoExperimentalFeatureManager.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 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.experimental;
+
+import android.car.Car;
+import android.car.CarManagerBase;
+import android.car.annotation.RequiredFeature;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+/**
+ * Demo CarManager API for demonstrating experimental feature / API usage. This will never go to
+ * production.
+ *
+ * @hide
+ */
+@RequiredFeature(ExperimentalCar.TEST_EXPERIMENTAL_FEATURE_SERVICE)
+public final class CarTestDemoExperimentalFeatureManager extends CarManagerBase {
+
+ private final ITestDemoExperimental mService;
+
+ /**
+ * Constructor parameters should remain this way for Car library to construct this.
+ */
+ public CarTestDemoExperimentalFeatureManager(Car car, IBinder service) {
+ super(car);
+ mService = ITestDemoExperimental.Stub.asInterface(service);
+ }
+
+ /**
+ * Send ping msg service. It will replay back with the same message.
+ */
+ public String ping(String msg) {
+ try {
+ return mService.ping(msg);
+ } catch (RemoteException e) {
+ // For experimental API, we just crash client.
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ protected void onCarDisconnected() {
+ // Nothing to do
+ }
+}
diff --git a/car-lib/src_feature_future/com/android/car/internal/FeatureConfiguration.java b/experimental/experimental_api/src/android/car/experimental/ExperimentalCar.java
index 66cff604c5..e6921a2729 100644
--- a/car-lib/src_feature_future/com/android/car/internal/FeatureConfiguration.java
+++ b/experimental/experimental_api/src/android/car/experimental/ExperimentalCar.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2019 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.
@@ -13,17 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.car.internal;
+
+package android.car.experimental;
+
+import android.car.annotation.ExperimentalFeature;
/**
- * Class to hold static boolean flag for enabling / disabling features.
- *
+ * Top level class for experimental Car features
* @hide
*/
-public class FeatureConfiguration {
- /** Enable future feature by default. */
- public static final boolean DEFAULT = true;
- /** product configuration in CarInfoManager */
- public static final boolean ENABLE_PRODUCT_CONFIGURATION_INFO = DEFAULT;
- public static final boolean ENABLE_VEHICLE_MAP_SERVICE = DEFAULT;
+public final class ExperimentalCar {
+ /**
+ * Service for testing experimental feature
+ *
+ * @hide
+ */
+ @ExperimentalFeature
+ public static final String TEST_EXPERIMENTAL_FEATURE_SERVICE =
+ "android.car.experimental.test_demo_experimental_feature_service";
}
diff --git a/experimental/experimental_api/src/android/car/experimental/ITestDemoExperimental.aidl b/experimental/experimental_api/src/android/car/experimental/ITestDemoExperimental.aidl
new file mode 100644
index 0000000000..b5ce7e1e37
--- /dev/null
+++ b/experimental/experimental_api/src/android/car/experimental/ITestDemoExperimental.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2019 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.experimental;
+
+/** @hide */
+interface ITestDemoExperimental {
+ String ping(in String msg);
+} \ No newline at end of file
diff --git a/experimental/service/Android.bp b/experimental/service/Android.bp
new file mode 100644
index 0000000000..f2fba99097
--- /dev/null
+++ b/experimental/service/Android.bp
@@ -0,0 +1,55 @@
+// Copyright (C) 2019 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.
+//
+//
+
+// Build the Experimental Car service.
+
+
+android_app {
+ name: "ExperimentalCarService",
+
+ srcs: [
+ "src/**/*.java"
+ ],
+
+ resource_dirs: ["res"],
+
+ platform_apis: true,
+
+ // Each update should be signed by OEMs
+ certificate: "platform",
+ privileged: true,
+
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ enabled: false,
+ },
+
+ libs: ["android.car"],
+
+ static_libs: [
+ "car-service-common-util-static-lib",
+ "car-experimental-api-static-lib",
+ ],
+
+ required: ["privapp_whitelist_com.android.experimentalcar"],
+
+ // Disable build in PDK, missing aidl import breaks build
+ product_variables: {
+ pdk: {
+ enabled: false,
+ },
+ },
+}
diff --git a/experimental/service/AndroidManifest.xml b/experimental/service/AndroidManifest.xml
new file mode 100644
index 0000000000..c28f400d67
--- /dev/null
+++ b/experimental/service/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ package="com.android.experimentalcar"
+ coreApp="true"
+ android:sharedUserId="android.uid.system">
+
+ <original-package android:name="com.android.experimentalcar" />
+
+ <application android:label="@string/app_title"
+ android:directBootAware="true"
+ android:allowBackup="false"
+ android:persistent="false">
+ <service android:name=".ExperimentalCarService"
+ android:singleUser="true">
+ </service>
+ </application>
+</manifest>
diff --git a/experimental/service/proguard.flags b/experimental/service/proguard.flags
new file mode 100644
index 0000000000..22cc22df14
--- /dev/null
+++ b/experimental/service/proguard.flags
@@ -0,0 +1,3 @@
+-verbose
+-keep @com.android.internal.annotations.VisibleForTesting class *
+
diff --git a/experimental/service/res/values/strings.xml b/experimental/service/res/values/strings.xml
new file mode 100644
index 0000000000..8e081cd78f
--- /dev/null
+++ b/experimental/service/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_title" translatable="false">Experimental Car service</string>
+</resources> \ No newline at end of file
diff --git a/experimental/service/src/com/android/experimentalcar/ExperimentalCarService.java b/experimental/service/src/com/android/experimentalcar/ExperimentalCarService.java
new file mode 100644
index 0000000000..bc40af5b93
--- /dev/null
+++ b/experimental/service/src/com/android/experimentalcar/ExperimentalCarService.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 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 com.android.experimentalcar;
+
+import android.app.Service;
+import android.car.Car;
+import android.content.Intent;
+import android.os.IBinder;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Top class to keep all experimental features.
+ */
+public class ExperimentalCarService extends Service {
+
+ private Car mCar;
+ private final IExperimentalCarImpl mIExperimentalCarImpl = new IExperimentalCarImpl(this);
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ // This is for crashing this service when car service crashes.
+ mCar = Car.createCar(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ mIExperimentalCarImpl.release();
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // keep it alive.
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mIExperimentalCarImpl;
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ mIExperimentalCarImpl.dump(fd, writer, args);
+ }
+}
diff --git a/experimental/service/src/com/android/experimentalcar/IExperimentalCarImpl.java b/experimental/service/src/com/android/experimentalcar/IExperimentalCarImpl.java
new file mode 100644
index 0000000000..14c7f38b21
--- /dev/null
+++ b/experimental/service/src/com/android/experimentalcar/IExperimentalCarImpl.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2019 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 com.android.experimentalcar;
+
+import android.annotation.MainThread;
+import android.annotation.Nullable;
+import android.car.IExperimentalCar;
+import android.car.IExperimentalCarHelper;
+import android.car.experimental.CarTestDemoExperimentalFeatureManager;
+import android.car.experimental.ExperimentalCar;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.car.CarServiceBase;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Implements IExperimentalCar for experimental features.
+ */
+public final class IExperimentalCarImpl extends IExperimentalCar.Stub {
+
+ private static final String TAG = "CAR.EXPIMPL";
+
+ private static final List<String> ALL_AVAILABLE_FEATURES = Arrays.asList(
+ ExperimentalCar.TEST_EXPERIMENTAL_FEATURE_SERVICE
+ );
+
+ private final Context mContext;
+
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private boolean mReleased;
+
+ @GuardedBy("mLock")
+ private IExperimentalCarHelper mHelper;
+
+ @GuardedBy("mLock")
+ private ArrayList<CarServiceBase> mRunningServices = new ArrayList<>();
+
+ public IExperimentalCarImpl(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void init(IExperimentalCarHelper helper, List<String> enabledFeatures) {
+ // From car service or unit testing only
+ assertCallingFromSystemProcessOrSelf();
+
+ // dispatch to main thread as release is always done in main.
+ mMainThreadHandler.post(() -> {
+ synchronized (mLock) {
+ if (mReleased) {
+ Log.w(TAG, "init binder call after onDestroy, will ignore");
+ return;
+ }
+ }
+ ArrayList<CarServiceBase> services = new ArrayList<>();
+ ArrayList<String> startedFeatures = new ArrayList<>();
+ ArrayList<String> classNames = new ArrayList<>();
+ ArrayList<IBinder> binders = new ArrayList<>();
+
+ // This cannot address inter-dependency. That requires re-ordering this in dependency
+ // order.
+ // That should be done when we find such needs. For now, each feature inside here should
+ // not have inter-dependency as they are all optional.
+ for (String feature : enabledFeatures) {
+ CarServiceBase service = constructServiceForFeature(feature);
+ if (service == null) {
+ Log.e(TAG, "Failed to construct requested feature:" + feature);
+ continue;
+ }
+ service.init();
+ services.add(service);
+ startedFeatures.add(feature);
+ // If it is not IBinder, then it is internal feature.
+ if (service instanceof IBinder) {
+ binders.add((IBinder) service);
+ } else {
+ binders.add(null);
+ }
+ classNames.add(getClassNameForFeature(feature));
+ }
+ try {
+ helper.onInitComplete(ALL_AVAILABLE_FEATURES, startedFeatures, classNames, binders);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Car service crashed?", e);
+ // will be destroyed soon. Just continue and register services for possible cleanup.
+ }
+ synchronized (mLock) {
+ mHelper = helper;
+ mRunningServices.addAll(services);
+ }
+ });
+ }
+
+ // should be called in Service.onDestroy
+ @MainThread
+ void release() {
+ // Copy to handle call release without lock
+ ArrayList<CarServiceBase> services;
+ synchronized (mLock) {
+ if (mReleased) {
+ return;
+ }
+ mReleased = true;
+ services = new ArrayList<>(mRunningServices);
+ mRunningServices.clear();
+ }
+ for (CarServiceBase service : services) {
+ service.release();
+ }
+ }
+
+ /** dump */
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ ArrayList<CarServiceBase> services;
+ synchronized (mLock) {
+ writer.println("mReleased:" + mReleased);
+ writer.println("ALL_AVAILABLE_FEATURES:" + ALL_AVAILABLE_FEATURES);
+ services = new ArrayList<>(mRunningServices);
+ }
+ writer.println(" Number of running services:" + services.size());
+ int i = 0;
+ for (CarServiceBase service : services) {
+ writer.print(i + ":");
+ service.dump(writer);
+ i++;
+ }
+ }
+
+ @Nullable
+ private String getClassNameForFeature(String featureName) {
+ switch (featureName) {
+ case ExperimentalCar.TEST_EXPERIMENTAL_FEATURE_SERVICE:
+ return CarTestDemoExperimentalFeatureManager.class.getName();
+ default:
+ return null;
+ }
+ }
+
+ @Nullable
+ private CarServiceBase constructServiceForFeature(String featureName) {
+ switch (featureName) {
+ case ExperimentalCar.TEST_EXPERIMENTAL_FEATURE_SERVICE:
+ return new TestDemoExperimentalFeatureService();
+ default:
+ return null;
+ }
+ }
+
+ private static void assertCallingFromSystemProcessOrSelf() {
+ int uid = Binder.getCallingUid();
+ int pid = Binder.getCallingPid();
+ if (uid != Process.SYSTEM_UID && pid != Process.myPid()) {
+ throw new SecurityException("Only allowed from system or self, uid:" + uid
+ + " pid:" + pid);
+ }
+ }
+}
diff --git a/experimental/service/src/com/android/experimentalcar/TestDemoExperimentalFeatureService.java b/experimental/service/src/com/android/experimentalcar/TestDemoExperimentalFeatureService.java
new file mode 100644
index 0000000000..1309dd2907
--- /dev/null
+++ b/experimental/service/src/com/android/experimentalcar/TestDemoExperimentalFeatureService.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 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 com.android.experimentalcar;
+
+import android.car.experimental.ITestDemoExperimental;
+
+import com.android.car.CarServiceBase;
+
+import java.io.PrintWriter;
+
+/**
+ * Demo service for testing experimental feature.
+ */
+public final class TestDemoExperimentalFeatureService extends ITestDemoExperimental.Stub
+ implements CarServiceBase {
+
+ @Override
+ public void init() {
+ // Nothing to do
+ }
+
+ @Override
+ public void release() {
+ // Nothing to do
+ }
+
+ @Override
+ public void dump(PrintWriter writer) {
+ writer.println("*TestExperimentalFeatureService*");
+ }
+
+ @Override
+ public String ping(String msg) {
+ return msg;
+ }
+}
diff --git a/service/Android.bp b/service/Android.bp
index f832902dc7..0035441c88 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -68,6 +68,23 @@ android_app {
},
}
+java_library {
+
+ name: "car-service-common-util-static-lib",
+
+ srcs: [
+ "src/com/android/car/CarServiceBase.java",
+ "src/com/android/car/CarServiceUtils.java",
+ "src/com/android/car/CarLog.java",
+ ],
+
+ product_variables: {
+ pdk: {
+ enabled: false,
+ },
+ },
+}
+
//####################################################################################
// Build a static library to help mocking various car services in testing. This is meant to be used
// for internal unit tests around the car service.
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 82a4896d3e..01cdc03b31 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -503,6 +503,15 @@
android:label="@string/car_permission_label_car_test_service"
android:description="@string/car_permission_desc_car_test_service" />
+ <!-- Allows system app to enable / disable / query features in the system.
+ <p>Protection level: signature|privileged
+ -->
+ <permission
+ android:name="android.car.permission.CONTROL_CAR_FEATURES"
+ android:protectionLevel="signature|privileged"
+ android:label="@string/car_permission_label_control_car_features"
+ android:description="@string/car_permission_desc_control_car_features" />
+
<!-- Allows an application to read vendor properties related with windows.
<p>Protection level: signature|privileged
-->
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 8134ce1feb..ab98f3e43e 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -284,4 +284,23 @@
<integer name="config_mediaSourceChangedAutoplay">2</integer>
<!-- Configuration to enable media center to autoplay on boot -->
<integer name="config_mediaBootAutoplay">2</integer>
+
+ <!--
+ Specifies optional features that can be enabled by this image. Note that vhal can disable
+ them depending on product variation.
+ Feature name can be either service name defined in Car.*_SERVICE for Car*Manager or any
+ optional feature defined under @OptionalFeature annotation.
+ Note that '/' is used to have subfeature under main feature like "MAIN_FEATURE/SUB_FEATURE".
+
+ Some examples are:
+ <item>storage_monitoring</item>
+ <item>com.android.car.user.CarUserNoticeService</item>
+ <item>com.example.Feature/SubFeature</item>
+
+ The default list defined below will enable all optional features defined.
+ -->
+ <string-array translatable="false" name="config_allowed_optional_car_features">
+ <item>com.android.car.user.CarUserNoticeService</item>
+ <item>storage_monitoring</item>
+ </string-array>
</resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 340f95a76a..a24c240668 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -450,6 +450,11 @@
<!-- Permission text: apps control vendor properties in category 10 [CHAR LIMIT=NONE] -->
<string name="car_permission_desc_set_car_vendor_category_10">Control vendor specific properties in category 10.</string>
+ <!-- Permission text: enable or disable car's features [CHAR LIMIT=NONE] -->
+ <string name="car_permission_label_control_car_features">Enable or disable car\u2019s features</string>
+ <!-- Permission text: apps control vendor properties in category 10 [CHAR LIMIT=NONE] -->
+ <string name="car_permission_desc_control_car_features">Enable or disable car\u2019s features.</string>
+
<!-- The default name of device enrolled as trust device [CHAR LIMIT=NONE] -->
<string name="trust_device_default_name">My Device</string>
diff --git a/service/src/com/android/car/CarExperimentalFeatureServiceController.java b/service/src/com/android/car/CarExperimentalFeatureServiceController.java
new file mode 100644
index 0000000000..c28bf9f0c1
--- /dev/null
+++ b/service/src/com/android/car/CarExperimentalFeatureServiceController.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2019 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 com.android.car;
+
+import android.car.IExperimentalCar;
+import android.car.IExperimentalCarHelper;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Controls binding to ExperimentalCarService and interfaces for experimental features.
+ */
+public final class CarExperimentalFeatureServiceController implements CarServiceBase {
+
+ private static final String TAG = "CAR.EXPERIMENTAL";
+
+ private final Context mContext;
+
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ IExperimentalCar experimentalCar;
+ synchronized (mLock) {
+ experimentalCar = IExperimentalCar.Stub.asInterface(service);
+ mExperimentalCar = experimentalCar;
+ }
+ if (experimentalCar == null) {
+ Log.e(TAG, "Experimental car returned null binder");
+ return;
+ }
+ CarFeatureController featureController = CarLocalServices.getService(
+ CarFeatureController.class);
+ List<String> enabledExperimentalFeatures =
+ featureController.getEnabledExperimentalFeatures();
+ try {
+ experimentalCar.init(mHelper, enabledExperimentalFeatures);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Experimental car service crashed", e);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ resetFeatures();
+ }
+ };
+
+ private final IExperimentalCarHelper mHelper = new IExperimentalCarHelper.Stub() {
+ @Override
+ public void onInitComplete(List<String> allAvailableFeatures, List<String> startedFeatures,
+ List<String> classNames, List<IBinder> binders) {
+ if (allAvailableFeatures == null) {
+ Log.e(TAG, "Experimental car passed null allAvailableFeatures");
+ return;
+ }
+ if (startedFeatures == null || classNames == null || binders == null) {
+ Log.i(TAG, "Nothing enabled in Experimental car");
+ return;
+ }
+ int sizeOfStartedFeatures = startedFeatures.size();
+ if (sizeOfStartedFeatures != classNames.size()
+ || sizeOfStartedFeatures != binders.size()) {
+ Log.e(TAG,
+ "Experimental car passed wrong lists of enabled features, startedFeatures:"
+ + startedFeatures + " classNames:" + classNames + " binders:" + binders);
+ }
+ // Do conversion to make indexed accesses
+ ArrayList<String> classNamesInArray = new ArrayList<>(classNames);
+ ArrayList<IBinder> bindersInArray = new ArrayList<>(binders);
+ int i = 0;
+ synchronized (mLock) {
+ for (String feature : startedFeatures) {
+ mEnabledFeatures.put(feature, new FeatureInfo(classNamesInArray.get(i),
+ bindersInArray.get(i)));
+ }
+ }
+ CarFeatureController featureController = CarLocalServices.getService(
+ CarFeatureController.class);
+ featureController.setAvailableExperimentalFeatureList(allAvailableFeatures);
+ Log.i(TAG, "Available experimental features:" + allAvailableFeatures);
+ Log.i(TAG, "Started experimental features:" + startedFeatures);
+ }
+ };
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private IExperimentalCar mExperimentalCar;
+
+ @GuardedBy("mLock")
+ private final ArrayMap<String, FeatureInfo> mEnabledFeatures = new ArrayMap<>();
+
+ @GuardedBy("mLock")
+ private boolean mBound;
+
+ private static class FeatureInfo {
+ public final String className;
+ public final IBinder binder;
+
+ FeatureInfo(String className, IBinder binder) {
+ this.className = className;
+ this.binder = binder;
+ }
+ }
+
+ public CarExperimentalFeatureServiceController(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void init() {
+ // Do binding only for real car servie
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName("com.android.experimentalcar",
+ "com.android.experimentalcar.ExperimentalCarService"));
+ boolean bound = bindService(intent);
+ if (!bound) {
+ Log.e(TAG, "Cannot bind to experimental car service, intent:" + intent);
+ }
+ synchronized (mLock) {
+ mBound = bound;
+ }
+ }
+
+ /**
+ * Bind service. Separated for testing.
+ * Test will override this. Default behavior will not bind if it is not real run (=system uid).
+ */
+ @VisibleForTesting
+ public boolean bindService(Intent intent) {
+ int myUid = Process.myUid();
+ if (myUid != Process.SYSTEM_UID) {
+ Log.w(TAG, "Binding experimental service skipped as this may be test env, uid:"
+ + myUid);
+ return false;
+ }
+ try {
+ return mContext.bindServiceAsUser(intent, mServiceConnection,
+ Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
+ } catch (Exception e) {
+ // Do not crash car service for case like package not found and etc.
+ Log.e(TAG, "Cannot bind to experimental car service", e);
+ return false;
+ }
+ }
+
+ @Override
+ public void release() {
+ synchronized (mLock) {
+ if (mBound) {
+ mContext.unbindService(mServiceConnection);
+ }
+ mBound = false;
+ resetFeatures();
+ }
+ }
+
+ @Override
+ public void dump(PrintWriter writer) {
+ writer.println("*CarExperimentalFeatureServiceController*");
+
+ synchronized (mLock) {
+ writer.println(" mEnabledFeatures, number of features:" + mEnabledFeatures.size()
+ + ", format: (feature, class)");
+ for (int i = 0; i < mEnabledFeatures.size(); i++) {
+ String feature = mEnabledFeatures.keyAt(i);
+ FeatureInfo info = mEnabledFeatures.valueAt(i);
+ writer.println(feature + "," + info.className);
+ }
+ writer.println("mBound:" + mBound);
+ }
+ }
+
+ /**
+ * Returns class name for experimental feature.
+ */
+ public String getCarManagerClassForFeature(String featureName) {
+ FeatureInfo info;
+ synchronized (mLock) {
+ info = mEnabledFeatures.get(featureName);
+ }
+ if (info == null) {
+ return null;
+ }
+ return info.className;
+ }
+
+ /**
+ * Returns service binder for experimental feature.
+ */
+ public IBinder getCarService(String serviceName) {
+ FeatureInfo info;
+ synchronized (mLock) {
+ info = mEnabledFeatures.get(serviceName);
+ }
+ if (info == null) {
+ return null;
+ }
+ return info.binder;
+ }
+
+ private void resetFeatures() {
+ synchronized (mLock) {
+ mExperimentalCar = null;
+ mEnabledFeatures.clear();
+ }
+ }
+}
diff --git a/service/src/com/android/car/CarFeatureController.java b/service/src/com/android/car/CarFeatureController.java
new file mode 100644
index 0000000000..fdaa1c5e1c
--- /dev/null
+++ b/service/src/com/android/car/CarFeatureController.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2019 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 com.android.car;
+
+import android.annotation.NonNull;
+import android.car.Car;
+import android.car.Car.FeaturerRequestEnum;
+import android.car.CarFeatures;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Component controlling the feature of car.
+ */
+public final class CarFeatureController implements CarServiceBase {
+
+ private static final String TAG = "CAR.FEATURE";
+
+ // Use HaseSet for better search performance. Memory consumption is fixed and it not an issue.
+ // Should keep alphabetical order under each bucket.
+ // Update CarFeatureTest as well when this is updated.
+ private static final HashSet<String> MANDATORY_FEATURES = new HashSet<>(Arrays.asList(
+ Car.APP_FOCUS_SERVICE,
+ Car.AUDIO_SERVICE,
+ Car.BLUETOOTH_SERVICE,
+ Car.CAR_BUGREPORT_SERVICE,
+ Car.CAR_DRIVING_STATE_SERVICE,
+ Car.CAR_MEDIA_SERVICE,
+ Car.CAR_NAVIGATION_SERVICE,
+ Car.CAR_OCCUPANT_ZONE_SERVICE,
+ Car.CAR_USER_SERVICE,
+ Car.INFO_SERVICE,
+ Car.PACKAGE_SERVICE,
+ Car.POWER_SERVICE,
+ Car.PROJECTION_SERVICE,
+ Car.PROPERTY_SERVICE,
+ Car.TEST_SERVICE,
+ // Deprecated, but still should be supported
+ Car.SENSOR_SERVICE,
+ Car.CAR_INSTRUMENT_CLUSTER_SERVICE,
+ Car.CABIN_SERVICE,
+ Car.HVAC_SERVICE,
+ Car.VENDOR_EXTENSION_SERVICE,
+ // Candidate for Optional, but stay mandatory for now until final decision is made.
+ Car.CAR_CONFIGURATION_SERVICE,
+ Car.CAR_TRUST_AGENT_ENROLLMENT_SERVICE,
+ Car.DIAGNOSTIC_SERVICE,
+ Car.CAR_UX_RESTRICTION_SERVICE,
+ // Marked as optional, but requires additional work
+ Car.VMS_SUBSCRIBER_SERVICE
+ ));
+
+ private static final HashSet<String> OPTIONAL_FEATURES = new HashSet<>(Arrays.asList(
+ Car.STORAGE_MONITORING_SERVICE,
+ CarFeatures.FEATURE_CAR_USER_NOTICE_SERVICE
+ ));
+
+ private static final String FEATURE_CONFIG_FILE_NAME = "car_feature_config.txt";
+
+ // Last line starts with this with number of features for extra sanity check.
+ private static final String CONFIG_FILE_LAST_LINE_MARKER = ",,";
+
+ // Set once in constructor and not updated. Access it without lock so that it can be accessed
+ // quickly.
+ private final HashSet<String> mEnabledFeatures;
+
+ private final Context mContext;
+
+ private final List<String> mDefaultEnabledFeaturesFromConfig;
+ private final List<String> mDisabledFeaturesFromVhal;
+
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final AtomicFile mFeatureConfigFile;
+
+ @GuardedBy("mLock")
+ private final List<String> mPendingEnabledFeatures = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private final List<String> mPendingDisabledFeatures = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private HashSet<String> mAvailableExperimentalFeatures = new HashSet<>();
+
+ private final Runnable mDefaultConfigWriter = () -> {
+ persistToFeatureConfigFile();
+ };
+
+ public CarFeatureController(@NonNull Context context,
+ @NonNull String[] defaultEnabledFeaturesFromConfig,
+ @NonNull String[] disabledFeaturesFromVhal, @NonNull File dataDir) {
+ mContext = context;
+ mDefaultEnabledFeaturesFromConfig = Arrays.asList(defaultEnabledFeaturesFromConfig);
+ mDisabledFeaturesFromVhal = Arrays.asList(disabledFeaturesFromVhal);
+ mEnabledFeatures = new HashSet<>(MANDATORY_FEATURES);
+ mFeatureConfigFile = new AtomicFile(new File(dataDir, FEATURE_CONFIG_FILE_NAME), TAG);
+ boolean shouldLoadDefaultConfig = !mFeatureConfigFile.exists();
+ if (!shouldLoadDefaultConfig) {
+ if (!loadFromConfigFileLocked()) {
+ shouldLoadDefaultConfig = true;
+ }
+ }
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ // Separate if to use this as backup for failure in loadFromConfigFileLocked()
+ if (shouldLoadDefaultConfig) {
+ parseDefaultConfig();
+ dispatchDefaultConfigUpdate();
+ }
+ }
+
+ @Override
+ public void init() {
+ // nothing should be done here. This should work with only constructor.
+ }
+
+ @Override
+ public void release() {
+ // nothing should be done here.
+ }
+
+ @Override
+ public void dump(PrintWriter writer) {
+ writer.println("*CarFeatureController*");
+ writer.println(" mEnabledFeatures:" + mEnabledFeatures);
+ writer.println(" mDefaultEnabledFeaturesFromConfig:" + mDefaultEnabledFeaturesFromConfig);
+ writer.println(" mDisabledFeaturesFromVhal:" + mDisabledFeaturesFromVhal);
+ synchronized (mLock) {
+ writer.println(" mAvailableExperimentalFeatures:" + mAvailableExperimentalFeatures);
+ writer.println(" mPendingEnabledFeatures:" + mPendingEnabledFeatures);
+ writer.println(" mPendingDisabledFeatures:" + mPendingDisabledFeatures);
+ }
+ }
+
+ /** Check {@link Car#isFeatureEnabled(String)} */
+ public boolean isFeatureEnabled(String featureName) {
+ return mEnabledFeatures.contains(featureName);
+ }
+
+ @FeaturerRequestEnum
+ private int checkFeatureExisting(String featureName) {
+ if (MANDATORY_FEATURES.contains(featureName)) {
+ return Car.FEATURE_REQUEST_MANDATORY;
+ }
+ if (!OPTIONAL_FEATURES.contains(featureName)) {
+ synchronized (mLock) {
+ if (!mAvailableExperimentalFeatures.contains(featureName)) {
+ Log.e(TAG, "enableFeature requested for non-existing feature:"
+ + featureName);
+ return Car.FEATURE_REQUEST_NOT_EXISTING;
+ }
+ }
+ }
+ return Car.FEATURE_REQUEST_SUCCESS;
+ }
+
+ /** Check {@link Car#enableFeature(String)} */
+ public int enableFeature(String featureName) {
+ assertPermission();
+ int checkResult = checkFeatureExisting(featureName);
+ if (checkResult != Car.FEATURE_REQUEST_SUCCESS) {
+ return checkResult;
+ }
+
+ boolean alreadyEnabled = mEnabledFeatures.contains(featureName);
+ boolean shouldUpdateConfigFile = false;
+ synchronized (mLock) {
+ if (mPendingDisabledFeatures.remove(featureName)) {
+ shouldUpdateConfigFile = true;
+ }
+ if (!mPendingEnabledFeatures.contains(featureName) && !alreadyEnabled) {
+ shouldUpdateConfigFile = true;
+ mPendingEnabledFeatures.add(featureName);
+ }
+ }
+ if (shouldUpdateConfigFile) {
+ Log.w(TAG, "Enabling feature in config file:" + featureName);
+ dispatchDefaultConfigUpdate();
+ }
+ if (alreadyEnabled) {
+ return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE;
+ } else {
+ return Car.FEATURE_REQUEST_SUCCESS;
+ }
+ }
+
+ /** Check {@link Car#disableFeature(String)} */
+ public int disableFeature(String featureName) {
+ assertPermission();
+ int checkResult = checkFeatureExisting(featureName);
+ if (checkResult != Car.FEATURE_REQUEST_SUCCESS) {
+ return checkResult;
+ }
+
+ boolean alreadyDisabled = !mEnabledFeatures.contains(featureName);
+ boolean shouldUpdateConfigFile = false;
+ synchronized (mLock) {
+ if (mPendingEnabledFeatures.remove(featureName)) {
+ shouldUpdateConfigFile = true;
+ }
+ if (!mPendingDisabledFeatures.contains(featureName) && !alreadyDisabled) {
+ shouldUpdateConfigFile = true;
+ mPendingDisabledFeatures.add(featureName);
+ }
+ }
+ if (shouldUpdateConfigFile) {
+ Log.w(TAG, "Disabling feature in config file:" + featureName);
+ dispatchDefaultConfigUpdate();
+ }
+ if (alreadyDisabled) {
+ return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE;
+ } else {
+ return Car.FEATURE_REQUEST_SUCCESS;
+ }
+ }
+
+ /**
+ * Set available experimental features. Only features set through this call will be allowed to
+ * be enabled for experimental features. Setting this is not allowed for USER build.
+ *
+ * @return True if set is allowed and set. False if experimental feature is not allowed.
+ */
+ public boolean setAvailableExperimentalFeatureList(List<String> experimentalFeatures) {
+ assertPermission();
+ if (Build.IS_USER) {
+ Log.e(TAG, "Experimental feature list set for USER build",
+ new RuntimeException());
+ return false;
+ }
+ synchronized (mLock) {
+ mAvailableExperimentalFeatures.clear();
+ mAvailableExperimentalFeatures.addAll(experimentalFeatures);
+ }
+ return true;
+ }
+
+ /** Check {@link Car#getAllEnabledFeatures()} */
+ public List<String> getAllEnabledFeatures() {
+ assertPermission();
+ return new ArrayList<>(mEnabledFeatures);
+ }
+
+ /** Check {@link Car#getAllPendingDisabledFeatures()} */
+ public List<String> getAllPendingDisabledFeatures() {
+ assertPermission();
+ synchronized (mLock) {
+ return new ArrayList<>(mPendingDisabledFeatures);
+ }
+ }
+
+ /** Check {@link Car#getAllPendingEnabledFeatures()} */
+ public List<String> getAllPendingEnabledFeatures() {
+ assertPermission();
+ synchronized (mLock) {
+ return new ArrayList<>(mPendingEnabledFeatures);
+ }
+ }
+
+ /** Returns currently enabled experimental features */
+ public @NonNull List<String> getEnabledExperimentalFeatures() {
+ if (Build.IS_USER) {
+ Log.e(TAG, "getEnabledExperimentalFeatures called in USER build",
+ new RuntimeException());
+ return Collections.emptyList();
+ }
+ ArrayList<String> experimentalFeature = new ArrayList<>();
+ for (String feature: mEnabledFeatures) {
+ if (MANDATORY_FEATURES.contains(feature)) {
+ continue;
+ }
+ if (OPTIONAL_FEATURES.contains(feature)) {
+ continue;
+ }
+ experimentalFeature.add(feature);
+ }
+ return experimentalFeature;
+ }
+
+ void handleCorruptConfigFileLocked(String msg, String line) {
+ Log.e(TAG, msg + ", considered as corrupt, line:" + line);
+ mEnabledFeatures.clear();
+ }
+
+ private boolean loadFromConfigFileLocked() {
+ // done without lock, should be only called from constructor.
+ FileInputStream fis;
+ try {
+ fis = mFeatureConfigFile.openRead();
+ } catch (FileNotFoundException e) {
+ Log.i(TAG, "Feature config file not found, this could be 1st boot");
+ return false;
+ }
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(fis, StandardCharsets.UTF_8))) {
+ boolean lastLinePassed = false;
+ while (true) {
+ String line = reader.readLine();
+ if (line == null) {
+ if (!lastLinePassed) {
+ handleCorruptConfigFileLocked("No last line checksum", "");
+ return false;
+ }
+ break;
+ }
+ if (lastLinePassed && !line.isEmpty()) {
+ handleCorruptConfigFileLocked(
+ "Config file has additional line after last line marker", line);
+ return false;
+ } else {
+ if (line.startsWith(CONFIG_FILE_LAST_LINE_MARKER)) {
+ int numberOfFeatures;
+ try {
+ numberOfFeatures = Integer.valueOf(line.substring(
+ CONFIG_FILE_LAST_LINE_MARKER.length()));
+ } catch (NumberFormatException e) {
+ handleCorruptConfigFileLocked(
+ "Config file has corrupt last line, not a number",
+ line);
+ return false;
+ }
+ int actualNumberOfFeatures = mEnabledFeatures.size();
+ if (numberOfFeatures != actualNumberOfFeatures) {
+ handleCorruptConfigFileLocked(
+ "Config file has wrong number of features, expected:"
+ + numberOfFeatures
+ + " actual:" + actualNumberOfFeatures, line);
+ return false;
+ }
+ lastLinePassed = true;
+ } else {
+ mEnabledFeatures.add(line);
+ }
+ }
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Cannot load config file", e);
+ return false;
+ }
+ Log.i(TAG, "Loaded features:" + mEnabledFeatures);
+ return true;
+ }
+
+ private void persistToFeatureConfigFile() {
+ HashSet<String> features = new HashSet<>(mEnabledFeatures);
+ synchronized (mLock) {
+ features.removeAll(mPendingDisabledFeatures);
+ features.addAll(mPendingEnabledFeatures);
+ FileOutputStream fos;
+ try {
+ fos = mFeatureConfigFile.startWrite();
+ } catch (IOException e) {
+ Log.e(TAG, "Cannot create config file", e);
+ return;
+ }
+ try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos,
+ StandardCharsets.UTF_8))) {
+ Log.i(TAG, "Updating features:" + features);
+ for (String feature : features) {
+ writer.write(feature);
+ writer.newLine();
+ }
+ writer.write(CONFIG_FILE_LAST_LINE_MARKER + features.size());
+ writer.flush();
+ mFeatureConfigFile.finishWrite(fos);
+ } catch (IOException e) {
+ mFeatureConfigFile.failWrite(fos);
+ Log.e(TAG, "Cannot create config file", e);
+ }
+ }
+ }
+
+ private void assertPermission() {
+ ICarImpl.assertPermission(mContext, Car.PERMISSION_CONTROL_CAR_FEATURES);
+ }
+
+ private void dispatchDefaultConfigUpdate() {
+ mHandler.removeCallbacks(mDefaultConfigWriter);
+ mHandler.post(mDefaultConfigWriter);
+ }
+
+ private void parseDefaultConfig() {
+ for (String feature : mDefaultEnabledFeaturesFromConfig) {
+ if (!OPTIONAL_FEATURES.contains(feature)) {
+ throw new IllegalArgumentException(
+ "config_default_enabled_optional_car_features include non-optional "
+ + "features:" + feature);
+ }
+ if (mDisabledFeaturesFromVhal.contains(feature)) {
+ continue;
+ }
+ mEnabledFeatures.add(feature);
+ }
+ Log.i(TAG, "Loaded default features:" + mEnabledFeatures);
+ }
+}
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index b77e50f7e1..f64eed9aad 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -22,6 +22,7 @@ import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.UiModeManager;
import android.car.Car;
+import android.car.CarFeatures;
import android.car.ICar;
import android.car.cluster.renderer.IInstrumentClusterNavigation;
import android.car.userlib.CarUserManagerHelper;
@@ -51,7 +52,6 @@ import com.android.car.audio.CarAudioService;
import com.android.car.cluster.InstrumentClusterService;
import com.android.car.garagemode.GarageModeService;
import com.android.car.hal.VehicleHal;
-import com.android.car.internal.FeatureConfiguration;
import com.android.car.pm.CarPackageManagerService;
import com.android.car.stats.CarStatsService;
import com.android.car.systeminterface.SystemInterface;
@@ -80,6 +80,8 @@ public class ICarImpl extends ICar.Stub {
private final Context mContext;
private final VehicleHal mHal;
+ private final CarFeatureController mFeatureController;
+
private final SystemInterface mSystemInterface;
private final SystemActivityMonitoringService mSystemActivityMonitoringService;
@@ -115,6 +117,7 @@ public class ICarImpl extends ICar.Stub {
private final VmsPublisherService mVmsPublisherService;
private final CarBugreportManagerService mCarBugreportManagerService;
private final CarStatsService mCarStatsService;
+ private final CarExperimentalFeatureServiceController mCarExperimentalFeatureServiceController;
private final CarServiceBase[] mAllServices;
@@ -137,12 +140,20 @@ public class ICarImpl extends ICar.Stub {
mContext = serviceContext;
mSystemInterface = systemInterface;
mHal = new VehicleHal(serviceContext, vehicle);
+ Resources res = mContext.getResources();
+ String[] defaultEnabledFeatures = res.getStringArray(
+ R.array.config_allowed_optional_car_features);
+ // Do this before any other service components to allow feature check. It should work
+ // even without init.
+ // TODO (b/144504820) Add vhal plumbing
+ mFeatureController = new CarFeatureController(serviceContext, defaultEnabledFeatures,
+ /* disabledFeaturesFromVhal= */ new String[0], mSystemInterface.getSystemCarDir());
+ CarLocalServices.addService(CarFeatureController.class, mFeatureController);
mVehicleInterfaceName = vehicleInterfaceName;
mUserManagerHelper = new CarUserManagerHelper(serviceContext);
UserManager userManager =
(UserManager) serviceContext.getSystemService(Context.USER_SERVICE);
- final Resources res = mContext.getResources();
- final int maxRunningUsers = res.getInteger(
+ int maxRunningUsers = res.getInteger(
com.android.internal.R.integer.config_multiuserMaxRunningUsers);
mCarUserService = new CarUserService(serviceContext, mUserManagerHelper, userManager,
ActivityManager.getService(), maxRunningUsers);
@@ -150,7 +161,11 @@ public class ICarImpl extends ICar.Stub {
mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
mCarPowerManagementService = new CarPowerManagementService(mContext, mHal.getPowerHal(),
systemInterface, mUserManagerHelper);
- mCarUserNoticeService = new CarUserNoticeService(serviceContext);
+ if (mFeatureController.isFeatureEnabled(CarFeatures.FEATURE_CAR_USER_NOTICE_SERVICE)) {
+ mCarUserNoticeService = new CarUserNoticeService(serviceContext);
+ } else {
+ mCarUserNoticeService = null;
+ }
mCarPropertyService = new CarPropertyService(serviceContext, mHal.getPropertyHal());
mCarDrivingStateService = new CarDrivingStateService(serviceContext, mCarPropertyService);
mCarUXRestrictionsService = new CarUxRestrictionsManagerService(serviceContext,
@@ -183,14 +198,24 @@ public class ICarImpl extends ICar.Stub {
mVmsPublisherService = new VmsPublisherService(
serviceContext, mCarStatsService, mVmsBrokerService, mVmsClientManager);
mCarDiagnosticService = new CarDiagnosticService(serviceContext, mHal.getDiagnosticHal());
- mCarStorageMonitoringService = new CarStorageMonitoringService(serviceContext,
- systemInterface);
+ if (mFeatureController.isFeatureEnabled(Car.STORAGE_MONITORING_SERVICE)) {
+ mCarStorageMonitoringService = new CarStorageMonitoringService(serviceContext,
+ systemInterface);
+ } else {
+ mCarStorageMonitoringService = null;
+ }
mCarConfigurationService =
new CarConfigurationService(serviceContext, new JsonReaderImpl());
mCarLocationService = new CarLocationService(serviceContext);
mCarTrustedDeviceService = new CarTrustedDeviceService(serviceContext);
mCarMediaService = new CarMediaService(serviceContext);
mCarBugreportManagerService = new CarBugreportManagerService(serviceContext);
+ if (!Build.IS_USER) {
+ mCarExperimentalFeatureServiceController = new CarExperimentalFeatureServiceController(
+ serviceContext);
+ } else {
+ mCarExperimentalFeatureServiceController = null;
+ }
CarLocalServices.addService(CarPowerManagementService.class, mCarPowerManagementService);
CarLocalServices.addService(CarPropertyService.class, mCarPropertyService);
@@ -203,6 +228,7 @@ public class ICarImpl extends ICar.Stub {
// Be careful with order. Service depending on other service should be inited later.
List<CarServiceBase> allServices = new ArrayList<>();
+ allServices.add(mFeatureController);
allServices.add(mCarUserService);
allServices.add(mSystemActivityMonitoringService);
allServices.add(mCarPowerManagementService);
@@ -213,7 +239,7 @@ public class ICarImpl extends ICar.Stub {
allServices.add(mCarPackageManagerService);
allServices.add(mCarInputService);
allServices.add(mGarageModeService);
- allServices.add(mCarUserNoticeService);
+ addServiceIfNonNull(allServices, mCarUserNoticeService);
allServices.add(mAppFocusService);
allServices.add(mCarAudioService);
allServices.add(mCarNightService);
@@ -224,7 +250,7 @@ public class ICarImpl extends ICar.Stub {
allServices.add(mCarBluetoothService);
allServices.add(mCarProjectionService);
allServices.add(mCarDiagnosticService);
- allServices.add(mCarStorageMonitoringService);
+ addServiceIfNonNull(allServices, mCarStorageMonitoringService);
allServices.add(mCarConfigurationService);
allServices.add(mVmsClientManager);
allServices.add(mVmsSubscriberService);
@@ -233,9 +259,17 @@ public class ICarImpl extends ICar.Stub {
allServices.add(mCarMediaService);
allServices.add(mCarLocationService);
allServices.add(mCarBugreportManagerService);
+ // Always put mCarExperimentalFeatureServiceController in last.
+ addServiceIfNonNull(allServices, mCarExperimentalFeatureServiceController);
mAllServices = allServices.toArray(new CarServiceBase[allServices.size()]);
}
+ private void addServiceIfNonNull(List<CarServiceBase> services, CarServiceBase service) {
+ if (service != null) {
+ services.add(service);
+ }
+ }
+
@MainThread
void init() {
mBootTiming = new TimingsTraceLog(VHAL_TIMING_TAG, Trace.TRACE_TAG_HAL);
@@ -288,6 +322,49 @@ public class ICarImpl extends ICar.Stub {
mCarUserService.onSwitchUser(userId);
}
+ @Override
+ public boolean isFeatureEnabled(String featureName) {
+ return mFeatureController.isFeatureEnabled(featureName);
+ }
+
+ @Override
+ public int enableFeature(String featureName) {
+ // permission check inside the controller
+ return mFeatureController.enableFeature(featureName);
+ }
+
+ @Override
+ public int disableFeature(String featureName) {
+ // permission check inside the controller
+ return mFeatureController.disableFeature(featureName);
+ }
+
+ @Override
+ public List<String> getAllEnabledFeatures() {
+ // permission check inside the controller
+ return mFeatureController.getAllEnabledFeatures();
+ }
+
+ @Override
+ public List<String> getAllPendingDisabledFeatures() {
+ // permission check inside the controller
+ return mFeatureController.getAllPendingDisabledFeatures();
+ }
+
+ @Override
+ public List<String> getAllPendingEnabledFeatures() {
+ // permission check inside the controller
+ return mFeatureController.getAllPendingEnabledFeatures();
+ }
+
+ @Override
+ public String getCarManagerClassForFeature(String featureName) {
+ if (mCarExperimentalFeatureServiceController == null) {
+ return null;
+ }
+ return mCarExperimentalFeatureServiceController.getCarManagerClassForFeature(featureName);
+ }
+
static void assertCallingFromSystemProcess() {
int uid = Binder.getCallingUid();
if (uid != Process.SYSTEM_UID) {
@@ -310,6 +387,10 @@ public class ICarImpl extends ICar.Stub {
@Override
public IBinder getCarService(String serviceName) {
+ if (!mFeatureController.isFeatureEnabled(serviceName)) {
+ Log.w(CarLog.TAG_SERVICE, "getCarService for disabled service:" + serviceName);
+ return null;
+ }
switch (serviceName) {
case Car.AUDIO_SERVICE:
return mCarAudioService;
@@ -376,8 +457,15 @@ public class ICarImpl extends ICar.Stub {
case Car.CAR_USER_SERVICE:
return mCarUserService;
default:
- Log.w(CarLog.TAG_SERVICE, "getCarService for unknown service:" + serviceName);
- return null;
+ IBinder service = null;
+ if (mCarExperimentalFeatureServiceController != null) {
+ service = mCarExperimentalFeatureServiceController.getCarService(serviceName);
+ }
+ if (service == null) {
+ Log.w(CarLog.TAG_SERVICE, "getCarService for unknown service:"
+ + serviceName);
+ }
+ return service;
}
}
@@ -494,7 +582,6 @@ public class ICarImpl extends ICar.Stub {
if (args == null || args.length == 0 || (args.length > 0 && "-a".equals(args[0]))) {
writer.println("*Dump car service*");
- writer.println("*FutureConfig, DEFAULT:" + FeatureConfiguration.DEFAULT);
writer.println("*Dump all services*");
dumpAllServices(writer);
@@ -618,6 +705,8 @@ public class ICarImpl extends ICar.Stub {
private static final String COMMAND_SET_UID_TO_ZONE = "set-zoneid-for-uid";
private static final String COMMAND_START_FIXED_ACTIVITY_MODE = "start-fixed-activity-mode";
private static final String COMMAND_STOP_FIXED_ACTIVITY_MODE = "stop-fixed-activity-mode";
+ private static final String COMMAND_ENABLE_FEATURE = "enable-feature";
+ private static final String COMMAND_DISABLE_FEATURE = "disable-feature";
private static final String PARAM_DAY_MODE = "day";
private static final String PARAM_NIGHT_MODE = "night";
@@ -697,6 +786,12 @@ public class ICarImpl extends ICar.Stub {
pw.println("\tstop-fixed-mode displayId");
pw.println("\t Stop fixed Activity mode for the given display. "
+ "The Activity will not be restarted upon crash.");
+ pw.println("\tenable-feature featureName");
+ pw.println("\t Enable the requested feature. Change will happen after reboot.");
+ pw.println("\t This requires root/su.");
+ pw.println("\tdisable-feature featureName");
+ pw.println("\t Disable the requested feature. Change will happen after reboot");
+ pw.println("\t This requires root/su.");
}
private int dumpInvalidArguments(PrintWriter pw) {
@@ -840,6 +935,18 @@ public class ICarImpl extends ICar.Stub {
case COMMAND_STOP_FIXED_ACTIVITY_MODE:
handleStopFixedMode(args, writer);
break;
+ case COMMAND_ENABLE_FEATURE:
+ if (args.length != 2) {
+ return dumpInvalidArguments(writer);
+ }
+ handleEnableDisableFeature(args, writer, /* enable= */ true);
+ break;
+ case COMMAND_DISABLE_FEATURE:
+ if (args.length != 2) {
+ return dumpInvalidArguments(writer);
+ }
+ handleEnableDisableFeature(args, writer, /* enable= */ false);
+ break;
default:
writer.println("Unknown command: \"" + arg + "\"");
dumpHelp(writer);
@@ -892,6 +999,48 @@ public class ICarImpl extends ICar.Stub {
mFixedActivityService.stopFixedActivityMode(displayId);
}
+ private void handleEnableDisableFeature(String[] args, PrintWriter writer, boolean enable) {
+ if (Binder.getCallingUid() != Process.ROOT_UID) {
+ writer.println("Only allowed to root/su");
+ return;
+ }
+ String featureName = args[1];
+ long id = Binder.clearCallingIdentity();
+ // no permission check here
+ int r;
+ if (enable) {
+ r = mFeatureController.enableFeature(featureName);
+ } else {
+ r = mFeatureController.disableFeature(featureName);
+ }
+ switch (r) {
+ case Car.FEATURE_REQUEST_SUCCESS:
+ if (enable) {
+ writer.println("Enabled feature:" + featureName);
+ } else {
+ writer.println("Disabled feature:" + featureName);
+ }
+ break;
+ case Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE:
+ if (enable) {
+ writer.println("Already enabled:" + featureName);
+ } else {
+ writer.println("Already disabled:" + featureName);
+ }
+ break;
+ case Car.FEATURE_REQUEST_MANDATORY:
+ writer.println("Cannot change mandatory feature:" + featureName);
+ break;
+ case Car.FEATURE_REQUEST_NOT_EXISTING:
+ writer.println("Non-existing feature:" + featureName);
+ break;
+ default:
+ writer.println("Unknown error:" + r);
+ break;
+ }
+ Binder.restoreCallingIdentity(id);
+ }
+
private void forceDayNightMode(String arg, PrintWriter writer) {
int mode;
switch (arg) {
diff --git a/tests/EmbeddedKitchenSinkApp/Android.mk b/tests/EmbeddedKitchenSinkApp/Android.mk
index 923252ddfc..b3cff4dd45 100644
--- a/tests/EmbeddedKitchenSinkApp/Android.mk
+++ b/tests/EmbeddedKitchenSinkApp/Android.mk
@@ -52,7 +52,8 @@ LOCAL_STATIC_JAVA_LIBRARIES += \
vehicle-hal-support-lib-for-test \
com.android.car.keventreader-client \
guava \
- android.car.cluster.navigation
+ android.car.cluster.navigation \
+ car-experimental-api-static-lib
LOCAL_JAVA_LIBRARIES += android.car
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index fc16ba4cd9..a30b9b45b7 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -96,6 +96,9 @@
<uses-permission android:name="android.car.permission.GET_CAR_VENDOR_CATEGORY_SEAT" />
<uses-permission android:name="android.car.permission.GET_CAR_VENDOR_CATEGORY_INFO" />
<uses-permission android:name="android.car.permission.SET_CAR_VENDOR_CATEGORY_INFO" />
+
+ <uses-permission android:name="android.car.permission.CONTROL_CAR_FEATURES"/>
+
<application android:label="@string/app_title"
android:icon="@drawable/ic_launcher">
<uses-library android:name="android.test.runner"/>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/experimental_feature_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/experimental_feature_test.xml
new file mode 100644
index 0000000000..6119157eec
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/experimental_feature_test.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/button_experimental_ping"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/experimental_ping" />
+ <TextView
+ android:id="@+id/experimental_ping_msg"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/empty"
+ android:layout_weight="1" />
+ </LinearLayout>
+ </LinearLayout>
+ </ScrollView>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 0d563694dc..dce047f503 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -337,4 +337,7 @@
<string name="always_crashing_activity" translatable="false">Always Crash Activity</string>
<string name="no_crash_activity" translatable="false">No Crash Activity</string>
<string name="empty_activity" translatable="false">Empty Activity</string>
+
+ <!-- ExperimentalFeatureTest -->
+ <string name="experimental_ping" translatable="false">Ping Service</string>
</resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index 898290dbbf..cb101042c2 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -53,6 +53,7 @@ import com.google.android.car.kitchensink.connectivity.ConnectivityFragment;
import com.google.android.car.kitchensink.cube.CubesTestFragment;
import com.google.android.car.kitchensink.diagnostic.DiagnosticTestFragment;
import com.google.android.car.kitchensink.displayinfo.DisplayInfoFragment;
+import com.google.android.car.kitchensink.experimental.ExperimentalFeatureTestFragment;
import com.google.android.car.kitchensink.hvac.HvacTestFragment;
import com.google.android.car.kitchensink.notification.NotificationFragment;
import com.google.android.car.kitchensink.orientation.OrientationTestFragment;
@@ -170,6 +171,7 @@ public class KitchenSinkActivity extends FragmentActivity {
new FragmentMenuEntry("cubes test", CubesTestFragment.class),
new FragmentMenuEntry("diagnostic", DiagnosticTestFragment.class),
new FragmentMenuEntry("display info", DisplayInfoFragment.class),
+ new FragmentMenuEntry("experimental feature", ExperimentalFeatureTestFragment.class),
new FragmentMenuEntry("hvac", HvacTestFragment.class),
new FragmentMenuEntry("inst cluster", InstrumentClusterFragment.class),
// TODO (b/141774865) Enable after b/141635607 is fixed
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/experimental/ExperimentalFeatureTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/experimental/ExperimentalFeatureTestFragment.java
new file mode 100644
index 0000000000..9136a9d820
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/experimental/ExperimentalFeatureTestFragment.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 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 com.google.android.car.kitchensink.experimental;
+
+import android.car.Car;
+import android.car.experimental.CarTestDemoExperimentalFeatureManager;
+import android.car.experimental.ExperimentalCar;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+
+import com.google.android.car.kitchensink.KitchenSinkActivity;
+import com.google.android.car.kitchensink.R;
+
+public class ExperimentalFeatureTestFragment extends Fragment {
+
+ private static final String TAG = "ExperimentalFeature";
+
+ private static final String[] PING_MSGS = {
+ "Hello, world",
+ "This is 1st experimental feature",
+ };
+
+ private int mCurrentMsgIndex = 0;
+ private TextView mPingMsgTextView;
+ private Button mPingButton;
+
+ private CarTestDemoExperimentalFeatureManager mDemoManager;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
+ View view = inflater.inflate(R.layout.experimental_feature_test, container, false);
+ mPingMsgTextView = view.findViewById(R.id.experimental_ping_msg);
+ mPingButton = view.findViewById(R.id.button_experimental_ping);
+ Car car = ((KitchenSinkActivity) getHost()).getCar();
+ if (car.isFeatureEnabled(ExperimentalCar.TEST_EXPERIMENTAL_FEATURE_SERVICE)) {
+ mDemoManager = (CarTestDemoExperimentalFeatureManager) car.getCarManager(
+ ExperimentalCar.TEST_EXPERIMENTAL_FEATURE_SERVICE);
+ mPingMsgTextView.setText("feature enabled");
+ } else {
+ Log.w(TAG, "ExperimentalCar.TEST_EXPERIMENTAL_FEATURE_SERVICE not enabled");
+ mPingButton.setActivated(false);
+ mPingMsgTextView.setText("feature disabled");
+ }
+ view.findViewById(R.id.button_experimental_ping).setOnClickListener(
+ (View v) -> {
+ if (mDemoManager == null) {
+ return;
+ }
+ String msg = pickMsg();
+ mPingMsgTextView.setText(mDemoManager.ping(msg));
+ });
+ return view;
+ }
+
+ private String pickMsg() {
+ String msg = PING_MSGS[mCurrentMsgIndex];
+ mCurrentMsgIndex++;
+ if (mCurrentMsgIndex >= PING_MSGS.length) {
+ mCurrentMsgIndex = 0;
+ }
+ return msg;
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java
index 481820bbd4..b76cd249dd 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java
@@ -213,13 +213,19 @@ public class StorageLifetimeFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
+ if (!mActivity.getCar().isFeatureEnabled(Car.STORAGE_MONITORING_SERVICE)) {
+ Log.w(TAG, "STORAGE_MONITORING_SERVICE not supported");
+ return;
+ }
reloadInfo();
registerListener();
}
@Override
public void onPause() {
- unregisterListener();
+ if (mActivity.getCar().isFeatureEnabled(Car.STORAGE_MONITORING_SERVICE)) {
+ unregisterListener();
+ }
super.onPause();
}
}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarFeatureTest.java b/tests/android_car_api_test/src/android/car/apitest/CarFeatureTest.java
new file mode 100644
index 0000000000..73c300bc7d
--- /dev/null
+++ b/tests/android_car_api_test/src/android/car/apitest/CarFeatureTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.apitest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.car.Car;
+import android.car.CarFeatures;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CarFeatureTest extends CarApiTestBase {
+ // List in CarFeatureController should be inline with this.
+ private static final List<String> MANDATORY_FEATURES = Arrays.asList(
+ Car.APP_FOCUS_SERVICE,
+ Car.AUDIO_SERVICE,
+ Car.BLUETOOTH_SERVICE,
+ Car.CAR_BUGREPORT_SERVICE,
+ Car.CAR_DRIVING_STATE_SERVICE,
+ Car.CAR_MEDIA_SERVICE,
+ Car.CAR_NAVIGATION_SERVICE,
+ Car.CAR_OCCUPANT_ZONE_SERVICE,
+ Car.CAR_USER_SERVICE,
+ Car.INFO_SERVICE,
+ Car.PACKAGE_SERVICE,
+ Car.POWER_SERVICE,
+ Car.PROJECTION_SERVICE,
+ Car.PROPERTY_SERVICE,
+ Car.TEST_SERVICE,
+ // Deprecated, but still should be supported
+ Car.SENSOR_SERVICE,
+ Car.CAR_INSTRUMENT_CLUSTER_SERVICE,
+ Car.CABIN_SERVICE,
+ Car.HVAC_SERVICE,
+ Car.VENDOR_EXTENSION_SERVICE,
+ // Candidate for Optional, but stay mandatory for now until final decision is made.
+ Car.CAR_CONFIGURATION_SERVICE,
+ Car.CAR_TRUST_AGENT_ENROLLMENT_SERVICE,
+ Car.DIAGNOSTIC_SERVICE,
+ Car.CAR_UX_RESTRICTION_SERVICE,
+ // Marked as optional, but requires additional work
+ Car.VMS_SUBSCRIBER_SERVICE
+ );
+
+ private static final List<String> OPTIONAL_FEATURES = Arrays.asList(
+ Car.STORAGE_MONITORING_SERVICE,
+ CarFeatures.FEATURE_CAR_USER_NOTICE_SERVICE
+ );
+
+ private static final String NON_EXISTING_FEATURE = "ThisFeatureDoesNotExist";
+
+ @Test
+ public void checkMandatoryFeatures() {
+ Car car = getCar();
+ assertThat(car).isNotNull();
+ for (String feature : MANDATORY_FEATURES) {
+ assertThat(car.isFeatureEnabled(feature)).isTrue();
+ }
+ }
+
+ @Test
+ public void toggleOptionalFeature() {
+ Car car = getCar();
+ assertThat(car).isNotNull();
+ for (String feature : OPTIONAL_FEATURES) {
+ boolean enabled = getCar().isFeatureEnabled(feature);
+ toggleOptionalFeature(feature, !enabled, enabled);
+ toggleOptionalFeature(feature, enabled, enabled);
+ }
+ }
+
+ @Test
+ public void testGetAllEnabledFeatures() {
+ Car car = getCar();
+ assertThat(car).isNotNull();
+ List<String> allEnabledFeatures = car.getAllEnabledFeatures();
+ assertThat(allEnabledFeatures).isNotEmpty();
+ for (String feature : MANDATORY_FEATURES) {
+ assertThat(allEnabledFeatures).contains(feature);
+ }
+ }
+
+ @Test
+ public void testEnableDisableForMandatoryFeatures() {
+ for (String feature : MANDATORY_FEATURES) {
+ assertThat(getCar().enableFeature(feature)).isEqualTo(Car.FEATURE_REQUEST_MANDATORY);
+ assertThat(getCar().disableFeature(feature)).isEqualTo(Car.FEATURE_REQUEST_MANDATORY);
+ }
+ }
+
+ @Test
+ public void testEnableDisableForNonExistingFeature() {
+ assertThat(getCar().enableFeature(NON_EXISTING_FEATURE)).isEqualTo(
+ Car.FEATURE_REQUEST_NOT_EXISTING);
+ assertThat(getCar().disableFeature(NON_EXISTING_FEATURE)).isEqualTo(
+ Car.FEATURE_REQUEST_NOT_EXISTING);
+ }
+
+ private void toggleOptionalFeature(String feature, boolean enable, boolean originallyEnabled) {
+ if (enable) {
+ if (originallyEnabled) {
+ assertThat(getCar().enableFeature(feature)).isEqualTo(
+ Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE);
+ } else {
+ assertThat(getCar().enableFeature(feature)).isEqualTo(Car.FEATURE_REQUEST_SUCCESS);
+ assertThat(getCar().getAllPendingEnabledFeatures()).contains(feature);
+ }
+ assertThat(getCar().getAllPendingDisabledFeatures()).doesNotContain(feature);
+ } else {
+ if (originallyEnabled) {
+ assertThat(getCar().disableFeature(feature)).isEqualTo(Car.FEATURE_REQUEST_SUCCESS);
+ assertThat(getCar().getAllPendingDisabledFeatures()).contains(feature);
+ } else {
+ assertThat(getCar().disableFeature(feature)).isEqualTo(
+ Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE);
+ }
+ assertThat(getCar().getAllPendingEnabledFeatures()).doesNotContain(feature);
+ }
+ }
+}
diff --git a/tests/carservice_unit_test/src/android/car/CarTest.java b/tests/carservice_unit_test/src/android/car/CarTest.java
index 342ff332ec..490dd124f2 100644
--- a/tests/carservice_unit_test/src/android/car/CarTest.java
+++ b/tests/carservice_unit_test/src/android/car/CarTest.java
@@ -48,6 +48,8 @@ import org.mockito.MockitoSession;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.quality.Strictness;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -79,6 +81,41 @@ public class CarTest {
}
@Override
+ public boolean isFeatureEnabled(String featureName) {
+ return false;
+ }
+
+ @Override
+ public int enableFeature(String featureName) {
+ return Car.FEATURE_REQUEST_SUCCESS;
+ }
+
+ @Override
+ public int disableFeature(String featureName) {
+ return Car.FEATURE_REQUEST_SUCCESS;
+ }
+
+ @Override
+ public List<String> getAllEnabledFeatures() {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public List<String> getAllPendingDisabledFeatures() {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public List<String> getAllPendingEnabledFeatures() {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public String getCarManagerClassForFeature(String featureName) {
+ return null;
+ }
+
+ @Override
public android.os.IBinder getCarService(java.lang.String serviceName) {
return null;
}