WifiConfigManagerNew: handle User Switches
On user switch WifiConfigManager needs to
1. Switch the user store file in WifiConfigStore.
2. Check if the user is already unlocked.
3. If user is already unlocked, reload all the configurations
from the store files. This would ensure that the old user's
private networks are hidden.
4. If user is not unlocked, wait for the user to unlock to reload all
the configurations from the store files.
5. Write the store files again to write any private networks of the new
user that were in the shared store perviously to the user's store file.
Since we no longer have a single store file for all users, we don't need
the |ConfigurationMap.handleUserSwitch| method which was essentially
doing the same thing as above (but all user's networks were always
loaded in memory). So mark the method deprecated and remove it once the
old WifiConfigManager is removed.
BUG: 30783976
TEST: Added unit test
Change-Id: I4269b27ab5534b2e4ed4dfed1abd8327d354c66a
diff --git a/service/java/com/android/server/wifi/ConfigurationMap.java b/service/java/com/android/server/wifi/ConfigurationMap.java
index a94ff0d..bb8849c 100644
--- a/service/java/com/android/server/wifi/ConfigurationMap.java
+++ b/service/java/com/android/server/wifi/ConfigurationMap.java
@@ -86,14 +86,16 @@
*
* @param userId the id of the new foreground user
* @return a list of {@link WifiConfiguration}s that became hidden because of the user switch
+ * @deprecated Remove this once we migrate to {@link WifiConfigManagerNew}.
*/
+ @Deprecated
public List<WifiConfiguration> handleUserSwitch(int userId) {
mPerIDForCurrentUser.clear();
mPerFQDNForCurrentUser.clear();
mHiddenNetworkIdsForCurrentUser.clear();
final List<UserInfo> previousUserProfiles = mUserManager.getProfiles(mCurrentUserId);
- mCurrentUserId = userId;
+ setNewUser(userId);
final List<UserInfo> currentUserProfiles = mUserManager.getProfiles(mCurrentUserId);
final List<WifiConfiguration> hiddenConfigurations = new ArrayList<>();
@@ -115,6 +117,15 @@
return hiddenConfigurations;
}
+ /**
+ * Sets the new foreground user ID.
+ *
+ * @param userId the id of the new foreground user
+ */
+ public void setNewUser(int userId) {
+ mCurrentUserId = userId;
+ }
+
// RO methods:
public WifiConfiguration getForAllUsers(int netid) {
return mPerID.get(netid);
diff --git a/service/java/com/android/server/wifi/WifiConfigManagerNew.java b/service/java/com/android/server/wifi/WifiConfigManagerNew.java
index 9c073a9..512633c 100644
--- a/service/java/com/android/server/wifi/WifiConfigManagerNew.java
+++ b/service/java/com/android/server/wifi/WifiConfigManagerNew.java
@@ -45,7 +45,10 @@
import com.android.server.LocalServices;
import com.android.server.wifi.util.ScanResultUtil;
+import org.xmlpull.v1.XmlPullParserException;
+
import java.io.FileDescriptor;
+import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.BitSet;
@@ -233,6 +236,11 @@
*/
private int mCurrentUserId = UserHandle.USER_SYSTEM;
/**
+ *
+ * Flag to indicate that the new user's store has not yet been read since user switch.
+ */
+ private boolean mPendingUnlockStoreRead = false;
+ /**
* This is keeping track of the last network ID assigned. Any new networks will be assigned
* |mLastNetworkId + 1| as network ID.
*/
@@ -1673,30 +1681,126 @@
}
/**
- * Read the config store and load the in-memory lists from the store data retrieved.
+ * Helper method to clear internal databases.
+ * This method clears the:
+ * - List of configured networks.
+ * - Map of scan detail caches.
+ * - List of deleted ephemeral networks.
+ */
+ private void clearInternalData() {
+ mConfiguredNetworks.clear();
+ mDeletedEphemeralSSIDs.clear();
+ mScanDetailCaches.clear();
+ }
+
+ /**
+ * Helper method to perform the following operations during user switch:
+ * - Load from the new store files.
+ * - Save the store files again to migrate any user specific networks from the shared store
+ * to user store.
+ */
+ private void loadFromStoreAndMigrateAfterUserSwitch() {
+ if (loadFromStore()) {
+ saveToStore(true);
+ mPendingUnlockStoreRead = false;
+ }
+ }
+
+ /**
+ * Handles the switch to a different foreground user:
+ * - Flush the current state to the old user's store file.
+ * - Switch the user specific store file.
+ * - Reload the networks from the store files (shared & user).
+ * - Write the store files to move any user specific private networks from shared store to user
+ * store.
+ *
+ * Need to be called when {@link com.android.server.SystemService#onSwitchUser(int)} is invoked.
+ *
+ * @param userId The identifier of the new foreground user, after the switch.
+ */
+ public void handleUserSwitch(int userId) {
+ if (userId == mCurrentUserId) {
+ Log.w(TAG, "User already in foreground " + userId);
+ return;
+ }
+ if (mUserManager.isUserUnlockingOrUnlocked(mCurrentUserId)) {
+ saveToStore(true);
+ }
+ mCurrentUserId = userId;
+ mConfiguredNetworks.setNewUser(userId);
+ clearInternalData();
+
+ // Switch out the user store file.
+ mWifiConfigStore.switchUserStore(mWifiConfigStore.createUserFile(userId));
+ if (mUserManager.isUserUnlockingOrUnlocked(mCurrentUserId)) {
+ loadFromStoreAndMigrateAfterUserSwitch();
+ } else {
+ // Since the new user is not logged-in yet, we cannot read data from the store files
+ // yet.
+ mPendingUnlockStoreRead = true;
+ Log.i(TAG, "Waiting for user unlock to load from store");
+ }
+ }
+
+ /**
+ * Handles the unlock of foreground user. This maybe needed to read the store file if the user's
+ * CE storage is not visible when {@link #handleUserSwitch(int)} is invoked.
+ *
+ * Need to be called when {@link com.android.server.SystemService#onUnlockUser(int)} is invoked.
+ *
+ * @param userId The identifier of the user that unlocked.
+ */
+ public void handleUserUnlock(int userId) {
+ if (userId == mCurrentUserId && mPendingUnlockStoreRead) {
+ loadFromStoreAndMigrateAfterUserSwitch();
+ }
+ }
+
+ /**
+ * Handles the stop of foreground user. This is needed to write the store file to flush
+ * out any pending data before the user's CE store storage is unavailable.
+ *
+ * Need to be called when {@link com.android.server.SystemService#onStopUser(int)} is invoked.
+ *
+ * @param userId The identifier of the user that stopped.
+ */
+ public void handleUserStop(int userId) {
+ if (userId == mCurrentUserId && mUserManager.isUserUnlockingOrUnlocked(mCurrentUserId)) {
+ saveToStore(true);
+ clearInternalData();
+ mCurrentUserId = UserHandle.USER_SYSTEM;
+ }
+ }
+
+ /**
+ * Read the config store and load the in-memory lists from the store data retrieved and sends
+ * out the networks changed broadcast.
+ *
* This reads all the network configurations from:
* 1. Shared WifiConfigStore.xml
* 2. User WifiConfigStore.xml
* 3. PerProviderSubscription.conf
+ * @return true on success, false otherwise.
*/
- private void loadFromStore() {
+ private boolean loadFromStore() {
WifiConfigStoreData storeData;
long readStartTime = mClock.getElapsedSinceBootMillis();
try {
storeData = mWifiConfigStore.read();
- } catch (Exception e) {
- Log.wtf(TAG, "Reading from new store failed " + e + ". All saved networks are lost!");
- return;
+ } catch (IOException e) {
+ Log.wtf(TAG, "Reading from new store failed. All saved networks are lost!", e);
+ return false;
+ } catch (XmlPullParserException e) {
+ Log.wtf(TAG, "XML deserialization of store failed. All saved networks are lost!", e);
+ return false;
}
long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
Log.d(TAG, "Loading from store completed in " + readTime + " ms.");
// Clear out all the existing in-memory lists and load the lists from what was retrieved
// from the config store.
- mConfiguredNetworks.clear();
- mDeletedEphemeralSSIDs.clear();
- mScanDetailCaches.clear();
+ clearInternalData();
for (WifiConfiguration configuration : storeData.getConfigurations()) {
configuration.networkId = mLastNetworkId++;
if (mVerboseLoggingEnabled) {
@@ -1710,6 +1814,8 @@
if (mConfiguredNetworks.sizeForAllUsers() == 0) {
Log.w(TAG, "No stored networks found.");
}
+ sendConfiguredNetworksChangedBroadcast();
+ return true;
}
/**
@@ -1746,10 +1852,14 @@
long writeStartTime = mClock.getElapsedSinceBootMillis();
try {
mWifiConfigStore.write(forceWrite, storeData);
- } catch (Exception e) {
- Log.wtf(TAG, "Writing to store failed " + e + ". Saved networks maybe lost!");
+ } catch (IOException e) {
+ Log.wtf(TAG, "Writing to store failed. Saved networks maybe lost!", e);
+ return false;
+ } catch (XmlPullParserException e) {
+ Log.wtf(TAG, "XML serialization for store failed. Saved networks maybe lost!", e);
return false;
}
+
long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime;
Log.d(TAG, "Writing to store completed in " + writeTime + " ms.");
return true;
diff --git a/service/java/com/android/server/wifi/WifiConfigStoreData.java b/service/java/com/android/server/wifi/WifiConfigStoreData.java
index 55b3b52..9a796eb 100644
--- a/service/java/com/android/server/wifi/WifiConfigStoreData.java
+++ b/service/java/com/android/server/wifi/WifiConfigStoreData.java
@@ -23,6 +23,7 @@
import android.util.Pair;
import android.util.Xml;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FastXmlSerializer;
import com.android.server.wifi.util.XmlUtil;
import com.android.server.wifi.util.XmlUtil.IpConfigurationXmlUtil;
@@ -126,6 +127,26 @@
}
/**
+ * Returns the list of shared network configurations in the store data instance.
+ *
+ * @return List of WifiConfiguration objects corresponding to the networks.
+ */
+ @VisibleForTesting
+ public List<WifiConfiguration> getSharedConfigurations() {
+ return mSharedConfigurations;
+ }
+
+ /**
+ * Returns the list of user network configurations in the store data instance.
+ *
+ * @return List of WifiConfiguration objects corresponding to the networks.
+ */
+ @VisibleForTesting
+ public List<WifiConfiguration> getUserConfigurations() {
+ return mUserConfigurations;
+ }
+
+ /**
* Returns the set of all deleted ephemeral SSIDs in the store data instance.
*
* @return List of Strings corresponding to the SSIDs of deleted ephemeral networks.
diff --git a/service/java/com/android/server/wifi/WifiConfigStoreNew.java b/service/java/com/android/server/wifi/WifiConfigStoreNew.java
index cd0e1d2..72f5d63 100644
--- a/service/java/com/android/server/wifi/WifiConfigStoreNew.java
+++ b/service/java/com/android/server/wifi/WifiConfigStoreNew.java
@@ -237,9 +237,9 @@
* @param userStore StoreFile instance pointing to the user specific store file. This should
* be retrieved using {@link #createUserFile(int)} method.
*/
- public void handleUserSwitch(StoreFile userStore) throws IOException {
- // Flush out any stored data if present before switching the user stores.
- writeBufferedData();
+ public void switchUserStore(StoreFile userStore) {
+ // Stop any pending buffered writes, if any.
+ stopBufferedWriteAlarm();
mUserStore = userStore;
}
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerNewTest.java b/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerNewTest.java
index 88baa1e..799f3d2 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerNewTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerNewTest.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
import android.net.IpConfiguration;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
@@ -1578,6 +1579,161 @@
network2.networkId, 1).size());
}
+ /**
+ * Verifies the foreground user switch using {@link WifiConfigManagerNew#handleUserSwitch(int)}
+ * and ensures that any non current user private networks are moved to shared store file.
+ */
+ @Test
+ public void testHandleUserSwitchPushesOtherPrivateNetworksToSharedStore() throws Exception {
+ // Set up the user profiles stuff. Needed for |WifiConfigurationUtil.isVisibleToAnyProfile|
+ int user1 = UserHandle.USER_SYSTEM;
+ int user2 = UserHandle.USER_SYSTEM + 1;
+ setupUserProfiles(user1);
+ setupUserProfiles(user2);
+
+ int appId = 674;
+
+ // Create 3 networks. 1 for user1, 1 for user2 and 1 shared.
+ final WifiConfiguration user1Network = WifiConfigurationTestUtil.createPskNetwork();
+ user1Network.shared = false;
+ user1Network.creatorUid = UserHandle.getUid(user1, appId);
+ final WifiConfiguration user2Network = WifiConfigurationTestUtil.createPskNetwork();
+ user2Network.shared = false;
+ user2Network.creatorUid = UserHandle.getUid(user2, appId);
+ final WifiConfiguration sharedNetwork = WifiConfigurationTestUtil.createPskNetwork();
+
+ // Set up the store data first that is loaded.
+ List<WifiConfiguration> sharedNetworks = new ArrayList<WifiConfiguration>() {
+ {
+ add(sharedNetwork);
+ add(user2Network);
+ }
+ };
+ List<WifiConfiguration> userNetworks = new ArrayList<WifiConfiguration>() {
+ {
+ add(user1Network);
+ }
+ };
+ WifiConfigStoreData loadStoreData =
+ new WifiConfigStoreData(sharedNetworks, userNetworks, new HashSet<String>());
+ when(mWifiConfigStore.read()).thenReturn(loadStoreData);
+
+ // Now switch the user to user2
+ mWifiConfigManager.handleUserSwitch(user2);
+
+ // Set the expected network list before comparing. user1Network should be in shared data.
+ // Note: In the real world, user1Network will no longer be visible now because it should
+ // already be in user1's private store file. But, we're purposefully exposing it
+ // via |loadStoreData| to test if other user's private networks are pushed to shared store.
+ List<WifiConfiguration> expectedSharedNetworks = new ArrayList<WifiConfiguration>() {
+ {
+ add(sharedNetwork);
+ add(user1Network);
+ }
+ };
+ List<WifiConfiguration> expectedUserNetworks = new ArrayList<WifiConfiguration>() {
+ {
+ add(user2Network);
+ }
+ };
+ // Capture the written data for the old user and ensure that it was empty.
+ WifiConfigStoreData writtenStoreData = captureWriteStoreData();
+ assertTrue(writtenStoreData.getConfigurations().isEmpty());
+
+ // Now capture the next written data and ensure that user1Network is now in shared data.
+ writtenStoreData = captureWriteStoreData();
+ WifiConfigurationTestUtil.assertConfigurationsEqualForConfigManagerAddOrUpdate(
+ expectedSharedNetworks, writtenStoreData.getSharedConfigurations());
+ WifiConfigurationTestUtil.assertConfigurationsEqualForConfigManagerAddOrUpdate(
+ expectedUserNetworks, writtenStoreData.getUserConfigurations());
+ }
+
+ /**
+ * Verifies the foreground user switch using {@link WifiConfigManagerNew#handleUserSwitch(int)}
+ * and {@link WifiConfigManagerNew#handleUserUnlock(int)} and ensures that the new store is
+ * read immediately if the user is unlocked during the switch.
+ */
+ @Test
+ public void testHandleUserSwitchWhenUnlocked() throws Exception {
+ // Set up the user profiles stuff. Needed for |WifiConfigurationUtil.isVisibleToAnyProfile|
+ int user1 = UserHandle.USER_SYSTEM;
+ int user2 = UserHandle.USER_SYSTEM + 1;
+ setupUserProfiles(user1);
+ setupUserProfiles(user2);
+
+ when(mWifiConfigStore.read()).thenReturn(
+ new WifiConfigStoreData(
+ new ArrayList<WifiConfiguration>(), new ArrayList<WifiConfiguration>(),
+ new HashSet<String>()));
+
+ // user2 is unlocked and switched to foreground.
+ when(mUserManager.isUserUnlockingOrUnlocked(user2)).thenReturn(true);
+ mWifiConfigManager.handleUserSwitch(user2);
+ // Ensure that the read was invoked.
+ mContextConfigStoreMockOrder.verify(mWifiConfigStore).read();
+
+ // Unlock the user2 and ensure that we don't read the data now.
+ mWifiConfigManager.handleUserUnlock(user2);
+ mContextConfigStoreMockOrder.verify(mWifiConfigStore, never()).read();
+ }
+
+ /**
+ * Verifies the foreground user switch using {@link WifiConfigManagerNew#handleUserSwitch(int)}
+ * and {@link WifiConfigManagerNew#handleUserUnlock(int)} and ensures that the new store is not
+ * read until the user is unlocked.
+ */
+ public void testHandleUserSwitchWhenLocked() throws Exception {
+ // Set up the user profiles stuff. Needed for |WifiConfigurationUtil.isVisibleToAnyProfile|
+ int user1 = UserHandle.USER_SYSTEM;
+ int user2 = UserHandle.USER_SYSTEM + 1;
+ setupUserProfiles(user1);
+ setupUserProfiles(user2);
+
+ // user2 is locked and switched to foreground.
+ when(mUserManager.isUserUnlockingOrUnlocked(user2)).thenReturn(false);
+ mWifiConfigManager.handleUserSwitch(user2);
+
+ // Ensure that the read was not invoked.
+ mContextConfigStoreMockOrder.verify(mWifiConfigStore, never()).read();
+
+ // Now try unlocking some other user (user1), this should be ignored.
+ mWifiConfigManager.handleUserUnlock(user1);
+ mContextConfigStoreMockOrder.verify(mWifiConfigStore, never()).read();
+
+ when(mWifiConfigStore.read()).thenReturn(
+ new WifiConfigStoreData(
+ new ArrayList<WifiConfiguration>(), new ArrayList<WifiConfiguration>(),
+ new HashSet<String>()));
+
+ // Unlock the user2 and ensure that we read the data now.
+ mWifiConfigManager.handleUserUnlock(user2);
+ mContextConfigStoreMockOrder.verify(mWifiConfigStore).read();
+ }
+
+ /**
+ * Verifies that the foreground user stop using {@link WifiConfigManagerNew#handleUserStop(int)}
+ * and ensures that the store is written only when the foreground user is stopped.
+ */
+ @Test
+ public void testHandleUserStop() throws Exception {
+ // Set up the user profiles stuff. Needed for |WifiConfigurationUtil.isVisibleToAnyProfile|
+ int user1 = UserHandle.USER_SYSTEM;
+ int user2 = UserHandle.USER_SYSTEM + 1;
+ setupUserProfiles(user1);
+ setupUserProfiles(user2);
+
+ // Try stopping background user2 first, this should not do anything.
+ when(mUserManager.isUserUnlockingOrUnlocked(user2)).thenReturn(false);
+ mWifiConfigManager.handleUserStop(user2);
+ mContextConfigStoreMockOrder.verify(mWifiConfigStore, never()).read();
+
+ // Now try stopping the foreground user1, this should trigger a write to store.
+ mWifiConfigManager.handleUserStop(user1);
+ mContextConfigStoreMockOrder.verify(mWifiConfigStore, never()).read();
+ mContextConfigStoreMockOrder.verify(mWifiConfigStore).write(
+ anyBoolean(), any(WifiConfigStoreData.class));
+ }
+
private void createWifiConfigManager() {
mWifiConfigManager =
new WifiConfigManagerNew(
@@ -1703,28 +1859,36 @@
}
/**
- * Returns whether the provided network was in the store data or not.
+ * Helper method to capture the store data written in WifiConfigStore.write() method.
*/
- private boolean isNetworkInConfigStoreData(WifiConfiguration configuration) {
+ private WifiConfigStoreData captureWriteStoreData() {
try {
ArgumentCaptor<WifiConfigStoreData> storeDataCaptor =
ArgumentCaptor.forClass(WifiConfigStoreData.class);
mContextConfigStoreMockOrder.verify(mWifiConfigStore)
.write(anyBoolean(), storeDataCaptor.capture());
-
- WifiConfigStoreData storeData = storeDataCaptor.getValue();
-
- boolean foundNetworkInStoreData = false;
- for (WifiConfiguration retrievedConfig : storeData.getConfigurations()) {
- if (retrievedConfig.configKey().equals(configuration.configKey())) {
- foundNetworkInStoreData = true;
- }
- }
- return foundNetworkInStoreData;
+ return storeDataCaptor.getValue();
} catch (Exception e) {
fail("Exception encountered during write " + e);
}
- return false;
+ return null;
+ }
+
+ /**
+ * Returns whether the provided network was in the store data or not.
+ */
+ private boolean isNetworkInConfigStoreData(WifiConfiguration configuration) {
+ WifiConfigStoreData storeData = captureWriteStoreData();
+ if (storeData == null) {
+ return false;
+ }
+ boolean foundNetworkInStoreData = false;
+ for (WifiConfiguration retrievedConfig : storeData.getConfigurations()) {
+ if (retrievedConfig.configKey().equals(configuration.configKey())) {
+ foundNetworkInStoreData = true;
+ }
+ }
+ return foundNetworkInStoreData;
}
/**
@@ -2105,4 +2269,17 @@
retrievedNetwork.getNetworkSelectionStatus().getHasEverConnected());
}
+ /**
+ * Sets up a user profiles for WifiConfigManager testing.
+ *
+ * @param userId Id of the user.
+ */
+ private void setupUserProfiles(int userId) {
+ final UserInfo userInfo =
+ new UserInfo(userId, Integer.toString(userId), UserInfo.FLAG_PRIMARY);
+ List<UserInfo> userProfiles = Arrays.asList(userInfo);
+ when(mUserManager.getProfiles(userId)).thenReturn(userProfiles);
+ when(mUserManager.isUserUnlockingOrUnlocked(userId)).thenReturn(true);
+ }
+
}