From b6f787c4df8e111bdee75062c6a38f606c876858 Mon Sep 17 00:00:00 2001 From: Robin Lee Date: Tue, 5 Jul 2016 10:21:28 +0100 Subject: [PATCH] Show a disclaimer about enabling vpn lockdown Lockdown is now the default option, not best-effort mode. It's easier to shoot oneself in the foot now so we'll show a warning to explain that before switching it on. Bug: 29052115 Bug: 29076208 Test: com.android.settings.vpn2.AppSettingsTest Change-Id: Ia6845e6a7d57baa5476b8a021fb1255fd74aabea --- res/values/strings.xml | 14 ++- .../settings/vpn2/AppDialogFragment.java | 8 +- .../settings/vpn2/AppManagementFragment.java | 91 ++++++--------- .../settings/vpn2/ConfigDialogFragment.java | 97 +++++++++++---- .../vpn2/ConfirmLockdownFragment.java | 110 ++++++++++++++++++ src/com/android/settings/vpn2/VpnUtils.java | 35 +++++- 6 files changed, 263 insertions(+), 92 deletions(-) create mode 100644 src/com/android/settings/vpn2/ConfirmLockdownFragment.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 740657708f8..d466879638e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5347,9 +5347,19 @@ Forget VPN - Replace existing VPN? + Replace existing VPN? + + Set always-on VPN? + + By turning on this setting, you won\'t have an Internet connection until the VPN successfully connects + + Your existing VPN will be replaced, and you won\'t have an Internet connection until the VPN successfully connects + + You\'re already connected to an always-on VPN. If you connect to a different one, your existing VPN will be replaced, and always-on mode will turn off. - You\'re already connected to a VPN. If you connect to a different one, your existing VPN will be replaced. + You\'re already connected to a VPN. If you connect to a different one, your existing VPN will be replaced. + + Turn on %1$s can\'t connect diff --git a/src/com/android/settings/vpn2/AppDialogFragment.java b/src/com/android/settings/vpn2/AppDialogFragment.java index 6093e89edad..4075c01e522 100644 --- a/src/com/android/settings/vpn2/AppDialogFragment.java +++ b/src/com/android/settings/vpn2/AppDialogFragment.java @@ -158,7 +158,7 @@ public class AppDialogFragment extends InstrumentedDialogFragment implements App } final int userId = getUserId(); try { - if (mPackageInfo.packageName.equals(getConnectedPackage(mService, userId))) { + if (mPackageInfo.packageName.equals(VpnUtils.getConnectedPackage(mService, userId))) { mService.setAlwaysOnVpnPackage(userId, null, /* lockdownEnabled */ false); mService.prepareVpn(mPackageInfo.packageName, VpnConfig.LEGACY_VPN, userId); } @@ -176,10 +176,4 @@ public class AppDialogFragment extends InstrumentedDialogFragment implements App private int getUserId() { return UserHandle.getUserId(mPackageInfo.applicationInfo.uid); } - - private static String getConnectedPackage(IConnectivityManager service, final int userId) - throws RemoteException { - final VpnConfig config = service.getVpnConfig(userId); - return config != null ? config.user : null; - } } diff --git a/src/com/android/settings/vpn2/AppManagementFragment.java b/src/com/android/settings/vpn2/AppManagementFragment.java index 1a3000e1c3f..68e0659c102 100644 --- a/src/com/android/settings/vpn2/AppManagementFragment.java +++ b/src/com/android/settings/vpn2/AppManagementFragment.java @@ -21,18 +21,21 @@ import android.app.AppOpsManager; import android.app.Dialog; import android.app.DialogFragment; import android.content.Context; -import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.net.ConnectivityManager; +import android.net.IConnectivityManager; import android.os.Build; import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.support.v7.preference.Preference; +import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -51,7 +54,8 @@ import java.util.List; import static android.app.AppOpsManager.OP_ACTIVATE_VPN; public class AppManagementFragment extends SettingsPreferenceFragment - implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener { + implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener, + ConfirmLockdownFragment.ConfirmLockdownListener { private static final String TAG = "AppManagementFragment"; @@ -63,6 +67,7 @@ public class AppManagementFragment extends SettingsPreferenceFragment private PackageManager mPackageManager; private ConnectivityManager mConnectivityManager; + private IConnectivityManager mConnectivityService; // VPN app info private final int mUserId = UserHandle.myUserId(); @@ -108,6 +113,8 @@ public class AppManagementFragment extends SettingsPreferenceFragment mPackageManager = getContext().getPackageManager(); mConnectivityManager = getContext().getSystemService(ConnectivityManager.class); + mConnectivityService = IConnectivityManager.Stub + .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); mPreferenceVersion = findPreference(KEY_VERSION); mPreferenceAlwaysOn = (RestrictedSwitchPreference) findPreference(KEY_ALWAYS_ON_VPN); @@ -169,13 +176,23 @@ public class AppManagementFragment extends SettingsPreferenceFragment return true; } - private boolean onAlwaysOnVpnClick(final boolean isChecked) { - if (isChecked && isLegacyVpnLockDownOrAnotherPackageAlwaysOn()) { - // Show dialog if user replace always-on-vpn package and show not checked first - ReplaceExistingVpnFragment.show(this); + private boolean onAlwaysOnVpnClick(final boolean alwaysOnSetting) { + final boolean replacing = isAnotherVpnActive(); + final boolean wasAlwaysOn = VpnUtils.isAlwaysOnOrLegacyLockdownActive(getActivity()); + if (ConfirmLockdownFragment.shouldShow(replacing, wasAlwaysOn, alwaysOnSetting)) { + // Place a dialog to confirm that traffic should be locked down. + final Bundle options = null; + ConfirmLockdownFragment.show(this, replacing, wasAlwaysOn, alwaysOnSetting, options); return false; - } else { - return setAlwaysOnVpnByUI(isChecked); + } + // No need to show the dialog. Change the setting straight away. + return setAlwaysOnVpnByUI(alwaysOnSetting); + } + + @Override + public void onConfirmLockdown(Bundle options, boolean isEnabled) { + if (setAlwaysOnVpnByUI(isEnabled)) { + updateUI(); } } @@ -197,7 +214,7 @@ public class AppManagementFragment extends SettingsPreferenceFragment private boolean setAlwaysOnVpn(boolean isEnabled) { return mConnectivityManager.setAlwaysOnVpnPackageForUser(mUserId, - isEnabled ? mPackageName : null, /* lockdownEnabled */ false); + isEnabled ? mPackageName : null, /* lockdownEnabled */ true); } @VisibleForTesting @@ -293,15 +310,17 @@ public class AppManagementFragment extends SettingsPreferenceFragment return !ArrayUtils.isEmpty(ops); } - private boolean isLegacyVpnLockDownOrAnotherPackageAlwaysOn() { - if (mUserId == UserHandle.USER_SYSTEM) { - String lockdownKey = VpnUtils.getLockdownVpn(); - if (lockdownKey != null) { - return true; - } + /** + * @return {@code true} if another VPN (VpnService or legacy) is connected or set as always-on. + */ + private boolean isAnotherVpnActive() { + try { + final VpnConfig config = mConnectivityService.getVpnConfig(mUserId); + return config != null && !TextUtils.equals(config.user, mPackageName); + } catch (RemoteException e) { + Log.w(TAG, "Failure to look up active VPN", e); + return false; } - - return getAlwaysOnVpnPackage() != null && !isVpnAlwaysOn(); } public static class CannotConnectFragment extends InstrumentedDialogFragment { @@ -334,42 +353,4 @@ public class AppManagementFragment extends SettingsPreferenceFragment .create(); } } - - public static class ReplaceExistingVpnFragment extends InstrumentedDialogFragment - implements DialogInterface.OnClickListener { - private static final String TAG = "ReplaceExistingVpn"; - - @Override - public int getMetricsCategory() { - return MetricsEvent.DIALOG_VPN_REPLACE_EXISTING; - } - - public static void show(AppManagementFragment parent) { - if (parent.getFragmentManager().findFragmentByTag(TAG) == null) { - final ReplaceExistingVpnFragment frag = new ReplaceExistingVpnFragment(); - frag.setTargetFragment(parent, 0); - frag.show(parent.getFragmentManager(), TAG); - } - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.vpn_replace_always_on_vpn_title) - .setMessage(getActivity().getString(R.string.vpn_replace_always_on_vpn_message)) - .setNegativeButton(getActivity().getString(R.string.vpn_cancel), null) - .setPositiveButton(getActivity().getString(R.string.vpn_replace), this) - .create(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (getTargetFragment() instanceof AppManagementFragment) { - final AppManagementFragment target = (AppManagementFragment) getTargetFragment(); - if (target.setAlwaysOnVpnByUI(true)) { - target.updateUI(); - } - } - } - } } diff --git a/src/com/android/settings/vpn2/ConfigDialogFragment.java b/src/com/android/settings/vpn2/ConfigDialogFragment.java index 1a180c571de..9dbf752c06e 100644 --- a/src/com/android/settings/vpn2/ConfigDialogFragment.java +++ b/src/com/android/settings/vpn2/ConfigDialogFragment.java @@ -16,6 +16,7 @@ package com.android.settings.vpn2; +import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.content.Context; @@ -29,6 +30,7 @@ import android.os.UserHandle; import android.security.Credentials; import android.security.KeyStore; import android.util.Log; +import android.view.View; import android.widget.Toast; import com.android.internal.logging.MetricsProto; @@ -41,8 +43,9 @@ import com.android.settings.core.instrumentation.InstrumentedDialogFragment; /** * Fragment wrapper around a {@link ConfigDialog}. */ -public class ConfigDialogFragment extends InstrumentedDialogFragment - implements DialogInterface.OnClickListener { +public class ConfigDialogFragment extends InstrumentedDialogFragment implements + DialogInterface.OnClickListener, DialogInterface.OnShowListener, View.OnClickListener, + ConfirmLockdownFragment.ConfirmLockdownListener { private static final String TAG_CONFIG_DIALOG = "vpnconfigdialog"; private static final String TAG = "ConfigDialogFragment"; @@ -103,7 +106,31 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment boolean editing = args.getBoolean(ARG_EDITING); boolean exists = args.getBoolean(ARG_EXISTS); - return new ConfigDialog(getActivity(), this, profile, editing, exists); + final Dialog dialog = new ConfigDialog(getActivity(), this, profile, editing, exists); + dialog.setOnShowListener(this); + return dialog; + } + + /** + * Override for the default onClick handler which also calls dismiss(). + * + * @see DialogInterface.OnClickListener#onClick(DialogInterface, int) + */ + @Override + public void onShow(DialogInterface dialogInterface) { + ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this); + } + + @Override + public void onClick(View positiveButton) { + onClick(getDialog(), AlertDialog.BUTTON_POSITIVE); + } + + @Override + public void onConfirmLockdown(Bundle options, boolean isEnabled) { + VpnProfile profile = (VpnProfile) options.getParcelable(ARG_PROFILE); + connect(profile, isEnabled); + dismiss(); } @Override @@ -112,24 +139,24 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment VpnProfile profile = dialog.getProfile(); if (button == DialogInterface.BUTTON_POSITIVE) { - // Update KeyStore entry - KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(), - KeyStore.UID_SELF, /* flags */ 0); - - // Flush out previous connection, which may be an old version of the profile - if (!disconnect(profile)) { - Log.w(TAG, "Unable to remove previous connection. Continuing anyway."); - } - - updateLockdownVpn(dialog.isVpnAlwaysOn(), profile); - - // If we are not editing, connect! - if (!dialog.isEditing() && !VpnUtils.isVpnLockdown(profile.key)) { - try { - connect(profile); - } catch (RemoteException e) { - Log.e(TAG, "Failed to connect", e); + // 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()); + try { + final boolean replace = VpnUtils.isVpnActive(getContext()); + if (shouldConnect && !isConnected(profile) && + ConfirmLockdownFragment.shouldShow(replace, wasAlwaysOn, shouldLockdown)) { + final Bundle opts = new Bundle(); + opts.putParcelable(ARG_PROFILE, profile); + ConfirmLockdownFragment.show(this, replace, wasAlwaysOn, shouldLockdown, opts); + } else if (shouldConnect) { + connect(profile, shouldLockdown); + } else { + save(profile, false); } + } catch (RemoteException e) { + Log.w(TAG, "Failed to check active VPN state. Skipping.", e); } } else if (button == DialogInterface.BUTTON_NEUTRAL) { // Disable profile if connected @@ -175,11 +202,31 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment } } - private void connect(VpnProfile profile) throws RemoteException { - try { - mService.startLegacyVpn(profile); - } catch (IllegalStateException e) { - Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); + private void save(VpnProfile profile, boolean lockdown) { + KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(), + KeyStore.UID_SELF, /* flags */ 0); + + // Flush out old version of profile + disconnect(profile); + + // Notify lockdown VPN that the profile has changed. + updateLockdownVpn(lockdown, profile); + } + + private void connect(VpnProfile profile, boolean lockdown) { + save(profile, lockdown); + + // 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()); + try { + mService.startLegacyVpn(profile); + } catch (IllegalStateException e) { + Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect", e); + } } } diff --git a/src/com/android/settings/vpn2/ConfirmLockdownFragment.java b/src/com/android/settings/vpn2/ConfirmLockdownFragment.java new file mode 100644 index 00000000000..4d643cfd87e --- /dev/null +++ b/src/com/android/settings/vpn2/ConfirmLockdownFragment.java @@ -0,0 +1,110 @@ +/* + * 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 android.app.Fragment; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.os.Bundle; + +import com.android.internal.logging.MetricsProto.MetricsEvent; +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +public class ConfirmLockdownFragment extends InstrumentedDialogFragment + implements DialogInterface.OnClickListener { + public interface ConfirmLockdownListener { + public void onConfirmLockdown(Bundle options, boolean isEnabled); + } + + private static final String TAG = "ConfirmLockdown"; + + @Override + public int getMetricsCategory() { + return MetricsEvent.DIALOG_VPN_REPLACE_EXISTING; + } + + private static final String ARG_REPLACING = "replacing"; + private static final String ARG_LOCKDOWN_SRC = "lockdown_old"; + private static final String ARG_LOCKDOWN_DST = "lockdown_new"; + private static final String ARG_OPTIONS = "options"; + + public static boolean shouldShow(boolean replacing, boolean fromLockdown, boolean toLockdown) { + // We only need to show this if we are: + // - replacing an existing connection + // - switching on always-on mode where it was not enabled before. + return replacing || (toLockdown && !fromLockdown); + } + + public static void show(Fragment parent, boolean replacing, + boolean fromLockdown, boolean toLockdown, Bundle options) { + if (parent.getFragmentManager().findFragmentByTag(TAG) != null) { + // Already exists. Don't show it twice. + return; + } + final Bundle args = new Bundle(); + args.putBoolean(ARG_REPLACING, replacing); + args.putBoolean(ARG_LOCKDOWN_SRC, fromLockdown); + args.putBoolean(ARG_LOCKDOWN_DST, toLockdown); + args.putParcelable(ARG_OPTIONS, options); + + final ConfirmLockdownFragment frag = new ConfirmLockdownFragment(); + frag.setArguments(args); + frag.setTargetFragment(parent, 0); + frag.show(parent.getFragmentManager(), TAG); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final boolean replacing = getArguments().getBoolean(ARG_REPLACING); + final boolean wasAlwaysOn = getArguments().getBoolean(ARG_LOCKDOWN_SRC); + final boolean nowAlwaysOn = getArguments().getBoolean(ARG_LOCKDOWN_DST); + + final int titleId = replacing ? R.string.vpn_replace_vpn_title : R.string.vpn_set_vpn_title; + final int actionId = + (replacing ? R.string.vpn_replace : + (nowAlwaysOn ? R.string.vpn_turn_on : R.string.okay)); + final int messageId; + if (nowAlwaysOn) { + messageId = replacing + ? R.string.vpn_replace_always_on_vpn_enable_message + : R.string.vpn_first_always_on_vpn_message; + } else { + messageId = wasAlwaysOn + ? R.string.vpn_replace_always_on_vpn_disable_message + : R.string.vpn_replace_vpn_message; + } + + return new AlertDialog.Builder(getActivity()) + .setTitle(titleId) + .setMessage(messageId) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(actionId, this) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (getTargetFragment() instanceof ConfirmLockdownListener) { + ((ConfirmLockdownListener) getTargetFragment()).onConfirmLockdown( + getArguments().getParcelable(ARG_OPTIONS), + getArguments().getBoolean(ARG_LOCKDOWN_DST)); + } + } +} + diff --git a/src/com/android/settings/vpn2/VpnUtils.java b/src/com/android/settings/vpn2/VpnUtils.java index 6afa79b73bd..5990381833b 100644 --- a/src/com/android/settings/vpn2/VpnUtils.java +++ b/src/com/android/settings/vpn2/VpnUtils.java @@ -17,16 +17,20 @@ package com.android.settings.vpn2; import android.content.Context; import android.net.ConnectivityManager; +import android.net.IConnectivityManager; +import android.os.RemoteException; +import android.os.ServiceManager; import android.security.Credentials; import android.security.KeyStore; +import com.android.internal.net.VpnConfig; + /** * Utility functions for vpn. * * Keystore methods should only be called in system user */ public class VpnUtils { - public static String getLockdownVpn() { final byte[] value = KeyStore.getInstance().get(Credentials.LOCKDOWN_VPN); return value == null ? null : new String(value); @@ -35,17 +39,42 @@ public class VpnUtils { public static void clearLockdownVpn(Context context) { KeyStore.getInstance().delete(Credentials.LOCKDOWN_VPN); // Always notify ConnectivityManager after keystore update - context.getSystemService(ConnectivityManager.class).updateLockdownVpn(); + getConnectivityManager(context).updateLockdownVpn(); } public static void setLockdownVpn(Context context, String lockdownKey) { KeyStore.getInstance().put(Credentials.LOCKDOWN_VPN, lockdownKey.getBytes(), KeyStore.UID_SELF, /* flags */ 0); // Always notify ConnectivityManager after keystore update - context.getSystemService(ConnectivityManager.class).updateLockdownVpn(); + getConnectivityManager(context).updateLockdownVpn(); } public static boolean isVpnLockdown(String key) { return key.equals(getLockdownVpn()); } + + public static boolean isAlwaysOnOrLegacyLockdownActive(Context context) { + final int userId = context.getUserId(); + return getLockdownVpn() != null + || getConnectivityManager(context).getAlwaysOnVpnPackageForUser(userId) != null; + } + + public static boolean isVpnActive(Context context) throws RemoteException { + return getIConnectivityManager().getVpnConfig(context.getUserId()) != null; + } + + public static String getConnectedPackage(IConnectivityManager service, final int userId) + throws RemoteException { + final VpnConfig config = service.getVpnConfig(userId); + return config != null ? config.user : null; + } + + private static ConnectivityManager getConnectivityManager(Context context) { + return context.getSystemService(ConnectivityManager.class); + } + + private static IConnectivityManager getIConnectivityManager() { + return IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + } }