Merge "vpn2: show third-party VPN services"
diff --git a/res/layout/preference_vpn.xml b/res/layout/preference_vpn.xml
new file mode 100644
index 0000000..95d3253
--- /dev/null
+++ b/res/layout/preference_vpn.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+    <View
+        android:id="@+id/divider_manage"
+        android:layout_width="2dip"
+        android:layout_height="match_parent"
+        android:layout_marginTop="5dip"
+        android:layout_marginBottom="5dip"
+        android:background="@android:drawable/divider_horizontal_dark" />
+    <ImageView
+        android:id="@+id/manage"
+        android:layout_width="wrap_content"
+        android:layout_height="fill_parent"
+        android:paddingStart="16dip"
+        android:paddingEnd="16dip"
+        android:src="@drawable/ic_sysbar_quicksettings"
+        android:contentDescription="@string/settings_label"
+        android:layout_gravity="center"
+        android:background="?android:attr/selectableItemBackground" />
+</LinearLayout>
diff --git a/res/layout/vpn_dialog.xml b/res/layout/vpn_dialog.xml
index 034b6bf..d7e7f95 100644
--- a/res/layout/vpn_dialog.xml
+++ b/res/layout/vpn_dialog.xml
@@ -20,7 +20,7 @@
     <LinearLayout android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="vertical"
-            android:padding="3mm">
+            android:padding="8dp">
 
         <LinearLayout android:id="@+id/editor"
                 android:layout_width="match_parent"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 72adf4e..b019f11 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -5241,14 +5241,24 @@
 
     <!-- Button label to cancel changing a VPN profile. [CHAR LIMIT=40] -->
     <string name="vpn_cancel">Cancel</string>
+    <!-- Button label to finish editing a VPN profile. [CHAR LIMIT=40] -->
+    <string name="vpn_done">Dismiss</string>
     <!-- Button label to save a VPN profile. [CHAR LIMIT=40] -->
     <string name="vpn_save">Save</string>
     <!-- Button label to connect to a VPN profile. [CHAR LIMIT=40] -->
     <string name="vpn_connect">Connect</string>
     <!-- Dialog title to edit a VPN profile. [CHAR LIMIT=40] -->
     <string name="vpn_edit">Edit VPN profile</string>
+    <!-- Button label to forget a VPN profile. [CHAR LIMIT=40] -->
+    <string name="vpn_forget">Forget</string>
     <!-- Dialog title to connect to a VPN profile. [CHAR LIMIT=40] -->
     <string name="vpn_connect_to">Connect to <xliff:g id="profile" example="School">%s</xliff:g></string>
+    <!-- Dialog message body to disconnect from a VPN profile. -->
+    <string name="vpn_disconnect_confirm">Disconnect this VPN.</string>
+    <!-- Button label to disconnect from a VPN profile. [CHAR LIMIT=40] -->
+    <string name="vpn_disconnect">Disconnect</string>
+    <!-- Field label to show the version number for a VPN app. [CHAR LIMIT=40] -->
+    <string name="vpn_version">Version <xliff:g id="version" example="3.3.0">%s</xliff:g></string>
 
     <!-- Preference title for VPN settings. [CHAR LIMIT=40] -->
     <string name="vpn_title">VPN</string>
