1 /*
2  * Copyright (C) 2023 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.server.deviceconfig;
18 
19 import android.annotation.SuppressLint;
20 import android.provider.DeviceConfig;
21 import android.util.Slog;
22 
23 import java.io.IOException;
24 import java.net.URI;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.util.stream.Stream;
28 
29 /**
30  * @hide
31  */
32 public class DeviceConfigBootstrapValues {
33     private static final String TAG = "DeviceConfig";
34     private static final String SYSTEM_OVERRIDES_PATH = "file:///system/etc/device-config-defaults";
35     private static final String META_NAMESPACE = "DeviceConfigBootstrapValues";
36     private static final String META_KEY = "processed_values";
37 
38     private final String defaultValuesPath;
39 
DeviceConfigBootstrapValues()40     public DeviceConfigBootstrapValues() {
41         this(SYSTEM_OVERRIDES_PATH);
42     }
43 
DeviceConfigBootstrapValues(String defaultValuesPath)44     public DeviceConfigBootstrapValues(String defaultValuesPath) {
45         this.defaultValuesPath = defaultValuesPath;
46     }
47 
48     /**
49      * Performs the logic to apply bootstrap values when needed.
50      *
51      * If a file with the bootstrap values exists and they haven't been parsed before,
52      * it will parse the file and apply the values.
53      *
54      * @throws IOException if there's a problem reading the bootstrap file
55      * @throws RuntimeException if setting the values in DeviceConfig throws an exception
56      */
applyValuesIfNeeded()57     public void applyValuesIfNeeded() throws IOException {
58         if (getPath().toFile().exists()) {
59             if (checkIfHasAlreadyParsedBootstrapValues()) {
60                 Slog.i(TAG, "Bootstrap values already parsed, not processing again");
61             } else {
62                 parseAndApplyBootstrapValues();
63                 Slog.i(TAG, "Parsed bootstrap values");
64             }
65         } else {
66             Slog.i(TAG, "Bootstrap values not found");
67         }
68     }
69 
70     @SuppressLint("MissingPermission")
checkIfHasAlreadyParsedBootstrapValues()71     private boolean checkIfHasAlreadyParsedBootstrapValues() {
72         DeviceConfig.Properties properties = DeviceConfig.getProperties(META_NAMESPACE);
73         return properties.getKeyset().size() > 0;
74     }
75 
76     @SuppressLint("MissingPermission")
parseAndApplyBootstrapValues()77     private void parseAndApplyBootstrapValues() throws IOException {
78         Path path = getPath();
79         try (Stream<String> lines = Files.lines(path)) {
80             lines.forEach(line -> processLine(line));
81         }
82         // store a property in DeviceConfig so that we know we have successufully
83         // processed this
84         writeToDeviceConfig(META_NAMESPACE, META_KEY, "true");
85     }
86 
processLine(String line)87     private void processLine(String line) {
88         // contents for each line:
89         // <namespace>:<package>.<flag-name>=[enabled|disabled]
90         // we actually use <package>.<flag-name> combined in calls into DeviceConfig
91         int namespaceDelimiter = line.indexOf(':');
92         String namespace = line.substring(0, namespaceDelimiter);
93         if (namespaceDelimiter < 1) {
94             throw new IllegalArgumentException("Unexpectedly found : at index "
95                     + namespaceDelimiter);
96         }
97         int valueDelimiter = line.indexOf('=');
98         if (valueDelimiter < 5) {
99             throw new IllegalArgumentException("Unexpectedly found = at index " + valueDelimiter);
100         }
101         String key = line.substring(namespaceDelimiter + 1, valueDelimiter);
102         String value = line.substring(valueDelimiter + 1);
103         String val;
104         if ("enabled".equals(value)) {
105             val = "true";
106         } else if ("disabled".equals(value)) {
107             val = "false";
108         } else {
109             throw new IllegalArgumentException("Received unexpected value: " + value);
110         }
111         writeToDeviceConfig(namespace, key, val);
112     }
113 
114     @SuppressLint("MissingPermission")
writeToDeviceConfig(String namespace, String key, String value)115     private void writeToDeviceConfig(String namespace, String key, String value) {
116         boolean result = DeviceConfig.setProperty(namespace, key, value, /* makeDefault= */ true);
117         if (!result) {
118             throw new RuntimeException("Failed to set DeviceConfig property [" + namespace + "] "
119                     + key + "=" + value);
120         }
121     }
122 
getPath()123     private Path getPath() {
124         return Path.of(URI.create(defaultValuesPath));
125     }
126 }
127