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()); + } +}