From 393857be9cc0dd9438cb9c00cf1b05855bef39c1 Mon Sep 17 00:00:00 2001 From: Robin Lee Date: Fri, 4 Nov 2016 14:48:02 +0000 Subject: [PATCH 1/3] VpnSettings: show connected VPN even if deleted So there's a way to disconnect from it, if someone deletes all the keystore entries and the VPN doesn't actually exist any more (but is still sitting around in memory somewhere keeping the connection alive). Bug: 29093779 Fix: 32880676 Test: runtest -x tests/app/src/com/android/settings/vpn2/VpnTests.java Change-Id: I97671a74af746e5baaa5be0b5cff24e2b1766f53 --- .../android/settings/vpn2/VpnSettings.java | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java index c2ed1c0da3f..37bed6d2c14 100644 --- a/src/com/android/settings/vpn2/VpnSettings.java +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -49,6 +49,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; @@ -96,8 +97,9 @@ public class VpnSettings extends RestrictedSettingsFragment implements private Map mLegacyVpnPreferences = new ArrayMap<>(); private Map mAppPreferences = new ArrayMap<>(); - private HandlerThread mUpdaterThread; + @GuardedBy("this") private Handler mUpdater; + private HandlerThread mUpdaterThread; private LegacyVpnInfo mConnectedLegacyVpn; private boolean mUnavailable; @@ -181,11 +183,9 @@ public class VpnSettings extends RestrictedSettingsFragment implements mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback); // Trigger a refresh - if (mUpdater == null) { - mUpdaterThread = new HandlerThread("Refresh VPN list in background"); - mUpdaterThread.start(); - mUpdater = new Handler(mUpdaterThread.getLooper(), this); - } + mUpdaterThread = new HandlerThread("Refresh VPN list in background"); + mUpdaterThread.start(); + mUpdater = new Handler(mUpdaterThread.getLooper(), this); mUpdater.sendEmptyMessage(RESCAN_MESSAGE); } @@ -199,7 +199,7 @@ public class VpnSettings extends RestrictedSettingsFragment implements // Stop monitoring mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); - if (mUpdater != null) { + synchronized (this) { mUpdater.removeCallbacksAndMessages(null); mUpdater = null; mUpdaterThread.quit(); @@ -211,8 +211,6 @@ public class VpnSettings extends RestrictedSettingsFragment implements @Override @WorkerThread public boolean handleMessage(Message message) { - mUpdater.removeMessages(RESCAN_MESSAGE); - // Run heavy RPCs before switching to UI thread final List vpnProfiles = loadVpnProfiles(mKeyStore); final List vpnApps = getVpnApps(getActivity(), /* includeProfiles */ true); @@ -223,6 +221,13 @@ public class VpnSettings extends RestrictedSettingsFragment implements final Set alwaysOnAppVpnInfos = getAlwaysOnAppVpnInfos(); final String lockdownVpnKey = VpnUtils.getLockdownVpn(); + synchronized (this) { + if (mUpdater != null) { + mUpdater.removeMessages(RESCAN_MESSAGE); + mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS); + } + } + // Refresh list of VPNs getActivity().runOnUiThread(new Runnable() { @Override @@ -235,8 +240,9 @@ public class VpnSettings extends RestrictedSettingsFragment implements // Find new VPNs by subtracting existing ones from the full set final Set updates = new ArraySet<>(); + // Add legacy VPNs for (VpnProfile profile : vpnProfiles) { - LegacyVpnPreference p = findOrCreatePreference(profile); + LegacyVpnPreference p = findOrCreatePreference(profile, true); if (connectedLegacyVpns.containsKey(profile.key)) { p.setState(connectedLegacyVpns.get(profile.key).state); } else { @@ -245,6 +251,17 @@ public class VpnSettings extends RestrictedSettingsFragment implements p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(profile.key)); updates.add(p); } + + // Show connected VPNs even if the original entry in keystore is gone + for (LegacyVpnInfo vpn : connectedLegacyVpns.values()) { + final VpnProfile stubProfile = new VpnProfile(vpn.key); + LegacyVpnPreference p = findOrCreatePreference(stubProfile, false); + p.setState(vpn.state); + p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(vpn.key)); + updates.add(p); + } + + // Add VpnService VPNs for (AppVpnInfo app : vpnApps) { AppPreference p = findOrCreatePreference(app); if (connectedAppVpns.contains(app)) { @@ -276,8 +293,6 @@ public class VpnSettings extends RestrictedSettingsFragment implements } } }); - - mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS); return true; } @@ -361,16 +376,20 @@ public class VpnSettings extends RestrictedSettingsFragment implements }; @UiThread - private LegacyVpnPreference findOrCreatePreference(VpnProfile profile) { + private LegacyVpnPreference findOrCreatePreference(VpnProfile profile, boolean update) { LegacyVpnPreference pref = mLegacyVpnPreferences.get(profile.key); - if (pref == null) { + boolean created = false; + if (pref == null ) { pref = new LegacyVpnPreference(getPrefContext()); pref.setOnGearClickListener(mGearListener); pref.setOnPreferenceClickListener(this); mLegacyVpnPreferences.put(profile.key, pref); + created = true; + } + if (created || update) { + // This can change call-to-call because the profile can update and keep the same key. + pref.setProfile(profile); } - // This may change as the profile can update and keep the same key. - pref.setProfile(profile); return pref; } From 4f0a0d0a40b9ab8a94ee6b97c3425a9da46c78c3 Mon Sep 17 00:00:00 2001 From: Robin Lee Date: Fri, 4 Nov 2016 15:48:44 +0000 Subject: [PATCH 2/3] VpnSettings: slightly more robust callback context Test: runtest -x com/android/settings/vpn2/VpnTests.java Change-Id: I45fa0509c56211602f6abd55a2f44cdf76f28829 --- .../settings/vpn2/ConfigDialogFragment.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/com/android/settings/vpn2/ConfigDialogFragment.java b/src/com/android/settings/vpn2/ConfigDialogFragment.java index 86245d45d4b..d9f35af5dfa 100644 --- a/src/com/android/settings/vpn2/ConfigDialogFragment.java +++ b/src/com/android/settings/vpn2/ConfigDialogFragment.java @@ -55,6 +55,7 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface( ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + private Context mContext; private boolean mUnlocking = false; @@ -78,6 +79,12 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements frag.show(parent.getFragmentManager(), TAG_CONFIG_DIALOG); } + @Override + public void onAttach(final Context context) { + super.onAttach(context); + mContext = context; + } + @Override public void onResume() { super.onResume(); @@ -86,7 +93,7 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements if (!KeyStore.getInstance().isUnlocked()) { if (!mUnlocking) { // Let us unlock KeyStore. See you later! - Credentials.getInstance().unlock(getActivity()); + Credentials.getInstance().unlock(mContext); } else { // We already tried, but it is still not working! dismiss(); @@ -142,9 +149,9 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements // Possibly throw up a dialog to explain lockdown VPN. final boolean shouldLockdown = dialog.isVpnAlwaysOn(); final boolean shouldConnect = shouldLockdown || !dialog.isEditing(); - final boolean wasAlwaysOn = VpnUtils.isAlwaysOnOrLegacyLockdownActive(getContext()); + final boolean wasAlwaysOn = VpnUtils.isAlwaysOnOrLegacyLockdownActive(mContext); try { - final boolean replace = VpnUtils.isVpnActive(getContext()); + final boolean replace = VpnUtils.isVpnActive(mContext); if (shouldConnect && !isConnected(profile) && ConfirmLockdownFragment.shouldShow(replace, wasAlwaysOn, shouldLockdown)) { final Bundle opts = new Bundle(); @@ -185,19 +192,19 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements if (isVpnAlwaysOn) { // Show toast if vpn profile is not valid if (!profile.isValidLockdownProfile()) { - Toast.makeText(getContext(), R.string.vpn_lockdown_config_error, + Toast.makeText(mContext, R.string.vpn_lockdown_config_error, Toast.LENGTH_LONG).show(); return; } - final ConnectivityManager conn = ConnectivityManager.from(getActivity()); + final ConnectivityManager conn = ConnectivityManager.from(mContext); conn.setAlwaysOnVpnPackageForUser(UserHandle.myUserId(), null, /* lockdownEnabled */ false); - VpnUtils.setLockdownVpn(getContext(), profile.key); + VpnUtils.setLockdownVpn(mContext, profile.key); } else { // update only if lockdown vpn has been changed if (VpnUtils.isVpnLockdown(profile.key)) { - VpnUtils.clearLockdownVpn(getContext()); + VpnUtils.clearLockdownVpn(mContext); } } } @@ -219,11 +226,11 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements // Now try to start the VPN - this is not necessary if the profile is set as lockdown, // because just saving the profile in this mode will start a connection. if (!VpnUtils.isVpnLockdown(profile.key)) { - VpnUtils.clearLockdownVpn(getContext()); + VpnUtils.clearLockdownVpn(mContext); try { mService.startLegacyVpn(profile); } catch (IllegalStateException e) { - Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); + Toast.makeText(mContext, R.string.vpn_no_network, Toast.LENGTH_LONG).show(); } catch (RemoteException e) { Log.e(TAG, "Failed to connect", e); } @@ -241,7 +248,7 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements if (!isConnected(profile)) { return true; } - VpnUtils.clearLockdownVpn(getContext()); + VpnUtils.clearLockdownVpn(mContext); return mService.prepareVpn(null, VpnConfig.LEGACY_VPN, UserHandle.myUserId()); } catch (RemoteException e) { Log.e(TAG, "Failed to disconnect", e); From 9c2758f407165fffc732c94d77514088220aeba9 Mon Sep 17 00:00:00 2001 From: Robin Lee Date: Thu, 24 Nov 2016 15:46:54 +0000 Subject: [PATCH 3/3] VpnSettings PreferenceList tests For validating that when VPNs are added / removed, the right set of changes are made to the PreferenceGroup in which they are supposed to be shown. Bug: 30998549 Bug: 29093779 Test: runtest -x packages/apps/Settings/tests/unit/src/com/android/settings/vpn2/PreferenceListTest.java Change-Id: I9394db0e78cc984ab62e3670aa0a7942ae767a66 --- .../android/settings/vpn2/VpnSettings.java | 190 +++++++++++------- .../settings/vpn2/PreferenceListTest.java | 159 +++++++++++++++ 2 files changed, 279 insertions(+), 70 deletions(-) create mode 100644 tests/unit/src/com/android/settings/vpn2/PreferenceListTest.java diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java index 37bed6d2c14..3aac6395a28 100644 --- a/src/com/android/settings/vpn2/VpnSettings.java +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -50,6 +50,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; @@ -63,6 +64,7 @@ import com.android.settingslib.RestrictedLockUtils; import com.google.android.collect.Lists; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -221,81 +223,129 @@ public class VpnSettings extends RestrictedSettingsFragment implements final Set alwaysOnAppVpnInfos = getAlwaysOnAppVpnInfos(); final String lockdownVpnKey = VpnUtils.getLockdownVpn(); + // Refresh list of VPNs + getActivity().runOnUiThread(new UpdatePreferences(this) + .legacyVpns(vpnProfiles, connectedLegacyVpns, lockdownVpnKey) + .appVpns(vpnApps, connectedAppVpns, alwaysOnAppVpnInfos)); + synchronized (this) { if (mUpdater != null) { mUpdater.removeMessages(RESCAN_MESSAGE); mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS); } } - - // Refresh list of VPNs - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - // Can't do anything useful if the context has gone away - if (!isAdded()) { - return; - } - - // Find new VPNs by subtracting existing ones from the full set - final Set updates = new ArraySet<>(); - - // Add legacy VPNs - for (VpnProfile profile : vpnProfiles) { - LegacyVpnPreference p = findOrCreatePreference(profile, true); - if (connectedLegacyVpns.containsKey(profile.key)) { - p.setState(connectedLegacyVpns.get(profile.key).state); - } else { - p.setState(LegacyVpnPreference.STATE_NONE); - } - p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(profile.key)); - updates.add(p); - } - - // Show connected VPNs even if the original entry in keystore is gone - for (LegacyVpnInfo vpn : connectedLegacyVpns.values()) { - final VpnProfile stubProfile = new VpnProfile(vpn.key); - LegacyVpnPreference p = findOrCreatePreference(stubProfile, false); - p.setState(vpn.state); - p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(vpn.key)); - updates.add(p); - } - - // Add VpnService VPNs - for (AppVpnInfo app : vpnApps) { - AppPreference p = findOrCreatePreference(app); - if (connectedAppVpns.contains(app)) { - p.setState(AppPreference.STATE_CONNECTED); - } else { - p.setState(AppPreference.STATE_DISCONNECTED); - } - p.setAlwaysOn(alwaysOnAppVpnInfos.contains(app)); - updates.add(p); - } - - // Trim out deleted VPN preferences - mLegacyVpnPreferences.values().retainAll(updates); - mAppPreferences.values().retainAll(updates); - - final PreferenceGroup vpnGroup = getPreferenceScreen(); - for (int i = vpnGroup.getPreferenceCount() - 1; i >= 0; i--) { - Preference p = vpnGroup.getPreference(i); - if (updates.contains(p)) { - updates.remove(p); - } else { - vpnGroup.removePreference(p); - } - } - - // Show any new preferences on the screen - for (Preference pref : updates) { - vpnGroup.addPreference(pref); - } - } - }); return true; } + @VisibleForTesting + static class UpdatePreferences implements Runnable { + private List vpnProfiles = Collections.emptyList(); + private List vpnApps = Collections.emptyList(); + + private Map connectedLegacyVpns = + Collections.emptyMap(); + private Set connectedAppVpns = Collections.emptySet(); + + private Set alwaysOnAppVpnInfos = Collections.emptySet(); + private String lockdownVpnKey = null; + + private final VpnSettings mSettings; + + public UpdatePreferences(VpnSettings settings) { + mSettings = settings; + } + + public final UpdatePreferences legacyVpns(List vpnProfiles, + Map connectedLegacyVpns, String lockdownVpnKey) { + this.vpnProfiles = vpnProfiles; + this.connectedLegacyVpns = connectedLegacyVpns; + this.lockdownVpnKey = lockdownVpnKey; + return this; + } + + public final UpdatePreferences appVpns(List vpnApps, + Set connectedAppVpns, Set alwaysOnAppVpnInfos) { + this.vpnApps = vpnApps; + this.connectedAppVpns = connectedAppVpns; + this.alwaysOnAppVpnInfos = alwaysOnAppVpnInfos; + return this; + } + + @Override @UiThread + public void run() { + if (!mSettings.canAddPreferences()) { + return; + } + + // Find new VPNs by subtracting existing ones from the full set + final Set updates = new ArraySet<>(); + + // Add legacy VPNs + for (VpnProfile profile : vpnProfiles) { + LegacyVpnPreference p = mSettings.findOrCreatePreference(profile, true); + if (connectedLegacyVpns.containsKey(profile.key)) { + p.setState(connectedLegacyVpns.get(profile.key).state); + } else { + p.setState(LegacyVpnPreference.STATE_NONE); + } + p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(profile.key)); + updates.add(p); + } + + // Show connected VPNs even if the original entry in keystore is gone + for (LegacyVpnInfo vpn : connectedLegacyVpns.values()) { + final VpnProfile stubProfile = new VpnProfile(vpn.key); + LegacyVpnPreference p = mSettings.findOrCreatePreference(stubProfile, false); + p.setState(vpn.state); + p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(vpn.key)); + updates.add(p); + } + + // Add VpnService VPNs + for (AppVpnInfo app : vpnApps) { + AppPreference p = mSettings.findOrCreatePreference(app); + if (connectedAppVpns.contains(app)) { + p.setState(AppPreference.STATE_CONNECTED); + } else { + p.setState(AppPreference.STATE_DISCONNECTED); + } + p.setAlwaysOn(alwaysOnAppVpnInfos.contains(app)); + updates.add(p); + } + + // Trim out deleted VPN preferences + mSettings.setShownPreferences(updates); + } + } + + @VisibleForTesting + public boolean canAddPreferences() { + return isAdded(); + } + + @VisibleForTesting @UiThread + public void setShownPreferences(final Collection updates) { + mLegacyVpnPreferences.values().retainAll(updates); + mAppPreferences.values().retainAll(updates); + + // Change {@param updates} in-place to only contain new preferences that were not already + // added to the preference screen. + final PreferenceGroup vpnGroup = getPreferenceScreen(); + for (int i = vpnGroup.getPreferenceCount() - 1; i >= 0; i--) { + Preference p = vpnGroup.getPreference(i); + if (updates.contains(p)) { + updates.remove(p); + } else { + vpnGroup.removePreference(p); + } + } + + // Show any new preferences on the screen + for (Preference pref : updates) { + vpnGroup.addPreference(pref); + } + } + @Override public boolean onPreferenceClick(Preference preference) { if (preference instanceof LegacyVpnPreference) { @@ -375,8 +425,8 @@ public class VpnSettings extends RestrictedSettingsFragment implements } }; - @UiThread - private LegacyVpnPreference findOrCreatePreference(VpnProfile profile, boolean update) { + @VisibleForTesting @UiThread + public LegacyVpnPreference findOrCreatePreference(VpnProfile profile, boolean update) { LegacyVpnPreference pref = mLegacyVpnPreferences.get(profile.key); boolean created = false; if (pref == null ) { @@ -393,8 +443,8 @@ public class VpnSettings extends RestrictedSettingsFragment implements return pref; } - @UiThread - private AppPreference findOrCreatePreference(AppVpnInfo app) { + @VisibleForTesting @UiThread + public AppPreference findOrCreatePreference(AppVpnInfo app) { AppPreference pref = mAppPreferences.get(app); if (pref == null) { pref = new AppPreference(getPrefContext(), app.userId, app.packageName); diff --git a/tests/unit/src/com/android/settings/vpn2/PreferenceListTest.java b/tests/unit/src/com/android/settings/vpn2/PreferenceListTest.java new file mode 100644 index 00000000000..40958bae313 --- /dev/null +++ b/tests/unit/src/com/android/settings/vpn2/PreferenceListTest.java @@ -0,0 +1,159 @@ +/* + * 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.settings.vpn2; + +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import android.content.Context; +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.text.TextUtils; + +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnProfile; +import com.android.settings.R; +import com.android.settings.vpn2.VpnSettings; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class PreferenceListTest extends AndroidTestCase { + private static final String TAG = "PreferenceListTest"; + + @Mock VpnSettings mSettings; + + final Map mLegacyMocks = new HashMap<>(); + final Map mAppMocks = new HashMap<>(); + + @Override + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mLegacyMocks.clear(); + mAppMocks.clear(); + + doAnswer(invocation -> { + final String key = ((VpnProfile)(invocation.getArguments()[0])).key; + if (!mLegacyMocks.containsKey(key)) { + mLegacyMocks.put(key, mock(LegacyVpnPreference.class)); + } + return mLegacyMocks.get(key); + }).when(mSettings).findOrCreatePreference(any(VpnProfile.class), anyBoolean()); + + doAnswer(invocation -> { + final AppVpnInfo key = (AppVpnInfo)(invocation.getArguments()[0]); + if (!mAppMocks.containsKey(key)) { + mAppMocks.put(key, mock(AppPreference.class)); + } + return mAppMocks.get(key); + }).when(mSettings).findOrCreatePreference(any(AppVpnInfo.class)); + + doNothing().when(mSettings).setShownPreferences(any()); + doReturn(true).when(mSettings).canAddPreferences(); + } + + @SmallTest + public void testNothingShownByDefault() { + final VpnSettings.UpdatePreferences updater = new VpnSettings.UpdatePreferences(mSettings); + updater.run(); + + verify(mSettings, never()).findOrCreatePreference(any(VpnProfile.class), anyBoolean()); + assertEquals(0, mLegacyMocks.size()); + assertEquals(0, mAppMocks.size()); + } + + @SmallTest + public void testDisconnectedLegacyVpnShown() { + final VpnProfile vpnProfile = new VpnProfile("test-disconnected"); + + final VpnSettings.UpdatePreferences updater = new VpnSettings.UpdatePreferences(mSettings); + updater.legacyVpns( + /* vpnProfiles */ Collections.singletonList(vpnProfile), + /* connectedLegacyVpns */ Collections.emptyMap(), + /* lockdownVpnKey */ null); + updater.run(); + + verify(mSettings, times(1)).findOrCreatePreference(any(VpnProfile.class), eq(true)); + assertEquals(1, mLegacyMocks.size()); + assertEquals(0, mAppMocks.size()); + } + + @SmallTest + public void testConnectedLegacyVpnShownIfDeleted() { + final LegacyVpnInfo connectedLegacyVpn =new LegacyVpnInfo(); + connectedLegacyVpn.key = "test-connected"; + + final VpnSettings.UpdatePreferences updater = new VpnSettings.UpdatePreferences(mSettings); + updater.legacyVpns( + /* vpnProfiles */ Collections.emptyList(), + /* connectedLegacyVpns */ new HashMap() {{ + put(connectedLegacyVpn.key, connectedLegacyVpn); + }}, + /* lockdownVpnKey */ null); + updater.run(); + + verify(mSettings, times(1)).findOrCreatePreference(any(VpnProfile.class), eq(false)); + assertEquals(1, mLegacyMocks.size()); + assertEquals(0, mAppMocks.size()); + } + + @SmallTest + public void testConnectedLegacyVpnShownExactlyOnce() { + final VpnProfile vpnProfile = new VpnProfile("test-no-duplicates"); + final LegacyVpnInfo connectedLegacyVpn = new LegacyVpnInfo(); + connectedLegacyVpn.key = new String(vpnProfile.key); + + final VpnSettings.UpdatePreferences updater = new VpnSettings.UpdatePreferences(mSettings); + updater.legacyVpns( + /* vpnProfiles */ Collections.singletonList(vpnProfile), + /* connectedLegacyVpns */ new HashMap() {{ + put(connectedLegacyVpn.key, connectedLegacyVpn); + }}, + /* lockdownVpnKey */ null); + updater.run(); + + final ArgumentMatcher equalsFake = new ArgumentMatcher() { + @Override + public boolean matches(final Object arg) { + if (arg == vpnProfile) return true; + if (arg == null) return false; + return TextUtils.equals(((VpnProfile) arg).key, vpnProfile.key); + } + }; + + // The VPN profile should have been used to create a preference and set up at laest once + // with update=true to fill in all the fields. + verify(mSettings, atLeast(1)).findOrCreatePreference(argThat(equalsFake), eq(true)); + + // ...But no other VPN profile key should ever have been passed in. + verify(mSettings, never()).findOrCreatePreference(not(argThat(equalsFake)), anyBoolean()); + + // And so we should still have exactly 1 preference created. + assertEquals(1, mLegacyMocks.size()); + assertEquals(0, mAppMocks.size()); + } +}