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