diff --git a/src/com/android/settings/vpn2/AppDialog.java b/src/com/android/settings/vpn2/AppDialog.java
new file mode 100644
index 0000000..2145297
--- /dev/null
+++ b/src/com/android/settings/vpn2/AppDialog.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 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.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.internal.net.VpnConfig;
+import com.android.settings.R;
+
+/**
+ * UI for managing the connection controlled by an app.
+ *
+ * Among the actions available are (depending on context):
+ * <ul>
+ *   <li><strong>Forget</strong>: revoke the managing app's VPN permission</li>
+ *   <li><strong>Dismiss</strong>: continue to use the VPN</li>
+ * </ul>
+ *
+ * {@see ConfigDialog}
+ */
+class AppDialog extends AlertDialog implements DialogInterface.OnClickListener {
+    private final PackageInfo mPkgInfo;
+    private final Listener mListener;
+    private final boolean mConnected;
+
+    AppDialog(Context context, Listener listener, PackageInfo pkgInfo, boolean connected) {
+        super(context);
+
+        mListener = listener;
+        mPkgInfo = pkgInfo;
+        mConnected = connected;
+    }
+
+    public final PackageInfo getPackageInfo() {
+        return mPkgInfo;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedState) {
+        CharSequence vpnName;
+        try {
+            vpnName = VpnConfig.getVpnLabel(getContext(), mPkgInfo.packageName);
+        } catch (PackageManager.NameNotFoundException ex) {
+            vpnName = mPkgInfo.packageName;
+        }
+
+        setTitle(vpnName);
+        setMessage(getContext().getString(R.string.vpn_version, mPkgInfo.versionName));
+
+        createButtons();
+        super.onCreate(savedState);
+    }
+
+    protected void createButtons() {
+        Context context = getContext();
+
+        if (mConnected) {
+            // Forget the network
+            setButton(DialogInterface.BUTTON_NEGATIVE,
+                    context.getString(R.string.vpn_forget), this);
+        }
+
+        // Dismiss
+        setButton(DialogInterface.BUTTON_POSITIVE,
+                context.getString(R.string.vpn_done), this);
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        if (which == DialogInterface.BUTTON_NEGATIVE) {
+            mListener.onForget(dialog);
+        }
+        dismiss();
+    }
+
+    public interface Listener {
+        public void onForget(DialogInterface dialog);
+    }
+}
diff --git a/src/com/android/settings/vpn2/AppDialogFragment.java b/src/com/android/settings/vpn2/AppDialogFragment.java
new file mode 100644
index 0000000..fc8d9e3
--- /dev/null
+++ b/src/com/android/settings/vpn2/AppDialogFragment.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2015 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.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.IConnectivityManager;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.net.VpnConfig;
+import com.android.settings.R;
+
+/**
+ * Fragment wrapper around an {@link AppDialog}.
+ */
+public class AppDialogFragment extends DialogFragment implements AppDialog.Listener {
+    private static final String TAG_APP_DIALOG = "vpnappdialog";
+    private static final String TAG = "AppDialogFragment";
+
+    private static final String ARG_MANAGING = "managing";
+    private static final String ARG_PACKAGE = "package";
+    private static final String ARG_CONNECTED = "connected";
+
+    private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface(
+            ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
+
+    public static void show(VpnSettings parent, PackageInfo pkgInfo, boolean managing,
+            boolean connected) {
+        if (!parent.isAdded()) return;
+
+        Bundle args = new Bundle();
+        args.putParcelable(ARG_PACKAGE, pkgInfo);
+        args.putBoolean(ARG_MANAGING, managing);
+        args.putBoolean(ARG_CONNECTED, connected);
+
+        final AppDialogFragment frag = new AppDialogFragment();
+        frag.setArguments(args);
+        frag.setTargetFragment(parent, 0);
+        frag.show(parent.getFragmentManager(), TAG_APP_DIALOG);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        Bundle args = getArguments();
+        PackageInfo pkgInfo = (PackageInfo) args.getParcelable(ARG_PACKAGE);
+        boolean managing = args.getBoolean(ARG_MANAGING);
+        boolean connected = args.getBoolean(ARG_CONNECTED);
+
+        if (managing) {
+            return new AppDialog(getActivity(), this, pkgInfo, connected);
+        } else {
+            // Build an AlertDialog with an option to disconnect.
+
+            CharSequence vpnName;
+            try {
+                vpnName = VpnConfig.getVpnLabel(getActivity(), pkgInfo.packageName);
+            } catch (PackageManager.NameNotFoundException ex) {
+                vpnName = pkgInfo.packageName;
+            }
+
+            AlertDialog.Builder dlog = new AlertDialog.Builder(getActivity())
+                    .setTitle(vpnName)
+                    .setMessage(getActivity().getString(R.string.vpn_disconnect_confirm))
+                    .setNegativeButton(getActivity().getString(R.string.vpn_cancel), null);
+
+            if (connected) {
+                dlog.setPositiveButton(getActivity().getString(R.string.vpn_disconnect),
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                onDisconnect(dialog);
+                            }
+                        });
+            }
+            return dlog.create();
+        }
+    }
+
+    @Override
+    public void dismiss() {
+        ((VpnSettings) getTargetFragment()).update();
+        super.dismiss();
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        dismiss();
+        super.onCancel(dialog);
+    }
+
+    @Override
+    public void onForget(final DialogInterface dialog) {
+        PackageInfo pkgInfo = (PackageInfo) getArguments().getParcelable(ARG_PACKAGE);
+        final String pkg = pkgInfo.packageName;
+        try {
+            VpnConfig vpnConfig = mService.getVpnConfig();
+            if (vpnConfig != null && pkg.equals(vpnConfig.user) && !vpnConfig.legacy) {
+                mService.setVpnPackageAuthorization(false);
+                onDisconnect(dialog);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to forget authorization for " + pkg, e);
+        }
+    }
+
+    private void onDisconnect(final DialogInterface dialog) {
+        PackageInfo pkgInfo = (PackageInfo) getArguments().getParcelable(ARG_PACKAGE);
+        try {
+            mService.prepareVpn(pkgInfo.packageName, VpnConfig.LEGACY_VPN);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to disconnect package " + pkgInfo.packageName, e);
+        }
+    }
+}
diff --git a/src/com/android/settings/vpn2/AppPreference.java b/src/com/android/settings/vpn2/AppPreference.java
new file mode 100644
index 0000000..1935dd8
--- /dev/null
+++ b/src/com/android/settings/vpn2/AppPreference.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2015 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.AppGlobals;
+import android.content.Context;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.preference.Preference;
+import android.view.View.OnClickListener;
+
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.settings.R;
+
+/**
+ * {@link android.preference.Preference} containing information about a VPN
+ * application. Tracks the package name and connection state.
+ */
+public class AppPreference extends ManageablePreference {
+    public static final int STATE_CONNECTED = LegacyVpnInfo.STATE_CONNECTED;
+    public static final int STATE_DISCONNECTED = LegacyVpnInfo.STATE_DISCONNECTED;
+
+    private int mState = STATE_DISCONNECTED;
+    private String mPackageName;
+    private String mName;
+    private int mUid;
+
+    public AppPreference(Context context, OnClickListener onManage, final String packageName,
+            int uid) {
+        super(context, null /* attrs */, onManage);
+        mPackageName = packageName;
+        mUid = uid;
+        update();
+    }
+
+    public PackageInfo getPackageInfo() {
+        UserHandle user = new UserHandle(UserHandle.getUserId(mUid));
+        try {
+            IPackageManager ipm = AppGlobals.getPackageManager();
+            return ipm.getPackageInfo(mPackageName, 0 /* flags */, user.getIdentifier());
+        } catch (RemoteException rme) {
+            return null;
+        }
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    public int getUid() {
+        return mUid;
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    public void setState(int state) {
+        mState = state;
+        update();
+    }
+
+    private void update() {
+        final String[] states = getContext().getResources().getStringArray(R.array.vpn_states);
+        setSummary(mState != STATE_DISCONNECTED ? states[mState] : "");
+
+        mName = mPackageName;
+        Drawable icon = null;
+        try {
+            // Make all calls to the package manager as the appropriate user.
+            int userId = UserHandle.getUserId(mUid);
+            Context userContext = getContext().createPackageContextAsUser(
+                    getContext().getPackageName(), 0 /* flags */, new UserHandle(userId));
+            PackageManager pm = userContext.getPackageManager();
+
+            // Fetch icon and VPN label
+            PackageInfo pkgInfo = pm.getPackageInfo(mPackageName, 0 /* flags */);
+            if (pkgInfo != null) {
+                icon = pkgInfo.applicationInfo.loadIcon(pm);
+                mName = VpnConfig.getVpnLabel(userContext, mPackageName).toString();
+            }
+        } catch (PackageManager.NameNotFoundException nnfe) {
+            // Failed - use default app label and icon as fallback
+        }
+        if (icon == null) {
+            icon = getContext().getPackageManager().getDefaultActivityIcon();
+        }
+        setTitle(mName);
+        setIcon(icon);
+
+        notifyHierarchyChanged();
+    }
+
+    public int compareTo(Preference preference) {
+        if (preference instanceof AppPreference) {
+            AppPreference another = (AppPreference) preference;
+            int result;
+            if ((result = another.mState - mState) == 0 &&
+                    (result = mName.compareToIgnoreCase(another.mName)) == 0 &&
+                    (result = mPackageName.compareTo(another.mPackageName)) == 0) {
+                result = mUid - another.mUid;
+            }
+            return result;
+        } else if (preference instanceof ConfigPreference) {
+            // Use comparator from ConfigPreference
+            ConfigPreference another = (ConfigPreference) preference;
+            return -another.compareTo(this);
+        } else {
+            return super.compareTo(preference);
+        }
+    }
+}
+
diff --git a/src/com/android/settings/vpn2/VpnDialog.java b/src/com/android/settings/vpn2/ConfigDialog.java
similarity index 95%
rename from src/com/android/settings/vpn2/VpnDialog.java
rename to src/com/android/settings/vpn2/ConfigDialog.java
index 2f95bce..57f43f4 100644
--- a/src/com/android/settings/vpn2/VpnDialog.java
+++ b/src/com/android/settings/vpn2/ConfigDialog.java
@@ -16,9 +16,6 @@
 
 package com.android.settings.vpn2;
 
-import com.android.internal.net.VpnProfile;
-import com.android.settings.R;
-
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -35,15 +32,26 @@
 import android.widget.Spinner;
 import android.widget.TextView;
 
+import com.android.internal.net.VpnProfile;
+import com.android.settings.R;
+
 import java.net.InetAddress;
 
-class VpnDialog extends AlertDialog implements TextWatcher,
+/**
+ * Dialog showing information about a VPN configuration. The dialog
+ * can be launched to either edit or prompt for credentials to connect
+ * to a user-added VPN.
+ *
+ * {@see AppDialog}
+ */
+class ConfigDialog extends AlertDialog implements TextWatcher,
         View.OnClickListener, AdapterView.OnItemSelectedListener {
     private final KeyStore mKeyStore = KeyStore.getInstance();
     private final DialogInterface.OnClickListener mListener;
     private final VpnProfile mProfile;
 
     private boolean mEditing;
+    private boolean mExists;
 
     private View mView;
 
@@ -64,19 +72,20 @@
     private Spinner mIpsecServerCert;
     private CheckBox mSaveLogin;
 
-    VpnDialog(Context context, DialogInterface.OnClickListener listener,
-            VpnProfile profile, boolean editing) {
+    ConfigDialog(Context context, DialogInterface.OnClickListener listener,
+            VpnProfile profile, boolean editing, boolean exists) {
         super(context);
+
         mListener = listener;
         mProfile = profile;
         mEditing = editing;
+        mExists = exists;
     }
 
     @Override
     protected void onCreate(Bundle savedState) {
         mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
         setView(mView);
-        setInverseBackgroundForced(true);
 
         Context context = getContext();
 
@@ -154,6 +163,12 @@
                 onClick(showOptions);
             }
 
+            // Create a button to forget the profile if it has already been saved..
+            if (mExists) {
+                setButton(DialogInterface.BUTTON_NEUTRAL,
+                        context.getString(R.string.vpn_forget), mListener);
+            }
+
             // Create a button to save the profile.
             setButton(DialogInterface.BUTTON_POSITIVE,
                     context.getString(R.string.vpn_save), mListener);
@@ -173,7 +188,7 @@
                 context.getString(R.string.vpn_cancel), mListener);
 
         // Let AlertDialog create everything.
-        super.onCreate(null);
+        super.onCreate(savedState);
 
         // Disable the action button if necessary.
         getButton(DialogInterface.BUTTON_POSITIVE)
diff --git a/src/com/android/settings/vpn2/ConfigDialogFragment.java b/src/com/android/settings/vpn2/ConfigDialogFragment.java
new file mode 100644
index 0000000..42e1614
--- /dev/null
+++ b/src/com/android/settings/vpn2/ConfigDialogFragment.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2015 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.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.IConnectivityManager;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.security.Credentials;
+import android.security.KeyStore;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
+import com.android.settings.R;
+
+/**
+ * Fragment wrapper around a {@link ConfigDialog}.
+ */
+public class ConfigDialogFragment extends DialogFragment implements
+        DialogInterface.OnClickListener {
+    private static final String TAG_CONFIG_DIALOG = "vpnconfigdialog";
+    private static final String TAG = "ConfigDialogFragment";
+
+    private static final String ARG_PROFILE = "profile";
+    private static final String ARG_EDITING = "editing";
+    private static final String ARG_EXISTS = "exists";
+
+    private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface(
+            ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
+
+    private boolean mUnlocking = false;
+
+    public static void show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists) {
+        if (!parent.isAdded()) return;
+
+        Bundle args = new Bundle();
+        args.putParcelable(ARG_PROFILE, profile);
+        args.putBoolean(ARG_EDITING, edit);
+        args.putBoolean(ARG_EXISTS, exists);
+
+        final ConfigDialogFragment frag = new ConfigDialogFragment();
+        frag.setArguments(args);
+        frag.setTargetFragment(parent, 0);
+        frag.show(parent.getFragmentManager(), TAG_CONFIG_DIALOG);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        // Check KeyStore here, so others do not need to deal with it.
+        if (!KeyStore.getInstance().isUnlocked()) {
+            if (!mUnlocking) {
+                // Let us unlock KeyStore. See you later!
+                Credentials.getInstance().unlock(getActivity());
+            } else {
+                // We already tried, but it is still not working!
+                dismiss();
+            }
+            mUnlocking = !mUnlocking;
+            return;
+        }
+
+        // Now KeyStore is always unlocked. Reset the flag.
+        mUnlocking = false;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        Bundle args = getArguments();
+        VpnProfile profile = (VpnProfile) args.getParcelable(ARG_PROFILE);
+        boolean editing = args.getBoolean(ARG_EDITING);
+        boolean exists = args.getBoolean(ARG_EXISTS);
+
+        return new ConfigDialog(getActivity(), this, profile, editing, exists);
+    }
+
+    @Override
+    public void onClick(DialogInterface dialogInterface, int button) {
+        ConfigDialog dialog = (ConfigDialog) getDialog();
+        VpnProfile profile = dialog.getProfile();
+
+        if (button == DialogInterface.BUTTON_POSITIVE) {
+            // Update KeyStore entry
+            KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(),
+                    KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED);
+
+            // Flush out old version of profile
+            disconnect(profile);
+
+            // If we are not editing, connect!
+            if (!dialog.isEditing()) {
+                try {
+                    connect(profile);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to connect", e);
+                }
+            }
+        } else if (button == DialogInterface.BUTTON_NEUTRAL) {
+            // Disable profile if connected
+            disconnect(profile);
+
+            // Delete from KeyStore
+            KeyStore.getInstance().delete(Credentials.VPN + profile.key, KeyStore.UID_SELF);
+        }
+        dismiss();
+    }
+
+    @Override
+    public void dismiss() {
+        ((VpnSettings) getTargetFragment()).update();
+        super.dismiss();
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        dismiss();
+        super.onCancel(dialog);
+    }
+
+    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 disconnect(VpnProfile profile) {
+        try {
+            LegacyVpnInfo connected = mService.getLegacyVpnInfo();
+            if (connected != null && profile.key.equals(connected.key)) {
+                mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to disconnect", e);
+        }
+    }
+}
diff --git a/src/com/android/settings/vpn2/ConfigPreference.java b/src/com/android/settings/vpn2/ConfigPreference.java
new file mode 100644
index 0000000..4e6e16f
--- /dev/null
+++ b/src/com/android/settings/vpn2/ConfigPreference.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 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.content.Context;
+import android.preference.Preference;
+import android.view.View.OnClickListener;
+
+import static com.android.internal.net.LegacyVpnInfo.STATE_CONNECTED;
+
+import com.android.internal.net.VpnProfile;
+import com.android.settings.R;
+
+/**
+ * {@link android.preference.Preference} referencing a VPN
+ * configuration. Tracks the underlying profile and its connection
+ * state.
+ */
+public class ConfigPreference extends ManageablePreference {
+    private VpnProfile mProfile;
+    private int mState = -1;
+
+    ConfigPreference(Context context, OnClickListener onManage, VpnProfile profile) {
+        super(context, null /* attrs */, onManage);
+        setProfile(profile);
+    }
+
+    public VpnProfile getProfile() {
+        return mProfile;
+    }
+
+    public void setProfile(VpnProfile profile) {
+        mProfile = profile;
+        update();
+    }
+
+    public void setState(int state) {
+        mState = state;
+        update();
+    }
+
+    private void update() {
+        if (mState < 0) {
+            setSummary("");
+        } else {
+            String[] states = getContext().getResources()
+                    .getStringArray(R.array.vpn_states);
+            setSummary(states[mState]);
+        }
+        setIcon(R.mipmap.ic_launcher_settings);
+        setTitle(mProfile.name);
+        notifyHierarchyChanged();
+    }
+
+    @Override
+    public int compareTo(Preference preference) {
+        if (preference instanceof ConfigPreference) {
+            ConfigPreference another = (ConfigPreference) preference;
+            int result;
+            if ((result = another.mState - mState) == 0 &&
+                    (result = mProfile.name.compareTo(another.mProfile.name)) == 0 &&
+                    (result = mProfile.type - another.mProfile.type) == 0) {
+                result = mProfile.key.compareTo(another.mProfile.key);
+            }
+            return result;
+        } else if (preference instanceof AppPreference) {
+            // Try to sort connected VPNs first
+            AppPreference another = (AppPreference) preference;
+            if (mState != STATE_CONNECTED && another.getState() == AppPreference.STATE_CONNECTED) {
+                return 1;
+            }
+            // Show configured VPNs before app VPNs
+            return -1;
+        } else {
+            return super.compareTo(preference);
+        }
+    }
+}
+
diff --git a/src/com/android/settings/vpn2/LockdownConfigFragment.java b/src/com/android/settings/vpn2/LockdownConfigFragment.java
new file mode 100644
index 0000000..f36cb46
--- /dev/null
+++ b/src/com/android/settings/vpn2/LockdownConfigFragment.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2015 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.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.os.Bundle;
+import android.security.Credentials;
+import android.security.KeyStore;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.android.internal.net.VpnProfile;
+import com.android.settings.R;
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Dialog to configure always-on VPN.
+ */
+public class LockdownConfigFragment extends DialogFragment {
+    private List<VpnProfile> mProfiles;
+    private List<CharSequence> mTitles;
+    private int mCurrentIndex;
+
+    private static final String TAG_LOCKDOWN = "lockdown";
+
+    private static class TitleAdapter extends ArrayAdapter<CharSequence> {
+        public TitleAdapter(Context context, List<CharSequence> objects) {
+            super(context, com.android.internal.R.layout.select_dialog_singlechoice_material,
+                    android.R.id.text1, objects);
+        }
+    }
+
+    public static void show(VpnSettings parent) {
+        if (!parent.isAdded()) return;
+
+        final LockdownConfigFragment dialog = new LockdownConfigFragment();
+        dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN);
+    }
+
+    private static String getStringOrNull(KeyStore keyStore, String key) {
+        if (!keyStore.isUnlocked()) {
+            return null;
+        }
+        final byte[] value = keyStore.get(key);
+        return value == null ? null : new String(value);
+    }
+
+    private void initProfiles(KeyStore keyStore, Resources res) {
+        final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN);
+
+        mProfiles = VpnSettings.loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP);
+        mTitles = new ArrayList<>(1 + mProfiles.size());
+        mTitles.add(res.getText(R.string.vpn_lockdown_none));
+
+        mCurrentIndex = 0;
+        for (VpnProfile profile : mProfiles) {
+            if (TextUtils.equals(profile.key, lockdownKey)) {
+                mCurrentIndex = mTitles.size();
+            }
+            mTitles.add(profile.name);
+        }
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Context context = getActivity();
+        final KeyStore keyStore = KeyStore.getInstance();
+
+        initProfiles(keyStore, context.getResources());
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
+
+        builder.setTitle(R.string.vpn_menu_lockdown);
+
+        final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false);
+        final ListView listView = (ListView) view.findViewById(android.R.id.list);
+        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        listView.setAdapter(new TitleAdapter(context, mTitles));
+        listView.setItemChecked(mCurrentIndex, true);
+        builder.setView(view);
+
+        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                final int newIndex = listView.getCheckedItemPosition();
+                if (mCurrentIndex == newIndex) return;
+
+                if (newIndex == 0) {
+                    keyStore.delete(Credentials.LOCKDOWN_VPN);
+                } else {
+                    final VpnProfile profile = mProfiles.get(newIndex - 1);
+                    if (!profile.isValidLockdownProfile()) {
+                        Toast.makeText(context, R.string.vpn_lockdown_config_error,
+                                Toast.LENGTH_LONG).show();
+                        return;
+                    }
+                    keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes(),
+                            KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED);
+                }
+
+                // kick profiles since we changed them
+                ConnectivityManager.from(getActivity()).updateLockdownVpn();
+            }
+        });
+
+        return builder.create();
+    }
+}
+
diff --git a/src/com/android/settings/vpn2/ManageablePreference.java b/src/com/android/settings/vpn2/ManageablePreference.java
new file mode 100644
index 0000000..5e507c1
--- /dev/null
+++ b/src/com/android/settings/vpn2/ManageablePreference.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 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.content.Context;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import com.android.settings.R;
+
+/**
+ * Preference with an additional gear icon. Touching the gear icon triggers an
+ * onChange event.
+ */
+public class ManageablePreference extends Preference {
+    OnClickListener mListener;
+    View mManageView;
+
+    public ManageablePreference(Context context, AttributeSet attrs, OnClickListener onManage) {
+        super(context, attrs);
+        mListener = onManage;
+        setPersistent(false);
+        setOrder(0);
+        setWidgetLayoutResource(R.layout.preference_vpn);
+    }
+
+    @Override
+    protected void onBindView(View view) {
+        mManageView = view.findViewById(R.id.manage);
+        mManageView.setOnClickListener(mListener);
+        mManageView.setTag(this);
+        super.onBindView(view);
+    }
+}
diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java
index 04853f1..a333de9 100644
--- a/src/com/android/settings/vpn2/VpnSettings.java
+++ b/src/com/android/settings/vpn2/VpnSettings.java
@@ -16,39 +16,36 @@
 
 package com.android.settings.vpn2;
 
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.DialogFragment;
+import android.app.AppOpsManager;
 import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.Resources;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
 import android.net.IConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.os.UserManager;
 import android.preference.Preference;
 import android.preference.PreferenceGroup;
 import android.preference.PreferenceScreen;
 import android.security.Credentials;
 import android.security.KeyStore;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.LayoutInflater;
