blob: b505ea8551cca673566ef3994deb39adb6e166df [file] [log] [blame]
Keun young Park9a91efb2019-11-15 18:10:47 -08001/*
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
17package com.android.car;
18
19import android.annotation.NonNull;
20import android.car.Car;
21import android.car.Car.FeaturerRequestEnum;
22import android.car.CarFeatures;
23import android.content.Context;
24import android.os.Build;
25import android.os.Handler;
26import android.os.HandlerThread;
27import android.util.AtomicFile;
Felipe Leme176a5fd2021-01-20 15:48:33 -080028import android.util.IndentingPrintWriter;
Mark Tabry614e06e2020-03-15 03:30:01 -070029import android.util.Pair;
Eric Jeongbd5fb562020-12-21 13:49:40 -080030import android.util.Slog;
Keun young Park9a91efb2019-11-15 18:10:47 -080031
32import com.android.internal.annotations.GuardedBy;
Keun young Park1fd33fe2019-12-19 18:25:14 -080033import com.android.internal.annotations.VisibleForTesting;
Keun young Park9a91efb2019-11-15 18:10:47 -080034
35import java.io.BufferedReader;
36import java.io.BufferedWriter;
37import java.io.File;
38import java.io.FileInputStream;
39import java.io.FileNotFoundException;
40import java.io.FileOutputStream;
41import java.io.IOException;
42import java.io.InputStreamReader;
43import java.io.OutputStreamWriter;
Keun young Park9a91efb2019-11-15 18:10:47 -080044import java.nio.charset.StandardCharsets;
45import java.util.ArrayList;
46import java.util.Arrays;
Mark Tabry614e06e2020-03-15 03:30:01 -070047import java.util.Collection;
Keun young Park9a91efb2019-11-15 18:10:47 -080048import java.util.Collections;
49import java.util.HashSet;
50import java.util.List;
51
52/**
53 * Component controlling the feature of car.
54 */
55public final class CarFeatureController implements CarServiceBase {
56
57 private static final String TAG = "CAR.FEATURE";
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_BUGREPORT_SERVICE,
67 Car.CAR_DRIVING_STATE_SERVICE,
Keun young Park401479c2020-02-19 14:15:51 -080068 Car.CAR_INPUT_SERVICE,
Keun young Park9a91efb2019-11-15 18:10:47 -080069 Car.CAR_MEDIA_SERVICE,
Keun young Park9a91efb2019-11-15 18:10:47 -080070 Car.CAR_OCCUPANT_ZONE_SERVICE,
71 Car.CAR_USER_SERVICE,
Keun young Parkc6d80af2020-01-17 18:14:28 -080072 Car.CAR_UX_RESTRICTION_SERVICE,
Keun young Park9a91efb2019-11-15 18:10:47 -080073 Car.INFO_SERVICE,
74 Car.PACKAGE_SERVICE,
75 Car.POWER_SERVICE,
76 Car.PROJECTION_SERVICE,
77 Car.PROPERTY_SERVICE,
78 Car.TEST_SERVICE,
Eric Jeong2cd573a2020-02-20 18:48:05 -080079 Car.CAR_WATCHDOG_SERVICE,
Felipe Leme55236722020-10-16 16:54:32 -070080 Car.CAR_DEVICE_POLICY_SERVICE,
Keun young Parkc6d80af2020-01-17 18:14:28 -080081 // All items below here are deprecated, but still should be supported
Keun young Park9a91efb2019-11-15 18:10:47 -080082 Car.CAR_INSTRUMENT_CLUSTER_SERVICE,
83 Car.CABIN_SERVICE,
84 Car.HVAC_SERVICE,
Keun young Parkc6d80af2020-01-17 18:14:28 -080085 Car.SENSOR_SERVICE,
86 Car.VENDOR_EXTENSION_SERVICE
Keun young Park9a91efb2019-11-15 18:10:47 -080087 ));
88
89 private static final HashSet<String> OPTIONAL_FEATURES = new HashSet<>(Arrays.asList(
Keun young Parkc6d80af2020-01-17 18:14:28 -080090 CarFeatures.FEATURE_CAR_USER_NOTICE_SERVICE,
Yuncheol Heo9a4eb7c2020-05-01 13:41:45 -070091 Car.CAR_NAVIGATION_SERVICE,
Keun young Parkc6d80af2020-01-17 18:14:28 -080092 Car.DIAGNOSTIC_SERVICE,
Michael Kellerac2ed202020-01-30 14:11:16 -080093 Car.OCCUPANT_AWARENESS_SERVICE,
Keun young Park9a91efb2019-11-15 18:10:47 -080094 Car.STORAGE_MONITORING_SERVICE,
Mark Tabry614e06e2020-03-15 03:30:01 -070095 Car.VEHICLE_MAP_SERVICE
Keun young Park9a91efb2019-11-15 18:10:47 -080096 ));
97
Mark Tabry614e06e2020-03-15 03:30:01 -070098 // Features that depend on another feature being enabled (i.e. legacy API support).
99 // For example, VMS_SUBSCRIBER_SERVICE will be enabled if VEHICLE_MAP_SERVICE is enabled
100 // and disabled if VEHICLE_MAP_SERVICE is disabled.
101 private static final List<Pair<String, String>> SUPPORT_FEATURES = Arrays.asList(
102 Pair.create(Car.VEHICLE_MAP_SERVICE, Car.VMS_SUBSCRIBER_SERVICE)
103 );
104
Keun young Park9a91efb2019-11-15 18:10:47 -0800105 private static final String FEATURE_CONFIG_FILE_NAME = "car_feature_config.txt";
106
Mayank Garg0f382c42020-08-04 12:57:51 -0700107 // Last line starts with this with number of features for extra confidence check.
Keun young Park9a91efb2019-11-15 18:10:47 -0800108 private static final String CONFIG_FILE_LAST_LINE_MARKER = ",,";
109
110 // Set once in constructor and not updated. Access it without lock so that it can be accessed
111 // quickly.
112 private final HashSet<String> mEnabledFeatures;
113
114 private final Context mContext;
115
116 private final List<String> mDefaultEnabledFeaturesFromConfig;
117 private final List<String> mDisabledFeaturesFromVhal;
118
Keun young Parkb241d022020-04-20 20:31:34 -0700119 private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
120 getClass().getSimpleName());
121 private final Handler mHandler = new Handler(mHandlerThread.getLooper());
Keun young Park9a91efb2019-11-15 18:10:47 -0800122 private final Object mLock = new Object();
123
124 @GuardedBy("mLock")
125 private final AtomicFile mFeatureConfigFile;
126
127 @GuardedBy("mLock")
128 private final List<String> mPendingEnabledFeatures = new ArrayList<>();
129
130 @GuardedBy("mLock")
131 private final List<String> mPendingDisabledFeatures = new ArrayList<>();
132
133 @GuardedBy("mLock")
134 private HashSet<String> mAvailableExperimentalFeatures = new HashSet<>();
135
Keun young Park9a91efb2019-11-15 18:10:47 -0800136 public CarFeatureController(@NonNull Context context,
137 @NonNull String[] defaultEnabledFeaturesFromConfig,
138 @NonNull String[] disabledFeaturesFromVhal, @NonNull File dataDir) {
139 mContext = context;
140 mDefaultEnabledFeaturesFromConfig = Arrays.asList(defaultEnabledFeaturesFromConfig);
141 mDisabledFeaturesFromVhal = Arrays.asList(disabledFeaturesFromVhal);
Eric Jeongbd5fb562020-12-21 13:49:40 -0800142 Slog.i(TAG, "mDefaultEnabledFeaturesFromConfig:" + mDefaultEnabledFeaturesFromConfig
Keun young Park1fd33fe2019-12-19 18:25:14 -0800143 + ",mDisabledFeaturesFromVhal:" + mDisabledFeaturesFromVhal);
Keun young Park9a91efb2019-11-15 18:10:47 -0800144 mEnabledFeatures = new HashSet<>(MANDATORY_FEATURES);
145 mFeatureConfigFile = new AtomicFile(new File(dataDir, FEATURE_CONFIG_FILE_NAME), TAG);
146 boolean shouldLoadDefaultConfig = !mFeatureConfigFile.exists();
147 if (!shouldLoadDefaultConfig) {
148 if (!loadFromConfigFileLocked()) {
149 shouldLoadDefaultConfig = true;
150 }
151 }
Keun young Parkf7134692020-03-10 15:41:03 -0700152 if (!checkMandatoryFeaturesLocked()) { // mandatory feature missing, force default config
153 mEnabledFeatures.clear();
154 mEnabledFeatures.addAll(MANDATORY_FEATURES);
155 shouldLoadDefaultConfig = true;
156 }
Keun young Park9a91efb2019-11-15 18:10:47 -0800157 // Separate if to use this as backup for failure in loadFromConfigFileLocked()
158 if (shouldLoadDefaultConfig) {
159 parseDefaultConfig();
160 dispatchDefaultConfigUpdate();
161 }
Mark Tabry614e06e2020-03-15 03:30:01 -0700162 addSupportFeatures(mEnabledFeatures);
Keun young Park9a91efb2019-11-15 18:10:47 -0800163 }
164
Keun young Park1fd33fe2019-12-19 18:25:14 -0800165 @VisibleForTesting
166 List<String> getDisabledFeaturesFromVhal() {
167 return mDisabledFeaturesFromVhal;
168 }
169
Keun young Park9a91efb2019-11-15 18:10:47 -0800170 @Override
171 public void init() {
172 // nothing should be done here. This should work with only constructor.
173 }
174
175 @Override
176 public void release() {
177 // nothing should be done here.
178 }
179
180 @Override
Felipe Leme176a5fd2021-01-20 15:48:33 -0800181 public void dump(IndentingPrintWriter writer) {
Keun young Park9a91efb2019-11-15 18:10:47 -0800182 writer.println("*CarFeatureController*");
183 writer.println(" mEnabledFeatures:" + mEnabledFeatures);
184 writer.println(" mDefaultEnabledFeaturesFromConfig:" + mDefaultEnabledFeaturesFromConfig);
185 writer.println(" mDisabledFeaturesFromVhal:" + mDisabledFeaturesFromVhal);
186 synchronized (mLock) {
187 writer.println(" mAvailableExperimentalFeatures:" + mAvailableExperimentalFeatures);
188 writer.println(" mPendingEnabledFeatures:" + mPendingEnabledFeatures);
189 writer.println(" mPendingDisabledFeatures:" + mPendingDisabledFeatures);
190 }
191 }
192
193 /** Check {@link Car#isFeatureEnabled(String)} */
194 public boolean isFeatureEnabled(String featureName) {
195 return mEnabledFeatures.contains(featureName);
196 }
197
Keun young Parkf7134692020-03-10 15:41:03 -0700198 private boolean checkMandatoryFeaturesLocked() {
199 // Ensure that mandatory features are always there
200 for (String feature: MANDATORY_FEATURES) {
201 if (!mEnabledFeatures.contains(feature)) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800202 Slog.e(TAG, "Mandatory feature missing in mEnabledFeatures:" + feature);
Keun young Parkf7134692020-03-10 15:41:03 -0700203 return false;
204 }
205 }
206 return true;
207 }
208
Keun young Park9a91efb2019-11-15 18:10:47 -0800209 @FeaturerRequestEnum
210 private int checkFeatureExisting(String featureName) {
211 if (MANDATORY_FEATURES.contains(featureName)) {
212 return Car.FEATURE_REQUEST_MANDATORY;
213 }
214 if (!OPTIONAL_FEATURES.contains(featureName)) {
215 synchronized (mLock) {
216 if (!mAvailableExperimentalFeatures.contains(featureName)) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800217 Slog.e(TAG, "enableFeature requested for non-existing feature:"
Keun young Park9a91efb2019-11-15 18:10:47 -0800218 + featureName);
219 return Car.FEATURE_REQUEST_NOT_EXISTING;
220 }
221 }
222 }
223 return Car.FEATURE_REQUEST_SUCCESS;
224 }
225
226 /** Check {@link Car#enableFeature(String)} */
227 public int enableFeature(String featureName) {
228 assertPermission();
229 int checkResult = checkFeatureExisting(featureName);
230 if (checkResult != Car.FEATURE_REQUEST_SUCCESS) {
231 return checkResult;
232 }
233
234 boolean alreadyEnabled = mEnabledFeatures.contains(featureName);
235 boolean shouldUpdateConfigFile = false;
236 synchronized (mLock) {
237 if (mPendingDisabledFeatures.remove(featureName)) {
238 shouldUpdateConfigFile = true;
239 }
240 if (!mPendingEnabledFeatures.contains(featureName) && !alreadyEnabled) {
241 shouldUpdateConfigFile = true;
242 mPendingEnabledFeatures.add(featureName);
243 }
244 }
245 if (shouldUpdateConfigFile) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800246 Slog.w(TAG, "Enabling feature in config file:" + featureName);
Keun young Park9a91efb2019-11-15 18:10:47 -0800247 dispatchDefaultConfigUpdate();
248 }
249 if (alreadyEnabled) {
250 return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE;
251 } else {
252 return Car.FEATURE_REQUEST_SUCCESS;
253 }
254 }
255
256 /** Check {@link Car#disableFeature(String)} */
257 public int disableFeature(String featureName) {
258 assertPermission();
259 int checkResult = checkFeatureExisting(featureName);
260 if (checkResult != Car.FEATURE_REQUEST_SUCCESS) {
261 return checkResult;
262 }
263
264 boolean alreadyDisabled = !mEnabledFeatures.contains(featureName);
265 boolean shouldUpdateConfigFile = false;
266 synchronized (mLock) {
267 if (mPendingEnabledFeatures.remove(featureName)) {
268 shouldUpdateConfigFile = true;
269 }
270 if (!mPendingDisabledFeatures.contains(featureName) && !alreadyDisabled) {
271 shouldUpdateConfigFile = true;
272 mPendingDisabledFeatures.add(featureName);
273 }
274 }
275 if (shouldUpdateConfigFile) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800276 Slog.w(TAG, "Disabling feature in config file:" + featureName);
Keun young Park9a91efb2019-11-15 18:10:47 -0800277 dispatchDefaultConfigUpdate();
278 }
279 if (alreadyDisabled) {
280 return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE;
281 } else {
282 return Car.FEATURE_REQUEST_SUCCESS;
283 }
284 }
285
286 /**
287 * Set available experimental features. Only features set through this call will be allowed to
288 * be enabled for experimental features. Setting this is not allowed for USER build.
289 *
290 * @return True if set is allowed and set. False if experimental feature is not allowed.
291 */
292 public boolean setAvailableExperimentalFeatureList(List<String> experimentalFeatures) {
293 assertPermission();
294 if (Build.IS_USER) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800295 Slog.e(TAG, "Experimental feature list set for USER build",
Keun young Park9a91efb2019-11-15 18:10:47 -0800296 new RuntimeException());
297 return false;
298 }
299 synchronized (mLock) {
300 mAvailableExperimentalFeatures.clear();
301 mAvailableExperimentalFeatures.addAll(experimentalFeatures);
302 }
303 return true;
304 }
305
306 /** Check {@link Car#getAllEnabledFeatures()} */
307 public List<String> getAllEnabledFeatures() {
308 assertPermission();
309 return new ArrayList<>(mEnabledFeatures);
310 }
311
312 /** Check {@link Car#getAllPendingDisabledFeatures()} */
313 public List<String> getAllPendingDisabledFeatures() {
314 assertPermission();
315 synchronized (mLock) {
316 return new ArrayList<>(mPendingDisabledFeatures);
317 }
318 }
319
320 /** Check {@link Car#getAllPendingEnabledFeatures()} */
321 public List<String> getAllPendingEnabledFeatures() {
322 assertPermission();
323 synchronized (mLock) {
324 return new ArrayList<>(mPendingEnabledFeatures);
325 }
326 }
327
328 /** Returns currently enabled experimental features */
329 public @NonNull List<String> getEnabledExperimentalFeatures() {
330 if (Build.IS_USER) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800331 Slog.e(TAG, "getEnabledExperimentalFeatures called in USER build",
Keun young Park9a91efb2019-11-15 18:10:47 -0800332 new RuntimeException());
333 return Collections.emptyList();
334 }
335 ArrayList<String> experimentalFeature = new ArrayList<>();
336 for (String feature: mEnabledFeatures) {
337 if (MANDATORY_FEATURES.contains(feature)) {
338 continue;
339 }
340 if (OPTIONAL_FEATURES.contains(feature)) {
341 continue;
342 }
343 experimentalFeature.add(feature);
344 }
345 return experimentalFeature;
346 }
347
348 void handleCorruptConfigFileLocked(String msg, String line) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800349 Slog.e(TAG, msg + ", considered as corrupt, line:" + line);
Keun young Park9a91efb2019-11-15 18:10:47 -0800350 mEnabledFeatures.clear();
351 }
352
353 private boolean loadFromConfigFileLocked() {
354 // done without lock, should be only called from constructor.
355 FileInputStream fis;
356 try {
357 fis = mFeatureConfigFile.openRead();
358 } catch (FileNotFoundException e) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800359 Slog.i(TAG, "Feature config file not found, this could be 1st boot");
Keun young Park9a91efb2019-11-15 18:10:47 -0800360 return false;
361 }
362 try (BufferedReader reader = new BufferedReader(
363 new InputStreamReader(fis, StandardCharsets.UTF_8))) {
364 boolean lastLinePassed = false;
365 while (true) {
366 String line = reader.readLine();
367 if (line == null) {
368 if (!lastLinePassed) {
369 handleCorruptConfigFileLocked("No last line checksum", "");
370 return false;
371 }
372 break;
373 }
374 if (lastLinePassed && !line.isEmpty()) {
375 handleCorruptConfigFileLocked(
376 "Config file has additional line after last line marker", line);
377 return false;
378 } else {
379 if (line.startsWith(CONFIG_FILE_LAST_LINE_MARKER)) {
380 int numberOfFeatures;
381 try {
Mark Tabry614e06e2020-03-15 03:30:01 -0700382 numberOfFeatures = Integer.parseInt(line.substring(
Keun young Park9a91efb2019-11-15 18:10:47 -0800383 CONFIG_FILE_LAST_LINE_MARKER.length()));
384 } catch (NumberFormatException e) {
385 handleCorruptConfigFileLocked(
386 "Config file has corrupt last line, not a number",
387 line);
388 return false;
389 }
390 int actualNumberOfFeatures = mEnabledFeatures.size();
391 if (numberOfFeatures != actualNumberOfFeatures) {
392 handleCorruptConfigFileLocked(
393 "Config file has wrong number of features, expected:"
394 + numberOfFeatures
395 + " actual:" + actualNumberOfFeatures, line);
396 return false;
397 }
398 lastLinePassed = true;
399 } else {
400 mEnabledFeatures.add(line);
401 }
402 }
403 }
404 } catch (IOException e) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800405 Slog.w(TAG, "Cannot load config file", e);
Keun young Park9a91efb2019-11-15 18:10:47 -0800406 return false;
407 }
Eric Jeongbd5fb562020-12-21 13:49:40 -0800408 Slog.i(TAG, "Loaded features:" + mEnabledFeatures);
Keun young Park9a91efb2019-11-15 18:10:47 -0800409 return true;
410 }
411
Mark Tabry1b14be32020-03-25 11:53:58 -0700412 private void persistToFeatureConfigFile(HashSet<String> features) {
Mark Tabry614e06e2020-03-15 03:30:01 -0700413 removeSupportFeatures(features);
Keun young Park9a91efb2019-11-15 18:10:47 -0800414 synchronized (mLock) {
415 features.removeAll(mPendingDisabledFeatures);
416 features.addAll(mPendingEnabledFeatures);
417 FileOutputStream fos;
418 try {
419 fos = mFeatureConfigFile.startWrite();
420 } catch (IOException e) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800421 Slog.e(TAG, "Cannot create config file", e);
Keun young Park9a91efb2019-11-15 18:10:47 -0800422 return;
423 }
424 try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos,
425 StandardCharsets.UTF_8))) {
Eric Jeongbd5fb562020-12-21 13:49:40 -0800426 Slog.i(TAG, "Updating features:" + features);
Keun young Park9a91efb2019-11-15 18:10:47 -0800427 for (String feature : features) {
428 writer.write(feature);
429 writer.newLine();
430 }
431 writer.write(CONFIG_FILE_LAST_LINE_MARKER + features.size());
432 writer.flush();
433 mFeatureConfigFile.finishWrite(fos);
434 } catch (IOException e) {
435 mFeatureConfigFile.failWrite(fos);
Eric Jeongbd5fb562020-12-21 13:49:40 -0800436 Slog.e(TAG, "Cannot create config file", e);
Keun young Park9a91efb2019-11-15 18:10:47 -0800437 }
438 }
439 }
440
441 private void assertPermission() {
442 ICarImpl.assertPermission(mContext, Car.PERMISSION_CONTROL_CAR_FEATURES);
443 }
444
445 private void dispatchDefaultConfigUpdate() {
Mark Tabry1b14be32020-03-25 11:53:58 -0700446 mHandler.removeCallbacksAndMessages(null);
447 HashSet<String> featuresToPersist = new HashSet<>(mEnabledFeatures);
448 mHandler.post(() -> persistToFeatureConfigFile(featuresToPersist));
Keun young Park9a91efb2019-11-15 18:10:47 -0800449 }
450
451 private void parseDefaultConfig() {
452 for (String feature : mDefaultEnabledFeaturesFromConfig) {
453 if (!OPTIONAL_FEATURES.contains(feature)) {
454 throw new IllegalArgumentException(
455 "config_default_enabled_optional_car_features include non-optional "
456 + "features:" + feature);
457 }
458 if (mDisabledFeaturesFromVhal.contains(feature)) {
459 continue;
460 }
461 mEnabledFeatures.add(feature);
462 }
Eric Jeongbd5fb562020-12-21 13:49:40 -0800463 Slog.i(TAG, "Loaded default features:" + mEnabledFeatures);
Keun young Park9a91efb2019-11-15 18:10:47 -0800464 }
Mark Tabry614e06e2020-03-15 03:30:01 -0700465
466 private static void addSupportFeatures(Collection<String> features) {
467 SUPPORT_FEATURES.stream()
468 .filter(entry -> features.contains(entry.first))
469 .forEach(entry -> features.add(entry.second));
470 }
471
472 private static void removeSupportFeatures(Collection<String> features) {
473 SUPPORT_FEATURES.stream()
474 .filter(entry -> features.contains(entry.first))
475 .forEach(entry -> features.remove(entry.second));
476 }
Keun young Park9a91efb2019-11-15 18:10:47 -0800477}