Always-on VPN.

Adds support for always-on VPN profiles. Users pick an always-on VPN
from list of existing VPN profiles, which must use an IP address for
both VPN server and DNS.  Moved "add" operation into action bar.

Bug: 5756357
Change-Id: I4c7ed7f2a3b027be1baf65c08213336a61f3acfe
diff --git a/res/layout/vpn_lockdown_editor.xml b/res/layout/vpn_lockdown_editor.xml
new file mode 100644
index 0000000..933c5ec
--- /dev/null
+++ b/res/layout/vpn_lockdown_editor.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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:orientation="vertical"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingLeft="16dip"
+        android:paddingRight="16dip"
+        android:paddingTop="8dip"
+        android:paddingBottom="8dip"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="@string/vpn_lockdown_summary" />
+
+    <ListView
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/res/menu/vpn.xml b/res/menu/vpn.xml
new file mode 100644
index 0000000..dd8f64c
--- /dev/null
+++ b/res/menu/vpn.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/vpn_create"
+        android:title="@string/vpn_create"
+        android:icon="@drawable/ic_menu_add"
+        android:showAsAction="always" />
+    <item
+        android:id="@+id/vpn_lockdown"
+        android:title="@string/vpn_menu_lockdown"
+        android:showAsAction="never" />
+</menu>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ea7273c..eab6718 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4034,6 +4034,15 @@
     <string name="vpn_menu_edit">Edit profile</string>
     <!-- Menu item to delete a VPN profile. [CHAR LIMIT=40] -->
     <string name="vpn_menu_delete">Delete profile</string>
+    <!-- Menu item to select always-on VPN profile. [CHAR LIMIT=40] -->
+    <string name="vpn_menu_lockdown">Always-on VPN</string>
+
+    <!-- Summary describing the always-on VPN feature. [CHAR LIMIT=NONE] -->
+    <string name="vpn_lockdown_summary">Select a VPN profile to always remain connected to. Network traffic will only be allowed when connected to this VPN.</string>
+    <!-- List item indicating that no always-on VPN is selected. [CHAR LIMIT=64] -->
+    <string name="vpn_lockdown_none">None</string>
+    <!-- Error indicating that the selected VPN doesn't meet requirements. [CHAR LIMIT=NONE] -->
+    <string name="vpn_lockdown_config_error">Always-on VPN requires an IP address for both server and DNS.</string>
 
     <!-- Toast message when there is no network connection to start VPN. [CHAR LIMIT=100] -->
     <string name="vpn_no_network">There is no network connection. Please try again later.</string>
diff --git a/res/xml/vpn_settings2.xml b/res/xml/vpn_settings2.xml
index 38632cc..08075a6 100644
--- a/res/xml/vpn_settings2.xml
+++ b/res/xml/vpn_settings2.xml
@@ -16,8 +16,4 @@
 
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
         android:title="@string/vpn_title">
-    <Preference android:key="add_network"
-            android:title="@string/vpn_create"
-            android:order="1"
-            android:persistent="false"/>
 </PreferenceScreen>
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index f8c23d7..8e3987f 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -24,6 +24,7 @@
 import com.android.settings.bluetooth.BluetoothEnabler;
 import com.android.settings.deviceinfo.Memory;
 import com.android.settings.fuelgauge.PowerUsageSummary;
+import com.android.settings.vpn2.VpnSettings;
 import com.android.settings.wifi.WifiEnabler;
 
 import android.accounts.Account;
@@ -361,7 +362,8 @@
                 WirelessSettings.class.getName().equals(fragmentName) ||
                 SoundSettings.class.getName().equals(fragmentName) ||
                 PrivacySettings.class.getName().equals(fragmentName) ||
-                ManageAccountsSettings.class.getName().equals(fragmentName)) {
+                ManageAccountsSettings.class.getName().equals(fragmentName) ||
+                VpnSettings.class.getName().equals(fragmentName)) {
             intent.putExtra(EXTRA_CLEAR_UI_OPTIONS, true);
         }
 
diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java
index ecc80b0..07aa04f 100644
--- a/src/com/android/settings/vpn2/VpnSettings.java
+++ b/src/com/android/settings/vpn2/VpnSettings.java
@@ -16,8 +16,13 @@
 
 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.net.IConnectivityManager;
 import android.os.Bundle;
 import android.os.Handler;
@@ -27,13 +32,18 @@
 import android.preference.PreferenceGroup;
 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.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.Toast;
 
 import com.android.internal.net.LegacyVpnInfo;
@@ -41,15 +51,21 @@
 import com.android.internal.net.VpnProfile;
 import com.android.settings.R;
 import com.android.settings.SettingsPreferenceFragment;
+import com.google.android.collect.Lists;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 public class VpnSettings extends SettingsPreferenceFragment implements
         Handler.Callback, Preference.OnPreferenceClickListener,
         DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
-
     private static final String TAG = "VpnSettings";
 
+    private static final String TAG_LOCKDOWN = "lockdown";
+
+    // TODO: migrate to using DialogFragment when editing
+
     private final IConnectivityManager mService = IConnectivityManager.Stub
             .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
     private final KeyStore mKeyStore = KeyStore.getInstance();
@@ -67,8 +83,9 @@
     @Override
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
+
+        setHasOptionsMenu(true);
         addPreferencesFromResource(R.xml.vpn_settings2);
-        getPreferenceScreen().setOrderingAsAdded(false);
 
         if (savedState != null) {
             VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"),
@@ -81,6 +98,35 @@
     }
 
     @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.vpn, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            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))) {
+                    ++millis;
+                }
+                mDialog = new VpnDialog(
+                        getActivity(), this, new VpnProfile(Long.toHexString(millis)), true);
+                mDialog.setOnDismissListener(this);
+                mDialog.show();
+                return true;
+            }
+            case R.id.vpn_lockdown: {
+                LockdownConfigFragment.show(this);
+                return true;
+            }
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
     public void onSaveInstanceState(Bundle savedState) {
         // We do not save view hierarchy, as they are just profiles.
         if (mDialog != null) {
@@ -119,24 +165,14 @@
             mPreferences = new HashMap<String, VpnPreference>();
             PreferenceGroup group = getPreferenceScreen();
 
-            String[] keys = mKeyStore.saw(Credentials.VPN);
-            if (keys != null && keys.length > 0) {
-                Context context = getActivity();
-
-                for (String key : keys) {
-                    VpnProfile profile = VpnProfile.decode(key,
-                            mKeyStore.get(Credentials.VPN + key));
-                    if (profile == null) {
-                        Log.w(TAG, "bad profile: key = " + key);
-                        mKeyStore.delete(Credentials.VPN + key);
-                    } else {
-                        VpnPreference preference = new VpnPreference(context, profile);
-                        mPreferences.put(key, preference);
-                        group.addPreference(preference);
-                    }
-                }
+            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);
             }
-            group.findPreference("add_network").setOnPreferenceClickListener(this);
         }
 
         // Show the dialog if there is one.
@@ -191,6 +227,7 @@
                 preference.update(profile);
             } else {
                 preference = new VpnPreference(getActivity(), profile);
+                preference.setOnPreferenceClickListener(this);
                 mPreferences.put(profile.key, preference);
                 getPreferenceScreen().addPreference(preference);
             }
@@ -340,7 +377,7 @@
         return R.string.help_url_vpn;
     }
 
-    private class VpnPreference extends Preference {
+    private static class VpnPreference extends Preference {
         private VpnProfile mProfile;
         private int mState = -1;
 
@@ -348,7 +385,6 @@
             super(context);
             setPersistent(false);
             setOrder(0);
-            setOnPreferenceClickListener(VpnSettings.this);
 
             mProfile = profile;
             update();
@@ -396,4 +432,109 @@
             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_holo,
+                        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);
+            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());
+                    }
+
+                    // kick profiles since we changed them
+                    ConnectivityManager.from(getActivity()).updateLockdownVpn();
+                }
+            });
+
+            return builder.create();
+        }
+    }
+
+    private static List<VpnProfile> loadVpnProfiles(KeyStore keyStore) {
+        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) {
+                    result.add(profile);
+                }
+            }
+        }
+        return result;
+    }
 }