+import android.util.SparseArray;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.AdapterView.AdapterContextMenuInfo;
-import android.widget.ArrayAdapter;
-import android.widget.ListView;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.net.LegacyVpnInfo;
@@ -61,33 +58,39 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 
-public class VpnSettings extends SettingsPreferenceFragment implements
-        Handler.Callback, Preference.OnPreferenceClickListener,
-        DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
-    private static final String TAG = "VpnSettings";
+import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
 
-    private static final String TAG_LOCKDOWN = "lockdown";
+/**
+ * Settings screen listing VPNs. Configured VPNs and networks managed by apps
+ * are shown in the same list.
+ */
+public class VpnSettings extends SettingsPreferenceFragment implements
+        Handler.Callback, Preference.OnPreferenceClickListener {
+    private static final String LOG_TAG = "VpnSettings";
 
     private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN";
+    private static final NetworkRequest VPN_REQUEST = new NetworkRequest.Builder()
+            .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+            .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+            .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+            .build();
 
-    // TODO: migrate to using DialogFragment when editing
-
-    private final IConnectivityManager mService = IConnectivityManager.Stub
+    private final IConnectivityManager mConnectivityService = IConnectivityManager.Stub
             .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
-    private final KeyStore mKeyStore = KeyStore.getInstance();
-    private boolean mUnlocking = false;
+    private ConnectivityManager mConnectivityManager;
+    private UserManager mUserManager;
 
-    private HashMap<String, VpnPreference> mPreferences = new HashMap<String, VpnPreference>();
-    private VpnDialog mDialog;
+    private final KeyStore mKeyStore = KeyStore.getInstance();
+
+    private HashMap<String, ConfigPreference> mConfigPreferences = new HashMap<>();
+    private HashMap<String, AppPreference> mAppPreferences = new HashMap<>();
 
     private Handler mUpdater;
-    private LegacyVpnInfo mInfo;
-    private UserManager mUm;
-
-    // The key of the profile for the current ContextMenu.
-    private String mSelectedKey;
+    private LegacyVpnInfo mConnectedLegacyVpn;
+    private HashSet<String> mConnectedVpns = new HashSet<>();
 
     private boolean mUnavailable;
 
@@ -100,25 +103,24 @@
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
 
-        mUm = (UserManager) getSystemService(Context.USER_SERVICE);
-
-        if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) {
+        mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
+        if (mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) {
             mUnavailable = true;
             setPreferenceScreen(new PreferenceScreen(getActivity(), null));
             return;
         }
 
+        mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+        mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback);
+
         setHasOptionsMenu(true);
         addPreferencesFromResource(R.xml.vpn_settings2);
