| /* |
| * 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 static java.lang.Math.toIntExact; |
| |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| 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 android.util.SparseArray; |
| import android.util.Xml; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.AtomicFile; |
| import com.android.internal.util.FastXmlSerializer; |
| import com.android.internal.util.Preconditions; |
| import com.android.server.wifi.util.DataIntegrityChecker; |
| import com.android.server.wifi.util.XmlUtil; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.nio.charset.StandardCharsets; |
| import java.security.DigestException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| |
| /** |
| * This class provides a mechanism to save data to persistent store files {@link StoreFile}. |
| * Modules can register a {@link StoreData} instance indicating the {@StoreFile} into which they |
| * want to save their data to. |
| * |
| * NOTE: |
| * <li>Modules can register their {@StoreData} using |
| * {@link WifiConfigStore#registerStoreData(StoreData)} directly, but should |
| * use {@link WifiConfigManager#saveToStore(boolean)} for any writes.</li> |
| * <li>{@link WifiConfigManager} controls {@link WifiConfigStore} and initiates read at bootup and |
| * store file changes on user switch.</li> |
| * <li>Not thread safe!</li> |
| */ |
| public class WifiConfigStore { |
| /** |
| * Config store file for general shared store file. |
| */ |
| public static final int STORE_FILE_SHARED_GENERAL = 0; |
| /** |
| * Config store file for general user store file. |
| */ |
| public static final int STORE_FILE_USER_GENERAL = 1; |
| /** |
| * Config store file for network suggestions user store file. |
| */ |
| public static final int STORE_FILE_USER_NETWORK_SUGGESTIONS = 2; |
| |
| @IntDef(prefix = { "STORE_FILE_" }, value = { |
| STORE_FILE_SHARED_GENERAL, |
| STORE_FILE_USER_GENERAL, |
| STORE_FILE_USER_NETWORK_SUGGESTIONS |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface StoreFileId { } |
| |
| private static final String XML_TAG_DOCUMENT_HEADER = "WifiConfigStoreData"; |
| private static final String XML_TAG_VERSION = "Version"; |
| /** |
| * Current config store data version. This will be incremented for any additions. |
| */ |
| private static final int CURRENT_CONFIG_STORE_DATA_VERSION = 1; |
| /** This list of older versions will be used to restore data from older config store. */ |
| /** |
| * First version of the config store data format. |
| */ |
| private static final int INITIAL_CONFIG_STORE_DATA_VERSION = 1; |
| |
| /** |
| * 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"; |
| /** |
| * 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; |
| /** |
| * Config store file name for general shared store file. |
| */ |
| private static final String STORE_FILE_NAME_SHARED_GENERAL = "WifiConfigStore.xml"; |
| /** |
| * Config store file name for general user store file. |
| */ |
| private static final String STORE_FILE_NAME_USER_GENERAL = "WifiConfigStore.xml"; |
| /** |
| * Config store file name for network suggestions user store file. |
| */ |
| private static final String STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS = |
| "WifiConfigStoreNetworkSuggestions.xml"; |
| /** |
| * Mapping of Store file Id to Store file names. |
| */ |
| private static final SparseArray<String> STORE_ID_TO_FILE_NAME = |
| new SparseArray<String>() {{ |
| put(STORE_FILE_SHARED_GENERAL, STORE_FILE_NAME_SHARED_GENERAL); |
| put(STORE_FILE_USER_GENERAL, STORE_FILE_NAME_USER_GENERAL); |
| put(STORE_FILE_USER_NETWORK_SUGGESTIONS, STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS); |
| }}; |
| |
| /** |
| * 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; |
| private final WifiMetrics mWifiMetrics; |
| /** |
| * Shared config store file instance. There is 1 shared store file: |
| * {@link #STORE_FILE_NAME_SHARED_GENERAL}. |
| */ |
| private StoreFile mSharedStore; |
| /** |
| * User specific store file instances. There are 2 user store files: |
| * {@link #STORE_FILE_NAME_USER_GENERAL} & {@link #STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS}. |
| */ |
| private List<StoreFile> mUserStores; |
| /** |
| * 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); |
| } |
| } |
| }; |
| |
| /** |
| * List of data containers. |
| */ |
| private final List<StoreData> mStoreDataList; |
| |
| /** |
| * 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 wifiMetrics Metrics instance. |
| * @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, WifiMetrics wifiMetrics, |
| StoreFile sharedStore) { |
| |
| mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); |
| mEventHandler = new Handler(looper); |
| mClock = clock; |
| mWifiMetrics = wifiMetrics; |
| mStoreDataList = new ArrayList<>(); |
| |
| // 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 |switchUserStoresAndRead|. |
| mUserStores = null; |
| } |
| |
| /** |
| * Set the user store files. |
| * (Useful for mocking in unit tests). |
| * @param userStores List of {@link StoreFile} created using {@link #createUserFiles(int)}. |
| */ |
| public void setUserStores(@NonNull List<StoreFile> userStores) { |
| Preconditions.checkNotNull(userStores); |
| mUserStores = userStores; |
| } |
| |
| /** |
| * Register a {@link StoreData} to read/write data from/to a store. A {@link StoreData} is |
| * responsible for a block of data in the store file, and provides serialization/deserialization |
| * functions for those data. |
| * |
| * @param storeData The store data to be registered to the config store |
| * @return true if registered successfully, false if the store file name is not valid. |
| */ |
| public boolean registerStoreData(@NonNull StoreData storeData) { |
| if (storeData == null) { |
| Log.e(TAG, "Unable to register null store data"); |
| return false; |
| } |
| int storeFileId = storeData.getStoreFileId(); |
| if (STORE_ID_TO_FILE_NAME.get(storeFileId) == null) { |
| Log.e(TAG, "Invalid shared store file specified" + storeFileId); |
| return false; |
| } |
| mStoreDataList.add(storeData); |
| return true; |
| } |
| |
| /** |
| * 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. |
| * @param fileId Identifier for the file. See {@link StoreFileId}. |
| * @return new instance of the store file or null if the directory cannot be created. |
| */ |
| private static @Nullable StoreFile createFile(File storeBaseDir, @StoreFileId int fileId) { |
| File storeDir = new File(storeBaseDir, STORE_DIRECTORY_NAME); |
| if (!storeDir.exists()) { |
| if (!storeDir.mkdir()) { |
| Log.w(TAG, "Could not create store directory " + storeDir); |
| return null; |
| } |
| } |
| return new StoreFile(new File(storeDir, STORE_ID_TO_FILE_NAME.get(fileId)), fileId); |
| } |
| |
| /** |
| * 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 @Nullable StoreFile createSharedFile() { |
| return createFile(Environment.getDataMiscDirectory(), STORE_FILE_SHARED_GENERAL); |
| } |
| |
| /** |
| * Create new instances of the user specific store files. |
| * The user store file is inside the user's encrypted data directory. |
| * |
| * @param userId userId corresponding to the currently logged-in user. |
| * @return List of new instances of the store files created or null if the directory cannot be |
| * created. |
| */ |
| public static @Nullable List<StoreFile> createUserFiles(int userId) { |
| List<StoreFile> storeFiles = new ArrayList<>(); |
| for (int fileId : Arrays.asList( |
| STORE_FILE_USER_GENERAL, STORE_FILE_USER_NETWORK_SUGGESTIONS)) { |
| StoreFile storeFile = createFile(Environment.getDataMiscCeDirectory(userId), fileId); |
| if (storeFile == null) { |
| return null; |
| } |
| storeFiles.add(storeFile); |
| } |
| return storeFiles; |
| } |
| |
| /** |
| * 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() { |
| // Checking for the shared store file existence is sufficient since this is guaranteed |
| // to be present on migrated devices. |
| return mSharedStore.exists(); |
| } |
| |
| /** |
| * Retrieve the list of {@link StoreData} instances registered for the provided |
| * {@link StoreFile}. |
| */ |
| private List<StoreData> retrieveStoreDataListForStoreFile(@NonNull StoreFile storeFile) { |
| return mStoreDataList |
| .stream() |
| .filter(s -> s.getStoreFileId() == storeFile.mFileId) |
| .collect(Collectors.toList()); |
| } |
| |
| /** |
| * Check if any of the provided list of {@link StoreData} instances registered |
| * for the provided {@link StoreFile }have indicated that they have new data to serialize. |
| */ |
| private boolean hasNewDataToSerialize(@NonNull StoreFile storeFile) { |
| List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile); |
| return storeDataList.stream().anyMatch(s -> s.hasNewDataToSerialize()); |
| } |
| |
| /** |
| * API to write the data provided by registered 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. |
| */ |
| public void write(boolean forceSync) |
| throws XmlPullParserException, IOException { |
| boolean hasAnyNewData = false; |
| // Serialize the provided data and send it to the respective stores. The actual write will |
| // be performed later depending on the |forceSync| flag . |
| if (hasNewDataToSerialize(mSharedStore)) { |
| byte[] sharedDataBytes = serializeData(mSharedStore); |
| mSharedStore.storeRawDataToWrite(sharedDataBytes); |
| hasAnyNewData = true; |
| } |
| if (mUserStores != null) { |
| for (StoreFile userStoreFile : mUserStores) { |
| if (hasNewDataToSerialize(userStoreFile)) { |
| byte[] userDataBytes = serializeData(userStoreFile); |
| userStoreFile.storeRawDataToWrite(userDataBytes); |
| hasAnyNewData = true; |
| } |
| } |
| } |
| |
| if (hasAnyNewData) { |
| // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides |
| // any pending buffer writes. |
| if (forceSync) { |
| writeBufferedData(); |
| } else { |
| startBufferedWriteAlarm(); |
| } |
| } else if (forceSync && mBufferedWritePending) { |
| // no new data to write, but there is a pending buffered write. So, |forceSync| should |
| // flush that out. |
| writeBufferedData(); |
| } |
| } |
| |
| /** |
| * Serialize all the data from all the {@link StoreData} clients registered for the provided |
| * {@link StoreFile}. |
| * |
| * @param storeFile StoreFile that we want to write to. |
| * @return byte[] of serialized bytes |
| * @throws XmlPullParserException |
| * @throws IOException |
| */ |
| private byte[] serializeData(@NonNull StoreFile storeFile) |
| throws XmlPullParserException, IOException { |
| List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile); |
| |
| final XmlSerializer out = new FastXmlSerializer(); |
| final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| out.setOutput(outputStream, StandardCharsets.UTF_8.name()); |
| |
| XmlUtil.writeDocumentStart(out, XML_TAG_DOCUMENT_HEADER); |
| XmlUtil.writeNextValue(out, XML_TAG_VERSION, CURRENT_CONFIG_STORE_DATA_VERSION); |
| for (StoreData storeData : storeDataList) { |
| String tag = storeData.getName(); |
| XmlUtil.writeNextSectionStart(out, tag); |
| storeData.serializeData(out); |
| XmlUtil.writeNextSectionEnd(out, tag); |
| } |
| XmlUtil.writeDocumentEnd(out, XML_TAG_DOCUMENT_HEADER); |
| |
| return outputStream.toByteArray(); |
| } |
| |
| /** |
| * 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 (mUserStores != null) { |
| for (StoreFile userStoreFile : mUserStores) { |
| userStoreFile.writeBufferedRawData(); |
| } |
| } |
| long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime; |
| try { |
| mWifiMetrics.noteWifiConfigStoreWriteDuration(toIntExact(writeTime)); |
| } catch (ArithmeticException e) { |
| // Silently ignore on any overflow errors. |
| } |
| 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. |
| */ |
| public void read() throws XmlPullParserException, IOException { |
| // Reset both share and user store data. |
| resetStoreData(mSharedStore); |
| if (mUserStores != null) { |
| for (StoreFile userStoreFile : mUserStores) { |
| resetStoreData(userStoreFile); |
| } |
| } |
| |
| long readStartTime = mClock.getElapsedSinceBootMillis(); |
| byte[] sharedDataBytes = mSharedStore.readRawData(); |
| deserializeData(sharedDataBytes, mSharedStore); |
| if (mUserStores != null) { |
| for (StoreFile userStoreFile : mUserStores) { |
| byte[] userDataBytes = userStoreFile.readRawData(); |
| deserializeData(userDataBytes, userStoreFile); |
| } |
| } |
| long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; |
| try { |
| mWifiMetrics.noteWifiConfigStoreReadDuration(toIntExact(readTime)); |
| } catch (ArithmeticException e) { |
| // Silently ignore on any overflow errors. |
| } |
| Log.d(TAG, "Reading from all stores completed in " + readTime + " ms."); |
| } |
| |
| /** |
| * Handles a user switch. This method changes the user specific store files and reads from the |
| * new user's store files. |
| * |
| * @param userStores List of {@link StoreFile} created using {@link #createUserFiles(int)}. |
| */ |
| public void switchUserStoresAndRead(@NonNull List<StoreFile> userStores) |
| throws XmlPullParserException, IOException { |
| Preconditions.checkNotNull(userStores); |
| // Reset user store data. |
| if (mUserStores != null) { |
| for (StoreFile userStoreFile : mUserStores) { |
| resetStoreData(userStoreFile); |
| } |
| } |
| |
| // Stop any pending buffered writes, if any. |
| stopBufferedWriteAlarm(); |
| mUserStores = userStores; |
| |
| // Now read from the user store file. |
| long readStartTime = mClock.getElapsedSinceBootMillis(); |
| for (StoreFile userStoreFile : mUserStores) { |
| byte[] userDataBytes = userStoreFile.readRawData(); |
| deserializeData(userDataBytes, userStoreFile); |
| } |
| long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; |
| mWifiMetrics.noteWifiConfigStoreReadDuration(toIntExact(readTime)); |
| Log.d(TAG, "Reading from user stores completed in " + readTime + " ms."); |
| } |
| |
| /** |
| * Reset data for all {@link StoreData} instances registered for this {@link StoreFile}. |
| */ |
| private void resetStoreData(@NonNull StoreFile storeFile) { |
| for (StoreData storeData: retrieveStoreDataListForStoreFile(storeFile)) { |
| storeData.resetData(); |
| } |
| } |
| |
| // Inform all the provided store data clients that there is nothing in the store for them. |
| private void indicateNoDataForStoreDatas(Collection<StoreData> storeDataSet) |
| throws XmlPullParserException, IOException { |
| for (StoreData storeData : storeDataSet) { |
| storeData.deserializeData(null, 0); |
| } |
| } |
| |
| /** |
| * Deserialize data from a {@link StoreFile} for all {@link StoreData} instances registered. |
| * |
| * @param dataBytes The data to parse |
| * @param storeFile StoreFile that we read from. Will be used to retrieve the list of clients |
| * who have data to deserialize from this file. |
| * |
| * @throws XmlPullParserException |
| * @throws IOException |
| */ |
| private void deserializeData(@NonNull byte[] dataBytes, @NonNull StoreFile storeFile) |
| throws XmlPullParserException, IOException { |
| List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile); |
| if (dataBytes == null) { |
| indicateNoDataForStoreDatas(storeDataList); |
| return; |
| } |
| final XmlPullParser in = Xml.newPullParser(); |
| final ByteArrayInputStream inputStream = new ByteArrayInputStream(dataBytes); |
| in.setInput(inputStream, StandardCharsets.UTF_8.name()); |
| |
| // Start parsing the XML stream. |
| int rootTagDepth = in.getDepth() + 1; |
| parseDocumentStartAndVersionFromXml(in); |
| |
| String[] headerName = new String[1]; |
| Set<StoreData> storeDatasInvoked = new HashSet<>(); |
| while (XmlUtil.gotoNextSectionOrEnd(in, headerName, rootTagDepth)) { |
| // There can only be 1 store data matching the tag (O indicates a fatal |
| // error). |
| StoreData storeData = storeDataList.stream() |
| .filter(s -> s.getName().equals(headerName[0])) |
| .findAny() |
| .orElse(null); |
| if (storeData == null) { |
| throw new XmlPullParserException("Unknown store data: " + headerName[0] |
| + ". List of store data: " + storeDataList); |
| } |
| storeData.deserializeData(in, rootTagDepth + 1); |
| storeDatasInvoked.add(storeData); |
| } |
| // Inform all the other registered store data clients that there is nothing in the store |
| // for them. |
| Set<StoreData> storeDatasNotInvoked = new HashSet<>(storeDataList); |
| storeDatasNotInvoked.removeAll(storeDatasInvoked); |
| indicateNoDataForStoreDatas(storeDatasNotInvoked); |
| } |
| |
| /** |
| * Parse the document start and version from the XML stream. |
| * This is used for both the shared and user config store data. |
| * |
| * @param in XmlPullParser instance pointing to the XML stream. |
| * @return version number retrieved from the Xml stream. |
| */ |
| private static int parseDocumentStartAndVersionFromXml(XmlPullParser in) |
| throws XmlPullParserException, IOException { |
| XmlUtil.gotoDocumentStart(in, XML_TAG_DOCUMENT_HEADER); |
| int version = (int) XmlUtil.readNextValueWithName(in, XML_TAG_VERSION); |
| if (version < INITIAL_CONFIG_STORE_DATA_VERSION |
| || version > CURRENT_CONFIG_STORE_DATA_VERSION) { |
| throw new XmlPullParserException("Invalid version of data: " + version); |
| } |
| return version; |
| } |
| |
| /** |
| * Dump the local log buffer and other internal state of WifiConfigManager. |
| */ |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| pw.println("Dump of WifiConfigStore"); |
| pw.println("WifiConfigStore - Store Data Begin ----"); |
| for (StoreData storeData : mStoreDataList) { |
| pw.print("StoreData =>"); |
| pw.print(" "); |
| pw.print("Name: " + storeData.getName()); |
| pw.print(", "); |
| pw.print("File Id: " + storeData.getStoreFileId()); |
| pw.print(", "); |
| pw.println("File Name: " + STORE_ID_TO_FILE_NAME.get(storeData.getStoreFileId())); |
| } |
| pw.println("WifiConfigStore - Store Data End ----"); |
| } |
| |
| /** |
| * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read |
| * raw data from the persistent file with integrity. This class provides helper methods to |
| * read/write the entire file into a byte array. |
| * This helps to separate out the processing, parsing, and integrity checking 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; |
| /** |
| * The integrity file storing integrity checking data for the store file. |
| */ |
| private DataIntegrityChecker mDataIntegrityChecker; |
| /** |
| * {@link StoreFileId} Type of store file. |
| */ |
| private @StoreFileId int mFileId; |
| |
| public StoreFile(File file, @StoreFileId int fileId) { |
| mAtomicFile = new AtomicFile(file); |
| mFileName = mAtomicFile.getBaseFile().getAbsolutePath(); |
| mDataIntegrityChecker = new DataIntegrityChecker(mFileName); |
| mFileId = fileId; |
| } |
| |
| /** |
| * 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 or the data has |
| * been altered. |
| * @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 { |
| byte[] bytes = null; |
| try { |
| bytes = mAtomicFile.readFully(); |
| // Check that the file has not been altered since last writeBufferedRawData() |
| if (!mDataIntegrityChecker.isOk(bytes)) { |
| Log.wtf(TAG, "Data integrity problem with file: " + mFileName); |
| return null; |
| } |
| } catch (FileNotFoundException e) { |
| return null; |
| } catch (DigestException e) { |
| // When integrity checking is introduced. The existing data will have no related |
| // integrity file for validation. Thus, we will assume the existing data is correct |
| // and immediately create the integrity file. |
| Log.i(TAG, "isOK() had no integrity data to check; thus vacuously " |
| + "true. Running update now."); |
| mDataIntegrityChecker.update(bytes); |
| } |
| return bytes; |
| } |
| |
| /** |
| * 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) return; // No data to write for this file. |
| // 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; |
| } |
| // There was a legitimate change and update the integrity checker. |
| mDataIntegrityChecker.update(mWriteData); |
| // Reset the pending write data after write. |
| mWriteData = null; |
| } |
| } |
| |
| /** |
| * Interface to be implemented by a module that contained data in the config store file. |
| * |
| * The module will be responsible for serializing/deserializing their own data. |
| * Whenever {@link WifiConfigStore#read()} is invoked, all registered StoreData instances will |
| * be notified that a read was performed via {@link StoreData#deserializeData( |
| * XmlPullParser, int)} regardless of whether there is any data for them or not in the |
| * store file. |
| * |
| * Note: StoreData clients that need a config store read to kick-off operations should wait |
| * for the {@link StoreData#deserializeData(XmlPullParser, int)} invocation. |
| */ |
| public interface StoreData { |
| /** |
| * Serialize a XML data block to the output stream. |
| * |
| * @param out The output stream to serialize the data to |
| */ |
| void serializeData(XmlSerializer out) |
| throws XmlPullParserException, IOException; |
| |
| /** |
| * Deserialize a XML data block from the input stream. |
| * |
| * @param in The input stream to read the data from. This could be null if there is |
| * nothing in the store. |
| * @param outerTagDepth The depth of the outer tag in the XML document |
| * Note: This will be invoked every time a store file is read, even if there is nothing |
| * in the store for them. |
| */ |
| void deserializeData(@Nullable XmlPullParser in, int outerTagDepth) |
| throws XmlPullParserException, IOException; |
| |
| /** |
| * Reset configuration data. |
| */ |
| void resetData(); |
| |
| /** |
| * Check if there is any new data to persist from the last write. |
| * |
| * @return true if the module has new data to persist, false otherwise. |
| */ |
| boolean hasNewDataToSerialize(); |
| |
| /** |
| * Return the name of this store data. The data will be enclosed under this tag in |
| * the XML block. |
| * |
| * @return The name of the store data |
| */ |
| String getName(); |
| |
| /** |
| * File Id where this data needs to be written to. |
| * This should be one of {@link #STORE_FILE_SHARED_GENERAL}, |
| * {@link #STORE_FILE_USER_GENERAL} or |
| * {@link #STORE_FILE_USER_NETWORK_SUGGESTIONS}. |
| * |
| * Note: For most uses, the shared or user general store is sufficient. Creating and |
| * managing store files are expensive. Only use specific store files if you have a large |
| * amount of data which may not need to be persisted frequently (or at least not as |
| * frequently as the general store). |
| * @return Id of the file where this data needs to be persisted. |
| */ |
| @StoreFileId int getStoreFileId(); |
| } |
| } |