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