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