+    }
 
-        if (savedState != null) {
-            VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"),
-                    savedState.getByteArray("VpnProfile"));
-            if (profile != null) {
-                mDialog = new VpnDialog(getActivity(), this, profile,
-                        savedState.getBoolean("VpnEditing"));
-            }
-        }
+    @Override
+    public void onDestroy() {
+        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+        super.onDestroy();
     }
 
     @Override
@@ -143,13 +145,11 @@
             case R.id.vpn_create: {
                 // Generate a new key. Here we just use the current time.
                 long millis = System.currentTimeMillis();
-                while (mPreferences.containsKey(Long.toHexString(millis))) {
+                while (mConfigPreferences.containsKey(Long.toHexString(millis))) {
                     ++millis;
                 }
-                mDialog = new VpnDialog(
-                        getActivity(), this, new VpnProfile(Long.toHexString(millis)), true);
-                mDialog.setOnDismissListener(this);
-                mDialog.show();
+                VpnProfile profile = new VpnProfile(Long.toHexString(millis));
+                ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */);
                 return true;
             }
             case R.id.vpn_lockdown: {
@@ -161,18 +161,6 @@
     }
 
     @Override
-    public void onSaveInstanceState(Bundle savedState) {
-        // We do not save view hierarchy, as they are just profiles.
-        if (mDialog != null) {
-            VpnProfile profile = mDialog.getProfile();
-            savedState.putString("VpnKey", profile.key);
-            savedState.putByteArray("VpnProfile", profile.encode());
-            savedState.putBoolean("VpnEditing", mDialog.isEditing());
-        }
-        // else?
-    }
-
-    @Override
     public void onResume() {
         super.onResume();
 
@@ -191,42 +179,32 @@
             LockdownConfigFragment.show(this);
         }
 
