diff options
-rw-r--r-- | service/java/com/android/server/wifi/WifiConfigStore.java | 399 | ||||
-rw-r--r-- | tests/wifitests/src/com/android/server/wifi/WifiConfigStoreTest.java | 387 |
2 files changed, 529 insertions, 257 deletions
diff --git a/service/java/com/android/server/wifi/WifiConfigStore.java b/service/java/com/android/server/wifi/WifiConfigStore.java index 17a6670ff..00f09f571 100644 --- a/service/java/com/android/server/wifi/WifiConfigStore.java +++ b/service/java/com/android/server/wifi/WifiConfigStore.java @@ -16,6 +16,8 @@ package com.android.server.wifi; +import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AlarmManager; import android.content.Context; @@ -24,11 +26,13 @@ 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.XmlUtil; import org.xmlpull.v1.XmlPullParser; @@ -38,22 +42,56 @@ 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.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; /** - * 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! + * 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"; /** @@ -76,10 +114,6 @@ public class WifiConfigStore { */ 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"; @@ -88,6 +122,29 @@ public class WifiConfigStore { */ 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; @@ -100,13 +157,15 @@ public class WifiConfigStore { */ private final Clock mClock; /** - * Shared config store file instance. + * Shared config store file instance. There is 1 shared store file: + * {@link #STORE_FILE_NAME_SHARED_GENERAL}. */ private StoreFile mSharedStore; /** - * User specific store file instance. + * 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 StoreFile mUserStore; + private List<StoreFile> mUserStores; /** * Verbose logging flag. */ @@ -126,14 +185,13 @@ public class WifiConfigStore { } catch (IOException e) { Log.wtf(TAG, "Buffered write failed", e); } - } }; /** - * List of data container. + * List of data containers. */ - private final Map<String, StoreData> mStoreDataList; + private final List<StoreData> mStoreDataList; /** * Create a new instance of WifiConfigStore. @@ -151,33 +209,44 @@ public class WifiConfigStore { mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mEventHandler = new Handler(looper); mClock = clock; - mStoreDataList = new HashMap<>(); + 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 |switchUserStoreAndRead|. - mUserStore = null; + // CE storage is accessible via |switchUserStoresAndRead|. + mUserStores = null; } - public void setUserStore(StoreFile userStore) { - mUserStore = userStore; + /** + * 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 store. A {@link StoreData} is responsible - * for a block of data in the store file, and provides serialization/deserialization functions - * for those data. + * 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 succeeded + * @return true if registered successfully, false if the store file name is not valid. */ - public boolean registerStoreData(StoreData storeData) { + public boolean registerStoreData(@NonNull StoreData storeData) { if (storeData == null) { Log.e(TAG, "Unable to register null store data"); return false; } - mStoreDataList.put(storeData.getName(), storeData); + 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; } @@ -188,16 +257,18 @@ public class WifiConfigStore { * * @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. + * @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 StoreFile createFile(File storeBaseDir) { + private static 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_FILE_NAME)); + return new StoreFile(new File(storeDir, STORE_ID_TO_FILE_NAME.get(fileId)), fileId); } /** @@ -206,18 +277,23 @@ public class WifiConfigStore { * @return new instance of the store file or null if the directory cannot be created. */ public static StoreFile createSharedFile() { - return createFile(Environment.getDataMiscDirectory()); + return createFile(Environment.getDataMiscDirectory(), STORE_FILE_SHARED_GENERAL); } /** - * Create a new instance of the user specific store file. + * 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 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)); + * @return List of new instances of the store files created. + */ + public static List<StoreFile> createUserFiles(int userId) { + List<StoreFile> storeFiles = new ArrayList<>(); + storeFiles.add(createFile(Environment.getDataMiscCeDirectory(userId), + STORE_FILE_USER_GENERAL)); + storeFiles.add(createFile(Environment.getDataMiscCeDirectory(userId), + STORE_FILE_USER_NETWORK_SUGGESTIONS)); + return storeFiles; } /** @@ -234,7 +310,29 @@ public class WifiConfigStore { * @return true if any of the store file is present, false otherwise. */ public boolean areStoresPresent() { - return (mSharedStore.exists() || (mUserStore != null && mUserStore.exists())); + // 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()); } /** @@ -247,50 +345,62 @@ public class WifiConfigStore { */ 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 . - byte[] sharedDataBytes = serializeData(true); - mSharedStore.storeRawDataToWrite(sharedDataBytes); - if (mUserStore != null) { - byte[] userDataBytes = serializeData(false); - mUserStore.storeRawDataToWrite(userDataBytes); + 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; + } + } } - // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides any - // pending buffer writes. - if (forceSync) { + 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(); - } else { - startBufferedWriteAlarm(); } } /** - * Serialize share data or user data from all store data. + * Serialize all the data from all the {@link StoreData} clients registered for the provided + * {@link StoreFile}. * - * @param shareData Flag indicating share data + * @param storeFile StoreFile that we want to write to. * @return byte[] of serialized bytes * @throws XmlPullParserException * @throws IOException */ - private byte[] serializeData(boolean shareData) throws XmlPullParserException, 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 (Map.Entry<String, StoreData> entry : mStoreDataList.entrySet()) { - String tag = entry.getKey(); - StoreData storeData = entry.getValue(); - // Ignore this store data if this is for share file and the store data doesn't support - // share store. - if (shareData && !storeData.supportShareData()) { - continue; - } + for (StoreData storeData : storeDataList) { + String tag = storeData.getName(); XmlUtil.writeNextSectionStart(out, tag); - storeData.serializeData(out, shareData); + storeData.serializeData(out); XmlUtil.writeNextSectionEnd(out, tag); } XmlUtil.writeDocumentEnd(out, XML_TAG_DOCUMENT_HEADER); @@ -329,8 +439,10 @@ public class WifiConfigStore { long writeStartTime = mClock.getElapsedSinceBootMillis(); mSharedStore.writeBufferedRawData(); - if (mUserStore != null) { - mUserStore.writeBufferedRawData(); + if (mUserStores != null) { + for (StoreFile userStoreFile : mUserStores) { + userStoreFile.writeBufferedRawData(); + } } long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime; @@ -344,80 +456,87 @@ public class WifiConfigStore { */ public void read() throws XmlPullParserException, IOException { // Reset both share and user store data. - resetStoreData(true); - if (mUserStore != null) { - resetStoreData(false); + resetStoreData(mSharedStore); + if (mUserStores != null) { + for (StoreFile userStoreFile : mUserStores) { + resetStoreData(userStoreFile); + } } long readStartTime = mClock.getElapsedSinceBootMillis(); byte[] sharedDataBytes = mSharedStore.readRawData(); - byte[] userDataBytes = null; - if (mUserStore != null) { - userDataBytes = mUserStore.readRawData(); + deserializeData(sharedDataBytes, mSharedStore); + if (mUserStores != null) { + for (StoreFile userStoreFile : mUserStores) { + byte[] userDataBytes = userStoreFile.readRawData(); + deserializeData(userDataBytes, userStoreFile); + } } long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; - Log.d(TAG, "Reading from stores completed in " + readTime + " ms."); - deserializeData(sharedDataBytes, true); - if (mUserStore != null) { - deserializeData(userDataBytes, false); - } + Log.d(TAG, "Reading from all stores completed in " + readTime + " ms."); } /** - * Handles a user switch. This method changes the user specific store file and reads from the - * new user's store file. + * Handles a user switch. This method changes the user specific store files and reads from the + * new user's store files. * - * @param userStore StoreFile instance pointing to the user specific store file. This should - * be retrieved using {@link #createUserFile(int)} method. + * @param userStores List of {@link StoreFile} created using {@link #createUserFiles(int)}. */ - public void switchUserStoreAndRead(StoreFile userStore) + public void switchUserStoresAndRead(@NonNull List<StoreFile> userStores) throws XmlPullParserException, IOException { // Reset user store data. - resetStoreData(false); + if (mUserStores != null) { + for (StoreFile userStoreFile : mUserStores) { + resetStoreData(userStoreFile); + } + } // Stop any pending buffered writes, if any. stopBufferedWriteAlarm(); - mUserStore = userStore; + mUserStores = userStores; // Now read from the user store file. long readStartTime = mClock.getElapsedSinceBootMillis(); - byte[] userDataBytes = mUserStore.readRawData(); + for (StoreFile userStoreFile : mUserStores) { + byte[] userDataBytes = userStoreFile.readRawData(); + deserializeData(userDataBytes, userStoreFile); + } long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; - Log.d(TAG, "Reading from user store completed in " + readTime + " ms."); - deserializeData(userDataBytes, false); + Log.d(TAG, "Reading from user stores completed in " + readTime + " ms."); } /** - * Reset share data or user data in all store data. - * - * @param shareData Flag indicating share data + * Reset data for all {@link StoreData} instances registered for this {@link StoreFile}. */ - private void resetStoreData(boolean shareData) { - for (Map.Entry<String, StoreData> entry : mStoreDataList.entrySet()) { - entry.getValue().resetData(shareData); + 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, boolean shareData) + private void indicateNoDataForStoreDatas(Collection<StoreData> storeDataSet) throws XmlPullParserException, IOException { for (StoreData storeData : storeDataSet) { - storeData.deserializeData(null, 0, shareData); + storeData.deserializeData(null, 0); } } /** - * Deserialize share data or user data into store data. + * Deserialize data from a {@link StoreFile} for all {@link StoreData} instances registered. * * @param dataBytes The data to parse - * @param shareData The flag indicating share data + * @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(byte[] dataBytes, boolean shareData) + private void deserializeData(@NonNull byte[] dataBytes, @NonNull StoreFile storeFile) throws XmlPullParserException, IOException { + List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile); if (dataBytes == null) { - indicateNoDataForStoreDatas(mStoreDataList.values(), shareData); + indicateNoDataForStoreDatas(storeDataList); return; } final XmlPullParser in = Xml.newPullParser(); @@ -431,18 +550,24 @@ public class WifiConfigStore { String[] headerName = new String[1]; Set<StoreData> storeDatasInvoked = new HashSet<>(); while (XmlUtil.gotoNextSectionOrEnd(in, headerName, rootTagDepth)) { - StoreData storeData = mStoreDataList.get(headerName[0]); + // 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]); + throw new XmlPullParserException("Unknown store data: " + headerName[0] + + ". List of store data: " + storeDataList); } - storeData.deserializeData(in, rootTagDepth + 1, shareData); + 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<>(mStoreDataList.values()); + Set<StoreData> storeDatasNotInvoked = new HashSet<>(storeDataList); storeDatasNotInvoked.removeAll(storeDatasInvoked); - indicateNoDataForStoreDatas(storeDatasNotInvoked, shareData); + indicateNoDataForStoreDatas(storeDatasNotInvoked); } /** @@ -464,6 +589,24 @@ public class WifiConfigStore { } /** + * 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.println("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. This class provides helper methods to read/write the * entire file into a byte array. @@ -486,10 +629,15 @@ public class WifiConfigStore { * Store the file name for setting the file permissions/logging purposes. */ private String mFileName; + /** + * {@link StoreFileId} Type of store file. + */ + private @StoreFileId int mFileId; - public StoreFile(File file) { + public StoreFile(File file, @StoreFileId int fileId) { mAtomicFile = new AtomicFile(file); mFileName = mAtomicFile.getBaseFile().getAbsolutePath(); + mFileId = fileId; } /** @@ -562,48 +710,44 @@ public class WifiConfigStore { * 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, boolean)} regardless of whether there is any data for them or not in the + * 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, boolean)} invocation. + * for the {@link StoreData#deserializeData(XmlPullParser, int)} invocation. */ public interface StoreData { /** - * Serialize a XML data block to the output stream. The |shared| flag indicates if the - * output stream is backed by a share store or an user store. + * Serialize a XML data block to the output stream. * * @param out The output stream to serialize the data to - * @param shared Flag indicating if the output stream is backed by a share store or an - * user store */ - void serializeData(XmlSerializer out, boolean shared) + void serializeData(XmlSerializer out) throws XmlPullParserException, IOException; /** - * Deserialize a XML data block from the input stream. The |shared| flag indicates if the - * input stream is backed by a share store or an user store. When |shared| is set to true, - * the shared configuration data will be overwritten by the parsed data. Otherwise, - * the user configuration will be overwritten by the parsed data. + * 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 - * @Param shared Flag indicating if the input stream is backed by a share store or an - * user store - * Note: This will be invoked every time a store file is read. For example: clients - * will get 2 invocations on bootup, one for shared store file (shared=True) & - * one for user store file (shared=False). + * 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, boolean shared) + void deserializeData(@Nullable XmlPullParser in, int outerTagDepth) throws XmlPullParserException, IOException; /** - * Reset configuration data. The |shared| flag indicates which configuration data to - * reset. When |shared| is set to true, the shared configuration data will be reset. - * Otherwise, the user configuration data will be reset. + * 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. */ - void resetData(boolean shared); + boolean hasNewDataToSerialize(); /** * Return the name of this store data. The data will be enclosed under this tag in @@ -614,10 +758,17 @@ public class WifiConfigStore { String getName(); /** - * Flag indicating if shared configuration data is supported. + * 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}. * - * @return true if shared configuration data is supported + * 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. */ - boolean supportShareData(); + @StoreFileId int getStoreFileId(); } } diff --git a/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreTest.java b/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreTest.java index 1741acc4b..0240a67ab 100644 --- a/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreTest.java +++ b/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreTest.java @@ -27,6 +27,8 @@ import android.net.wifi.WifiConfiguration; import android.os.test.TestLooper; import android.support.test.filters.SmallTest; +import com.android.internal.util.ArrayUtils; +import com.android.server.wifi.WifiConfigStore.StoreData; import com.android.server.wifi.WifiConfigStore.StoreFile; import com.android.server.wifi.util.XmlUtil; @@ -53,13 +55,6 @@ import java.util.Set; */ @SmallTest public class WifiConfigStoreTest { - // Store file content without any data. - private static final String EMPTY_FILE_CONTENT = - "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" - + "<WifiConfigStoreData>\n" - + "<int name=\"Version\" value=\"1\" />\n" - + "</WifiConfigStoreData>\n"; - private static final String TEST_USER_DATA = "UserData"; private static final String TEST_SHARE_DATA = "ShareData"; private static final String TEST_CREATOR_NAME = "CreatorName"; @@ -150,7 +145,10 @@ public class WifiConfigStoreTest { @Mock private Clock mClock; private MockStoreFile mSharedStore; private MockStoreFile mUserStore; - private MockStoreData mStoreData; + private MockStoreFile mUserNetworkSuggestionsStore; + private List<StoreFile> mUserStores = new ArrayList<StoreFile>(); + private MockStoreData mSharedStoreData; + private MockStoreData mUserStoreData; /** * Test instance of WifiConfigStore. @@ -168,9 +166,15 @@ public class WifiConfigStoreTest { .thenReturn(mAlarmManager.getAlarmManager()); when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mPackageManager.getNameForUid(anyInt())).thenReturn(TEST_CREATOR_NAME); - mUserStore = new MockStoreFile(); - mSharedStore = new MockStoreFile(); - mStoreData = new MockStoreData(); + mSharedStore = new MockStoreFile(WifiConfigStore.STORE_FILE_SHARED_GENERAL); + mUserStore = new MockStoreFile(WifiConfigStore.STORE_FILE_USER_GENERAL); + mUserNetworkSuggestionsStore = + new MockStoreFile(WifiConfigStore.STORE_FILE_USER_NETWORK_SUGGESTIONS); + mUserStores.add(mUserStore); + mUserStores.add(mUserNetworkSuggestionsStore); + + mSharedStoreData = new MockStoreData(WifiConfigStore.STORE_FILE_SHARED_GENERAL); + mUserStoreData = new MockStoreData(WifiConfigStore.STORE_FILE_USER_GENERAL); } /** @@ -194,24 +198,20 @@ public class WifiConfigStoreTest { } /** - * Verify the contents of the config file with empty data. The data content should be the - * same as {@link #EMPTY_FILE_CONTENT}. + * Verify that no write occurs if there is {@link StoreData} registered for any + * {@link StoreFile}. * * @throws Exception */ @Test - public void testWriteWithEmptyData() throws Exception { + public void testWriteWithNoStoreData() throws Exception { // Perform force write to both share and user store file. - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.setUserStores(mUserStores); mWifiConfigStore.write(true); - assertFalse(mAlarmManager.isPending(WifiConfigStore.BUFFERED_WRITE_ALARM_TAG)); - assertTrue(mSharedStore.isStoreWritten()); - assertTrue(mUserStore.isStoreWritten()); - assertTrue(Arrays.equals(EMPTY_FILE_CONTENT.getBytes(StandardCharsets.UTF_8), - mSharedStore.getStoreBytes())); - assertTrue(Arrays.equals(EMPTY_FILE_CONTENT.getBytes(StandardCharsets.UTF_8), - mUserStore.getStoreBytes())); + assertFalse(mSharedStore.isStoreWritten()); + assertFalse(mUserStore.isStoreWritten()); + assertFalse(mUserNetworkSuggestionsStore.isStoreWritten()); } /** @@ -221,12 +221,17 @@ public class WifiConfigStoreTest { */ @Test public void testForceWrite() throws Exception { - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + // Register data container. + mWifiConfigStore.registerStoreData(mSharedStoreData); + mWifiConfigStore.registerStoreData(mUserStoreData); + + mWifiConfigStore.switchUserStoresAndRead(mUserStores); mWifiConfigStore.write(true); assertFalse(mAlarmManager.isPending(WifiConfigStore.BUFFERED_WRITE_ALARM_TAG)); assertTrue(mSharedStore.isStoreWritten()); assertTrue(mUserStore.isStoreWritten()); + assertFalse(mUserNetworkSuggestionsStore.isStoreWritten()); } /** @@ -235,18 +240,24 @@ public class WifiConfigStoreTest { */ @Test public void testBufferedWrite() throws Exception { - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + // Register data container. + mWifiConfigStore.registerStoreData(mSharedStoreData); + mWifiConfigStore.registerStoreData(mUserStoreData); + + mWifiConfigStore.switchUserStoresAndRead(mUserStores); mWifiConfigStore.write(false); assertTrue(mAlarmManager.isPending(WifiConfigStore.BUFFERED_WRITE_ALARM_TAG)); assertFalse(mSharedStore.isStoreWritten()); assertFalse(mUserStore.isStoreWritten()); + assertFalse(mUserNetworkSuggestionsStore.isStoreWritten()); // Now send the alarm and ensure that the writes happen. mAlarmManager.dispatch(WifiConfigStore.BUFFERED_WRITE_ALARM_TAG); mLooper.dispatchAll(); assertTrue(mSharedStore.isStoreWritten()); assertTrue(mUserStore.isStoreWritten()); + assertFalse(mUserNetworkSuggestionsStore.isStoreWritten()); } /** @@ -257,12 +268,14 @@ public class WifiConfigStoreTest { @Test public void testForceWriteAfterBufferedWrite() throws Exception { // Register a test data container with bogus data. - mWifiConfigStore.registerStoreData(mStoreData); - mStoreData.setShareData("abcds"); - mStoreData.setUserData("asdfa"); + mWifiConfigStore.registerStoreData(mSharedStoreData); + mWifiConfigStore.registerStoreData(mUserStoreData); + + mSharedStoreData.setData("abcds"); + mUserStoreData.setData("asdfa"); // Perform buffered write for both user and share store file. - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.switchUserStoresAndRead(mUserStores); mWifiConfigStore.write(false); assertTrue(mAlarmManager.isPending(WifiConfigStore.BUFFERED_WRITE_ALARM_TAG)); @@ -271,8 +284,8 @@ public class WifiConfigStoreTest { // Update the container with new set of data. The send a force write and ensure that the // writes have been performed and alarms have been stopped and updated data are written. - mStoreData.setUserData(TEST_USER_DATA); - mStoreData.setShareData(TEST_SHARE_DATA); + mUserStoreData.setData(TEST_USER_DATA); + mSharedStoreData.setData(TEST_SHARE_DATA); mWifiConfigStore.write(true); assertFalse(mAlarmManager.isPending(WifiConfigStore.BUFFERED_WRITE_ALARM_TAG)); @@ -281,35 +294,74 @@ public class WifiConfigStoreTest { // Verify correct data are loaded to the data container after a read. mWifiConfigStore.read(); - assertEquals(TEST_USER_DATA, mStoreData.getUserData()); - assertEquals(TEST_SHARE_DATA, mStoreData.getShareData()); + assertEquals(TEST_USER_DATA, mUserStoreData.getData()); + assertEquals(TEST_SHARE_DATA, mSharedStoreData.getData()); } /** + * Tests the force write with no new data after a buffered write. + * Expected behaviour: The force write should flush the previous buffered write and stop the + * buffer write alarms. + */ + @Test + public void testForceWriteWithNoNewDataAfterBufferedWrite() throws Exception { + // Register a test data container with bogus data. + mWifiConfigStore.registerStoreData(mSharedStoreData); + mWifiConfigStore.registerStoreData(mUserStoreData); + + mSharedStoreData.setData("abcds"); + mUserStoreData.setData("asdfa"); + + // Perform buffered write for both user and share store file. + mWifiConfigStore.switchUserStoresAndRead(mUserStores); + mWifiConfigStore.write(false); + + assertTrue(mAlarmManager.isPending(WifiConfigStore.BUFFERED_WRITE_ALARM_TAG)); + assertFalse(mSharedStore.isStoreWritten()); + assertFalse(mUserStore.isStoreWritten()); + + // Containers have no new data. + mUserStoreData.setHasAnyNewData(false); + mSharedStoreData.setHasAnyNewData(false); + mWifiConfigStore.write(true); + + assertFalse(mAlarmManager.isPending(WifiConfigStore.BUFFERED_WRITE_ALARM_TAG)); + assertTrue(mSharedStore.isStoreWritten()); + assertTrue(mUserStore.isStoreWritten()); + + // Verify correct data are loaded to the data container after a read. + mWifiConfigStore.read(); + assertEquals("abcds", mSharedStoreData.getData()); + assertEquals("asdfa", mUserStoreData.getData()); + } + + + /** * Tests the read API behaviour after a write to the store files. * Expected behaviour: The read should return the same data that was last written. */ @Test public void testReadAfterWrite() throws Exception { // Register data container. - mWifiConfigStore.registerStoreData(mStoreData); + mWifiConfigStore.registerStoreData(mSharedStoreData); + mWifiConfigStore.registerStoreData(mUserStoreData); // Read both share and user config store. - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.switchUserStoresAndRead(mUserStores); // Verify no data is read. - assertNull(mStoreData.getUserData()); - assertNull(mStoreData.getShareData()); + assertNull(mUserStoreData.getData()); + assertNull(mSharedStoreData.getData()); // Write share and user data. - mStoreData.setUserData(TEST_USER_DATA); - mStoreData.setShareData(TEST_SHARE_DATA); + mUserStoreData.setData(TEST_USER_DATA); + mSharedStoreData.setData(TEST_SHARE_DATA); mWifiConfigStore.write(true); // Read and verify the data content in the data container. mWifiConfigStore.read(); - assertEquals(TEST_USER_DATA, mStoreData.getUserData()); - assertEquals(TEST_SHARE_DATA, mStoreData.getShareData()); + assertEquals(TEST_USER_DATA, mUserStoreData.getData()); + assertEquals(TEST_SHARE_DATA, mSharedStoreData.getData()); } /** @@ -320,19 +372,25 @@ public class WifiConfigStoreTest { */ @Test public void testReadWithNoSharedStoreFileAndUserStoreNotVisible() throws Exception { - WifiConfigStore.StoreData storeData = mock(WifiConfigStore.StoreData.class); + StoreData sharedStoreData = mock(StoreData.class); + when(sharedStoreData.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_SHARED_GENERAL); + StoreData userStoreData = mock(StoreData.class); + when(userStoreData.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_USER_GENERAL); // Reading the mock store without a write should simulate the file not found case because // |readRawData| would return null. - mWifiConfigStore.registerStoreData(storeData); + mWifiConfigStore.registerStoreData(sharedStoreData); + mWifiConfigStore.registerStoreData(userStoreData); assertFalse(mWifiConfigStore.areStoresPresent()); mWifiConfigStore.read(); // Ensure that we got the call to deserialize empty shared data, but no user data. - verify(storeData).resetData(eq(true)); - verify(storeData).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData, never()).resetData(eq(false)); - verify(storeData, never()).deserializeData(eq(null), anyInt(), eq(false)); + verify(sharedStoreData).resetData(); + verify(sharedStoreData).deserializeData(eq(null), anyInt()); + verify(userStoreData, never()).resetData(); + verify(userStoreData, never()).deserializeData(any(), anyInt()); } /** @@ -342,21 +400,27 @@ public class WifiConfigStoreTest { */ @Test public void testReadWithNoStoreFiles() throws Exception { - WifiConfigStore.StoreData storeData = mock(WifiConfigStore.StoreData.class); + StoreData sharedStoreData = mock(StoreData.class); + when(sharedStoreData.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_SHARED_GENERAL); + StoreData userStoreData = mock(StoreData.class); + when(userStoreData.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_USER_GENERAL); // Reading the mock store without a write should simulate the file not found case because // |readRawData| would return null. - mWifiConfigStore.registerStoreData(storeData); + mWifiConfigStore.registerStoreData(sharedStoreData); + mWifiConfigStore.registerStoreData(userStoreData); // Read both share and user config store. - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.setUserStores(mUserStores); assertFalse(mWifiConfigStore.areStoresPresent()); mWifiConfigStore.read(); // Ensure that we got the call to deserialize empty shared & user data. - verify(storeData).resetData(eq(true)); - verify(storeData).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData, times(2)).resetData(eq(false)); - verify(storeData, times(2)).deserializeData(eq(null), anyInt(), eq(false)); + verify(userStoreData).resetData(); + verify(userStoreData).deserializeData(eq(null), anyInt()); + verify(sharedStoreData).resetData(); + verify(sharedStoreData).deserializeData(eq(null), anyInt()); } /** @@ -367,14 +431,14 @@ public class WifiConfigStoreTest { @Test public void testReadAfterWriteWithNoUserStore() throws Exception { // Setup data container. - mWifiConfigStore.registerStoreData(mStoreData); - mStoreData.setShareData(TEST_SHARE_DATA); + mWifiConfigStore.registerStoreData(mSharedStoreData); + mSharedStoreData.setData(TEST_SHARE_DATA); // Perform write for the share store file. mWifiConfigStore.write(true); mWifiConfigStore.read(); // Verify data content for both user and share data. - assertEquals(TEST_SHARE_DATA, mStoreData.getShareData()); + assertEquals(TEST_SHARE_DATA, mSharedStoreData.getData()); } /** @@ -386,21 +450,22 @@ public class WifiConfigStoreTest { @Test public void testReadWillResetStoreData() throws Exception { // Register and setup store data. - mWifiConfigStore.registerStoreData(mStoreData); + mWifiConfigStore.registerStoreData(mSharedStoreData); + mWifiConfigStore.registerStoreData(mUserStoreData); // Perform force write with empty data content to both user and share store file. - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.switchUserStoresAndRead(mUserStores); mWifiConfigStore.write(true); // Setup data container with some value. - mStoreData.setUserData(TEST_USER_DATA); - mStoreData.setShareData(TEST_SHARE_DATA); + mUserStoreData.setData(TEST_USER_DATA); + mSharedStoreData.setData(TEST_SHARE_DATA); // Perform read of both user and share store file and verify data in the data container // is in sync (empty) with what is in the file. mWifiConfigStore.read(); - assertNull(mStoreData.getShareData()); - assertNull(mStoreData.getUserData()); + assertNull(mSharedStoreData.getData()); + assertNull(mUserStoreData.getData()); } /** @@ -413,7 +478,7 @@ public class WifiConfigStoreTest { @Test public void testReadWifiConfigStoreData() throws Exception { // Setup network list. - NetworkListStoreData networkList = new NetworkListStoreData(mContext); + NetworkListStoreData networkList = new NetworkListUserStoreData(mContext); mWifiConfigStore.registerStoreData(networkList); WifiConfiguration openNetwork = WifiConfigurationTestUtil.createOpenNetwork(); openNetwork.creatorName = TEST_CREATOR_NAME; @@ -440,9 +505,9 @@ public class WifiConfigStoreTest { byte[] xmlBytes = xmlString.getBytes(StandardCharsets.UTF_8); mUserStore.storeRawDataToWrite(xmlBytes); - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.switchUserStoresAndRead(mUserStores); WifiConfigurationTestUtil.assertConfigurationsEqualForConfigStore( - userConfigs, networkList.getUserConfigurations()); + userConfigs, networkList.getConfigurations()); assertEquals(ssidList, deletedEphemeralSsids.getSsidList()); } @@ -455,10 +520,10 @@ public class WifiConfigStoreTest { @Test public void testWriteWifiConfigStoreData() throws Exception { // Setup user store. - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.switchUserStoresAndRead(mUserStores); // Setup network list store data. - NetworkListStoreData networkList = new NetworkListStoreData(mContext); + NetworkListStoreData networkList = new NetworkListUserStoreData(mContext); mWifiConfigStore.registerStoreData(networkList); WifiConfiguration openNetwork = WifiConfigurationTestUtil.createOpenNetwork(); openNetwork.creatorName = TEST_CREATOR_NAME; @@ -467,7 +532,7 @@ public class WifiConfigStoreTest { openNetwork.setRandomizedMacAddress(TEST_RANDOMIZED_MAC); List<WifiConfiguration> userConfigs = new ArrayList<>(); userConfigs.add(openNetwork); - networkList.setUserConfigurations(userConfigs); + networkList.setConfigurations(userConfigs); // Setup deleted ephemeral SSID list store data. DeletedEphemeralSsidsStoreData deletedEphemeralSsids = @@ -503,14 +568,12 @@ public class WifiConfigStoreTest { public void testReadWifiConfigStoreDataIndicateClientsThatThereIsNoDataForThem() throws Exception { // Set both the user store & shared store files. - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.switchUserStoresAndRead(mUserStores); String storeData1Name = "test1"; String storeData2Name = "test2"; - WifiConfigStore.StoreData storeData1 = mock(WifiConfigStore.StoreData.class); - WifiConfigStore.StoreData storeData2 = mock(WifiConfigStore.StoreData.class); - when(storeData1.getName()).thenReturn(storeData1Name); - when(storeData2.getName()).thenReturn(storeData2Name); + StoreData storeData1 = mock(StoreData.class); + StoreData storeData2 = mock(StoreData.class); assertTrue(mWifiConfigStore.registerStoreData(storeData1)); assertTrue(mWifiConfigStore.registerStoreData(storeData2)); @@ -524,58 +587,122 @@ public class WifiConfigStoreTest { storeData1Name, storeData2Name); // Scenario 1: StoreData1 in shared store file. + when(storeData1.getName()).thenReturn(storeData1Name); + when(storeData2.getName()).thenReturn(storeData2Name); + when(storeData1.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_SHARED_GENERAL); + when(storeData2.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_SHARED_GENERAL); mSharedStore.storeRawDataToWrite(fileContentsXmlStringWithOnlyStoreData1.getBytes()); mUserStore.storeRawDataToWrite(null); mWifiConfigStore.read(); - verify(storeData1).deserializeData(notNull(), anyInt(), eq(true)); - verify(storeData1, never()).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData1).deserializeData(eq(null), anyInt(), eq(false)); - verify(storeData2).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData2).deserializeData(eq(null), anyInt(), eq(false)); + verify(storeData1).deserializeData(notNull(), anyInt()); + verify(storeData1, never()).deserializeData(eq(null), anyInt()); + verify(storeData2).deserializeData(eq(null), anyInt()); reset(storeData1, storeData2); // Scenario 2: StoreData2 in user store file. + when(storeData1.getName()).thenReturn(storeData1Name); + when(storeData2.getName()).thenReturn(storeData2Name); + when(storeData1.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_USER_GENERAL); + when(storeData2.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_USER_GENERAL); mSharedStore.storeRawDataToWrite(null); mUserStore.storeRawDataToWrite(fileContentsXmlStringWithOnlyStoreData2.getBytes()); mWifiConfigStore.read(); - verify(storeData1).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData1).deserializeData(eq(null), anyInt(), eq(false)); - verify(storeData2).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData2).deserializeData(notNull(), anyInt(), eq(false)); - verify(storeData2, never()).deserializeData(eq(null), anyInt(), eq(false)); + verify(storeData1).deserializeData(eq(null), anyInt()); + verify(storeData2).deserializeData(notNull(), anyInt()); + verify(storeData2, never()).deserializeData(eq(null), anyInt()); reset(storeData1, storeData2); // Scenario 3: StoreData1 in shared store file & StoreData2 in user store file. + when(storeData1.getName()).thenReturn(storeData1Name); + when(storeData2.getName()).thenReturn(storeData2Name); + when(storeData1.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_SHARED_GENERAL); + when(storeData2.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_USER_GENERAL); mSharedStore.storeRawDataToWrite(fileContentsXmlStringWithOnlyStoreData1.getBytes()); mUserStore.storeRawDataToWrite(fileContentsXmlStringWithOnlyStoreData2.getBytes()); mWifiConfigStore.read(); - verify(storeData1).deserializeData(notNull(), anyInt(), eq(true)); - verify(storeData1, never()).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData1).deserializeData(eq(null), anyInt(), eq(false)); - verify(storeData2).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData2).deserializeData(notNull(), anyInt(), eq(false)); - verify(storeData2, never()).deserializeData(eq(null), anyInt(), eq(false)); + verify(storeData1).deserializeData(notNull(), anyInt()); + verify(storeData1, never()).deserializeData(eq(null), anyInt()); + verify(storeData2).deserializeData(notNull(), anyInt()); + verify(storeData2, never()).deserializeData(eq(null), anyInt()); reset(storeData1, storeData2); // Scenario 4: StoreData1 & StoreData2 in shared store file. + when(storeData1.getName()).thenReturn(storeData1Name); + when(storeData2.getName()).thenReturn(storeData2Name); + when(storeData1.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_SHARED_GENERAL); + when(storeData2.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_SHARED_GENERAL); mSharedStore.storeRawDataToWrite( fileContentsXmlStringWithStoreData1AndStoreData2.getBytes()); mUserStore.storeRawDataToWrite(null); mWifiConfigStore.read(); - verify(storeData1).deserializeData(notNull(), anyInt(), eq(true)); - verify(storeData1, never()).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData1).deserializeData(eq(null), anyInt(), eq(false)); - verify(storeData2).deserializeData(notNull(), anyInt(), eq(true)); - verify(storeData2, never()).deserializeData(eq(null), anyInt(), eq(true)); - verify(storeData2).deserializeData(eq(null), anyInt(), eq(false)); + verify(storeData1).deserializeData(notNull(), anyInt()); + verify(storeData1, never()).deserializeData(eq(null), anyInt()); + verify(storeData2).deserializeData(notNull(), anyInt()); + verify(storeData2, never()).deserializeData(eq(null), anyInt()); reset(storeData1, storeData2); } /** + * Tests the write API behavior when all the store data's registered for a given store file + * has no new data to write. + * Expected behaviour: The write should not trigger a new file write for that specific store + * file. + */ + @Test + public void testWriteWithNoNewData() throws Exception { + StoreData sharedStoreData = mock(StoreData.class); + when(sharedStoreData.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_SHARED_GENERAL); + when(sharedStoreData.hasNewDataToSerialize()).thenReturn(true); + when(sharedStoreData.getName()).thenReturn("sharedStoreData"); + + StoreData userStoreData = mock(StoreData.class); + when(userStoreData.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_USER_GENERAL); + when(userStoreData.hasNewDataToSerialize()).thenReturn(true); + when(userStoreData.getName()).thenReturn("userStoreData"); + + StoreData userStoreNetworkSuggestionsData = + mock(StoreData.class); + when(userStoreNetworkSuggestionsData.getStoreFileId()) + .thenReturn(WifiConfigStore.STORE_FILE_USER_NETWORK_SUGGESTIONS); + when(userStoreNetworkSuggestionsData.hasNewDataToSerialize()).thenReturn(false); + when(userStoreNetworkSuggestionsData.getName()) + .thenReturn("userStoreNetworkSuggestionsData"); + + assertTrue(mWifiConfigStore.registerStoreData(sharedStoreData)); + assertTrue(mWifiConfigStore.registerStoreData(userStoreData)); + assertTrue(mWifiConfigStore.registerStoreData(userStoreNetworkSuggestionsData)); + + // Write both share and user config store. + mWifiConfigStore.setUserStores(mUserStores); + + // Now trigger a write. + mWifiConfigStore.write(true); + + verify(sharedStoreData).hasNewDataToSerialize(); + verify(userStoreData).hasNewDataToSerialize(); + verify(userStoreNetworkSuggestionsData).hasNewDataToSerialize(); + + // Verify that we serialized data from the first 2 data source, but not from the last one. + verify(sharedStoreData).serializeData(any()); + verify(userStoreData).serializeData(any()); + verify(userStoreNetworkSuggestionsData, never()).serializeData(any()); + } + + /** * Verify that a XmlPullParserException will be thrown when reading an user store file * containing unknown data. * @@ -591,7 +718,7 @@ public class WifiConfigStoreTest { + "</UnknownTag>\n" + "</WifiConfigStoreData>\n"; mUserStore.storeRawDataToWrite(storeFileData.getBytes(StandardCharsets.UTF_8)); - mWifiConfigStore.switchUserStoreAndRead(mUserStore); + mWifiConfigStore.switchUserStoresAndRead(mUserStores); } /** @@ -621,8 +748,8 @@ public class WifiConfigStoreTest { private byte[] mStoreBytes; private boolean mStoreWritten; - public MockStoreFile() { - super(new File("MockStoreFile")); + MockStoreFile(@WifiConfigStore.StoreFileId int fileId) { + super(new File("MockStoreFile"), fileId); } @Override @@ -643,7 +770,9 @@ public class WifiConfigStoreTest { @Override public void writeBufferedRawData() { - mStoreWritten = true; + if (!ArrayUtils.isEmpty(mStoreBytes)) { + mStoreWritten = true; + } } public byte[] getStoreBytes() { @@ -658,71 +787,63 @@ public class WifiConfigStoreTest { /** * Mock data container for providing test data for the store file. */ - private class MockStoreData implements WifiConfigStore.StoreData { + private class MockStoreData implements StoreData { private static final String XML_TAG_TEST_HEADER = "TestHeader"; private static final String XML_TAG_TEST_DATA = "TestData"; - private String mShareData; - private String mUserData; + private @WifiConfigStore.StoreFileId int mFileId; + private String mData; + private boolean mHasAnyNewData = true; - MockStoreData() {} + MockStoreData(@WifiConfigStore.StoreFileId int fileId) { + mFileId = fileId; + } @Override - public void serializeData(XmlSerializer out, boolean shared) + public void serializeData(XmlSerializer out) throws XmlPullParserException, IOException { - if (shared) { - XmlUtil.writeNextValue(out, XML_TAG_TEST_DATA, mShareData); - } else { - XmlUtil.writeNextValue(out, XML_TAG_TEST_DATA, mUserData); - } + XmlUtil.writeNextValue(out, XML_TAG_TEST_DATA, mData); } @Override - public void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared) + public void deserializeData(XmlPullParser in, int outerTagDepth) throws XmlPullParserException, IOException { if (in == null) { return; } - if (shared) { - mShareData = (String) XmlUtil.readNextValueWithName(in, XML_TAG_TEST_DATA); - } else { - mUserData = (String) XmlUtil.readNextValueWithName(in, XML_TAG_TEST_DATA); - } + mData = (String) XmlUtil.readNextValueWithName(in, XML_TAG_TEST_DATA); } @Override - public void resetData(boolean shared) { - if (shared) { - mShareData = null; - } else { - mUserData = null; - } + public void resetData() { + mData = null; } @Override - public String getName() { - return XML_TAG_TEST_HEADER; + public boolean hasNewDataToSerialize() { + return mHasAnyNewData; } @Override - public boolean supportShareData() { - return true; + public String getName() { + return XML_TAG_TEST_HEADER; } - public String getShareData() { - return mShareData; + @Override + public @WifiConfigStore.StoreFileId int getStoreFileId() { + return mFileId; } - public void setShareData(String shareData) { - mShareData = shareData; + public String getData() { + return mData; } - public String getUserData() { - return mUserData; + public void setData(String data) { + mData = data; } - public void setUserData(String userData) { - mUserData = userData; + public void setHasAnyNewData(boolean hasAnyNewData) { + mHasAnyNewData = hasAnyNewData; } } } |