| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wifi; |
| |
| import android.app.AlarmManager; |
| import android.content.Context; |
| import android.os.Environment; |
| import android.os.FileUtils; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.AtomicFile; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| |
| /** |
| * This class provides the API's to save/load/modify network configurations from a persistent |
| * store. Uses keystore for certificate/key management operations. |
| * NOTE: This class should only be used from WifiConfigManager and is not thread-safe! |
| */ |
| public class WifiConfigStore { |
| /** |
| * Alarm tag to use for starting alarms for buffering file writes. |
| */ |
| @VisibleForTesting |
| public static final String BUFFERED_WRITE_ALARM_TAG = "WriteBufferAlarm"; |
| /** |
| * Log tag. |
| */ |
| private static final String TAG = "WifiConfigStore"; |
| /** |
| * Config store file name for both shared & user specific stores. |
| */ |
| private static final String STORE_FILE_NAME = "WifiConfigStore.xml"; |
| /** |
| * Directory to store the config store files in. |
| */ |
| private static final String STORE_DIRECTORY_NAME = "wifi"; |
| /** |
| * Time interval for buffering file writes for non-forced writes |
| */ |
| private static final int BUFFERED_WRITE_ALARM_INTERVAL_MS = 10 * 1000; |
| /** |
| * Handler instance to post alarm timeouts to |
| */ |
| private final Handler mEventHandler; |
| /** |
| * Alarm manager instance to start buffer timeout alarms. |
| */ |
| private final AlarmManager mAlarmManager; |
| /** |
| * Clock instance to retrieve timestamps for alarms. |
| */ |
| private final Clock mClock; |
| /** |
| * Shared config store file instance. |
| */ |
| private StoreFile mSharedStore; |
| /** |
| * User specific store file instance. |
| */ |
| private StoreFile mUserStore; |
| /** |
| * Verbose logging flag. |
| */ |
| private boolean mVerboseLoggingEnabled = false; |
| /** |
| * Flag to indicate if there is a buffered write pending. |
| */ |
| private boolean mBufferedWritePending = false; |
| /** |
| * Alarm listener for flushing out any buffered writes. |
| */ |
| private final AlarmManager.OnAlarmListener mBufferedWriteListener = |
| new AlarmManager.OnAlarmListener() { |
| public void onAlarm() { |
| try { |
| writeBufferedData(); |
| } catch (IOException e) { |
| Log.wtf(TAG, "Buffered write failed", e); |
| } |
| |
| } |
| }; |
| |
| /** |
| * Create a new instance of WifiConfigStore. |
| * Note: The store file instances have been made inputs to this class to ease unit-testing. |
| * |
| * @param context context to use for retrieving the alarm manager. |
| * @param looper looper instance to post alarm timeouts to. |
| * @param clock clock instance to retrieve timestamps for alarms. |
| * @param sharedStore StoreFile instance pointing to the shared store file. This should |
| * be retrieved using {@link #createSharedFile()} method. |
| */ |
| public WifiConfigStore(Context context, Looper looper, Clock clock, |
| StoreFile sharedStore) { |
| |
| mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); |
| mEventHandler = new Handler(looper); |
| mClock = clock; |
| |
| // Initialize the store files. |
| mSharedStore = sharedStore; |
| // The user store is initialized to null, this will be set when the user unlocks and |
| // CE storage is accessible via |switchUserStoreAndRead|. |
| mUserStore = null; |
| } |
| |
| /** |
| * Helper method to create a store file instance for either the shared store or user store. |
| * Note: The method creates the store directory if not already present. This may be needed for |
| * user store files. |
| * |
| * @param storeBaseDir Base directory under which the store file is to be stored. The store file |
| * will be at <storeBaseDir>/wifi/WifiConfigStore.xml. |
| * @return new instance of the store file. |
| */ |
| private static StoreFile createFile(File storeBaseDir) { |
| File storeDir = new File(storeBaseDir, STORE_DIRECTORY_NAME); |
| if (!storeDir.exists()) { |
| if (!storeDir.mkdir()) { |
| Log.w(TAG, "Could not create store directory " + storeDir); |
| } |
| } |
| return new StoreFile(new File(storeDir, STORE_FILE_NAME)); |
| } |
| |
| /** |
| * Create a new instance of the shared store file. |
| * |
| * @return new instance of the store file or null if the directory cannot be created. |
| */ |
| public static StoreFile createSharedFile() { |
| return createFile(Environment.getDataMiscDirectory()); |
| } |
| |
| /** |
| * Create a new instance of the user specific store file. |
| * The user store file is inside the user's encrypted data directory. |
| * |
| * @param userId userId corresponding to the currently logged-in user. |
| * @return new instance of the store file or null if the directory cannot be created. |
| */ |
| public static StoreFile createUserFile(int userId) { |
| return createFile(Environment.getDataMiscCeDirectory(userId)); |
| } |
| |
| /** |
| * Enable verbose logging. |
| */ |
| public void enableVerboseLogging(boolean verbose) { |
| mVerboseLoggingEnabled = verbose; |
| } |
| |
| /** |
| * API to check if any of the store files are present on the device. This can be used |
| * to detect if the device needs to perform data migration from legacy stores. |
| * |
| * @return true if any of the store file is present, false otherwise. |
| */ |
| public boolean areStoresPresent() { |
| return (mSharedStore.exists() || (mUserStore != null && mUserStore.exists())); |
| } |
| |
| /** |
| * API to write the provided store data to config stores. |
| * The method writes the user specific configurations to user specific config store and the |
| * shared configurations to shared config store. |
| * |
| * @param forceSync boolean to force write the config stores now. if false, the writes are |
| * buffered and written after the configured interval. |
| * @param storeData The entire data to be stored across all the config store files. |
| */ |
| public void write(boolean forceSync, WifiConfigStoreData storeData) |
| throws XmlPullParserException, IOException { |
| // Serialize the provided data and send it to the respective stores. The actual write will |
| // be performed later depending on the |forceSync| flag . |
| byte[] sharedDataBytes = storeData.createSharedRawData(); |
| mSharedStore.storeRawDataToWrite(sharedDataBytes); |
| if (mUserStore != null) { |
| byte[] userDataBytes = storeData.createUserRawData(); |
| mUserStore.storeRawDataToWrite(userDataBytes); |
| } |
| |
| // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides any |
| // pending buffer writes. |
| if (forceSync) { |
| writeBufferedData(); |
| } else { |
| startBufferedWriteAlarm(); |
| } |
| } |
| |
| /** |
| * Helper method to start a buffered write alarm if one doesn't already exist. |
| */ |
| private void startBufferedWriteAlarm() { |
| if (!mBufferedWritePending) { |
| mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, |
| mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS, |
| BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler); |
| mBufferedWritePending = true; |
| } |
| } |
| |
| /** |
| * Helper method to stop a buffered write alarm if one exists. |
| */ |
| private void stopBufferedWriteAlarm() { |
| if (mBufferedWritePending) { |
| mAlarmManager.cancel(mBufferedWriteListener); |
| mBufferedWritePending = false; |
| } |
| } |
| |
| /** |
| * Helper method to actually perform the writes to the file. This flushes out any write data |
| * being buffered in the respective stores and cancels any pending buffer write alarms. |
| */ |
| private void writeBufferedData() throws IOException { |
| stopBufferedWriteAlarm(); |
| |
| long writeStartTime = mClock.getElapsedSinceBootMillis(); |
| mSharedStore.writeBufferedRawData(); |
| if (mUserStore != null) { |
| mUserStore.writeBufferedRawData(); |
| } |
| long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime; |
| |
| Log.d(TAG, "Writing to stores completed in " + writeTime + " ms."); |
| } |
| |
| /** |
| * API to read the store data from the config stores. |
| * The method reads the user specific configurations from user specific config store and the |
| * shared configurations from the shared config store. |
| * |
| * @return storeData The entire data retrieved across all the config store files. |
| */ |
| public WifiConfigStoreData read() throws XmlPullParserException, IOException { |
| long readStartTime = mClock.getElapsedSinceBootMillis(); |
| byte[] sharedDataBytes = mSharedStore.readRawData(); |
| byte[] userDataBytes = null; |
| if (mUserStore != null) { |
| userDataBytes = mUserStore.readRawData(); |
| } |
| long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; |
| Log.d(TAG, "Reading from stores completed in " + readTime + " ms."); |
| |
| return WifiConfigStoreData.parseRawData(sharedDataBytes, userDataBytes); |
| } |
| |
| /** |
| * Handles a user switch. This method changes the user specific store file and reads from the |
| * new user's store file. |
| * |
| * @param userStore StoreFile instance pointing to the user specific store file. This should |
| * be retrieved using {@link #createUserFile(int)} method. |
| */ |
| public WifiConfigStoreData switchUserStoreAndRead(StoreFile userStore) |
| throws XmlPullParserException, IOException { |
| // Stop any pending buffered writes, if any. |
| stopBufferedWriteAlarm(); |
| mUserStore = userStore; |
| |
| // Now read from the user store file. |
| long readStartTime = mClock.getElapsedSinceBootMillis(); |
| byte[] userDataBytes = mUserStore.readRawData(); |
| long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; |
| Log.d(TAG, "Reading from user store completed in " + readTime + " ms."); |
| |
| return WifiConfigStoreData.parseRawData(null, userDataBytes); |
| } |
| |
| /** |
| * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read |
| * raw data from the persistent file. This class provides helper methods to read/write the |
| * entire file into a byte array. |
| * This helps to separate out the processing/parsing from the actual file writing. |
| */ |
| public static class StoreFile { |
| /** |
| * File permissions to lock down the file. |
| */ |
| private static final int FILE_MODE = 0600; |
| /** |
| * The store file to be written to. |
| */ |
| private final AtomicFile mAtomicFile; |
| /** |
| * This is an intermediate buffer to store the data to be written. |
| */ |
| private byte[] mWriteData; |
| /** |
| * Store the file name for setting the file permissions/logging purposes. |
| */ |
| private String mFileName; |
| |
| public StoreFile(File file) { |
| mAtomicFile = new AtomicFile(file); |
| mFileName = mAtomicFile.getBaseFile().getAbsolutePath(); |
| } |
| |
| /** |
| * Returns whether the store file already exists on disk or not. |
| * |
| * @return true if it exists, false otherwise. |
| */ |
| public boolean exists() { |
| return mAtomicFile.exists(); |
| } |
| |
| /** |
| * Read the entire raw data from the store file and return in a byte array. |
| * |
| * @return raw data read from the file or null if the file is not found. |
| * @throws IOException if an error occurs. The input stream is always closed by the method |
| * even when an exception is encountered. |
| */ |
| public byte[] readRawData() throws IOException { |
| try { |
| return mAtomicFile.readFully(); |
| } catch (FileNotFoundException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Store the provided byte array to be written when {@link #writeBufferedRawData()} method |
| * is invoked. |
| * This intermediate step is needed to help in buffering file writes. |
| * |
| * @param data raw data to be written to the file. |
| */ |
| public void storeRawDataToWrite(byte[] data) { |
| mWriteData = data; |
| } |
| |
| /** |
| * Write the stored raw data to the store file. |
| * After the write to file, the mWriteData member is reset. |
| * @throws IOException if an error occurs. The output stream is always closed by the method |
| * even when an exception is encountered. |
| */ |
| public void writeBufferedRawData() throws IOException { |
| if (mWriteData == null) { |
| Log.w(TAG, "No data stored for writing to file: " + mFileName); |
| return; |
| } |
| // Write the data to the atomic file. |
| FileOutputStream out = null; |
| try { |
| out = mAtomicFile.startWrite(); |
| FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1); |
| out.write(mWriteData); |
| mAtomicFile.finishWrite(out); |
| } catch (IOException e) { |
| if (out != null) { |
| mAtomicFile.failWrite(out); |
| } |
| throw e; |
| } |
| // Reset the pending write data after write. |
| mWriteData = null; |
| } |
| } |
| } |