diff options
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; } |