WifiConfigStore.java revision 1a2b2242a2f30e0ad6dfa1d43265a15059db2a8a
1/* 2 * Copyright (C) 2016 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 17package com.android.server.wifi; 18 19import android.app.AlarmManager; 20import android.content.Context; 21import android.os.Environment; 22import android.os.FileUtils; 23import android.os.Handler; 24import android.os.Looper; 25import android.util.Log; 26 27import com.android.internal.annotations.VisibleForTesting; 28import com.android.internal.os.AtomicFile; 29 30import org.xmlpull.v1.XmlPullParserException; 31 32import java.io.File; 33import java.io.FileNotFoundException; 34import java.io.FileOutputStream; 35import java.io.IOException; 36 37/** 38 * This class provides the API's to save/load/modify network configurations from a persistent 39 * store. Uses keystore for certificate/key management operations. 40 * NOTE: This class should only be used from WifiConfigManager and is not thread-safe! 41 */ 42public class WifiConfigStore { 43 /** 44 * Alarm tag to use for starting alarms for buffering file writes. 45 */ 46 @VisibleForTesting 47 public static final String BUFFERED_WRITE_ALARM_TAG = "WriteBufferAlarm"; 48 /** 49 * Log tag. 50 */ 51 private static final String TAG = "WifiConfigStore"; 52 /** 53 * Config store file name for both shared & user specific stores. 54 */ 55 private static final String STORE_FILE_NAME = "WifiConfigStore.xml"; 56 /** 57 * Directory to store the config store files in. 58 */ 59 private static final String STORE_DIRECTORY_NAME = "wifi"; 60 /** 61 * Time interval for buffering file writes for non-forced writes 62 */ 63 private static final int BUFFERED_WRITE_ALARM_INTERVAL_MS = 10 * 1000; 64 /** 65 * Handler instance to post alarm timeouts to 66 */ 67 private final Handler mEventHandler; 68 /** 69 * Alarm manager instance to start buffer timeout alarms. 70 */ 71 private final AlarmManager mAlarmManager; 72 /** 73 * Clock instance to retrieve timestamps for alarms. 74 */ 75 private final Clock mClock; 76 /** 77 * Shared config store file instance. 78 */ 79 private StoreFile mSharedStore; 80 /** 81 * User specific store file instance. 82 */ 83 private StoreFile mUserStore; 84 /** 85 * Verbose logging flag. 86 */ 87 private boolean mVerboseLoggingEnabled = false; 88 /** 89 * Flag to indicate if there is a buffered write pending. 90 */ 91 private boolean mBufferedWritePending = false; 92 /** 93 * Alarm listener for flushing out any buffered writes. 94 */ 95 private final AlarmManager.OnAlarmListener mBufferedWriteListener = 96 new AlarmManager.OnAlarmListener() { 97 public void onAlarm() { 98 try { 99 writeBufferedData(); 100 } catch (IOException e) { 101 Log.wtf(TAG, "Buffered write failed", e); 102 } 103 104 } 105 }; 106 107 /** 108 * Create a new instance of WifiConfigStore. 109 * Note: The store file instances have been made inputs to this class to ease unit-testing. 110 * 111 * @param context context to use for retrieving the alarm manager. 112 * @param looper looper instance to post alarm timeouts to. 113 * @param clock clock instance to retrieve timestamps for alarms. 114 * @param sharedStore StoreFile instance pointing to the shared store file. This should 115 * be retrieved using {@link #createSharedFile()} method. 116 */ 117 public WifiConfigStore(Context context, Looper looper, Clock clock, 118 StoreFile sharedStore) { 119 120 mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 121 mEventHandler = new Handler(looper); 122 mClock = clock; 123 124 // Initialize the store files. 125 mSharedStore = sharedStore; 126 // The user store is initialized to null, this will be set when the user unlocks and 127 // CE storage is accessible via |switchUserStoreAndRead|. 128 mUserStore = null; 129 } 130 131 /** 132 * Helper method to create a store file instance for either the shared store or user store. 133 * Note: The method creates the store directory if not already present. This may be needed for 134 * user store files. 135 * 136 * @param storeBaseDir Base directory under which the store file is to be stored. The store file 137 * will be at <storeBaseDir>/wifi/WifiConfigStore.xml. 138 * @return new instance of the store file. 139 */ 140 private static StoreFile createFile(File storeBaseDir) { 141 File storeDir = new File(storeBaseDir, STORE_DIRECTORY_NAME); 142 if (!storeDir.exists()) { 143 if (!storeDir.mkdir()) { 144 Log.w(TAG, "Could not create store directory " + storeDir); 145 } 146 } 147 return new StoreFile(new File(storeDir, STORE_FILE_NAME)); 148 } 149 150 /** 151 * Create a new instance of the shared store file. 152 * 153 * @return new instance of the store file or null if the directory cannot be created. 154 */ 155 public static StoreFile createSharedFile() { 156 return createFile(Environment.getDataMiscDirectory()); 157 } 158 159 /** 160 * Create a new instance of the user specific store file. 161 * The user store file is inside the user's encrypted data directory. 162 * 163 * @param userId userId corresponding to the currently logged-in user. 164 * @return new instance of the store file or null if the directory cannot be created. 165 */ 166 public static StoreFile createUserFile(int userId) { 167 return createFile(Environment.getDataMiscCeDirectory(userId)); 168 } 169 170 /** 171 * Enable verbose logging. 172 */ 173 public void enableVerboseLogging(boolean verbose) { 174 mVerboseLoggingEnabled = verbose; 175 } 176 177 /** 178 * API to check if any of the store files are present on the device. This can be used 179 * to detect if the device needs to perform data migration from legacy stores. 180 * 181 * @return true if any of the store file is present, false otherwise. 182 */ 183 public boolean areStoresPresent() { 184 return (mSharedStore.exists() || (mUserStore != null && mUserStore.exists())); 185 } 186 187 /** 188 * API to write the provided store data to config stores. 189 * The method writes the user specific configurations to user specific config store and the 190 * shared configurations to shared config store. 191 * 192 * @param forceSync boolean to force write the config stores now. if false, the writes are 193 * buffered and written after the configured interval. 194 * @param storeData The entire data to be stored across all the config store files. 195 */ 196 public void write(boolean forceSync, WifiConfigStoreData storeData) 197 throws XmlPullParserException, IOException { 198 // Serialize the provided data and send it to the respective stores. The actual write will 199 // be performed later depending on the |forceSync| flag . 200 byte[] sharedDataBytes = storeData.createSharedRawData(); 201 mSharedStore.storeRawDataToWrite(sharedDataBytes); 202 if (mUserStore != null) { 203 byte[] userDataBytes = storeData.createUserRawData(); 204 mUserStore.storeRawDataToWrite(userDataBytes); 205 } 206 207 // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides any 208 // pending buffer writes. 209 if (forceSync) { 210 writeBufferedData(); 211 } else { 212 startBufferedWriteAlarm(); 213 } 214 } 215 216 /** 217 * Helper method to start a buffered write alarm if one doesn't already exist. 218 */ 219 private void startBufferedWriteAlarm() { 220 if (!mBufferedWritePending) { 221 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 222 mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS, 223 BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler); 224 mBufferedWritePending = true; 225 } 226 } 227 228 /** 229 * Helper method to stop a buffered write alarm if one exists. 230 */ 231 private void stopBufferedWriteAlarm() { 232 if (mBufferedWritePending) { 233 mAlarmManager.cancel(mBufferedWriteListener); 234 mBufferedWritePending = false; 235 } 236 } 237 238 /** 239 * Helper method to actually perform the writes to the file. This flushes out any write data 240 * being buffered in the respective stores and cancels any pending buffer write alarms. 241 */ 242 private void writeBufferedData() throws IOException { 243 stopBufferedWriteAlarm(); 244 245 long writeStartTime = mClock.getElapsedSinceBootMillis(); 246 mSharedStore.writeBufferedRawData(); 247 if (mUserStore != null) { 248 mUserStore.writeBufferedRawData(); 249 } 250 long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime; 251 252 Log.d(TAG, "Writing to stores completed in " + writeTime + " ms."); 253 } 254 255 /** 256 * API to read the store data from the config stores. 257 * The method reads the user specific configurations from user specific config store and the 258 * shared configurations from the shared config store. 259 * 260 * @return storeData The entire data retrieved across all the config store files. 261 */ 262 public WifiConfigStoreData read() throws XmlPullParserException, IOException { 263 long readStartTime = mClock.getElapsedSinceBootMillis(); 264 byte[] sharedDataBytes = mSharedStore.readRawData(); 265 byte[] userDataBytes = null; 266 if (mUserStore != null) { 267 userDataBytes = mUserStore.readRawData(); 268 } 269 long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; 270 Log.d(TAG, "Reading from stores completed in " + readTime + " ms."); 271 272 return WifiConfigStoreData.parseRawData(sharedDataBytes, userDataBytes); 273 } 274 275 /** 276 * Handles a user switch. This method changes the user specific store file and reads from the 277 * new user's store file. 278 * 279 * @param userStore StoreFile instance pointing to the user specific store file. This should 280 * be retrieved using {@link #createUserFile(int)} method. 281 */ 282 public WifiConfigStoreData switchUserStoreAndRead(StoreFile userStore) 283 throws XmlPullParserException, IOException { 284 // Stop any pending buffered writes, if any. 285 stopBufferedWriteAlarm(); 286 mUserStore = userStore; 287 288 // Now read from the user store file. 289 long readStartTime = mClock.getElapsedSinceBootMillis(); 290 byte[] userDataBytes = mUserStore.readRawData(); 291 long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; 292 Log.d(TAG, "Reading from user store completed in " + readTime + " ms."); 293 294 return WifiConfigStoreData.parseRawData(null, userDataBytes); 295 } 296 297 /** 298 * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read 299 * raw data from the persistent file. This class provides helper methods to read/write the 300 * entire file into a byte array. 301 * This helps to separate out the processing/parsing from the actual file writing. 302 */ 303 public static class StoreFile { 304 /** 305 * File permissions to lock down the file. 306 */ 307 private static final int FILE_MODE = 0600; 308 /** 309 * The store file to be written to. 310 */ 311 private final AtomicFile mAtomicFile; 312 /** 313 * This is an intermediate buffer to store the data to be written. 314 */ 315 private byte[] mWriteData; 316 /** 317 * Store the file name for setting the file permissions/logging purposes. 318 */ 319 private String mFileName; 320 321 public StoreFile(File file) { 322 mAtomicFile = new AtomicFile(file); 323 mFileName = mAtomicFile.getBaseFile().getAbsolutePath(); 324 } 325 326 /** 327 * Returns whether the store file already exists on disk or not. 328 * 329 * @return true if it exists, false otherwise. 330 */ 331 public boolean exists() { 332 return mAtomicFile.exists(); 333 } 334 335 /** 336 * Read the entire raw data from the store file and return in a byte array. 337 * 338 * @return raw data read from the file or null if the file is not found. 339 * @throws IOException if an error occurs. The input stream is always closed by the method 340 * even when an exception is encountered. 341 */ 342 public byte[] readRawData() throws IOException { 343 try { 344 return mAtomicFile.readFully(); 345 } catch (FileNotFoundException e) { 346 return null; 347 } 348 } 349 350 /** 351 * Store the provided byte array to be written when {@link #writeBufferedRawData()} method 352 * is invoked. 353 * This intermediate step is needed to help in buffering file writes. 354 * 355 * @param data raw data to be written to the file. 356 */ 357 public void storeRawDataToWrite(byte[] data) { 358 mWriteData = data; 359 } 360 361 /** 362 * Write the stored raw data to the store file. 363 * After the write to file, the mWriteData member is reset. 364 * @throws IOException if an error occurs. The output stream is always closed by the method 365 * even when an exception is encountered. 366 */ 367 public void writeBufferedRawData() throws IOException { 368 if (mWriteData == null) { 369 Log.w(TAG, "No data stored for writing to file: " + mFileName); 370 return; 371 } 372 // Write the data to the atomic file. 373 FileOutputStream out = null; 374 try { 375 out = mAtomicFile.startWrite(); 376 FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1); 377 out.write(mWriteData); 378 mAtomicFile.finishWrite(out); 379 } catch (IOException e) { 380 if (out != null) { 381 mAtomicFile.failWrite(out); 382 } 383 throw e; 384 } 385 // Reset the pending write data after write. 386 mWriteData = null; 387 } 388 } 389} 390