Vpn settings per vpn

This CL adds a setting for each VPN
- When no_config_vpn user restriction is applied, user can't change anything in the page
- Launch the subsetting activity in the corresponding user to unlock keystore and force work challenge
- Show dialog when user replace always-on-VPN package
- When forget VPN, unset always-on-vpn

TODO: show per-VPN status in VPN list

Change-Id: Ica360ea44117db6a4ecfaed1eec6c188189c246c
diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java
index 5a76bf3..60973e1 100644
--- a/src/com/android/settings/Utils.java
+++ b/src/com/android/settings/Utils.java
@@ -521,21 +521,32 @@
     public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args,
             int titleResId, CharSequence title, boolean isShortcut,
             UserHandle userHandle) {
-        Intent intent = onBuildStartFragmentIntent(context, fragmentName, args,
-                null /* titleResPackageName */, titleResId, title, isShortcut);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        context.startActivityAsUser(intent, userHandle);
+        // workaround to avoid crash in b/17523189
+        if (userHandle.getIdentifier() == UserHandle.myUserId()) {
+            startWithFragment(context, fragmentName, args, null, 0, titleResId, title, isShortcut);
+        } else {
+            Intent intent = onBuildStartFragmentIntent(context, fragmentName, args,
+                    null /* titleResPackageName */, titleResId, title, isShortcut);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            context.startActivityAsUser(intent, userHandle);
+        }
     }
 
     public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args,
             String titleResPackageName, int titleResId, CharSequence title, boolean isShortcut,
             UserHandle userHandle) {
-        Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, titleResPackageName,
-                titleResId, title, isShortcut);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        context.startActivityAsUser(intent, userHandle);
+        // workaround to avoid crash in b/17523189
+        if (userHandle.getIdentifier() == UserHandle.myUserId()) {
+            startWithFragment(context, fragmentName, args, null, 0, titleResPackageName, titleResId,
+                    title, isShortcut);
+        } else {
+            Intent intent = onBuildStartFragmentIntent(context, fragmentName, args,
+                    titleResPackageName, titleResId, title, isShortcut);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            context.startActivityAsUser(intent, userHandle);
+        }
     }
 
     /**
diff --git a/src/com/android/settings/vpn2/AppDialogFragment.java b/src/com/android/settings/vpn2/AppDialogFragment.java
index 0e41117..e70b412 100644
--- a/src/com/android/settings/vpn2/AppDialogFragment.java
+++ b/src/com/android/settings/vpn2/AppDialogFragment.java
@@ -19,6 +19,7 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
+import android.app.Fragment;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.pm.PackageInfo;
@@ -45,13 +46,25 @@
     private static final String ARG_PACKAGE = "package";
 
     private PackageInfo mPackageInfo;
+    private Listener mListener;
 
     private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface(
             ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
 
-    public static void show(VpnSettings parent, PackageInfo packageInfo, String label,
+    public interface Listener {
+        public void onForget();
+        public void onCancel();
+    }
+
+    public static void show(Fragment parent, PackageInfo packageInfo, String label,
             boolean managing, boolean connected) {
-        if (!parent.isAdded()) return;
+        show(parent, null, packageInfo, label, managing, connected);
+    }
+
+    public static void show(Fragment parent, Listener listener, PackageInfo packageInfo,
+            String label, boolean managing, boolean connected) {
+        if (!parent.isAdded())
+            return;
 
         Bundle args = new Bundle();
         args.putParcelable(ARG_PACKAGE, packageInfo);
@@ -60,6 +73,7 @@
         args.putBoolean(ARG_CONNECTED, connected);
 
         final AppDialogFragment frag = new AppDialogFragment();
+        frag.mListener = listener;
         frag.setArguments(args);
         frag.setTargetFragment(parent, 0);
         frag.show(parent.getFragmentManager(), TAG_APP_DIALOG);
@@ -98,6 +112,9 @@
     @Override
     public void onCancel(DialogInterface dialog) {
         dismiss();
+        if (mListener != null) {
+            mListener.onCancel();
+        }
         super.onCancel(dialog);
     }
 
@@ -111,6 +128,10 @@
             Log.e(TAG, "Failed to forget authorization of " + mPackageInfo.packageName +
                     " for user " + userId, e);
         }
+
+        if (mListener != null) {
+            mListener.onForget();
+        }
     }
 
     private void onDisconnect(final DialogInterface dialog) {
diff --git a/src/com/android/settings/vpn2/AppManagementFragment.java b/src/com/android/settings/vpn2/AppManagementFragment.java
new file mode 100644
index 0000000..0707d12
--- /dev/null
+++ b/src/com/android/settings/vpn2/AppManagementFragment.java
@@ -0,0 +1,267 @@
+/*
+ * 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.AlertDialog;
+import android.app.AppOpsManager;
+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.content.pm.PackageManager.NameNotFoundException;
+import android.net.ConnectivityManager;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.support.v7.preference.Preference;
+import android.util.Log;
+
+import com.android.internal.logging.MetricsProto.MetricsEvent;
+import com.android.internal.net.VpnConfig;
+import com.android.settings.R;
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settingslib.RestrictedSwitchPreference;
+import com.android.settingslib.RestrictedPreference;
+import com.android.settings.Utils;
+
+import java.util.List;
+
+import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
+
+public class AppManagementFragment extends SettingsPreferenceFragment
+        implements Preference.OnPreferenceClickListener {
+
+    private static final String TAG = "AppManagementFragment";
+
+    private static final String ARG_PACKAGE_NAME = "package";
+
+    private static final String KEY_VERSION = "version";
+    private static final String KEY_ALWAYS_ON_VPN = "always_on_vpn";
+    private static final String KEY_FORGET_VPN = "forget_vpn";
+
+    private AppOpsManager mAppOpsManager;
+    private PackageManager mPackageManager;
+    private ConnectivityManager mConnectivityManager;
+
+    // VPN app info
+    private final int mUserId = UserHandle.myUserId();
+    private int mPackageUid;
+    private String mPackageName;
+    private PackageInfo mPackageInfo;
+    private String mVpnLabel;
+
+    // UI preference
+    private Preference mPreferenceVersion;
+    private RestrictedSwitchPreference mPreferenceAlwaysOn;
+    private RestrictedPreference mPreferenceForget;
+
+    // Listener
+    private final AppDialogFragment.Listener mForgetVpnDialogFragmentListener =
+            new AppDialogFragment.Listener() {
+        @Override
+        public void onForget() {
+            // Unset always-on-vpn when forgetting the VPN
+            if (isVpnAlwaysOn()) {
+                setAlwaysOnVpn(false);
+            }
+            // Also dismiss and go back to VPN list
+            finish();
+        }
+
+        @Override
+        public void onCancel() {
+            // do nothing
+        }
+    };
+
+    public static void show(Context context, AppPreference pref) {
+        Bundle args = new Bundle();
+        args.putString(ARG_PACKAGE_NAME, pref.getPackageName());
+        Utils.startWithFragmentAsUser(context, AppManagementFragment.class.getName(), args, -1,
+                pref.getLabel(), false, new UserHandle(pref.getUserId()));
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        addPreferencesFromResource(R.xml.vpn_app_management);
+
+        mPackageManager = getContext().getPackageManager();
+        mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
+        mConnectivityManager = getContext().getSystemService(ConnectivityManager.class);
+
+        mPreferenceVersion = findPreference(KEY_VERSION);
+        mPreferenceAlwaysOn = (RestrictedSwitchPreference) findPreference(KEY_ALWAYS_ON_VPN);
+        mPreferenceForget = (RestrictedPreference) findPreference(KEY_FORGET_VPN);
+
+        mPreferenceAlwaysOn.setOnPreferenceClickListener(this);
+        mPreferenceForget.setOnPreferenceClickListener(this);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        boolean isInfoLoaded = loadInfo();
+        if (isInfoLoaded) {
+            mPreferenceVersion.setTitle(
+                    getPrefContext().getString(R.string.vpn_version, mPackageInfo.versionName));
+            updateUI();
+        } else {
+            finish();
+        }
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        String key = preference.getKey();
+        switch (key) {
+            case KEY_FORGET_VPN:
+                return onForgetVpnClick();
+            case KEY_ALWAYS_ON_VPN:
+                return onAlwaysOnVpnClick();
+            default:
+                Log.w(TAG, "unknown key is clicked: " + key);
+                return false;
+        }
+    }
+
+    @Override
+    protected int getMetricsCategory() {
+        return MetricsEvent.VPN;
+    }
+
+    private boolean onForgetVpnClick() {
+        AppDialogFragment.show(this, mForgetVpnDialogFragmentListener, mPackageInfo, mVpnLabel,
+                true /* editing */, true);
+        return true;
+    }
+
+    private boolean onAlwaysOnVpnClick() {
+        final boolean isChecked = mPreferenceAlwaysOn.isChecked();
+        if (isChecked && isLegacyVpnLockDownOrAnotherPackageAlwaysOn()) {
+            // Show dialog if user replace always-on-vpn package and show not checked first
+            mPreferenceAlwaysOn.setChecked(false);
+            ReplaceExistingVpnFragment.show(this);
+        } else {
+            setAlwaysOnVpn(isChecked);
+        }
+        return true;
+    }
+
+    private void setAlwaysOnVpn(boolean isEnabled) {
+        // Only clear legacy lockdown vpn in system user.
+        if (mUserId == UserHandle.USER_SYSTEM) {
+            VpnUtils.clearLockdownVpn(getContext());
+        }
+        mConnectivityManager.setAlwaysOnVpnPackageForUser(mUserId, isEnabled ? mPackageName : null);
+        updateUI();
+    }
+
+    private void updateUI() {
+        if (isAdded()) {
+            mPreferenceAlwaysOn.setChecked(isVpnAlwaysOn());
+        }
+    }
+
+    private String getAlwaysOnVpnPackage() {
+        return mConnectivityManager.getAlwaysOnVpnPackageForUser(mUserId);
+    }
+
+    private boolean isVpnAlwaysOn() {
+        return mPackageName.equals(getAlwaysOnVpnPackage());
+    }
+
+    /**
+     * @return false if the intent doesn't contain an existing package or can't retrieve activated
+     * vpn info.
+     */
+    private boolean loadInfo() {
+        final Bundle args = getArguments();
+        if (args == null) {
+            Log.e(TAG, "empty bundle");
+            return false;
+        }
+
+        mPackageName = args.getString(ARG_PACKAGE_NAME);
+        if (mPackageName == null) {
+            Log.e(TAG, "empty package name");
+            return false;
+        }
+
+        try {
+            mPackageUid = mPackageManager.getPackageUid(mPackageName, /* PackageInfoFlags */ 0);
+            mPackageInfo = mPackageManager.getPackageInfo(mPackageName, /* PackageInfoFlags */ 0);
+            mVpnLabel = VpnConfig.getVpnLabel(getPrefContext(), mPackageName).toString();
+        } catch (NameNotFoundException nnfe) {
+            Log.e(TAG, "package not found", nnfe);
+            return false;
+        }
+
+        if (!isVpnActivated()) {
+            Log.e(TAG, "package didn't register VPN profile");
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean isVpnActivated() {
+        final List<AppOpsManager.PackageOps> apps = mAppOpsManager.getOpsForPackage(mPackageUid,
+                mPackageName, new int[]{OP_ACTIVATE_VPN});
+        return apps != null && apps.size() > 0 && apps.get(0) != null;
+    }
+
+    private boolean isLegacyVpnLockDownOrAnotherPackageAlwaysOn() {
+        if (mUserId == UserHandle.USER_SYSTEM) {
+            String lockdownKey = VpnUtils.getLockdownVpn();
+            if (lockdownKey != null) {
+                return true;
+            }
+        }
+
+        return getAlwaysOnVpnPackage() != null && !isVpnAlwaysOn();
+    }
+
+    public static class ReplaceExistingVpnFragment extends DialogFragment
+            implements DialogInterface.OnClickListener {
+
+        public static void show(AppManagementFragment parent) {
+            final ReplaceExistingVpnFragment frag = new ReplaceExistingVpnFragment();
+            frag.setTargetFragment(parent, 0);
+            frag.show(parent.getFragmentManager(), null);
+        }
+
+        @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_continue), this)
+                    .create();
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (getTargetFragment() instanceof AppManagementFragment) {
+                ((AppManagementFragment) getTargetFragment()).setAlwaysOnVpn(true);
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/vpn2/LockdownConfigFragment.java b/src/com/android/settings/vpn2/LockdownConfigFragment.java
index 1a06565..8f19fa6 100644
--- a/src/com/android/settings/vpn2/LockdownConfigFragment.java
+++ b/src/com/android/settings/vpn2/LockdownConfigFragment.java
@@ -71,14 +71,9 @@
         dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN);
     }
 