-        // Check KeyStore here, so others do not need to deal with it.
-        if (!mKeyStore.isUnlocked()) {
-            if (!mUnlocking) {
-                // Let us unlock KeyStore. See you later!
-                Credentials.getInstance().unlock(getActivity());
-            } else {
-                // We already tried, but it is still not working!
-                finishFragment();
-            }
-            mUnlocking = !mUnlocking;
-            return;
+        update();
+    }
+
+    public void update() {
+        // Pref group within which to list VPNs
+        PreferenceGroup vpnGroup = getPreferenceScreen();
+        vpnGroup.removeAll();
+        mConfigPreferences.clear();
+        mAppPreferences.clear();
+
+        // Fetch configured VPN profiles from KeyStore
+        for (VpnProfile profile : loadVpnProfiles(mKeyStore)) {
+            final ConfigPreference pref = new ConfigPreference(getActivity(), mManageListener,
+                    profile);
+            pref.setOnPreferenceClickListener(this);
+            mConfigPreferences.put(profile.key, pref);
+            vpnGroup.addPreference(pref);
         }
 
-        // Now KeyStore is always unlocked. Reset the flag.
-        mUnlocking = false;
-
-        // Currently we are the only user of profiles in KeyStore.
-        // Assuming KeyStore and KeyGuard do the right thing, we can
-        // safely cache profiles in the memory.
-        if (mPreferences.size() == 0) {
-            PreferenceGroup group = getPreferenceScreen();
-
-            final Context context = getActivity();
-            final List<VpnProfile> profiles = loadVpnProfiles(mKeyStore);
-            for (VpnProfile profile : profiles) {
-                final VpnPreference pref = new VpnPreference(context, profile);
-                pref.setOnPreferenceClickListener(this);
-                mPreferences.put(profile.key, pref);
-                group.addPreference(pref);
-            }
-        }
-
-        // Show the dialog if there is one.
-        if (mDialog != null) {
-            mDialog.setOnDismissListener(this);
-            mDialog.show();
+        // 3rd-party VPN apps can change elsewhere. Reload them every time.
+        for (AppOpsManager.PackageOps pkg : getVpnApps()) {
+            final AppPreference pref = new AppPreference(getActivity(), mManageListener,
+                    pkg.getPackageName(), pkg.getUid());
+            pref.setOnPreferenceClickListener(this);
+            mAppPreferences.put(pkg.getPackageName(), pref);
+            vpnGroup.addPreference(pref);
         }
 
         // Start monitoring.
@@ -234,172 +212,111 @@
             mUpdater = new Handler(this);
         }
         mUpdater.sendEmptyMessage(0);
-
-        // Register for context menu. Hmmm, getListView() is hidden?
-        registerForContextMenu(getListView());
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-
-        if (mUnavailable) {
-            return;
-        }
-
-        // Hide the dialog if there is one.
-        if (mDialog != null) {
-            mDialog.setOnDismissListener(null);
-            mDialog.dismiss();
-        }
-
-        // Unregister for context menu.
-        if (getView() != null) {
-            unregisterForContextMenu(getListView());
-        }
-    }
-
-    @Override
-    public void onDismiss(DialogInterface dialog) {
-        // Here is the exit of a dialog.
-        mDialog = null;
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int button) {
-        if (button == DialogInterface.BUTTON_POSITIVE) {
-            // Always save the profile.
-            VpnProfile profile = mDialog.getProfile();
-            mKeyStore.put(Credentials.VPN + profile.key, profile.encode(), KeyStore.UID_SELF,
-                    KeyStore.FLAG_ENCRYPTED);
-
-            // Update the preference.
-            VpnPreference preference = mPreferences.get(profile.key);
-            if (preference != null) {
-                disconnect(profile.key);
-                preference.update(profile);
-            } else {
-                preference = new VpnPreference(getActivity(), profile);
-                preference.setOnPreferenceClickListener(this);
-                mPreferences.put(profile.key, preference);
-                getPreferenceScreen().addPreference(preference);
-            }
-
-            // If we are not editing, connect!
-            if (!mDialog.isEditing()) {
-                try {
-                    connect(profile);
-                } catch (Exception e) {
-                    Log.e(TAG, "connect", e);
-                }
-            }
-        }
-    }
-
-    @Override
-    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
-        if (mDialog != null) {
-            Log.v(TAG, "onCreateContextMenu() is called when mDialog != null");
-            return;
-        }
-
-        if (info instanceof AdapterContextMenuInfo) {
-            Preference preference = (Preference) getListView().getItemAtPosition(
-                    ((AdapterContextMenuInfo) info).position);
-            if (preference instanceof VpnPreference) {
-                VpnProfile profile = ((VpnPreference) preference).getProfile();
-                mSelectedKey = profile.key;
-                menu.setHeaderTitle(profile.name);
-                menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit);
-                menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete);
-            }
-        }
-    }
-
-    @Override
-    public boolean onContextItemSelected(MenuItem item) {
-        if (mDialog != null) {
-            Log.v(TAG, "onContextItemSelected() is called when mDialog != null");
-            return false;
-        }
-
-        VpnPreference preference = mPreferences.get(mSelectedKey);
-        if (preference == null) {
-            Log.v(TAG, "onContextItemSelected() is called but no preference is found");
-            return false;
-        }
-
-        switch (item.getItemId()) {
-            case R.string.vpn_menu_edit:
-                mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true);
-                mDialog.setOnDismissListener(this);
-                mDialog.show();
-                return true;
-            case R.string.vpn_menu_delete:
-                disconnect(mSelectedKey);
-                getPreferenceScreen().removePreference(preference);
-                mPreferences.remove(mSelectedKey);
-                mKeyStore.delete(Credentials.VPN + mSelectedKey);
-                return true;
-        }
-        return false;
     }
 
     @Override
     public boolean onPreferenceClick(Preference preference) {
-        if (mDialog != null) {
-            Log.v(TAG, "onPreferenceClick() is called when mDialog != null");
-            return true;
-        }
-
-        if (preference instanceof VpnPreference) {
-            VpnProfile profile = ((VpnPreference) preference).getProfile();
-            if (mInfo != null && profile.key.equals(mInfo.key) &&
-                    mInfo.state == LegacyVpnInfo.STATE_CONNECTED) {
+        if (preference instanceof ConfigPreference) {
+            VpnProfile profile = ((ConfigPreference) preference).getProfile();
+            if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) &&
+                    mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) {
                 try {
-                    mInfo.intent.send();
+                    mConnectedLegacyVpn.intent.send();
                     return true;
                 } catch (Exception e) {
                     // ignore
                 }
             }
-            mDialog = new VpnDialog(getActivity(), this, profile, false);
-        } else {
-            // Generate a new key. Here we just use the current time.
-            long millis = System.currentTimeMillis();
-            while (mPreferences.containsKey(Long.toHexString(millis))) {
-                ++millis;
+            ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */);
+            return true;
+        } else if (preference instanceof AppPreference) {
+            AppPreference pref = (AppPreference) preference;
+            boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
+
+            if (!connected) {
+                try {
+                    UserHandle user = new UserHandle(UserHandle.getUserId(pref.getUid()));
+                    Context userContext = getActivity().createPackageContextAsUser(
+                            getActivity().getPackageName(), 0 /* flags */, user);
+                    PackageManager pm = userContext.getPackageManager();
+                    Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName());
+                    if (appIntent != null) {
+                        userContext.startActivityAsUser(appIntent, user);
+                        return true;
+                    }
+                } catch (PackageManager.NameNotFoundException nnfe) {
+                    // Fall through
+                }
             }
-            mDialog = new VpnDialog(getActivity(), this,
-                    new VpnProfile(Long.toHexString(millis)), true);
+
+            // Already onnected or no launch intent available - show an info dialog
+            PackageInfo pkgInfo = pref.getPackageInfo();
+            AppDialogFragment.show(this, pkgInfo, false /* editing */, connected);
+            return true;
         }
