• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.thread;
18 
19 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.ApexEnvironment;
24 import android.content.Context;
25 import android.net.thread.ThreadConfiguration;
26 import android.os.PersistableBundle;
27 import android.util.AtomicFile;
28 
29 import com.android.connectivity.resources.R;
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.net.module.util.SharedLog;
33 import com.android.server.connectivity.ConnectivityResources;
34 
35 import java.io.ByteArrayInputStream;
36 import java.io.ByteArrayOutputStream;
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.InputStream;
43 import java.util.HashMap;
44 import java.util.Map;
45 
46 /**
47  * Store persistent data for Thread network settings. These are key (string) / value pairs that are
48  * stored in ThreadPersistentSetting.xml file. The values allowed are those that can be serialized
49  * via {@link PersistableBundle}.
50  */
51 public class ThreadPersistentSettings {
52     private static final String TAG = "ThreadPersistentSettings";
53     private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
54 
55     /** File name used for storing settings. */
56     private static final String FILE_NAME = "ThreadPersistentSettings.xml";
57 
58     /** Current config store data version. This MUST be incremented for any incompatible changes. */
59     private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
60 
61     /**
62      * Stores the version of the data. This can be used to handle migration of data if some
63      * non-backward compatible change introduced.
64      */
65     private static final String KEY_VERSION = "version";
66 
67     /**
68      * Saves the boolean flag for Thread being enabled. The value defaults to resource overlay value
69      * {@code R.bool.config_thread_default_enabled}.
70      */
71     public static final Key<Boolean> KEY_THREAD_ENABLED = new Key<>("thread_enabled");
72 
73     /**
74      * Saves the boolean flag for border router being enabled. The value defaults to resource
75      * overlay value {@code R.bool.config_thread_border_router_default_enabled}.
76      */
77     private static final Key<Boolean> KEY_CONFIG_BORDER_ROUTER_ENABLED =
78             new Key<>("config_border_router_enabled");
79 
80     /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
81     private static final Key<Boolean> KEY_CONFIG_NAT64_ENABLED = new Key<>("config_nat64_enabled");
82 
83     /**
84      * Stores the Thread DHCPv6-PD feature toggle state, true for enabled and false for disabled.
85      */
86     private static final Key<Boolean> KEY_CONFIG_DHCP6_PD_ENABLED =
87             new Key<>("config_dhcp6_pd_enabled");
88 
89     /**
90      * Indicates that Thread was enabled (i.e. via the setEnabled() API) when the airplane mode is
91      * turned on in settings. When this value is {@code true}, the current airplane mode state will
92      * be ignored when evaluating the Thread enabled state.
93      */
94     public static final Key<Boolean> KEY_THREAD_ENABLED_IN_AIRPLANE_MODE =
95             new Key<>("thread_enabled_in_airplane_mode");
96 
97     /** Stores the Thread country code, null if no country code is stored. */
98     public static final Key<String> KEY_COUNTRY_CODE = new Key<>("thread_country_code");
99 
100     @GuardedBy("mLock")
101     private final AtomicFile mAtomicFile;
102 
103     private final Object mLock = new Object();
104 
105     private final Map<String, Object> mDefaultValues = new HashMap<>();
106 
107     @GuardedBy("mLock")
108     private final PersistableBundle mSettings = new PersistableBundle();
109 
110     private final ConnectivityResources mResources;
111 
newInstance(Context context)112     public static ThreadPersistentSettings newInstance(Context context) {
113         return new ThreadPersistentSettings(
114                 new AtomicFile(new File(getOrCreateThreadNetworkDir(), FILE_NAME)),
115                 new ConnectivityResources(context));
116     }
117 
118     @VisibleForTesting
ThreadPersistentSettings(AtomicFile atomicFile, ConnectivityResources resources)119     ThreadPersistentSettings(AtomicFile atomicFile, ConnectivityResources resources) {
120         mAtomicFile = atomicFile;
121         mResources = resources;
122 
123         mDefaultValues.put(
124                 KEY_THREAD_ENABLED.key,
125                 mResources.get().getBoolean(R.bool.config_thread_default_enabled));
126         mDefaultValues.put(
127                 KEY_CONFIG_BORDER_ROUTER_ENABLED.key,
128                 mResources.get().getBoolean(R.bool.config_thread_border_router_default_enabled));
129         mDefaultValues.put(KEY_CONFIG_NAT64_ENABLED.key, false);
130         mDefaultValues.put(KEY_CONFIG_DHCP6_PD_ENABLED.key, false);
131         mDefaultValues.put(KEY_THREAD_ENABLED_IN_AIRPLANE_MODE.key, false);
132         mDefaultValues.put(KEY_COUNTRY_CODE.key, null);
133     }
134 
135     /** Initialize the settings by reading from the settings file. */
initialize()136     public void initialize() {
137         readFromStoreFile();
138     }
139 
putObject(String key, @Nullable Object value)140     private void putObject(String key, @Nullable Object value) {
141         synchronized (mLock) {
142             if (value == null) {
143                 mSettings.putString(key, null);
144             } else if (value instanceof Boolean) {
145                 mSettings.putBoolean(key, (Boolean) value);
146             } else if (value instanceof Integer) {
147                 mSettings.putInt(key, (Integer) value);
148             } else if (value instanceof Long) {
149                 mSettings.putLong(key, (Long) value);
150             } else if (value instanceof Double) {
151                 mSettings.putDouble(key, (Double) value);
152             } else if (value instanceof String) {
153                 mSettings.putString(key, (String) value);
154             } else {
155                 throw new IllegalArgumentException("Unsupported type " + value.getClass());
156             }
157         }
158     }
159 
getObject(String key, T defaultValue)160     private <T> T getObject(String key, T defaultValue) {
161         Object value;
162         synchronized (mLock) {
163             if (defaultValue == null) {
164                 value = mSettings.getString(key, null);
165             } else if (defaultValue instanceof Boolean) {
166                 value = mSettings.getBoolean(key, (Boolean) defaultValue);
167             } else if (defaultValue instanceof Integer) {
168                 value = mSettings.getInt(key, (Integer) defaultValue);
169             } else if (defaultValue instanceof Long) {
170                 value = mSettings.getLong(key, (Long) defaultValue);
171             } else if (defaultValue instanceof Double) {
172                 value = mSettings.getDouble(key, (Double) defaultValue);
173             } else if (defaultValue instanceof String) {
174                 value = mSettings.getString(key, (String) defaultValue);
175             } else {
176                 throw new IllegalArgumentException("Unsupported type " + defaultValue.getClass());
177             }
178         }
179         return (T) value;
180     }
181 
182     /** Stores a value to the stored settings. */
put(Key<T> key, @Nullable T value)183     public <T> void put(Key<T> key, @Nullable T value) {
184         putObject(key.key, value);
185         writeToStoreFile();
186     }
187 
188     /** Retrieves a value from the stored settings. */
189     @Nullable
get(Key<T> key)190     public <T> T get(Key<T> key) {
191         T defaultValue = (T) mDefaultValues.get(key.key);
192         return getObject(key.key, defaultValue);
193     }
194 
195     /**
196      * Store a {@link ThreadConfiguration} to the persistent settings.
197      *
198      * @param configuration {@link ThreadConfiguration} to be stored.
199      * @return {@code true} if the configuration was changed, {@code false} otherwise.
200      */
putConfiguration(@onNull ThreadConfiguration configuration)201     public boolean putConfiguration(@NonNull ThreadConfiguration configuration) {
202         if (getConfiguration().equals(configuration)) {
203             return false;
204         }
205         put(KEY_CONFIG_BORDER_ROUTER_ENABLED, configuration.isBorderRouterEnabled());
206         put(KEY_CONFIG_NAT64_ENABLED, configuration.isNat64Enabled());
207         put(KEY_CONFIG_DHCP6_PD_ENABLED, configuration.isDhcpv6PdEnabled());
208         writeToStoreFile();
209         return true;
210     }
211 
212     /** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
getConfiguration()213     public ThreadConfiguration getConfiguration() {
214         return new ThreadConfiguration.Builder()
215                 .setBorderRouterEnabled(get(KEY_CONFIG_BORDER_ROUTER_ENABLED))
216                 .setNat64Enabled(get(KEY_CONFIG_NAT64_ENABLED))
217                 .setDhcpv6PdEnabled(get(KEY_CONFIG_DHCP6_PD_ENABLED))
218                 .build();
219     }
220 
221     /**
222      * Base class to store string key and its default value.
223      *
224      * @param <T> Type of the value.
225      */
226     public static final class Key<T> {
227         @VisibleForTesting final String key;
228 
Key(String key)229         private Key(String key) {
230             this.key = key;
231         }
232     }
233 
writeToStoreFile()234     private void writeToStoreFile() {
235         try {
236             final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
237             final PersistableBundle bundleToWrite;
238             synchronized (mLock) {
239                 bundleToWrite = new PersistableBundle(mSettings);
240             }
241             bundleToWrite.putInt(KEY_VERSION, CURRENT_SETTINGS_STORE_DATA_VERSION);
242             bundleToWrite.writeToStream(outputStream);
243             synchronized (mLock) {
244                 writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
245             }
246         } catch (IOException e) {
247             LOG.wtf("Write to store file failed", e);
248         }
249     }
250 
readFromStoreFile()251     private void readFromStoreFile() {
252         try {
253             final byte[] readData;
254             synchronized (mLock) {
255                 LOG.i("Reading from store file: " + mAtomicFile.getBaseFile());
256                 readData = readFromAtomicFile(mAtomicFile);
257             }
258             final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
259             final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream);
260             // Version unused for now. May be needed in the future for handling migrations.
261             bundleRead.remove(KEY_VERSION);
262             synchronized (mLock) {
263                 mSettings.putAll(bundleRead);
264             }
265         } catch (FileNotFoundException e) {
266             LOG.w("No store file to read " + e.getMessage());
267         } catch (IOException e) {
268             LOG.e("Read from store file failed", e);
269         }
270     }
271 
272     /**
273      * Read raw data from the atomic file. Note: This is a copy of {@link AtomicFile#readFully()}
274      * modified to use the passed in {@link InputStream} which was returned using {@link
275      * AtomicFile#openRead()}.
276      */
readFromAtomicFile(AtomicFile file)277     private static byte[] readFromAtomicFile(AtomicFile file) throws IOException {
278         FileInputStream stream = null;
279         try {
280             stream = file.openRead();
281             int pos = 0;
282             int avail = stream.available();
283             byte[] data = new byte[avail];
284             while (true) {
285                 int amt = stream.read(data, pos, data.length - pos);
286                 if (amt <= 0) {
287                     return data;
288                 }
289                 pos += amt;
290                 avail = stream.available();
291                 if (avail > data.length - pos) {
292                     byte[] newData = new byte[pos + avail];
293                     System.arraycopy(data, 0, newData, 0, pos);
294                     data = newData;
295                 }
296             }
297         } finally {
298             if (stream != null) stream.close();
299         }
300     }
301 
302     /** Write the raw data to the atomic file. */
writeToAtomicFile(AtomicFile file, byte[] data)303     private static void writeToAtomicFile(AtomicFile file, byte[] data) throws IOException {
304         // Write the data to the atomic file.
305         FileOutputStream out = null;
306         try {
307             out = file.startWrite();
308             out.write(data);
309             file.finishWrite(out);
310         } catch (IOException e) {
311             if (out != null) {
312                 file.failWrite(out);
313             }
314             throw e;
315         }
316     }
317 
318     /** Get device protected storage dir for the tethering apex. */
getOrCreateThreadNetworkDir()319     private static File getOrCreateThreadNetworkDir() {
320         final File threadnetworkDir;
321         final File apexDataDir =
322                 ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
323                         .getDeviceProtectedDataDir();
324         threadnetworkDir = new File(apexDataDir, "thread");
325 
326         if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
327             return threadnetworkDir;
328         }
329         throw new IllegalStateException(
330                 "Cannot write into thread network data directory: " + threadnetworkDir);
331     }
332 }
333