-    private static String getStringOrNull(KeyStore keyStore, String key) {
-        final byte[] value = keyStore.get(key);
-        return value == null ? null : new String(value);
-    }
-
     private void initProfiles(KeyStore keyStore, Resources res) {
         final ConnectivityManager cm = ConnectivityManager.from(getActivity());
-        final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN);
+        final String lockdownKey = VpnUtils.getLockdownVpn();
         final String alwaysOnPackage =  cm.getAlwaysOnVpnPackageForUser(UserHandle.myUserId());
 
         // Legacy VPN has a separate always-on mechanism which takes over the whole device, so
diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java
index e5a78b6..c457615 100644
--- a/src/com/android/settings/vpn2/VpnSettings.java
+++ b/src/com/android/settings/vpn2/VpnSettings.java
@@ -355,8 +355,7 @@
             } else if (tag instanceof AppPreference) {
                 AppPreference pref = (AppPreference) tag;
                 boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
-                AppDialogFragment.show(VpnSettings.this, pref.getPackageInfo(), pref.getLabel(),
-                        true /* editing */, connected);
+                AppManagementFragment.show(getPrefContext(), pref);
             }
         }
     };
diff --git a/src/com/android/settings/vpn2/VpnUtils.java b/src/com/android/settings/vpn2/VpnUtils.java
new file mode 100644
index 0000000..122816a
--- /dev/null
+++ b/src/com/android/settings/vpn2/VpnUtils.java
@@ -0,0 +1,38 @@
+/*
+ * 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.content.Context;
+import android.net.ConnectivityManager;
+import android.security.Credentials;
+import android.security.KeyStore;
+
+/**
+ * Utility functions for vpn
+ */
+public class VpnUtils {
+
+    public static String getLockdownVpn() {
+        final byte[] value = KeyStore.getInstance().get(Credentials.LOCKDOWN_VPN);
+        return value == null ? null : new String(value);
+    }
+
+    public static void clearLockdownVpn(Context context) {
+        KeyStore.getInstance().delete(Credentials.LOCKDOWN_VPN);
+        // Always notify ConnectivityManager after keystore update
+        context.getSystemService(ConnectivityManager.class).updateLockdownVpn();
+    }
+}