WifiConfigStore.java revision b88000ed5302860e71e32646695daf661f56d927
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"); 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 * @param userStore StoreFile instance pointing to the user specific store file. This should 117 * be retrieved using {@link #createUserFile(int)} method. 118 */ 119 public WifiConfigStore(Context context, Looper looper, Clock clock, 120 StoreFile sharedStore, StoreFile userStore) { 121 122 mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 123 mEventHandler = new Handler(looper); 124 mClock = clock; 125 126 // Initialize the store files. 127 mSharedStore = sharedStore; 128 mUserStore = userStore; 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.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 byte[] userDataBytes = storeData.createUserRawData(); 202 203 mSharedStore.storeRawDataToWrite(sharedDataBytes); 204 mUserStore.storeRawDataToWrite(userDataBytes); 205 206 // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides any 207 // pending buffer writes. 208 if (forceSync) { 209 writeBufferedData(); 210 } else { 211 startBufferedWriteAlarm(); 212 } 213 } 214 215 /** 216 * Helper method to start a buffered write alarm if one doesn't already exist. 217 */ 218 private void startBufferedWriteAlarm() { 219 if (!mBufferedWritePending) { 220 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 221 mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS, 222 BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler); 223 mBufferedWritePending = true; 224 } 225 } 226 227 /** 228 * Helper method to stop a buffered write alarm if one exists. 229 */ 230 private void stopBufferedWriteAlarm() { 231 if (mBufferedWritePending) { 232 mAlarmManager.cancel(mBufferedWriteListener); 233 mBufferedWritePending = false; 234 } 235 } 236 237 /** 238 * Helper method to actually perform the writes to the file. This flushes out any write data 239 * being buffered in the respective stores and cancels any pending buffer write alarms. 240 */ 241 private void writeBufferedData() throws IOException { 242 stopBufferedWriteAlarm(); 243 244 long writeStartTime = mClock.getElapsedSinceBootMillis(); 245 mSharedStore.writeBufferedRawData(); 246 mUserStore.writeBufferedRawData(); 247 long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime; 248 249 Log.d(TAG, "Writing to stores completed in " + writeTime + " ms."); 250 } 251 252 /** 253 * API to read the store data from the config stores. 254 * The method reads the user specific configurations from user specific config store and the 255 * shared configurations from the shared config store. 256 * 257 * @return storeData The entire data retrieved across all the config store files. 258 */ 259 public WifiConfigStoreData read() throws XmlPullParserException, IOException { 260 long readStartTime = mClock.getElapsedSinceBootMillis(); 261 byte[] sharedDataBytes = mSharedStore.readRawData(); 262 byte[] userDataBytes = mUserStore.readRawData(); 263 long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; 264 265 Log.d(TAG, "Reading from stores completed in " + readTime + " ms."); 266 267 return WifiConfigStoreData.parseRawData(sharedDataBytes, userDataBytes); 268 } 269 270 /** 271 * Handle a user switch. This changes the user specific store. 272 * 273 * @param userStore StoreFile instance pointing to the user specific store file. This should 274 * be retrieved using {@link #createUserFile(int)} method. 275 */ 276 public void switchUserStore(StoreFile userStore) { 277 // Stop any pending buffered writes, if any. 278 stopBufferedWriteAlarm(); 279 mUserStore = userStore; 280 } 281 282 /** 283 * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read 284 * raw data from the persistent file. This class provides helper methods to read/write the 285 * entire file into a byte array. 286 * This helps to separate out the processing/parsing from the actual file writing. 287 */ 288 public static class StoreFile { 289 /** 290 * File permissions to lock down the file. 291 */ 292 private static final int FILE_MODE = 0600; 293 /** 294 * The store file to be written to. 295 */ 296 private final AtomicFile mAtomicFile; 297 /** 298 * This is an intermediate buffer to store the data to be written. 299 */ 300 private byte[] mWriteData; 301 /** 302 * Store the file name for setting the file permissions/logging purposes. 303 */ 304 private String mFileName; 305 306 public StoreFile(File file) { 307 mAtomicFile = new AtomicFile(file); 308 mFileName = mAtomicFile.getBaseFile().getAbsolutePath(); 309 } 310 311 /** 312 * Returns whether the store file already exists on disk or not. 313 * 314 * @return true if it exists, false otherwise. 315 */ 316 public boolean exists() { 317 return mAtomicFile.exists(); 318 } 319 320 /** 321 * Read the entire raw data from the store file and return in a byte array. 322 * 323 * @return raw data read from the file or null if the file is not found. 324 * @throws IOException if an error occurs. The input stream is always closed by the method 325 * even when an exception is encountered. 326 */ 327 public byte[] readRawData() throws IOException { 328 try { 329 return mAtomicFile.readFully(); 330 } catch (FileNotFoundException e) { 331 return null; 332 } 333 } 334 335 /** 336 * Store the provided byte array to be written when {@link #writeBufferedRawData()} method 337 * is invoked. 338 * This intermediate step is needed to help in buffering file writes. 339 * 340 * @param data raw data to be written to the file. 341 */ 342 public void storeRawDataToWrite(byte[] data) { 343 mWriteData = data; 344 } 345 346 /** 347 * Write the stored raw data to the store file. 348 * After the write to file, the mWriteData member is reset. 349 * @throws IOException if an error occurs. The output stream is always closed by the method 350 * even when an exception is encountered. 351 */ 352 public void writeBufferedRawData() throws IOException { 353 if (mWriteData == null) { 354 Log.w(TAG, "No data stored for writing to file: " + mFileName); 355 return; 356 } 357 // Write the data to the atomic file. 358 FileOutputStream out = null; 359 try { 360 out = mAtomicFile.startWrite(); 361 FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1); 362 out.write(mWriteData); 363 mAtomicFile.finishWrite(out); 364 } catch (IOException e) { 365 if (out != null) { 366 mAtomicFile.failWrite(out); 367 } 368 throw e; 369 } 370 // Reset the pending write data after write. 371 mWriteData = null; 372 } 373 } 374} 375