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