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