-        mDialog.setOnDismissListener(this);
-        mDialog.show();
-        return true;
+        return false;
     }
 
+    private View.OnClickListener mManageListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            Object tag = view.getTag();
+
+            if (tag instanceof ConfigPreference) {
+                ConfigPreference pref = (ConfigPreference) tag;
+                ConfigDialogFragment.show(VpnSettings.this, pref.getProfile(), true /* editing */,
+                        true /* exists */);
+            } else if (tag instanceof AppPreference) {
+                AppPreference pref = (AppPreference) tag;
+                AppDialogFragment.show(VpnSettings.this, pref.getPackageInfo(), true /* editing */,
+                        (pref.getState() == AppPreference.STATE_CONNECTED) /* connected */);
+            }
+        }
+    };
+
     @Override
     public boolean handleMessage(Message message) {
         mUpdater.removeMessages(0);
 
         if (isResumed()) {
             try {
-                LegacyVpnInfo info = mService.getLegacyVpnInfo();
-                if (mInfo != null) {
-                    VpnPreference preference = mPreferences.get(mInfo.key);
+                // Legacy VPNs
+                LegacyVpnInfo info = mConnectivityService.getLegacyVpnInfo();
+                if (mConnectedLegacyVpn != null) {
+                    ConfigPreference preference = mConfigPreferences.get(mConnectedLegacyVpn.key);
                     if (preference != null) {
-                        preference.update(-1);
+                        preference.setState(-1);
                     }
-                    mInfo = null;
+                    mConnectedLegacyVpn = null;
                 }
                 if (info != null) {
-                    VpnPreference preference = mPreferences.get(info.key);
+                    ConfigPreference preference = mConfigPreferences.get(info.key);
                     if (preference != null) {
-                        preference.update(info.state);
-                        mInfo = info;
+                        preference.setState(info.state);
+                        mConnectedLegacyVpn = info;
                     }
                 }
-            } catch (Exception e) {
+
+                // VPN apps
+                for (String key : mConnectedVpns) {
+                    AppPreference preference = mAppPreferences.get(key);
+                    if (preference != null) {
+                        preference.setState(AppPreference.STATE_DISCONNECTED);
+                    }
+                }
+                mConnectedVpns.clear();
+                // TODO: also query VPN services in user profiles STOPSHIP
+                VpnConfig cfg = mConnectivityService.getVpnConfig();
+                if (cfg != null) {
+                    mConnectedVpns.add(cfg.user);
+                }
+                for (String key : mConnectedVpns) {
+                    AppPreference preference = mAppPreferences.get(key);
+                    if (preference != null) {
+                        preference.setState(AppPreference.STATE_CONNECTED);
+                    }
+                }
+            } catch (RemoteException e) {
                 // ignore
             }
             mUpdater.sendEmptyMessageDelayed(0, 1000);
@@ -407,186 +324,76 @@
         return true;
     }
 
-    private void connect(VpnProfile profile) throws Exception {
-        try {
-            mService.startLegacyVpn(profile);
-        } catch (IllegalStateException e) {
-            Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show();
-        }
-    }
-
-    private void disconnect(String key) {
-        if (mInfo != null && key.equals(mInfo.key)) {
-            try {
-                mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
-            } catch (Exception e) {
-                // ignore
+    private NetworkCallback mNetworkCallback = new NetworkCallback() {
+        @Override
+        public void onAvailable(Network network) {
+            if (mUpdater != null) {
+                mUpdater.sendEmptyMessage(0);
             }
         }
-    }
+
+        @Override
+        public void onLost(Network network) {
+            if (mUpdater != null) {
+                mUpdater.sendEmptyMessage(0);
+            }
+        }
+    };
 
     @Override
     protected int getHelpResource() {
         return R.string.help_url_vpn;
     }
 
-    private static class VpnPreference extends Preference {
-        private VpnProfile mProfile;
-        private int mState = -1;
+    private List<AppOpsManager.PackageOps> getVpnApps() {
+        List<AppOpsManager.PackageOps> result = Lists.newArrayList();
 
-        VpnPreference(Context context, VpnProfile profile) {
-            super(context);
-            setPersistent(false);
-            setOrder(0);
-
-            mProfile = profile;
-            update();
+        // Build a filter of currently active user profiles.
+        SparseArray<Boolean> currentProfileIds = new SparseArray<>();
+        for (UserHandle profile : mUserManager.getUserProfiles()) {
+            currentProfileIds.put(profile.getIdentifier(), Boolean.TRUE);
         }
 
-        VpnProfile getProfile() {
-            return mProfile;
-        }
-
-        void update(VpnProfile profile) {
-            mProfile = profile;
-            update();
-        }
-
-        void update(int state) {
-            mState = state;
-            update();
-        }
-
-        void update() {
-            if (mState < 0) {
-                String[] types = getContext().getResources()
-                        .getStringArray(R.array.vpn_types_long);
-                setSummary(types[mProfile.type]);
-            } else {
-                String[] states = getContext().getResources()
-                        .getStringArray(R.array.vpn_states);
-                setSummary(states[mState]);
-            }
-            setTitle(mProfile.name);
-            notifyHierarchyChanged();
-        }
-
-        @Override
-        public int compareTo(Preference preference) {
-            int result = -1;
-            if (preference instanceof VpnPreference) {
-                VpnPreference another = (VpnPreference) preference;
-                if ((result = another.mState - mState) == 0 &&
-                        (result = mProfile.name.compareTo(another.mProfile.name)) == 0 &&
-                        (result = mProfile.type - another.mProfile.type) == 0) {
-                    result = mProfile.key.compareTo(another.mProfile.key);
+        // Fetch VPN-enabled apps from AppOps.
+        AppOpsManager aom = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
+        List<AppOpsManager.PackageOps> apps = aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN});
+        if (apps != null) {
+            for (AppOpsManager.PackageOps pkg : apps) {
+                int userId = UserHandle.getUserId(pkg.getUid());
+                if (currentProfileIds.get(userId) == null) {
+                    // Skip packages for users outside of our profile group.
+                    continue;
+                }
+                // Look for a MODE_ALLOWED permission to activate VPN.
+                boolean allowed = false;
+                for (AppOpsManager.OpEntry op : pkg.getOps()) {
+                    if (op.getOp() == OP_ACTIVATE_VPN &&
+                            op.getMode() == AppOpsManager.MODE_ALLOWED) {
+                        allowed = true;
+                    }
+                }
+                if (allowed) {
+                    result.add(pkg);
                 }
             }
+        }
+        return result;
+    }
+
+    protected static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
+        final ArrayList<VpnProfile> result = Lists.newArrayList();
+
+        // This might happen if the user does not yet have a keystore. Quietly short-circuit because
+        // no keystore means no VPN configs.
+        if (!keyStore.isUnlocked()) {
             return result;
         }
-    }
 
-    /**
-     * Dialog to configure always-on VPN.
-     */
-    public static class LockdownConfigFragment extends DialogFragment {
-        private List<VpnProfile> mProfiles;
-        private List<CharSequence> mTitles;
-        private int mCurrentIndex;
-
-        private static class TitleAdapter extends ArrayAdapter<CharSequence> {
-            public TitleAdapter(Context context, List<CharSequence> objects) {
-                super(context, com.android.internal.R.layout.select_dialog_singlechoice_material,
-                        android.R.id.text1, objects);
-            }
-        }
-
-        public static void show(VpnSettings parent) {
-            if (!parent.isAdded()) return;
-
-            final LockdownConfigFragment dialog = new LockdownConfigFragment();
-            dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN);
-        }
-
-        private static String getStringOrNull(KeyStore keyStore, String key) {
-            final byte[] value = keyStore.get(Credentials.LOCKDOWN_VPN);
-            return value == null ? null : new String(value);
-        }
-
-        private void initProfiles(KeyStore keyStore, Resources res) {
-            final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN);
-
-            mProfiles = loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP);
-            mTitles = Lists.newArrayList();
-            mTitles.add(res.getText(R.string.vpn_lockdown_none));
-            mCurrentIndex = 0;
-
-            for (VpnProfile profile : mProfiles) {
-                if (TextUtils.equals(profile.key, lockdownKey)) {
-                    mCurrentIndex = mTitles.size();
-                }
-                mTitles.add(profile.name);
-            }
-        }
-
-        @Override
-        public Dialog onCreateDialog(Bundle savedInstanceState) {
-            final Context context = getActivity();
-            final KeyStore keyStore = KeyStore.getInstance();
-
-            initProfiles(keyStore, context.getResources());
-
-            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
-            final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
-
-            builder.setTitle(R.string.vpn_menu_lockdown);
-
-            final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false);
-            final ListView listView = (ListView) view.findViewById(android.R.id.list);
-            listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
-            listView.setAdapter(new TitleAdapter(context, mTitles));
-            listView.setItemChecked(mCurrentIndex, true);
-            builder.setView(view);
-
-            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
-                @Override
-                public void onClick(DialogInterface dialog, int which) {
-                    final int newIndex = listView.getCheckedItemPosition();
-                    if (mCurrentIndex == newIndex) return;
-
-                    if (newIndex == 0) {
-                        keyStore.delete(Credentials.LOCKDOWN_VPN);
-
-                    } else {
-                        final VpnProfile profile = mProfiles.get(newIndex - 1);
-                        if (!profile.isValidLockdownProfile()) {
-                            Toast.makeText(context, R.string.vpn_lockdown_config_error,
-                                    Toast.LENGTH_LONG).show();
-                            return;
-                        }
-                        keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes(),
-                                KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED);
-                    }
-
-                    // kick profiles since we changed them
-                    ConnectivityManager.from(getActivity()).updateLockdownVpn();
-                }
-            });
-
-            return builder.create();
-        }
-    }
-
-    private static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
-        final ArrayList<VpnProfile> result = Lists.newArrayList();
-        final String[] keys = keyStore.saw(Credentials.VPN);
-        if (keys != null) {
-            for (String key : keys) {
-                final VpnProfile profile = VpnProfile.decode(
-                        key, keyStore.get(Credentials.VPN + key));
-                if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {
-                    result.add(profile);
-                }
+        // We are the only user of profiles in KeyStore so no locks are needed.
+        for (String key : keyStore.saw(Credentials.VPN)) {
+            final VpnProfile profile = VpnProfile.decode(key, keyStore.get(Credentials.VPN + key));
+            if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {
+                result.add(profile);
             }
         }
         return result;