blob: 7a97231b6f9b5d2e7496387393677de1b9425ff7 [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;
28import android.util.Log;
29
30import com.android.internal.annotations.GuardedBy;
31
32import java.io.BufferedReader;
33import java.io.BufferedWriter;
34import java.io.File;
35import java.io.FileInputStream;
36import java.io.FileNotFoundException;
37import java.io.FileOutputStream;
38import java.io.IOException;
39import java.io.InputStreamReader;
40import java.io.OutputStreamWriter;
41import java.io.PrintWriter;
42import java.nio.charset.StandardCharsets;
43import java.util.ArrayList;
44import java.util.Arrays;
45import java.util.Collections;
46import java.util.HashSet;
47import java.util.List;
48
49/**
50 * Component controlling the feature of car.
51 */
52public final class CarFeatureController implements CarServiceBase {
53
54 private static final String TAG = "CAR.FEATURE";
55
56 // Use HaseSet for better search performance. Memory consumption is fixed and it not an issue.
57 // Should keep alphabetical order under each bucket.
58 // Update CarFeatureTest as well when this is updated.
59 private static final HashSet<String> MANDATORY_FEATURES = new HashSet<>(Arrays.asList(
60 Car.APP_FOCUS_SERVICE,
61 Car.AUDIO_SERVICE,
62 Car.BLUETOOTH_SERVICE,
63 Car.CAR_BUGREPORT_SERVICE,
Keun young Parkc6d80af2020-01-17 18:14:28 -080064 Car.CAR_CONFIGURATION_SERVICE,
Keun young Park9a91efb2019-11-15 18:10:47 -080065 Car.CAR_DRIVING_STATE_SERVICE,
Keun young Park401479c2020-02-19 14:15:51 -080066 Car.CAR_INPUT_SERVICE,
Keun young Park9a91efb2019-11-15 18:10:47 -080067 Car.CAR_MEDIA_SERVICE,
68 Car.CAR_NAVIGATION_SERVICE,
69 Car.CAR_OCCUPANT_ZONE_SERVICE,
Keun young Parkc6d80af2020-01-17 18:14:28 -080070 Car.CAR_TRUST_AGENT_ENROLLMENT_SERVICE,
Keun young Park9a91efb2019-11-15 18:10:47 -080071 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,
Keun young Parkc6d80af2020-01-17 18:14:28 -080080 // All items below here are deprecated, but still should be supported
Keun young Park9a91efb2019-11-15 18:10:47 -080081 Car.CAR_INSTRUMENT_CLUSTER_SERVICE,
82 Car.CABIN_SERVICE,
83 Car.HVAC_SERVICE,
Keun young Parkc6d80af2020-01-17 18:14:28 -080084 Car.SENSOR_SERVICE,
85 Car.VENDOR_EXTENSION_SERVICE
Keun young Park9a91efb2019-11-15 18:10:47 -080086 ));
87
88 private static final HashSet<String> OPTIONAL_FEATURES = new HashSet<>(Arrays.asList(
Keun young Parkc6d80af2020-01-17 18:14:28 -080089 CarFeatures.FEATURE_CAR_USER_NOTICE_SERVICE,
90 Car.DIAGNOSTIC_SERVICE,
Michael Kellerac2ed202020-01-30 14:11:16 -080091 Car.OCCUPANT_AWARENESS_SERVICE,
Keun young Park9a91efb2019-11-15 18:10:47 -080092 Car.STORAGE_MONITORING_SERVICE,
Mark Tabry6fa123d2020-01-10 19:52:59 -080093 Car.VEHICLE_MAP_SERVICE,
Keun young Parkc6d80af2020-01-17 18:14:28 -080094 Car.VMS_SUBSCRIBER_SERVICE
Keun young Park9a91efb2019-11-15 18:10:47 -080095 ));
96
97 private static final String FEATURE_CONFIG_FILE_NAME = "car_feature_config.txt";
98
99 // Last line starts with this with number of features for extra sanity check.
100 private static final String CONFIG_FILE_LAST_LINE_MARKER = ",,";
101
102 // Set once in constructor and not updated. Access it without lock so that it can be accessed
103 // quickly.
104 private final HashSet<String> mEnabledFeatures;
105
106 private final Context mContext;
107
108 private final List<String> mDefaultEnabledFeaturesFromConfig;
109 private final List<String> mDisabledFeaturesFromVhal;
110
111 private final HandlerThread mHandlerThread;
112 private final Handler mHandler;
113
114 private final Object mLock = new Object();
115
116 @GuardedBy("mLock")
117 private final AtomicFile mFeatureConfigFile;
118
119 @GuardedBy("mLock")
120 private final List<String> mPendingEnabledFeatures = new ArrayList<>();
121
122 @GuardedBy("mLock")
123 private final List<String> mPendingDisabledFeatures = new ArrayList<>();
124
125 @GuardedBy("mLock")
126 private HashSet<String> mAvailableExperimentalFeatures = new HashSet<>();
127
128 private final Runnable mDefaultConfigWriter = () -> {
129 persistToFeatureConfigFile();
130 };
131
132 public CarFeatureController(@NonNull Context context,
133 @NonNull String[] defaultEnabledFeaturesFromConfig,
134 @NonNull String[] disabledFeaturesFromVhal, @NonNull File dataDir) {
135 mContext = context;
136 mDefaultEnabledFeaturesFromConfig = Arrays.asList(defaultEnabledFeaturesFromConfig);
137 mDisabledFeaturesFromVhal = Arrays.asList(disabledFeaturesFromVhal);
138 mEnabledFeatures = new HashSet<>(MANDATORY_FEATURES);
139 mFeatureConfigFile = new AtomicFile(new File(dataDir, FEATURE_CONFIG_FILE_NAME), TAG);
140 boolean shouldLoadDefaultConfig = !mFeatureConfigFile.exists();
141 if (!shouldLoadDefaultConfig) {
142 if (!loadFromConfigFileLocked()) {
143 shouldLoadDefaultConfig = true;
144 }
145 }
146 mHandlerThread = new HandlerThread(TAG);
147 mHandlerThread.start();
148 mHandler = new Handler(mHandlerThread.getLooper());
Keun young Parkf7134692020-03-10 15:41:03 -0700149 if (!checkMandatoryFeaturesLocked()) { // mandatory feature missing, force default config
150 mEnabledFeatures.clear();
151 mEnabledFeatures.addAll(MANDATORY_FEATURES);
152 shouldLoadDefaultConfig = true;
153 }
Keun young Park9a91efb2019-11-15 18:10:47 -0800154 // Separate if to use this as backup for failure in loadFromConfigFileLocked()
155 if (shouldLoadDefaultConfig) {
156 parseDefaultConfig();
157 dispatchDefaultConfigUpdate();
158 }
159 }
160
161 @Override
162 public void init() {
163 // nothing should be done here. This should work with only constructor.
164 }
165
166 @Override
167 public void release() {
168 // nothing should be done here.
169 }
170
171 @Override
172 public void dump(PrintWriter writer) {
173 writer.println("*CarFeatureController*");
174 writer.println(" mEnabledFeatures:" + mEnabledFeatures);
175 writer.println(" mDefaultEnabledFeaturesFromConfig:" + mDefaultEnabledFeaturesFromConfig);
176 writer.println(" mDisabledFeaturesFromVhal:" + mDisabledFeaturesFromVhal);
177 synchronized (mLock) {
178 writer.println(" mAvailableExperimentalFeatures:" + mAvailableExperimentalFeatures);
179 writer.println(" mPendingEnabledFeatures:" + mPendingEnabledFeatures);
180 writer.println(" mPendingDisabledFeatures:" + mPendingDisabledFeatures);
181 }
182 }
183
184 /** Check {@link Car#isFeatureEnabled(String)} */
185 public boolean isFeatureEnabled(String featureName) {
186 return mEnabledFeatures.contains(featureName);
187 }
188
Keun young Parkf7134692020-03-10 15:41:03 -0700189 private boolean checkMandatoryFeaturesLocked() {
190 // Ensure that mandatory features are always there
191 for (String feature: MANDATORY_FEATURES) {
192 if (!mEnabledFeatures.contains(feature)) {
193 Log.e(TAG, "Mandatory feature missing in mEnabledFeatures:" + feature);
194 return false;
195 }
196 }
197 return true;
198 }
199
Keun young Park9a91efb2019-11-15 18:10:47 -0800200 @FeaturerRequestEnum
201 private int checkFeatureExisting(String featureName) {
202 if (MANDATORY_FEATURES.contains(featureName)) {
203 return Car.FEATURE_REQUEST_MANDATORY;
204 }
205 if (!OPTIONAL_FEATURES.contains(featureName)) {
206 synchronized (mLock) {
207 if (!mAvailableExperimentalFeatures.contains(featureName)) {
208 Log.e(TAG, "enableFeature requested for non-existing feature:"
209 + featureName);
210 return Car.FEATURE_REQUEST_NOT_EXISTING;
211 }
212 }
213 }
214 return Car.FEATURE_REQUEST_SUCCESS;
215 }
216
217 /** Check {@link Car#enableFeature(String)} */
218 public int enableFeature(String featureName) {
219 assertPermission();
220 int checkResult = checkFeatureExisting(featureName);
221 if (checkResult != Car.FEATURE_REQUEST_SUCCESS) {
222 return checkResult;
223 }
224
225 boolean alreadyEnabled = mEnabledFeatures.contains(featureName);
226 boolean shouldUpdateConfigFile = false;
227 synchronized (mLock) {
228 if (mPendingDisabledFeatures.remove(featureName)) {
229 shouldUpdateConfigFile = true;
230 }
231 if (!mPendingEnabledFeatures.contains(featureName) && !alreadyEnabled) {
232 shouldUpdateConfigFile = true;
233 mPendingEnabledFeatures.add(featureName);
234 }
235 }
236 if (shouldUpdateConfigFile) {
237 Log.w(TAG, "Enabling feature in config file:" + featureName);
238 dispatchDefaultConfigUpdate();
239 }
240 if (alreadyEnabled) {
241 return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE;
242 } else {
243 return Car.FEATURE_REQUEST_SUCCESS;
244 }
245 }
246
247 /** Check {@link Car#disableFeature(String)} */
248 public int disableFeature(String featureName) {
249 assertPermission();
250 int checkResult = checkFeatureExisting(featureName);
251 if (checkResult != Car.FEATURE_REQUEST_SUCCESS) {
252 return checkResult;
253 }
254
255 boolean alreadyDisabled = !mEnabledFeatures.contains(featureName);
256 boolean shouldUpdateConfigFile = false;
257 synchronized (mLock) {
258 if (mPendingEnabledFeatures.remove(featureName)) {
259 shouldUpdateConfigFile = true;
260 }
261 if (!mPendingDisabledFeatures.contains(featureName) && !alreadyDisabled) {
262 shouldUpdateConfigFile = true;
263 mPendingDisabledFeatures.add(featureName);
264 }
265 }
266 if (shouldUpdateConfigFile) {
267 Log.w(TAG, "Disabling feature in config file:" + featureName);
268 dispatchDefaultConfigUpdate();
269 }
270 if (alreadyDisabled) {
271 return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE;
272 } else {
273 return Car.FEATURE_REQUEST_SUCCESS;
274 }
275 }
276
277 /**
278 * Set available experimental features. Only features set through this call will be allowed to
279 * be enabled for experimental features. Setting this is not allowed for USER build.
280 *
281 * @return True if set is allowed and set. False if experimental feature is not allowed.
282 */
283 public boolean setAvailableExperimentalFeatureList(List<String> experimentalFeatures) {
284 assertPermission();
285 if (Build.IS_USER) {
286 Log.e(TAG, "Experimental feature list set for USER build",
287 new RuntimeException());
288 return false;
289 }
290 synchronized (mLock) {
291 mAvailableExperimentalFeatures.clear();
292 mAvailableExperimentalFeatures.addAll(experimentalFeatures);
293 }
294 return true;
295 }
296
297 /** Check {@link Car#getAllEnabledFeatures()} */
298 public List<String> getAllEnabledFeatures() {
299 assertPermission();
300 return new ArrayList<>(mEnabledFeatures);
301 }
302
303 /** Check {@link Car#getAllPendingDisabledFeatures()} */
304 public List<String> getAllPendingDisabledFeatures() {
305 assertPermission();
306 synchronized (mLock) {
307 return new ArrayList<>(mPendingDisabledFeatures);
308 }
309 }
310
311 /** Check {@link Car#getAllPendingEnabledFeatures()} */
312 public List<String> getAllPendingEnabledFeatures() {
313 assertPermission();
314 synchronized (mLock) {
315 return new ArrayList<>(mPendingEnabledFeatures);
316 }
317 }
318
319 /** Returns currently enabled experimental features */
320 public @NonNull List<String> getEnabledExperimentalFeatures() {
321 if (Build.IS_USER) {
322 Log.e(TAG, "getEnabledExperimentalFeatures called in USER build",
323 new RuntimeException());
324 return Collections.emptyList();
325 }
326 ArrayList<String> experimentalFeature = new ArrayList<>();
327 for (String feature: mEnabledFeatures) {
328 if (MANDATORY_FEATURES.contains(feature)) {
329 continue;
330 }
331 if (OPTIONAL_FEATURES.contains(feature)) {
332 continue;
333 }
334 experimentalFeature.add(feature);
335 }
336 return experimentalFeature;
337 }
338
339 void handleCorruptConfigFileLocked(String msg, String line) {
340 Log.e(TAG, msg + ", considered as corrupt, line:" + line);
341 mEnabledFeatures.clear();
342 }
343
344 private boolean loadFromConfigFileLocked() {
345 // done without lock, should be only called from constructor.
346 FileInputStream fis;
347 try {
348 fis = mFeatureConfigFile.openRead();
349 } catch (FileNotFoundException e) {
350 Log.i(TAG, "Feature config file not found, this could be 1st boot");
351 return false;
352 }
353 try (BufferedReader reader = new BufferedReader(
354 new InputStreamReader(fis, StandardCharsets.UTF_8))) {
355 boolean lastLinePassed = false;
356 while (true) {
357 String line = reader.readLine();
358 if (line == null) {
359 if (!lastLinePassed) {
360 handleCorruptConfigFileLocked("No last line checksum", "");
361 return false;
362 }
363 break;
364 }
365 if (lastLinePassed && !line.isEmpty()) {
366 handleCorruptConfigFileLocked(
367 "Config file has additional line after last line marker", line);
368 return false;
369 } else {
370 if (line.startsWith(CONFIG_FILE_LAST_LINE_MARKER)) {
371 int numberOfFeatures;
372 try {
373 numberOfFeatures = Integer.valueOf(line.substring(
374 CONFIG_FILE_LAST_LINE_MARKER.length()));
375 } catch (NumberFormatException e) {
376 handleCorruptConfigFileLocked(
377 "Config file has corrupt last line, not a number",
378 line);
379 return false;
380 }
381 int actualNumberOfFeatures = mEnabledFeatures.size();
382 if (numberOfFeatures != actualNumberOfFeatures) {
383 handleCorruptConfigFileLocked(
384 "Config file has wrong number of features, expected:"
385 + numberOfFeatures
386 + " actual:" + actualNumberOfFeatures, line);
387 return false;
388 }
389 lastLinePassed = true;
390 } else {
391 mEnabledFeatures.add(line);
392 }
393 }
394 }
395 } catch (IOException e) {
396 Log.w(TAG, "Cannot load config file", e);
397 return false;
398 }
399 Log.i(TAG, "Loaded features:" + mEnabledFeatures);
400 return true;
401 }
402
403 private void persistToFeatureConfigFile() {
404 HashSet<String> features = new HashSet<>(mEnabledFeatures);
405 synchronized (mLock) {
406 features.removeAll(mPendingDisabledFeatures);
407 features.addAll(mPendingEnabledFeatures);
408 FileOutputStream fos;
409 try {
410 fos = mFeatureConfigFile.startWrite();
411 } catch (IOException e) {
412 Log.e(TAG, "Cannot create config file", e);
413 return;
414 }
415 try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos,
416 StandardCharsets.UTF_8))) {
417 Log.i(TAG, "Updating features:" + features);
418 for (String feature : features) {
419 writer.write(feature);
420 writer.newLine();
421 }
422 writer.write(CONFIG_FILE_LAST_LINE_MARKER + features.size());
423 writer.flush();
424 mFeatureConfigFile.finishWrite(fos);
425 } catch (IOException e) {
426 mFeatureConfigFile.failWrite(fos);
427 Log.e(TAG, "Cannot create config file", e);
428 }
429 }
430 }
431
432 private void assertPermission() {
433 ICarImpl.assertPermission(mContext, Car.PERMISSION_CONTROL_CAR_FEATURES);
434 }
435
436 private void dispatchDefaultConfigUpdate() {
437 mHandler.removeCallbacks(mDefaultConfigWriter);
438 mHandler.post(mDefaultConfigWriter);
439 }
440
441 private void parseDefaultConfig() {
442 for (String feature : mDefaultEnabledFeaturesFromConfig) {
443 if (!OPTIONAL_FEATURES.contains(feature)) {
444 throw new IllegalArgumentException(
445 "config_default_enabled_optional_car_features include non-optional "
446 + "features:" + feature);
447 }
448 if (mDisabledFeaturesFromVhal.contains(feature)) {
449 continue;
450 }
451 mEnabledFeatures.add(feature);
452 }
453 Log.i(TAG, "Loaded default features:" + mEnabledFeatures);
454 }
455}