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