1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car; 18 19 import android.annotation.NonNull; 20 import android.car.Car; 21 import android.car.Car.FeaturerRequestEnum; 22 import android.car.CarFeatures; 23 import android.content.Context; 24 import android.os.Build; 25 import android.os.Handler; 26 import android.os.HandlerThread; 27 import android.util.AtomicFile; 28 import android.util.IndentingPrintWriter; 29 import android.util.Pair; 30 import android.util.Slog; 31 32 import com.android.internal.annotations.GuardedBy; 33 import com.android.internal.annotations.VisibleForTesting; 34 35 import java.io.BufferedReader; 36 import java.io.BufferedWriter; 37 import java.io.File; 38 import java.io.FileInputStream; 39 import java.io.FileNotFoundException; 40 import java.io.FileOutputStream; 41 import java.io.IOException; 42 import java.io.InputStreamReader; 43 import java.io.OutputStreamWriter; 44 import java.nio.charset.StandardCharsets; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.Collection; 48 import java.util.Collections; 49 import java.util.HashSet; 50 import java.util.List; 51 52 /** 53 * Component controlling the feature of car. 54 */ 55 public final class CarFeatureController implements CarServiceBase { 56 57 private static final String TAG = CarLog.tagFor(CarFeatureController.class); 58 59 // Use HaseSet for better search performance. Memory consumption is fixed and it not an issue. 60 // Should keep alphabetical order under each bucket. 61 // Update CarFeatureTest as well when this is updated. 62 private static final HashSet<String> MANDATORY_FEATURES = new HashSet<>(Arrays.asList( 63 Car.APP_FOCUS_SERVICE, 64 Car.AUDIO_SERVICE, 65 Car.BLUETOOTH_SERVICE, 66 Car.CAR_ACTIVITY_SERVICE, 67 Car.CAR_BUGREPORT_SERVICE, 68 Car.CAR_DEVICE_POLICY_SERVICE, 69 Car.CAR_DRIVING_STATE_SERVICE, 70 Car.CAR_INPUT_SERVICE, 71 Car.CAR_MEDIA_SERVICE, 72 Car.CAR_OCCUPANT_ZONE_SERVICE, 73 Car.CAR_USER_SERVICE, 74 Car.CAR_UX_RESTRICTION_SERVICE, 75 Car.CAR_WATCHDOG_SERVICE, 76 Car.INFO_SERVICE, 77 Car.PACKAGE_SERVICE, 78 Car.POWER_SERVICE, 79 Car.PROJECTION_SERVICE, 80 Car.PROPERTY_SERVICE, 81 Car.TEST_SERVICE, 82 // All items below here are deprecated, but still should be supported 83 Car.CABIN_SERVICE, 84 Car.HVAC_SERVICE, 85 Car.SENSOR_SERVICE, 86 Car.VENDOR_EXTENSION_SERVICE 87 )); 88 89 private static final HashSet<String> OPTIONAL_FEATURES = new HashSet<>(Arrays.asList( 90 CarFeatures.FEATURE_CAR_USER_NOTICE_SERVICE, 91 Car.CLUSTER_HOME_SERVICE, 92 Car.CAR_NAVIGATION_SERVICE, 93 Car.DIAGNOSTIC_SERVICE, 94 Car.OCCUPANT_AWARENESS_SERVICE, 95 Car.STORAGE_MONITORING_SERVICE, 96 Car.VEHICLE_MAP_SERVICE, 97 Car.CAR_TELEMETRY_SERVICE, 98 Car.CAR_EVS_SERVICE, 99 // All items below here are deprecated, but still could be supported 100 Car.CAR_INSTRUMENT_CLUSTER_SERVICE 101 )); 102 103 // This is a feature still under development and cannot be enabled in user build. 104 private static final HashSet<String> NON_USER_ONLY_FEATURES = new HashSet<>(Arrays.asList( 105 Car.CAR_TELEMETRY_SERVICE 106 )); 107 108 // Features that depend on another feature being enabled (i.e. legacy API support). 109 // For example, VMS_SUBSCRIBER_SERVICE will be enabled if VEHICLE_MAP_SERVICE is enabled 110 // and disabled if VEHICLE_MAP_SERVICE is disabled. 111 private static final List<Pair<String, String>> SUPPORT_FEATURES = Arrays.asList( 112 Pair.create(Car.VEHICLE_MAP_SERVICE, Car.VMS_SUBSCRIBER_SERVICE) 113 ); 114 115 private static final String FEATURE_CONFIG_FILE_NAME = "car_feature_config.txt"; 116 117 // Last line starts with this with number of features for extra confidence check. 118 private static final String CONFIG_FILE_LAST_LINE_MARKER = ",,"; 119 120 // Set once in constructor and not updated. Access it without lock so that it can be accessed 121 // quickly. 122 private final HashSet<String> mEnabledFeatures; 123 124 private final Context mContext; 125 126 private final List<String> mDefaultEnabledFeaturesFromConfig; 127 private final List<String> mDisabledFeaturesFromVhal; 128 129 private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread( 130 getClass().getSimpleName()); 131 private final Handler mHandler = new Handler(mHandlerThread.getLooper()); 132 private final Object mLock = new Object(); 133 134 @GuardedBy("mLock") 135 private final AtomicFile mFeatureConfigFile; 136 137 @GuardedBy("mLock") 138 private final List<String> mPendingEnabledFeatures = new ArrayList<>(); 139 140 @GuardedBy("mLock") 141 private final List<String> mPendingDisabledFeatures = new ArrayList<>(); 142 143 @GuardedBy("mLock") 144 private HashSet<String> mAvailableExperimentalFeatures = new HashSet<>(); 145 CarFeatureController(@onNull Context context, @NonNull String[] defaultEnabledFeaturesFromConfig, @NonNull String[] disabledFeaturesFromVhal, @NonNull File dataDir)146 public CarFeatureController(@NonNull Context context, 147 @NonNull String[] defaultEnabledFeaturesFromConfig, 148 @NonNull String[] disabledFeaturesFromVhal, @NonNull File dataDir) { 149 if (!Build.IS_USER) { 150 OPTIONAL_FEATURES.addAll(NON_USER_ONLY_FEATURES); 151 } 152 mContext = context; 153 mDefaultEnabledFeaturesFromConfig = Arrays.asList(defaultEnabledFeaturesFromConfig); 154 mDisabledFeaturesFromVhal = Arrays.asList(disabledFeaturesFromVhal); 155 Slog.i(TAG, "mDefaultEnabledFeaturesFromConfig:" + mDefaultEnabledFeaturesFromConfig 156 + ",mDisabledFeaturesFromVhal:" + mDisabledFeaturesFromVhal); 157 mEnabledFeatures = new HashSet<>(MANDATORY_FEATURES); 158 mFeatureConfigFile = new AtomicFile(new File(dataDir, FEATURE_CONFIG_FILE_NAME), TAG); 159 boolean shouldLoadDefaultConfig = !mFeatureConfigFile.exists(); 160 if (!shouldLoadDefaultConfig) { 161 if (!loadFromConfigFileLocked()) { 162 shouldLoadDefaultConfig = true; 163 } 164 } 165 if (!checkMandatoryFeaturesLocked()) { // mandatory feature missing, force default config 166 mEnabledFeatures.clear(); 167 mEnabledFeatures.addAll(MANDATORY_FEATURES); 168 shouldLoadDefaultConfig = true; 169 } 170 // Separate if to use this as backup for failure in loadFromConfigFileLocked() 171 if (shouldLoadDefaultConfig) { 172 parseDefaultConfig(); 173 dispatchDefaultConfigUpdate(); 174 } 175 addSupportFeatures(mEnabledFeatures); 176 } 177 178 @VisibleForTesting getDisabledFeaturesFromVhal()179 List<String> getDisabledFeaturesFromVhal() { 180 return mDisabledFeaturesFromVhal; 181 } 182 183 @Override init()184 public void init() { 185 // nothing should be done here. This should work with only constructor. 186 } 187 188 @Override release()189 public void release() { 190 // nothing should be done here. 191 } 192 193 @Override dump(IndentingPrintWriter writer)194 public void dump(IndentingPrintWriter writer) { 195 writer.println("*CarFeatureController*"); 196 writer.println(" mEnabledFeatures:" + mEnabledFeatures); 197 writer.println(" mDefaultEnabledFeaturesFromConfig:" + mDefaultEnabledFeaturesFromConfig); 198 writer.println(" mDisabledFeaturesFromVhal:" + mDisabledFeaturesFromVhal); 199 synchronized (mLock) { 200 writer.println(" mAvailableExperimentalFeatures:" + mAvailableExperimentalFeatures); 201 writer.println(" mPendingEnabledFeatures:" + mPendingEnabledFeatures); 202 writer.println(" mPendingDisabledFeatures:" + mPendingDisabledFeatures); 203 } 204 } 205 206 /** Check {@link Car#isFeatureEnabled(String)} */ isFeatureEnabled(String featureName)207 public boolean isFeatureEnabled(String featureName) { 208 return mEnabledFeatures.contains(featureName); 209 } 210 checkMandatoryFeaturesLocked()211 private boolean checkMandatoryFeaturesLocked() { 212 // Ensure that mandatory features are always there 213 for (String feature: MANDATORY_FEATURES) { 214 if (!mEnabledFeatures.contains(feature)) { 215 Slog.e(TAG, "Mandatory feature missing in mEnabledFeatures:" + feature); 216 return false; 217 } 218 } 219 return true; 220 } 221 222 @FeaturerRequestEnum checkFeatureExisting(String featureName)223 private int checkFeatureExisting(String featureName) { 224 if (MANDATORY_FEATURES.contains(featureName)) { 225 return Car.FEATURE_REQUEST_MANDATORY; 226 } 227 if (!OPTIONAL_FEATURES.contains(featureName)) { 228 synchronized (mLock) { 229 if (!mAvailableExperimentalFeatures.contains(featureName)) { 230 Slog.e(TAG, "enableFeature requested for non-existing feature:" 231 + featureName); 232 return Car.FEATURE_REQUEST_NOT_EXISTING; 233 } 234 } 235 } 236 return Car.FEATURE_REQUEST_SUCCESS; 237 } 238 239 /** Check {@link Car#enableFeature(String)} */ enableFeature(String featureName)240 public int enableFeature(String featureName) { 241 assertPermission(); 242 int checkResult = checkFeatureExisting(featureName); 243 if (checkResult != Car.FEATURE_REQUEST_SUCCESS) { 244 return checkResult; 245 } 246 247 boolean alreadyEnabled = mEnabledFeatures.contains(featureName); 248 boolean shouldUpdateConfigFile = false; 249 synchronized (mLock) { 250 if (mPendingDisabledFeatures.remove(featureName)) { 251 shouldUpdateConfigFile = true; 252 } 253 if (!mPendingEnabledFeatures.contains(featureName) && !alreadyEnabled) { 254 shouldUpdateConfigFile = true; 255 mPendingEnabledFeatures.add(featureName); 256 } 257 } 258 if (shouldUpdateConfigFile) { 259 Slog.w(TAG, "Enabling feature in config file:" + featureName); 260 dispatchDefaultConfigUpdate(); 261 } 262 if (alreadyEnabled) { 263 return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE; 264 } else { 265 return Car.FEATURE_REQUEST_SUCCESS; 266 } 267 } 268 269 /** Check {@link Car#disableFeature(String)} */ disableFeature(String featureName)270 public int disableFeature(String featureName) { 271 assertPermission(); 272 int checkResult = checkFeatureExisting(featureName); 273 if (checkResult != Car.FEATURE_REQUEST_SUCCESS) { 274 return checkResult; 275 } 276 277 boolean alreadyDisabled = !mEnabledFeatures.contains(featureName); 278 boolean shouldUpdateConfigFile = false; 279 synchronized (mLock) { 280 if (mPendingEnabledFeatures.remove(featureName)) { 281 shouldUpdateConfigFile = true; 282 } 283 if (!mPendingDisabledFeatures.contains(featureName) && !alreadyDisabled) { 284 shouldUpdateConfigFile = true; 285 mPendingDisabledFeatures.add(featureName); 286 } 287 } 288 if (shouldUpdateConfigFile) { 289 Slog.w(TAG, "Disabling feature in config file:" + featureName); 290 dispatchDefaultConfigUpdate(); 291 } 292 if (alreadyDisabled) { 293 return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE; 294 } else { 295 return Car.FEATURE_REQUEST_SUCCESS; 296 } 297 } 298 299 /** 300 * Set available experimental features. Only features set through this call will be allowed to 301 * be enabled for experimental features. Setting this is not allowed for USER build. 302 * 303 * @return True if set is allowed and set. False if experimental feature is not allowed. 304 */ setAvailableExperimentalFeatureList(List<String> experimentalFeatures)305 public boolean setAvailableExperimentalFeatureList(List<String> experimentalFeatures) { 306 assertPermission(); 307 if (Build.IS_USER) { 308 Slog.e(TAG, "Experimental feature list set for USER build", 309 new RuntimeException()); 310 return false; 311 } 312 synchronized (mLock) { 313 mAvailableExperimentalFeatures.clear(); 314 mAvailableExperimentalFeatures.addAll(experimentalFeatures); 315 } 316 return true; 317 } 318 319 /** Check {@link Car#getAllEnabledFeatures()} */ getAllEnabledFeatures()320 public List<String> getAllEnabledFeatures() { 321 assertPermission(); 322 return new ArrayList<>(mEnabledFeatures); 323 } 324 325 /** Check {@link Car#getAllPendingDisabledFeatures()} */ getAllPendingDisabledFeatures()326 public List<String> getAllPendingDisabledFeatures() { 327 assertPermission(); 328 synchronized (mLock) { 329 return new ArrayList<>(mPendingDisabledFeatures); 330 } 331 } 332 333 /** Check {@link Car#getAllPendingEnabledFeatures()} */ getAllPendingEnabledFeatures()334 public List<String> getAllPendingEnabledFeatures() { 335 assertPermission(); 336 synchronized (mLock) { 337 return new ArrayList<>(mPendingEnabledFeatures); 338 } 339 } 340 341 /** Returns currently enabled experimental features */ getEnabledExperimentalFeatures()342 public @NonNull List<String> getEnabledExperimentalFeatures() { 343 if (Build.IS_USER) { 344 Slog.e(TAG, "getEnabledExperimentalFeatures called in USER build", 345 new RuntimeException()); 346 return Collections.emptyList(); 347 } 348 ArrayList<String> experimentalFeature = new ArrayList<>(); 349 for (String feature: mEnabledFeatures) { 350 if (MANDATORY_FEATURES.contains(feature)) { 351 continue; 352 } 353 if (OPTIONAL_FEATURES.contains(feature)) { 354 continue; 355 } 356 experimentalFeature.add(feature); 357 } 358 return experimentalFeature; 359 } 360 handleCorruptConfigFileLocked(String msg, String line)361 void handleCorruptConfigFileLocked(String msg, String line) { 362 Slog.e(TAG, msg + ", considered as corrupt, line:" + line); 363 mEnabledFeatures.clear(); 364 } 365 loadFromConfigFileLocked()366 private boolean loadFromConfigFileLocked() { 367 // done without lock, should be only called from constructor. 368 FileInputStream fis; 369 try { 370 fis = mFeatureConfigFile.openRead(); 371 } catch (FileNotFoundException e) { 372 Slog.i(TAG, "Feature config file not found, this could be 1st boot"); 373 return false; 374 } 375 try (BufferedReader reader = new BufferedReader( 376 new InputStreamReader(fis, StandardCharsets.UTF_8))) { 377 boolean lastLinePassed = false; 378 while (true) { 379 String line = reader.readLine(); 380 if (line == null) { 381 if (!lastLinePassed) { 382 handleCorruptConfigFileLocked("No last line checksum", ""); 383 return false; 384 } 385 break; 386 } 387 if (lastLinePassed && !line.isEmpty()) { 388 handleCorruptConfigFileLocked( 389 "Config file has additional line after last line marker", line); 390 return false; 391 } else { 392 if (line.startsWith(CONFIG_FILE_LAST_LINE_MARKER)) { 393 int numberOfFeatures; 394 try { 395 numberOfFeatures = Integer.parseInt(line.substring( 396 CONFIG_FILE_LAST_LINE_MARKER.length())); 397 } catch (NumberFormatException e) { 398 handleCorruptConfigFileLocked( 399 "Config file has corrupt last line, not a number", 400 line); 401 return false; 402 } 403 int actualNumberOfFeatures = mEnabledFeatures.size(); 404 if (numberOfFeatures != actualNumberOfFeatures) { 405 handleCorruptConfigFileLocked( 406 "Config file has wrong number of features, expected:" 407 + numberOfFeatures 408 + " actual:" + actualNumberOfFeatures, line); 409 return false; 410 } 411 lastLinePassed = true; 412 } else { 413 mEnabledFeatures.add(line); 414 } 415 } 416 } 417 } catch (IOException e) { 418 Slog.w(TAG, "Cannot load config file", e); 419 return false; 420 } 421 Slog.i(TAG, "Loaded features:" + mEnabledFeatures); 422 return true; 423 } 424 persistToFeatureConfigFile(HashSet<String> features)425 private void persistToFeatureConfigFile(HashSet<String> features) { 426 removeSupportFeatures(features); 427 synchronized (mLock) { 428 features.removeAll(mPendingDisabledFeatures); 429 features.addAll(mPendingEnabledFeatures); 430 FileOutputStream fos; 431 try { 432 fos = mFeatureConfigFile.startWrite(); 433 } catch (IOException e) { 434 Slog.e(TAG, "Cannot create config file", e); 435 return; 436 } 437 try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, 438 StandardCharsets.UTF_8))) { 439 Slog.i(TAG, "Updating features:" + features); 440 for (String feature : features) { 441 writer.write(feature); 442 writer.newLine(); 443 } 444 writer.write(CONFIG_FILE_LAST_LINE_MARKER + features.size()); 445 writer.flush(); 446 mFeatureConfigFile.finishWrite(fos); 447 } catch (IOException e) { 448 mFeatureConfigFile.failWrite(fos); 449 Slog.e(TAG, "Cannot create config file", e); 450 } 451 } 452 } 453 assertPermission()454 private void assertPermission() { 455 ICarImpl.assertPermission(mContext, Car.PERMISSION_CONTROL_CAR_FEATURES); 456 } 457 dispatchDefaultConfigUpdate()458 private void dispatchDefaultConfigUpdate() { 459 mHandler.removeCallbacksAndMessages(null); 460 HashSet<String> featuresToPersist = new HashSet<>(mEnabledFeatures); 461 mHandler.post(() -> persistToFeatureConfigFile(featuresToPersist)); 462 } 463 parseDefaultConfig()464 private void parseDefaultConfig() { 465 for (String feature : mDefaultEnabledFeaturesFromConfig) { 466 if (mDisabledFeaturesFromVhal.contains(feature)) { 467 continue; 468 } 469 if (OPTIONAL_FEATURES.contains(feature)) { 470 mEnabledFeatures.add(feature); 471 } else if (NON_USER_ONLY_FEATURES.contains(feature)) { 472 Slog.e(TAG, 473 "config_default_enabled_optional_car_features including " 474 + "user build only feature, will be ignored:" + feature); 475 } else { 476 throw new IllegalArgumentException( 477 "config_default_enabled_optional_car_features include non-optional " 478 + "features:" + feature); 479 } 480 } 481 Slog.i(TAG, "Loaded default features:" + mEnabledFeatures); 482 } 483 addSupportFeatures(Collection<String> features)484 private static void addSupportFeatures(Collection<String> features) { 485 SUPPORT_FEATURES.stream() 486 .filter(entry -> features.contains(entry.first)) 487 .forEach(entry -> features.add(entry.second)); 488 } 489 removeSupportFeatures(Collection<String> features)490 private static void removeSupportFeatures(Collection<String> features) { 491 SUPPORT_FEATURES.stream() 492 .filter(entry -> features.contains(entry.first)) 493 .forEach(entry -> features.remove(entry.second)); 494 } 495 } 496