blob: fdaa1c5e1c616b98579f78ea64130bef10695e09 [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,
64 Car.CAR_DRIVING_STATE_SERVICE,
65 Car.CAR_MEDIA_SERVICE,
66 Car.CAR_NAVIGATION_SERVICE,
67 Car.CAR_OCCUPANT_ZONE_SERVICE,
68 Car.CAR_USER_SERVICE,
69 Car.INFO_SERVICE,
70 Car.PACKAGE_SERVICE,
71 Car.POWER_SERVICE,
72 Car.PROJECTION_SERVICE,
73 Car.PROPERTY_SERVICE,
74 Car.TEST_SERVICE,
75 // Deprecated, but still should be supported
76 Car.SENSOR_SERVICE,
77 Car.CAR_INSTRUMENT_CLUSTER_SERVICE,
78 Car.CABIN_SERVICE,
79 Car.HVAC_SERVICE,
80 Car.VENDOR_EXTENSION_SERVICE,
81 // Candidate for Optional, but stay mandatory for now until final decision is made.
82 Car.CAR_CONFIGURATION_SERVICE,
83 Car.CAR_TRUST_AGENT_ENROLLMENT_SERVICE,
84 Car.DIAGNOSTIC_SERVICE,
85 Car.CAR_UX_RESTRICTION_SERVICE,
86 // Marked as optional, but requires additional work
87 Car.VMS_SUBSCRIBER_SERVICE
88 ));
89
90 private static final HashSet<String> OPTIONAL_FEATURES = new HashSet<>(Arrays.asList(
91 Car.STORAGE_MONITORING_SERVICE,
92 CarFeatures.FEATURE_CAR_USER_NOTICE_SERVICE
93 ));
94
95 private static final String FEATURE_CONFIG_FILE_NAME = "car_feature_config.txt";
96
97 // Last line starts with this with number of features for extra sanity check.
98 private static final String CONFIG_FILE_LAST_LINE_MARKER = ",,";
99
100 // Set once in constructor and not updated. Access it without lock so that it can be accessed
101 // quickly.
102 private final HashSet<String> mEnabledFeatures;
103
104 private final Context mContext;
105
106 private final List<String> mDefaultEnabledFeaturesFromConfig;
107 private final List<String> mDisabledFeaturesFromVhal;
108
109 private final HandlerThread mHandlerThread;
110 private final Handler mHandler;
111
112 private final Object mLock = new Object();
113
114 @GuardedBy("mLock")
115 private final AtomicFile mFeatureConfigFile;
116
117 @GuardedBy("mLock")
118 private final List<String> mPendingEnabledFeatures = new ArrayList<>();
119
120 @GuardedBy("mLock")
121 private final List<String> mPendingDisabledFeatures = new ArrayList<>();
122
123 @GuardedBy("mLock")
124 private HashSet<String> mAvailableExperimentalFeatures = new HashSet<>();
125
126 private final Runnable mDefaultConfigWriter = () -> {
127 persistToFeatureConfigFile();
128 };
129
130 public CarFeatureController(@NonNull Context context,
131 @NonNull String[] defaultEnabledFeaturesFromConfig,
132 @NonNull String[] disabledFeaturesFromVhal, @NonNull File dataDir) {
133 mContext = context;
134 mDefaultEnabledFeaturesFromConfig = Arrays.asList(defaultEnabledFeaturesFromConfig);
135 mDisabledFeaturesFromVhal = Arrays.asList(disabledFeaturesFromVhal);
136 mEnabledFeatures = new HashSet<>(MANDATORY_FEATURES);
137 mFeatureConfigFile = new AtomicFile(new File(dataDir, FEATURE_CONFIG_FILE_NAME), TAG);
138 boolean shouldLoadDefaultConfig = !mFeatureConfigFile.exists();
139 if (!shouldLoadDefaultConfig) {
140 if (!loadFromConfigFileLocked()) {
141 shouldLoadDefaultConfig = true;
142 }
143 }
144 mHandlerThread = new HandlerThread(TAG);
145 mHandlerThread.start();
146 mHandler = new Handler(mHandlerThread.getLooper());
147 // Separate if to use this as backup for failure in loadFromConfigFileLocked()
148 if (shouldLoadDefaultConfig) {
149 parseDefaultConfig();
150 dispatchDefaultConfigUpdate();
151 }
152 }
153
154 @Override
155 public void init() {
156 // nothing should be done here. This should work with only constructor.
157 }
158
159 @Override
160 public void release() {
161 // nothing should be done here.
162 }
163
164 @Override
165 public void dump(PrintWriter writer) {
166 writer.println("*CarFeatureController*");
167 writer.println(" mEnabledFeatures:" + mEnabledFeatures);
168 writer.println(" mDefaultEnabledFeaturesFromConfig:" + mDefaultEnabledFeaturesFromConfig);
169 writer.println(" mDisabledFeaturesFromVhal:" + mDisabledFeaturesFromVhal);
170 synchronized (mLock) {
171 writer.println(" mAvailableExperimentalFeatures:" + mAvailableExperimentalFeatures);
172 writer.println(" mPendingEnabledFeatures:" + mPendingEnabledFeatures);
173 writer.println(" mPendingDisabledFeatures:" + mPendingDisabledFeatures);
174 }
175 }
176
177 /** Check {@link Car#isFeatureEnabled(String)} */
178 public boolean isFeatureEnabled(String featureName) {
179 return mEnabledFeatures.contains(featureName);
180 }
181
182 @FeaturerRequestEnum
183 private int checkFeatureExisting(String featureName) {
184 if (MANDATORY_FEATURES.contains(featureName)) {
185 return Car.FEATURE_REQUEST_MANDATORY;
186 }
187 if (!OPTIONAL_FEATURES.contains(featureName)) {
188 synchronized (mLock) {
189 if (!mAvailableExperimentalFeatures.contains(featureName)) {
190 Log.e(TAG, "enableFeature requested for non-existing feature:"
191 + featureName);
192 return Car.FEATURE_REQUEST_NOT_EXISTING;
193 }
194 }
195 }
196 return Car.FEATURE_REQUEST_SUCCESS;
197 }
198
199 /** Check {@link Car#enableFeature(String)} */
200 public int enableFeature(String featureName) {
201 assertPermission();
202 int checkResult = checkFeatureExisting(featureName);
203 if (checkResult != Car.FEATURE_REQUEST_SUCCESS) {
204 return checkResult;
205 }
206
207 boolean alreadyEnabled = mEnabledFeatures.contains(featureName);
208 boolean shouldUpdateConfigFile = false;
209 synchronized (mLock) {
210 if (mPendingDisabledFeatures.remove(featureName)) {
211 shouldUpdateConfigFile = true;
212 }
213 if (!mPendingEnabledFeatures.contains(featureName) && !alreadyEnabled) {
214 shouldUpdateConfigFile = true;
215 mPendingEnabledFeatures.add(featureName);
216 }
217 }
218 if (shouldUpdateConfigFile) {
219 Log.w(TAG, "Enabling feature in config file:" + featureName);
220 dispatchDefaultConfigUpdate();
221 }
222 if (alreadyEnabled) {
223 return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE;
224 } else {
225 return Car.FEATURE_REQUEST_SUCCESS;
226 }
227 }
228
229 /** Check {@link Car#disableFeature(String)} */
230 public int disableFeature(String featureName) {
231 assertPermission();
232 int checkResult = checkFeatureExisting(featureName);
233 if (checkResult != Car.FEATURE_REQUEST_SUCCESS) {
234 return checkResult;
235 }
236
237 boolean alreadyDisabled = !mEnabledFeatures.contains(featureName);
238 boolean shouldUpdateConfigFile = false;
239 synchronized (mLock) {
240 if (mPendingEnabledFeatures.remove(featureName)) {
241 shouldUpdateConfigFile = true;
242 }
243 if (!mPendingDisabledFeatures.contains(featureName) && !alreadyDisabled) {
244 shouldUpdateConfigFile = true;
245 mPendingDisabledFeatures.add(featureName);
246 }
247 }
248 if (shouldUpdateConfigFile) {
249 Log.w(TAG, "Disabling feature in config file:" + featureName);
250 dispatchDefaultConfigUpdate();
251 }
252 if (alreadyDisabled) {
253 return Car.FEATURE_REQUEST_ALREADY_IN_THE_STATE;
254 } else {
255 return Car.FEATURE_REQUEST_SUCCESS;
256 }
257 }
258
259 /**
260 * Set available experimental features. Only features set through this call will be allowed to
261 * be enabled for experimental features. Setting this is not allowed for USER build.
262 *
263 * @return True if set is allowed and set. False if experimental feature is not allowed.
264 */
265 public boolean setAvailableExperimentalFeatureList(List<String> experimentalFeatures) {
266 assertPermission();
267 if (Build.IS_USER) {
268 Log.e(TAG, "Experimental feature list set for USER build",
269 new RuntimeException());
270 return false;
271 }
272 synchronized (mLock) {
273 mAvailableExperimentalFeatures.clear();
274 mAvailableExperimentalFeatures.addAll(experimentalFeatures);
275 }
276 return true;
277 }
278
279 /** Check {@link Car#getAllEnabledFeatures()} */
280 public List<String> getAllEnabledFeatures() {
281 assertPermission();
282 return new ArrayList<>(mEnabledFeatures);
283 }
284
285 /** Check {@link Car#getAllPendingDisabledFeatures()} */
286 public List<String> getAllPendingDisabledFeatures() {
287 assertPermission();
288 synchronized (mLock) {
289 return new ArrayList<>(mPendingDisabledFeatures);
290 }
291 }
292
293 /** Check {@link Car#getAllPendingEnabledFeatures()} */
294 public List<String> getAllPendingEnabledFeatures() {
295 assertPermission();
296 synchronized (mLock) {
297 return new ArrayList<>(mPendingEnabledFeatures);
298 }
299 }
300
301 /** Returns currently enabled experimental features */
302 public @NonNull List<String> getEnabledExperimentalFeatures() {
303 if (Build.IS_USER) {
304 Log.e(TAG, "getEnabledExperimentalFeatures called in USER build",
305 new RuntimeException());
306 return Collections.emptyList();
307 }
308 ArrayList<String> experimentalFeature = new ArrayList<>();
309 for (String feature: mEnabledFeatures) {
310 if (MANDATORY_FEATURES.contains(feature)) {
311 continue;
312 }
313 if (OPTIONAL_FEATURES.contains(feature)) {
314 continue;
315 }
316 experimentalFeature.add(feature);
317 }
318 return experimentalFeature;
319 }
320
321 void handleCorruptConfigFileLocked(String msg, String line) {
322 Log.e(TAG, msg + ", considered as corrupt, line:" + line);
323 mEnabledFeatures.clear();
324 }
325
326 private boolean loadFromConfigFileLocked() {
327 // done without lock, should be only called from constructor.
328 FileInputStream fis;
329 try {
330 fis = mFeatureConfigFile.openRead();
331 } catch (FileNotFoundException e) {
332 Log.i(TAG, "Feature config file not found, this could be 1st boot");
333 return false;
334 }
335 try (BufferedReader reader = new BufferedReader(
336 new InputStreamReader(fis, StandardCharsets.UTF_8))) {
337 boolean lastLinePassed = false;
338 while (true) {
339 String line = reader.readLine();
340 if (line == null) {
341 if (!lastLinePassed) {
342 handleCorruptConfigFileLocked("No last line checksum", "");
343 return false;
344 }
345 break;
346 }
347 if (lastLinePassed && !line.isEmpty()) {
348 handleCorruptConfigFileLocked(
349 "Config file has additional line after last line marker", line);
350 return false;
351 } else {
352 if (line.startsWith(CONFIG_FILE_LAST_LINE_MARKER)) {
353 int numberOfFeatures;
354 try {
355 numberOfFeatures = Integer.valueOf(line.substring(
356 CONFIG_FILE_LAST_LINE_MARKER.length()));
357 } catch (NumberFormatException e) {
358 handleCorruptConfigFileLocked(
359 "Config file has corrupt last line, not a number",
360 line);
361 return false;
362 }
363 int actualNumberOfFeatures = mEnabledFeatures.size();
364 if (numberOfFeatures != actualNumberOfFeatures) {
365 handleCorruptConfigFileLocked(
366 "Config file has wrong number of features, expected:"
367 + numberOfFeatures
368 + " actual:" + actualNumberOfFeatures, line);
369 return false;
370 }
371 lastLinePassed = true;
372 } else {
373 mEnabledFeatures.add(line);
374 }
375 }
376 }
377 } catch (IOException e) {
378 Log.w(TAG, "Cannot load config file", e);
379 return false;
380 }
381 Log.i(TAG, "Loaded features:" + mEnabledFeatures);
382 return true;
383 }
384
385 private void persistToFeatureConfigFile() {
386 HashSet<String> features = new HashSet<>(mEnabledFeatures);
387 synchronized (mLock) {
388 features.removeAll(mPendingDisabledFeatures);
389 features.addAll(mPendingEnabledFeatures);
390 FileOutputStream fos;
391 try {
392 fos = mFeatureConfigFile.startWrite();
393 } catch (IOException e) {
394 Log.e(TAG, "Cannot create config file", e);
395 return;
396 }
397 try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos,
398 StandardCharsets.UTF_8))) {
399 Log.i(TAG, "Updating features:" + features);
400 for (String feature : features) {
401 writer.write(feature);
402 writer.newLine();
403 }
404 writer.write(CONFIG_FILE_LAST_LINE_MARKER + features.size());
405 writer.flush();
406 mFeatureConfigFile.finishWrite(fos);
407 } catch (IOException e) {
408 mFeatureConfigFile.failWrite(fos);
409 Log.e(TAG, "Cannot create config file", e);
410 }
411 }
412 }
413
414 private void assertPermission() {
415 ICarImpl.assertPermission(mContext, Car.PERMISSION_CONTROL_CAR_FEATURES);
416 }
417
418 private void dispatchDefaultConfigUpdate() {
419 mHandler.removeCallbacks(mDefaultConfigWriter);
420 mHandler.post(mDefaultConfigWriter);
421 }
422
423 private void parseDefaultConfig() {
424 for (String feature : mDefaultEnabledFeaturesFromConfig) {
425 if (!OPTIONAL_FEATURES.contains(feature)) {
426 throw new IllegalArgumentException(
427 "config_default_enabled_optional_car_features include non-optional "
428 + "features:" + feature);
429 }
430 if (mDisabledFeaturesFromVhal.contains(feature)) {
431 continue;
432 }
433 mEnabledFeatures.add(feature);
434 }
435 Log.i(TAG, "Loaded default features:" + mEnabledFeatures);
436 }
437}