Add VPN settings classes to Settings app.

PATCH SET 2:
+ Add import com.android.settings.R

PATCH SET 3:
+ Remove @Override interface methods to be compilable by Java 1.5.

PATCH SET 4:
+ Add import android.net.vpn.VpnManager

PATCH SET 5:
+ Add license headers.

PATCH SET 6:
+ Remove Constant.java and move the constants to VpnSettings.
+ Make AuthenticationActor implement DialogInterface's handlers.
+ Remove trailing spaces.

PATCH SET 7:
+ Remove default username.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f9758f3..2714457 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -110,6 +110,20 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".vpn.VpnSettings"
+                android:launchMode="singleTask">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.net.vpn.SETTINGS" />
+                <action android:name="android.net.vpn.INSTALL_PROFILE" />
+                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".vpn.VpnTypeSelection"></activity>
+        <activity android:name=".vpn.VpnEditor"></activity>
+
         <activity android:name="DateTimeSettings" android:label="@string/date_and_time"
                 >
             <intent-filter>
diff --git a/res/layout/vpn_connect_dialog_view.xml b/res/layout/vpn_connect_dialog_view.xml
new file mode 100644
index 0000000..540b404
--- /dev/null
+++ b/res/layout/vpn_connect_dialog_view.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent">
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+            android:orientation="horizontal"
+            android:layout_marginLeft="@dimen/vpn_connect_margin_left"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content">
+        <TextView android:id="@+id/username_str"
+                android:layout_width="@dimen/vpn_connect_input_box_label_width"
+                android:layout_height="wrap_content"
+                android:textSize="@dimen/vpn_connect_normal_text_size"
+                android:gravity="right"
+                android:layout_marginRight="@dimen/vpn_connect_input_box_padding"
+                android:text="@string/vpn_username_colon" />
+        <EditText android:id="@+id/username_value" 
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginRight="@dimen/vpn_connect_margin_right"
+                android:singleLine="True"/>
+    </LinearLayout>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+            android:orientation="horizontal"
+            android:layout_marginLeft="@dimen/vpn_connect_margin_left"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="10sp">
+        <TextView android:id="@+id/password_str"
+                android:layout_width="@dimen/vpn_connect_input_box_label_width"
+                android:layout_height="wrap_content"
+                android:textSize="@dimen/vpn_connect_normal_text_size"
+                android:gravity="right"
+                android:layout_marginRight="@dimen/vpn_connect_input_box_padding"
+                android:text="@string/vpn_password_colon" />
+        <EditText android:id="@+id/password_value" 
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginRight="@dimen/vpn_connect_margin_right"
+                android:password="True"
+                android:singleLine="True"/>
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100755
index 0000000..56bd60c
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <dimen name="vpn_connect_margin_left">5sp</dimen>
+    <dimen name="vpn_connect_margin_right">5sp</dimen>
+    <dimen name="vpn_connect_normal_text_size">16sp</dimen>
+    <dimen name="vpn_connect_input_box_label_width">90sp</dimen>
+    <dimen name="vpn_connect_input_box_width">200sp</dimen>
+    <dimen name="vpn_connect_input_box_padding">5sp</dimen>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c2f53b8..3b91ef6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1726,4 +1726,73 @@
 
     <!-- Power Control Widget -->
     <string name="gadget_title">Power Control</string>
+
+    <string name="vpn_settings_activity_title">VPN settings</string>
+
+    <string name="vpn_username_colon">User name:</string>
+    <string name="vpn_password_colon">Password:</string>
+    <string name="vpn_username">User name</string>
+    <string name="vpn_password">Password</string>
+    <string name="vpn_you_miss_a_field">You missed a field!</string>
+    <string name="vpn_please_fill_up">Please fill up \"%s\".</string>
+
+    <string name="vpn_connect_button">Connect</string>
+    <string name="vpn_cancel_button">Cancel</string>
+    <string name="vpn_yes_button">Yes</string>
+    <string name="vpn_no_button">No</string>
+    <string name="vpn_back_button">Back</string>
+    <string name="vpn_mistake_button">No, it's a mistake</string>
+
+    <string name="vpn_menu_save">Save</string>
+    <!-- Edit VPN screen menu option to discard the user's changes for this VPN -->
+    <string name="vpn_menu_cancel">Discard</string>
+    <string name="vpn_menu_connect">Connect</string>
+    <string name="vpn_menu_disconnect">Disconnect</string>
+    <string name="vpn_menu_edit">Edit</string>
+    <string name="vpn_menu_delete">Delete</string>
+
+    <!-- VPN error dialog title -->
+    <string name="vpn_error_title">Attention</string>
+    <string name="vpn_error_name_empty">VPN Name cannot be empty.</string>
+    <string name="vpn_error_server_name_empty">The Server Name field cannot be empty.</string>
+    <string name="vpn_error_duplicate_name">The VPN Name \'%s\' already exists. Find another name.</string>
+    <string name="vpn_error_user_certificate_not_selected">Need to select a user certificate.</string>
+    <string name="vpn_error_ca_certificate_not_selected">Need to select a CA certificate.</string>
+    <string name="vpn_error_userkey_not_selected">Need to select a userkey.</string>
+    <string name="vpn_confirm_profile_cancellation">Are you sure you don\'t want to create this profile?</string>
+    <string name="vpn_confirm_reconnect">The previous connection attempt failed. Do you want to try again?</string>
+
+    <string name="vpn_add_new_vpn">Add new VPN</string>
+    <string name="vpn_edit_title_add">Add new %s VPN</string>
+    <string name="vpn_edit_title_edit">Edit %s VPN</string>
+    <string name="vpn_type_title">Select VPN type</string>
+    <string name="vpns">VPN networks</string>
+    <!-- EditTextPreference summary text when no value has been set -->
+    <string name="vpn_not_set">Click to set the value</string>
+    <!-- EditTextPreference summary text when VPN is connecting -->
+    <string name="vpn_connecting">Connecting...</string>
+    <!-- EditTextPreference summary text when VPN is disconnecting -->
+    <string name="vpn_disconnecting">Disconnecting...</string>
+    <!-- EditTextPreference summary text when VPN is connected -->
+    <string name="vpn_connected">Connected</string>
+    <!-- EditTextPreference summary text when VPN is not connected -->
+    <string name="vpn_connect_hint">Select to connect</string>
+    <!-- dialog title when asking for username and password -->
+    <string name="vpn_connect_to">Connect to</string>
+
+    <string name="vpn_name">VPN Name</string>
+    <string name="vpn_name_summary">Give a name to this VPN;</string>
+
+    <string name="vpn_profile_added">'%s' is added</string>
+    <string name="vpn_profile_replaced">Changes are made to '%s'</string>
+
+    <string name="vpn_user_certificate_title">User Certificate</string>
+    <string name="vpn_ca_certificate_title">CA Certificate</string>
+    <string name="vpn_userkey_title">User Key</string>
+    <string name="vpn_server_name_title">Server Name</string>
+    <string name="vpn_dns_search_list_title">DNS Search List</string>
+
+    <string name="vpn_settings_category">VPN</string>
+    <string name="vpn_settings_title">VPN</string>
+    <string name="vpn_settings_summary">Set up and manage VPN configurations, connections</string>
 </resources>
diff --git a/res/xml/vpn_edit.xml b/res/xml/vpn_edit.xml
new file mode 100644
index 0000000..7976242
--- /dev/null
+++ b/res/xml/vpn_edit.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:settings="http://schemas.android.com/apk/res/com.android.settings">
+
+    <EditTextPreference
+        android:title="@string/vpn_name"
+        android:dialogTitle="@string/vpn_name"
+        android:key="vpn_name"
+        android:summary="@string/vpn_name_summary"
+        android:singleLine="true"/>
+
+</PreferenceScreen>   
diff --git a/res/xml/vpn_settings.xml b/res/xml/vpn_settings.xml
new file mode 100644
index 0000000..a1a4bf9
--- /dev/null
+++ b/res/xml/vpn_settings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+        android:title="@string/vpn_settings_activity_title">
+
+    <PreferenceScreen android:key="add_new_vpn"
+        android:title="@string/vpn_add_new_vpn">
+        <!--intent
+            android:action="android.intent.action.MAIN"
+            android:targetPackage="com.android.settings.vpn"
+            android:targetClass="com.android.settings.vpn.VpnEditor" /-->
+    </PreferenceScreen>
+
+    <!--CheckBoxPreference android:key="installer_enabled"
+            android:defaultValue="false"
+            android:title="@string/installer_enabled"
+            android:summaryOn="@string/installer_enabled_summary_on"
+            android:summaryOff="@string/installer_enabled_summary_off" /-->
+
+    <PreferenceCategory android:key="vpn_list" android:title="@string/vpns">
+    </PreferenceCategory>
+</PreferenceScreen>   
diff --git a/res/xml/vpn_type.xml b/res/xml/vpn_type.xml
new file mode 100644
index 0000000..c59b54a
--- /dev/null
+++ b/res/xml/vpn_type.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
+        android:title="@string/vpn_type_title">
+
+</PreferenceScreen>   
diff --git a/res/xml/wireless_settings.xml b/res/xml/wireless_settings.xml
index f81fb41..4731fb3 100644
--- a/res/xml/wireless_settings.xml
+++ b/res/xml/wireless_settings.xml
@@ -53,6 +53,16 @@
                 </PreferenceScreen>
 
                 <PreferenceScreen
+                    android:title="@string/vpn_settings_title"
+                    android:summary="@string/vpn_settings_summary"
+                    android:dependency="toggle_airplane">
+                    <intent
+                        android:action="android.intent.action.MAIN"
+                        android:targetPackage="com.android.settings"
+                        android:targetClass="com.android.settings.vpn.VpnSettings" />
+                </PreferenceScreen>
+
+                <PreferenceScreen
                     android:title="@string/network_settings_title"
                     android:summary="@string/network_settings_summary"
                     android:dependency="toggle_airplane">
diff --git a/src/com/android/settings/SecuritySettings.java b/src/com/android/settings/SecuritySettings.java
index 166fa44..cb37465 100644
--- a/src/com/android/settings/SecuritySettings.java
+++ b/src/com/android/settings/SecuritySettings.java
@@ -24,6 +24,7 @@
 import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.location.LocationManager;
+import android.net.vpn.VpnManager;
 import android.os.Bundle;
 import android.preference.CheckBoxPreference;
 import android.preference.Preference;
@@ -160,6 +161,17 @@
         showPassword.setPersistent(false);
         passwordsCat.addPreference(showPassword);
         
+        PreferenceScreen vpnPreferences = getPreferenceManager()
+                .createPreferenceScreen(this);
+        vpnPreferences.setTitle(R.string.vpn_settings_category);
+        vpnPreferences.setIntent(new VpnManager(this).createSettingsActivityIntent());
+
+        PreferenceCategory vpnCat = new PreferenceCategory(this);
+        vpnCat.setTitle(R.string.vpn_settings_title);
+        vpnCat.setSummary(R.string.vpn_settings_summary);
+        root.addPreference(vpnCat);
+        vpnCat.addPreference(vpnPreferences);
+
         return root;
     }
 
diff --git a/src/com/android/settings/vpn/AuthenticationActor.java b/src/com/android/settings/vpn/AuthenticationActor.java
new file mode 100644
index 0000000..364fd37
--- /dev/null
+++ b/src/com/android/settings/vpn/AuthenticationActor.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import com.android.settings.R;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.vpn.IVpnService;
+import android.net.vpn.VpnManager;
+import android.net.vpn.VpnProfile;
+import android.net.vpn.VpnState;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.IOException;
+
+/**
+ */
+public class AuthenticationActor implements VpnProfileActor,
+        DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+    private static final String TAG = AuthenticationActor.class.getName();
+    private static final int ONE_SECOND = 1000; // ms
+
+    private static final String STATE_IS_DIALOG_OPEN = "is_dialog_open";
+    private static final String STATE_USERNAME = "username";
+    private static final String STATE_PASSWORD = "password";
+
+    private Context mContext;
+    private TextView mUsernameView;
+    private TextView mPasswordView;
+
+    private VpnProfile mProfile;
+    private View mView;
+    private VpnManager mVpnManager;
+    private AlertDialog mConnectDialog;
+    private AlertDialog mDisconnectDialog;
+
+    public AuthenticationActor(Context context, VpnProfile p) {
+        mContext = context;
+        mProfile = p;
+        mVpnManager = new VpnManager(context);
+    }
+
+    //@Override
+    public VpnProfile getProfile() {
+        return mProfile;
+    }
+
+    //@Override
+    public synchronized void connect() {
+        connect("", "");
+    }
+
+    //@Override
+    public void onClick(DialogInterface dialog, int which) {
+        dismissConnectDialog();
+        switch (which) {
+        case DialogInterface.BUTTON1: // connect
+            if (validateInputs()) {
+                broadcastConnectivity(VpnState.CONNECTING);
+                connectInternal();
+            }
+            break;
+
+        case DialogInterface.BUTTON2: // cancel
+            broadcastConnectivity(VpnState.CANCELLED);
+            break;
+        }
+    }
+
+    //@Override
+    public void onCancel(DialogInterface dialog) {
+        dismissConnectDialog();
+        broadcastConnectivity(VpnState.CANCELLED);
+    }
+
+    private void connect(String username, String password) {
+        Context c = mContext;
+        mConnectDialog = new AlertDialog.Builder(c)
+                .setView(createConnectView(username, password))
+                .setTitle(c.getString(R.string.vpn_connect_to) + " "
+                        + mProfile.getName())
+                .setPositiveButton(c.getString(R.string.vpn_connect_button),
+                        this)
+                .setNegativeButton(c.getString(R.string.vpn_cancel_button),
+                        this)
+                .setOnCancelListener(this)
+                .create();
+        mConnectDialog.show();
+    }
+
+    //@Override
+    public synchronized void onSaveState(Bundle outState) {
+        outState.putBoolean(STATE_IS_DIALOG_OPEN, (mConnectDialog != null));
+        if (mConnectDialog != null) {
+            assert(mConnectDialog.isShowing());
+            outState.putBoolean(STATE_IS_DIALOG_OPEN, (mConnectDialog != null));
+            outState.putString(STATE_USERNAME,
+                    mUsernameView.getText().toString());
+            outState.putString(STATE_PASSWORD,
+                    mPasswordView.getText().toString());
+            dismissConnectDialog();
+        }
+    }
+
+    //@Override
+    public synchronized void onRestoreState(final Bundle savedState) {
+        boolean isDialogOpen = savedState.getBoolean(STATE_IS_DIALOG_OPEN);
+        if (isDialogOpen) {
+            connect(savedState.getString(STATE_USERNAME),
+                    savedState.getString(STATE_PASSWORD));
+        }
+    }
+
+    private synchronized void dismissConnectDialog() {
+        mConnectDialog.dismiss();
+        mConnectDialog = null;
+    }
+
+    private void connectInternal() {
+        mVpnManager.startVpnService();
+        ServiceConnection c = new ServiceConnection() {
+            public void onServiceConnected(ComponentName className,
+                    IBinder service) {
+                boolean success = false;
+                try {
+                    success = IVpnService.Stub.asInterface(service)
+                            .connect(mProfile,
+                                    mUsernameView.getText().toString(),
+                                    mPasswordView.getText().toString());
+                    mPasswordView.setText("");
+                } catch (Throwable e) {
+                    Log.e(TAG, "connect()", e);
+                    checkStatus();
+                } finally {
+                    mContext.unbindService(this);
+
+                    if (!success) {
+                        Log.d(TAG, "~~~~~~ connect() failed!");
+                        // TODO: pop up a dialog
+                        broadcastConnectivity(VpnState.IDLE);
+                    } else {
+                        Log.d(TAG, "~~~~~~ connect() succeeded!");
+                    }
+                }
+            }
+
+            public void onServiceDisconnected(ComponentName className) {
+                checkStatus();
+            }
+        };
+        if (!bindService(c)) broadcastConnectivity(VpnState.IDLE);
+    }
+
+    //@Override
+    public void disconnect() {
+        ServiceConnection c = new ServiceConnection() {
+            public void onServiceConnected(ComponentName className,
+                    IBinder service) {
+                try {
+                    IVpnService.Stub.asInterface(service).disconnect();
+                } catch (RemoteException e) {
+                    Log.e(TAG, "disconnect()", e);
+                    checkStatus();
+                } finally {
+                    mContext.unbindService(this);
+                    broadcastConnectivity(VpnState.IDLE);
+                }
+            }
+
+            public void onServiceDisconnected(ComponentName className) {
+                checkStatus();
+            }
+        };
+        bindService(c);
+    }
+
+    //@Override
+    public void checkStatus() {
+        ServiceConnection c = new ServiceConnection() {
+            public synchronized void onServiceConnected(ComponentName className,
+                    IBinder service) {
+                try {
+                    IVpnService.Stub.asInterface(service).checkStatus(mProfile);
+                } catch (Throwable e) {
+                    Log.e(TAG, "checkStatus()", e);
+                } finally {
+                    notify();
+                }
+            }
+
+            public void onServiceDisconnected(ComponentName className) {
+                // do nothing
+            }
+        };
+        if (bindService(c)) {
+            // wait for a second, let status propagate
+            wait(c, ONE_SECOND);
+        }
+        mContext.unbindService(c);
+    }
+
+    private boolean bindService(ServiceConnection c) {
+        return mVpnManager.bindVpnService(c);
+    }
+
+    private void broadcastConnectivity(VpnState s) {
+        mVpnManager.broadcastConnectivity(mProfile.getName(), s);
+    }
+
+    // returns true if inputs pass validation
+    private boolean validateInputs() {
+        Context c = mContext;
+        String error = null;
+        if (Util.isNullOrEmpty(mUsernameView.getText().toString())) {
+            error = c.getString(R.string.vpn_username);
+        } else if (Util.isNullOrEmpty(mPasswordView.getText().toString())) {
+            error = c.getString(R.string.vpn_password);
+        }
+        if (error == null) {
+            return true;
+        } else {
+            new AlertDialog.Builder(c)
+                    .setTitle(c.getString(R.string.vpn_you_miss_a_field))
+                    .setMessage(String.format(
+                            c.getString(R.string.vpn_please_fill_up), error))
+                    .setPositiveButton(c.getString(R.string.vpn_back_button),
+                            createBackButtonListener())
+                    .show();
+            return false;
+        }
+    }
+
+    private View createConnectView(String username, String password) {
+        View v = View.inflate(mContext, R.layout.vpn_connect_dialog_view, null);
+        mUsernameView = (TextView) v.findViewById(R.id.username_value);
+        mPasswordView = (TextView) v.findViewById(R.id.password_value);
+        mUsernameView.setText(username);
+        mPasswordView.setText(password);
+        copyFieldsFromOldView(v);
+        mView = v;
+        return v;
+    }
+
+    private void copyFieldsFromOldView(View newView) {
+        if (mView == null) return;
+        mUsernameView.setText(
+                ((TextView) mView.findViewById(R.id.username_value)).getText());
+        mPasswordView.setText(
+                ((TextView) mView.findViewById(R.id.password_value)).getText());
+    }
+
+    private DialogInterface.OnClickListener createBackButtonListener() {
+        return new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+                connect();
+            }
+        };
+    }
+
+    private void wait(Object o, int ms) {
+        synchronized (o) {
+            try {
+                o.wait(ms);
+            } catch (Exception e) {}
+        }
+    }
+}
diff --git a/src/com/android/settings/vpn/L2tpIpsecEditor.java b/src/com/android/settings/vpn/L2tpIpsecEditor.java
new file mode 100644
index 0000000..2bb4c8d
--- /dev/null
+++ b/src/com/android/settings/vpn/L2tpIpsecEditor.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import com.android.settings.R;
+
+import android.content.Context;
+import android.net.vpn.L2tpIpsecProfile;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceGroup;
+import android.security.Keystore;
+
+/**
+ * The class for editing {@link L2tpIpsecProfile}.
+ */
+class L2tpIpsecEditor extends SingleServerEditor {
+    private static final String TAG = L2tpIpsecEditor.class.getSimpleName();
+
+    private ListPreference mUserCertificate;
+    private ListPreference mCaCertificate;
+    private ListPreference mUserkey;
+
+    private L2tpIpsecProfile mProfile;
+
+    public L2tpIpsecEditor(L2tpIpsecProfile p) {
+        super(p);
+        mProfile = p;
+    }
+
+    //@Override
+    public void loadPreferencesTo(PreferenceGroup subsettings) {
+        super.loadPreferencesTo(subsettings);
+        Context c = subsettings.getContext();
+        subsettings.addPreference(createUserkeyPreference(c));
+        subsettings.addPreference(createUserCertificatePreference(c));
+        subsettings.addPreference(createCaCertificatePreference(c));
+        subsettings.addPreference(createDomainSufficesPreference(c));
+    }
+
+    //@Override
+    public String validate(Context c) {
+        String result = super.validate(c);
+        if (result != null) {
+            return result;
+        } else if (mProfile.isCustomized()) {
+            return null;
+        } else if (Util.isNullOrEmpty(mUserkey.getValue())) {
+            return c.getString(R.string.vpn_error_userkey_not_selected);
+        } else if (Util.isNullOrEmpty(mUserCertificate.getValue())) {
+            return c.getString(R.string.vpn_error_user_certificate_not_selected);
+        } else if (Util.isNullOrEmpty(mCaCertificate.getValue())) {
+            return c.getString(R.string.vpn_error_ca_certificate_not_selected);
+        } else {
+            return null;
+        }
+    }
+
+    private Preference createUserCertificatePreference(Context c) {
+        mUserCertificate = createListPreference(c,
+                R.string.vpn_user_certificate_title,
+                mProfile.getUserCertificate(),
+                Keystore.getInstance().getAllCertificateKeys(),
+                new Preference.OnPreferenceChangeListener() {
+                    public boolean onPreferenceChange(
+                            Preference pref, Object newValue) {
+                        mProfile.setUserCertificate((String) newValue);
+                        return onPreferenceChangeCommon(pref, newValue);
+                    }
+                });
+        return mUserCertificate;
+    }
+
+    private Preference createCaCertificatePreference(Context c) {
+        mCaCertificate = createListPreference(c,
+                R.string.vpn_ca_certificate_title,
+                mProfile.getCaCertificate(),
+                Keystore.getInstance().getAllCertificateKeys(),
+                new Preference.OnPreferenceChangeListener() {
+                    public boolean onPreferenceChange(
+                            Preference pref, Object newValue) {
+                        mProfile.setCaCertificate((String) newValue);
+                        return onPreferenceChangeCommon(pref, newValue);
+                    }
+                });
+        return mCaCertificate;
+    }
+
+    private Preference createUserkeyPreference(Context c) {
+        mUserkey = createListPreference(c,
+                R.string.vpn_userkey_title,
+                mProfile.getUserkey(),
+                Keystore.getInstance().getAllUserkeyKeys(),
+                new Preference.OnPreferenceChangeListener() {
+                    public boolean onPreferenceChange(
+                            Preference pref, Object newValue) {
+                        mProfile.setUserkey((String) newValue);
+                        return onPreferenceChangeCommon(pref, newValue);
+                    }
+                });
+        return mUserkey;
+    }
+
+    private ListPreference createListPreference(Context c, int titleResId,
+            String text, String[] keys,
+            Preference.OnPreferenceChangeListener listener) {
+        ListPreference pref = new ListPreference(c);
+        pref.setTitle(titleResId);
+        pref.setDialogTitle(titleResId);
+        pref.setPersistent(true);
+        pref.setEntries(keys);
+        pref.setEntryValues(keys);
+        pref.setValue(text);
+        pref.setSummary(checkNull(text, c));
+        pref.setOnPreferenceChangeListener(listener);
+        return pref;
+    }
+
+    private boolean onPreferenceChangeCommon(Preference pref, Object newValue) {
+        pref.setSummary(checkNull(newValue.toString(), pref.getContext()));
+        return true;
+    }
+}
diff --git a/src/com/android/settings/vpn/SingleServerEditor.java b/src/com/android/settings/vpn/SingleServerEditor.java
new file mode 100644
index 0000000..63964b4
--- /dev/null
+++ b/src/com/android/settings/vpn/SingleServerEditor.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import com.android.settings.R;
+
+import android.content.Context;
+import android.net.vpn.SingleServerProfile;
+import android.net.vpn.VpnProfile;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceGroup;
+
+/**
+ * The class for editing {@link SingleServerProfile}.
+ */
+class SingleServerEditor implements VpnProfileEditor {
+    private EditTextPreference mServerName;
+    private EditTextPreference mDomainSuffices;
+    private SingleServerProfile mProfile;
+
+    public SingleServerEditor(SingleServerProfile p) {
+        mProfile = p;
+    }
+
+    //@Override
+    public VpnProfile getProfile() {
+        return mProfile;
+    }
+
+    //@Override
+    public void loadPreferencesTo(PreferenceGroup subpanel) {
+        Context c = subpanel.getContext();
+        subpanel.addPreference(createServerNamePreference(c));
+    }
+
+    //@Override
+    public String validate(Context c) {
+        return (mProfile.isCustomized()
+                ? null
+                : (Util.isNullOrEmpty(mServerName.getText())
+                        ? c.getString(R.string.vpn_error_server_name_empty)
+                        : null));
+    }
+
+    /**
+     * Creates a preference for users to input domain suffices.
+     */
+    protected EditTextPreference createDomainSufficesPreference(Context c) {
+        EditTextPreference pref = mDomainSuffices = new EditTextPreference(c);
+        pref.setTitle(R.string.vpn_dns_search_list_title);
+        pref.setDialogTitle(R.string.vpn_dns_search_list_title);
+        pref.setPersistent(true);
+        pref.setText(mProfile.getDomainSuffices());
+        pref.setSummary(mProfile.getDomainSuffices());
+        pref.setOnPreferenceChangeListener(
+                new Preference.OnPreferenceChangeListener() {
+                    public boolean onPreferenceChange(
+                            Preference pref, Object newValue) {
+                        String v = ((String) newValue).trim();
+                        mProfile.setDomainSuffices(v);
+                        pref.setSummary(checkNull(v, pref.getContext()));
+                        return true;
+                    }
+                });
+        return pref;
+    }
+
+    private Preference createServerNamePreference(Context c) {
+        EditTextPreference serverName = mServerName = new EditTextPreference(c);
+        String title = c.getString(R.string.vpn_server_name_title);
+        serverName.setTitle(title);
+        serverName.setDialogTitle(title);
+        serverName.setSummary(checkNull(mProfile.getServerName(), c));
+        serverName.setText(mProfile.getServerName());
+        serverName.setPersistent(true);
+        serverName.setOnPreferenceChangeListener(
+                new Preference.OnPreferenceChangeListener() {
+                    public boolean onPreferenceChange(
+                            Preference pref, Object newValue) {
+                        String v = ((String) newValue).trim();
+                        mProfile.setServerName(v);
+                        pref.setSummary(checkNull(v, pref.getContext()));
+                        return true;
+                    }
+                });
+        return mServerName;
+    }
+
+
+   String checkNull(String value, Context c) {
+        return ((value != null && value.length() > 0)
+                ? value
+                : c.getString(R.string.vpn_not_set));
+   }
+}
diff --git a/src/com/android/settings/vpn/Util.java b/src/com/android/settings/vpn/Util.java
new file mode 100644
index 0000000..d7ba1f7
--- /dev/null
+++ b/src/com/android/settings/vpn/Util.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import com.android.settings.R;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.widget.Toast;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+class Util {
+
+    static void showShortToastMessage(Context context, String message) {
+        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+    }
+
+    static void showShortToastMessage(Context context, int messageId) {
+        Toast.makeText(context, messageId, Toast.LENGTH_SHORT).show();
+    }
+
+    static void showLongToastMessage(Context context, String message) {
+        Toast.makeText(context, message, Toast.LENGTH_LONG).show();
+    }
+
+    static void showLongToastMessage(Context context, int messageId) {
+        Toast.makeText(context, messageId, Toast.LENGTH_LONG).show();
+    }
+
+    static void showErrorMessage(Context c, String message) {
+        createErrorDialog(c, message, null).show();
+    }
+
+    static void showErrorMessage(Context c, String message,
+            DialogInterface.OnClickListener listener) {
+        createErrorDialog(c, message, listener).show();
+    }
+
+    static boolean isNullOrEmpty(String message) {
+        return ((message == null) || (message.length() == 0));
+    }
+
+    static String base64Encode(byte[] bytes) {
+        return new String(Base64.encodeBase64(bytes));
+    }
+
+    static void deleteFile(String path) {
+        deleteFile(new File(path));
+    }
+
+    static void deleteFile(String path, boolean toDeleteSelf) {
+        deleteFile(new File(path), toDeleteSelf);
+    }
+
+    static void deleteFile(File f) {
+        deleteFile(f, true);
+    }
+
+    static void deleteFile(File f, boolean toDeleteSelf) {
+        if (f.isDirectory()) {
+            for (File child : f.listFiles()) deleteFile(child, true);
+        }
+        if (toDeleteSelf) f.delete();
+    }
+
+    static boolean isFileOrEmptyDirectory(String path) {
+        File f = new File(path);
+        if (!f.isDirectory()) return true;
+
+        String[] list = f.list();
+        return ((list == null) || (list.length == 0));
+    }
+
+    static boolean copyFiles(String sourcePath , String targetPath)
+            throws IOException {
+        return copyFiles(new File(sourcePath), new File(targetPath));
+    }
+
+    // returns false if sourceLocation is the same as the targetLocation
+    static boolean copyFiles(File sourceLocation , File targetLocation)
+            throws IOException {
+        if (sourceLocation.equals(targetLocation)) return false;
+
+        if (sourceLocation.isDirectory()) {
+            if (!targetLocation.exists()) {
+                targetLocation.mkdir();
+            }
+            String[] children = sourceLocation.list();
+            for (int i=0; i<children.length; i++) {
+                copyFiles(new File(sourceLocation, children[i]),
+                        new File(targetLocation, children[i]));
+            }
+        } else if (sourceLocation.exists()) {
+            InputStream in = new FileInputStream(sourceLocation);
+            OutputStream out = new FileOutputStream(targetLocation);
+
+            // Copy the bits from instream to outstream
+            byte[] buf = new byte[1024];
+            int len;
+            while ((len = in.read(buf)) > 0) {
+                out.write(buf, 0, len);
+            }
+            in.close();
+            out.close();
+        }
+        return true;
+    }
+
+    private static AlertDialog createErrorDialog(Context c, String message,
+            DialogInterface.OnClickListener okListener) {
+        AlertDialog.Builder b = new AlertDialog.Builder(c)
+                .setTitle(R.string.vpn_error_title)
+                .setMessage(message);
+        if (okListener != null) {
+            b.setPositiveButton(R.string.vpn_back_button, okListener);
+        } else {
+            b.setPositiveButton(android.R.string.ok, null);
+        }
+        return b.create();
+    }
+
+    private Util() {
+    }
+}
diff --git a/src/com/android/settings/vpn/VpnEditor.java b/src/com/android/settings/vpn/VpnEditor.java
new file mode 100644
index 0000000..a37b335
--- /dev/null
+++ b/src/com/android/settings/vpn/VpnEditor.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import com.android.settings.R;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.vpn.L2tpIpsecProfile;
+import android.net.vpn.SingleServerProfile;
+import android.net.vpn.VpnProfile;
+import android.net.vpn.VpnType;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * The activity class for editing a new or existing VPN profile.
+ */
+public class VpnEditor extends PreferenceActivity {
+    private static final String TAG = VpnEditor.class.getSimpleName();
+
+    private static final int MENU_SAVE = Menu.FIRST;
+    private static final int MENU_CANCEL = Menu.FIRST + 1;
+
+    private EditTextPreference mName;
+
+    private VpnProfileEditor mProfileEditor;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Loads the XML preferences file
+        addPreferencesFromResource(R.xml.vpn_edit);
+
+        mName = (EditTextPreference) findPreference("vpn_name");
+        mName.setOnPreferenceChangeListener(
+                new Preference.OnPreferenceChangeListener() {
+                    public boolean onPreferenceChange(
+                            Preference pref, Object newValue) {
+                        setName((String) newValue);
+                        return true;
+                    }
+                });
+
+        if (savedInstanceState == null) {
+            VpnProfile p = getIntent().getParcelableExtra(
+                    VpnSettings.KEY_VPN_PROFILE);
+            initViewFor(p);
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        menu.add(0, MENU_SAVE, 0, R.string.vpn_menu_save)
+            .setIcon(android.R.drawable.ic_menu_save);
+        menu.add(0, MENU_CANCEL, 0, R.string.vpn_menu_cancel)
+            .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case MENU_SAVE:
+                if (validateAndSetResult()) {
+                    finish();
+                }
+                return true;
+            case MENU_CANCEL:
+                showCancellationConfirmDialog();
+                return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void initViewFor(VpnProfile profile) {
+        VpnProfileEditor editor = getEditor(profile);
+        VpnType type = profile.getType();
+        PreferenceGroup subsettings = getPreferenceScreen();
+
+        setTitle(profile);
+        setName(profile.getName());
+
+        editor.loadPreferencesTo(subsettings);
+        mProfileEditor = editor;
+    }
+
+    private void setTitle(VpnProfile profile) {
+        if (Util.isNullOrEmpty(profile.getName())) {
+            setTitle(String.format(getString(R.string.vpn_edit_title_add),
+                    profile.getType().getDisplayName()));
+        } else {
+            setTitle(String.format(getString(R.string.vpn_edit_title_edit),
+                    profile.getType().getDisplayName()));
+        }
+    }
+
+    private void setName(String newName) {
+        newName = (newName == null) ? "" : newName.trim();
+        mName.setText(newName);
+        mName.setSummary(Util.isNullOrEmpty(newName)
+                ? getString(R.string.vpn_name_summary)
+                : newName);
+    }
+
+    /**
+     * Checks the validity of the inputs and set the profile as result if valid.
+     * @return true if the result is successfully set
+     */
+    private boolean validateAndSetResult() {
+        String errorMsg = null;
+        if (Util.isNullOrEmpty(mName.getText())) {
+            errorMsg = getString(R.string.vpn_error_name_empty);
+        } else {
+            errorMsg = mProfileEditor.validate(this);
+        }
+
+        if (errorMsg != null) {
+            Util.showErrorMessage(this, errorMsg);
+            return false;
+        }
+
+        setResult(mProfileEditor.getProfile());
+        return true;
+    }
+
+    private void setResult(VpnProfile p) {
+        p.setName(mName.getText());
+        p.setId(Util.base64Encode(p.getName().getBytes()));
+        Intent intent = new Intent(this, VpnSettings.class);
+        intent.putExtra(VpnSettings.KEY_VPN_PROFILE, (Parcelable) p);
+        setResult(RESULT_OK, intent);
+    }
+
+    private VpnProfileEditor getEditor(VpnProfile p) {
+        if (p instanceof L2tpIpsecProfile) {
+            return new L2tpIpsecEditor((L2tpIpsecProfile) p);
+        } else if (p instanceof SingleServerProfile) {
+            return new SingleServerEditor((SingleServerProfile) p);
+        } else {
+            throw new RuntimeException("Unknown profile type: " + p.getType());
+        }
+    }
+
+    private void showCancellationConfirmDialog() {
+        new AlertDialog.Builder(this)
+                .setTitle(R.string.vpn_error_title)
+                .setMessage(R.string.vpn_confirm_profile_cancellation)
+                .setPositiveButton(R.string.vpn_yes_button,
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int w) {
+                                finish();
+                            }
+                        })
+                .setNegativeButton(R.string.vpn_mistake_button, null)
+                .show();
+    }
+}
diff --git a/src/com/android/settings/vpn/VpnProfileActor.java b/src/com/android/settings/vpn/VpnProfileActor.java
new file mode 100644
index 0000000..fb0e278
--- /dev/null
+++ b/src/com/android/settings/vpn/VpnProfileActor.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import android.net.vpn.VpnProfile;
+import android.os.Bundle;
+
+/**
+ * The interface to act on a {@link VpnProfile}.
+ */
+public interface VpnProfileActor {
+    VpnProfile getProfile();
+
+    /**
+     * Establishes a VPN connection.
+     */
+    void connect();
+
+    /**
+     * Tears down the connection.
+     */
+    void disconnect();
+
+    /**
+     * Checks the current status. The result is expected to be broadcast.
+     * Use {@link VpnManager#registerConnectivityReceiver()} to register a
+     * broadcast receiver and to receives the broadcast events.
+     */
+    void checkStatus();
+
+    /**
+     * Called to save the states when the device is rotated.
+     */
+    void onSaveState(Bundle outState);
+
+    /**
+     * Called to restore the states on the rotated screen.
+     */
+    void onRestoreState(Bundle savedState);
+}
diff --git a/src/com/android/settings/vpn/VpnProfileEditor.java b/src/com/android/settings/vpn/VpnProfileEditor.java
new file mode 100644
index 0000000..686e513
--- /dev/null
+++ b/src/com/android/settings/vpn/VpnProfileEditor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import android.content.Context;
+import android.net.vpn.VpnProfile;
+import android.preference.PreferenceGroup;
+
+/**
+ * The interface to set up preferences for editing a {@link VpnProfile}.
+ */
+public interface VpnProfileEditor {
+    VpnProfile getProfile();
+
+    /**
+     * Adds the preferences to the panel.
+     */
+    void loadPreferencesTo(PreferenceGroup subpanel);
+
+    /**
+     * Validates the inputs in the preferences.
+     *
+     * @return an error message that is ready to be displayed in a dialog; or
+     *      null if all the inputs are valid
+     */
+    String validate(Context c);
+}
diff --git a/src/com/android/settings/vpn/VpnSettings.java b/src/com/android/settings/vpn/VpnSettings.java
new file mode 100644
index 0000000..97a1440
--- /dev/null
+++ b/src/com/android/settings/vpn/VpnSettings.java
@@ -0,0 +1,585 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import com.android.settings.R;
+
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.vpn.VpnManager;
+import android.net.vpn.VpnProfile;
+import android.net.vpn.VpnState;
+import android.net.vpn.VpnType;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The preference activity for configuring VPN settings.
+ */
+public class VpnSettings extends PreferenceActivity {
+    // Key to the field exchanged for profile editing.
+    static final String KEY_VPN_PROFILE = "vpn_profile";
+
+    // Key to the field exchanged for VPN type selection.
+    static final String KEY_VPN_TYPE = "vpn_type";
+
+    private static final String TAG = VpnSettings.class.getSimpleName();
+
+    private static final String PREF_ADD_VPN = "add_new_vpn";
+    private static final String PREF_VPN_LIST = "vpn_list";
+
+    private static final String PROFILES_ROOT = VpnManager.PROFILES_PATH + "/";
+    private static final String PROFILE_OBJ_FILE = ".pobj";
+
+    private static final String STATE_ACTIVE_ACTOR = "active_actor";
+
+    private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1;
+    private static final int REQUEST_SELECT_VPN_TYPE = 2;
+
+    private static final int CONTEXT_MENU_CONNECT_ID = ContextMenu.FIRST + 0;
+    private static final int CONTEXT_MENU_DISCONNECT_ID = ContextMenu.FIRST + 1;
+    private static final int CONTEXT_MENU_EDIT_ID = ContextMenu.FIRST + 2;
+    private static final int CONTEXT_MENU_DELETE_ID = ContextMenu.FIRST + 3;
+
+    private PreferenceScreen mAddVpn;
+    private PreferenceCategory mVpnListContainer;
+
+    // profile name --> VpnPreference
+    private Map<String, VpnPreference> mVpnPreferenceMap;
+    private List<VpnProfile> mVpnProfileList;
+
+    private int mIndexOfEditedProfile = -1;
+
+    // profile engaged in a connection
+    private VpnProfile mActiveProfile;
+
+    // actor engaged in an action
+    private VpnProfileActor mActiveActor;
+
+    private VpnManager mVpnManager = new VpnManager(this);
+
+    private ConnectivityReceiver mConnectivityReceiver =
+            new ConnectivityReceiver();
+
+    private boolean mConnectingError;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        addPreferencesFromResource(R.xml.vpn_settings);
+
+        // restore VpnProfile list and construct VpnPreference map
+        mVpnListContainer = (PreferenceCategory) findPreference(PREF_VPN_LIST);
+        retrieveVpnListFromStorage();
+
+        // set up the "add vpn" preference
+        mAddVpn = (PreferenceScreen) findPreference(PREF_ADD_VPN);
+        mAddVpn.setOnPreferenceClickListener(
+                new OnPreferenceClickListener() {
+                    public boolean onPreferenceClick(Preference preference) {
+                        startVpnTypeSelection();
+                        return true;
+                    }
+                });
+
+        // for long-press gesture on a profile preference
+        registerForContextMenu(getListView());
+
+        // listen to vpn connectivity event
+        mVpnManager.registerConnectivityReceiver(mConnectivityReceiver);
+    }
+
+    @Override
+    protected synchronized void onSaveInstanceState(Bundle outState) {
+        if (mActiveActor == null) return;
+
+        mActiveActor.onSaveState(outState);
+        outState.putString(STATE_ACTIVE_ACTOR,
+                mActiveActor.getProfile().getName());
+    }
+
+    @Override
+    protected void onRestoreInstanceState(final Bundle savedState) {
+        String profileName = savedState.getString(STATE_ACTIVE_ACTOR);
+        if (Util.isNullOrEmpty(profileName)) return;
+
+        final VpnProfile p = mVpnPreferenceMap.get(profileName).mProfile;
+        mActiveActor = getActor(p);
+        mActiveActor.onRestoreState(savedState);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        unregisterForContextMenu(getListView());
+        mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver);
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v,
+            ContextMenuInfo menuInfo) {
+        super.onCreateContextMenu(menu, v, menuInfo);
+
+        VpnProfile p = getProfile(getProfilePositionFrom(
+                    (AdapterContextMenuInfo) menuInfo));
+        if (p != null) {
+            VpnState state = p.getState();
+            menu.setHeaderTitle(p.getName());
+
+            boolean isIdle = (state == VpnState.IDLE);
+            boolean isNotConnect =
+                    (isIdle || (state == VpnState.DISCONNECTING));
+            menu.add(0, CONTEXT_MENU_CONNECT_ID, 0, R.string.vpn_menu_connect)
+                    .setEnabled(isIdle && (mActiveProfile == null));
+            menu.add(0, CONTEXT_MENU_DISCONNECT_ID, 0, R.string.vpn_menu_disconnect)
+                    .setEnabled(!isIdle);
+            menu.add(0, CONTEXT_MENU_EDIT_ID, 0, R.string.vpn_menu_edit)
+                    .setEnabled(isNotConnect);
+            menu.add(0, CONTEXT_MENU_DELETE_ID, 0, R.string.vpn_menu_delete)
+                    .setEnabled(isNotConnect);
+        }
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        int position = getProfilePositionFrom(
+                (AdapterContextMenuInfo) item.getMenuInfo());
+        VpnProfile p = getProfile(position);
+
+        switch(item.getItemId()) {
+        case CONTEXT_MENU_CONNECT_ID:
+        case CONTEXT_MENU_DISCONNECT_ID:
+            connectOrDisconnect(p);
+            return true;
+
+        case CONTEXT_MENU_EDIT_ID:
+            mIndexOfEditedProfile = position;
+            startVpnEditor(p);
+            return true;
+
+        case CONTEXT_MENU_DELETE_ID:
+            deleteProfile(position);
+            return true;
+        }
+
+        return super.onContextItemSelected(item);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode,
+            Intent data) {
+        int index = mIndexOfEditedProfile;
+        mIndexOfEditedProfile = -1;
+
+        if ((resultCode == RESULT_CANCELED) || (data == null)) {
+            Log.v(TAG, "no result returned by editor");
+            return;
+        }
+
+        if (requestCode == REQUEST_SELECT_VPN_TYPE) {
+            String typeName = data.getStringExtra(KEY_VPN_TYPE);
+            startVpnEditor(createVpnProfile(typeName));
+        } else if (requestCode == REQUEST_ADD_OR_EDIT_PROFILE) {
+            VpnProfile p = data.getParcelableExtra(KEY_VPN_PROFILE);
+            if (p == null) {
+                Log.e(TAG, "null object returned by editor");
+                return;
+            }
+
+            if (checkDuplicateName(p, index)) {
+                final VpnProfile profile = p;
+                Util.showErrorMessage(this, String.format(
+                        getString(R.string.vpn_error_duplicate_name), p.getName()),
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int w) {
+                                startVpnEditor(profile);
+                            }
+                        });
+                return;
+            }
+
+            try {
+                if ((index < 0) || (index >= mVpnProfileList.size())) {
+                    addProfile(p);
+                    Util.showShortToastMessage(this, String.format(
+                            getString(R.string.vpn_profile_added), p.getName()));
+                } else {
+                    replaceProfile(index, p);
+                    Util.showShortToastMessage(this, String.format(
+                            getString(R.string.vpn_profile_replaced), p.getName()));
+                }
+            } catch (IOException e) {
+                final VpnProfile profile = p;
+                Util.showErrorMessage(this, e + ": " + e.getMessage(),
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int w) {
+                                startVpnEditor(profile);
+                            }
+                        });
+            }
+        } else {
+            throw new RuntimeException("unknown request code: " + requestCode);
+        }
+    }
+
+    // Replaces the profile at index in mVpnProfileList with p.
+    // Returns true if p's name is a duplicate.
+    private boolean checkDuplicateName(VpnProfile p, int index) {
+        List<VpnProfile> list = mVpnProfileList;
+        VpnPreference pref = mVpnPreferenceMap.get(p.getName());
+        if ((pref != null) && (index >= 0) && (index < list.size())) {
+            // not a duplicate if p is to replace the profile at index
+            if (pref.mProfile == list.get(index)) pref = null;
+        }
+        return (pref != null);
+    }
+
+    private int getProfilePositionFrom(AdapterContextMenuInfo menuInfo) {
+        // excludes mVpnListContainer and the preferences above it
+        return menuInfo.position - mVpnListContainer.getOrder() - 1;
+    }
+
+    // position: position in mVpnProfileList
+    private VpnProfile getProfile(int position) {
+        return ((position >= 0) ? mVpnProfileList.get(position) : null);
+    }
+
+    // position: position in mVpnProfileList
+    private void deleteProfile(int position) {
+        if ((position < 0) || (position >= mVpnProfileList.size())) return;
+        VpnProfile p = mVpnProfileList.remove(position);
+        VpnPreference pref = mVpnPreferenceMap.remove(p.getName());
+        mVpnListContainer.removePreference(pref);
+        removeProfileFromStorage(p);
+    }
+
+    private void addProfile(VpnProfile p) throws IOException {
+        saveProfileToStorage(p);
+        mVpnProfileList.add(p);
+        addPreferenceFor(p);
+        disableProfilePreferencesIfOneActive();
+    }
+
+    // Adds a preference in mVpnListContainer
+    private void addPreferenceFor(VpnProfile p) {
+        VpnPreference pref = new VpnPreference(this, p);
+        mVpnPreferenceMap.put(p.getName(), pref);
+        mVpnListContainer.addPreference(pref);
+
+        pref.setOnPreferenceClickListener(
+                new Preference.OnPreferenceClickListener() {
+                    public boolean onPreferenceClick(Preference pref) {
+                        connectOrDisconnect(((VpnPreference) pref).mProfile);
+                        return true;
+                    }
+                });
+    }
+
+    // index: index to mVpnProfileList
+    private void replaceProfile(int index, VpnProfile p) throws IOException {
+        Map<String, VpnPreference> map = mVpnPreferenceMap;
+        VpnProfile oldProfile = mVpnProfileList.set(index, p);
+        VpnPreference pref = map.remove(oldProfile.getName());
+        if (pref.mProfile != oldProfile) {
+            throw new RuntimeException("inconsistent state!");
+        }
+
+        // Copy config files and remove the old ones if they are in different
+        // directories.
+        if (Util.copyFiles(getProfileDir(oldProfile), getProfileDir(p))) {
+            removeProfileFromStorage(oldProfile);
+        }
+        saveProfileToStorage(p);
+
+        pref.setProfile(p);
+        map.put(p.getName(), pref);
+    }
+
+    private void startVpnTypeSelection() {
+        Intent intent = new Intent(this, VpnTypeSelection.class);
+        startActivityForResult(intent, REQUEST_SELECT_VPN_TYPE);
+    }
+
+    private void startVpnEditor(VpnProfile profile) {
+        Intent intent = new Intent(this, VpnEditor.class);
+        intent.putExtra(KEY_VPN_PROFILE, (Parcelable) profile);
+        startActivityForResult(intent, REQUEST_ADD_OR_EDIT_PROFILE);
+    }
+
+    // Do connect or disconnect based on the current state.
+    private synchronized void connectOrDisconnect(VpnProfile p) {
+        VpnPreference pref = mVpnPreferenceMap.get(p.getName());
+        switch (p.getState()) {
+            case IDLE:
+                changeState(p, VpnState.CONNECTING);
+                mActiveActor = getActor(p);
+                mActiveActor.connect();
+                break;
+
+            case CONNECTING:
+                // TODO: bring up a dialog to confirm disconnect
+                break;
+
+            case CONNECTED:
+                mConnectingError = false;
+                // pass through
+            case DISCONNECTING:
+                changeState(p, VpnState.DISCONNECTING);
+                getActor(p).disconnect();
+                break;
+        }
+    }
+
+    private void changeState(VpnProfile p, VpnState state) {
+        VpnState oldState = p.getState();
+        if (oldState == state) return;
+
+        Log.d(TAG, "changeState: " + p.getName() + ": " + state);
+        p.setState(state);
+        mVpnPreferenceMap.get(p.getName()).setSummary(
+                getProfileSummaryString(p));
+
+        switch (state) {
+        case CONNECTED:
+            mActiveActor = null;
+            // pass through
+        case CONNECTING:
+            mActiveProfile = p;
+            disableProfilePreferencesIfOneActive();
+            break;
+
+        case DISCONNECTING:
+            if (oldState == VpnState.CONNECTING) {
+                mConnectingError = true;
+            }
+            break;
+
+        case CANCELLED:
+            changeState(p, VpnState.IDLE);
+            break;
+
+        case IDLE:
+            assert(mActiveProfile != p);
+            mActiveProfile = null;
+            mActiveActor = null;
+            enableProfilePreferences();
+
+            if (oldState == VpnState.CONNECTING) mConnectingError = true;
+            if (mConnectingError) showReconnectDialog(p);
+            break;
+        }
+    }
+
+    private void showReconnectDialog(final VpnProfile p) {
+        new AlertDialog.Builder(this)
+                .setTitle(R.string.vpn_error_title)
+                .setMessage(R.string.vpn_confirm_reconnect)
+                .setPositiveButton(R.string.vpn_yes_button,
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int w) {
+                                dialog.dismiss();
+                                connectOrDisconnect(p);
+                            }
+                        })
+                .setNegativeButton(R.string.vpn_no_button, null)
+                .show();
+    }
+
+    private void disableProfilePreferencesIfOneActive() {
+        if (mActiveProfile == null) return;
+
+        for (VpnProfile p : mVpnProfileList) {
+            switch (p.getState()) {
+            case DISCONNECTING:
+            case IDLE:
+                mVpnPreferenceMap.get(p.getName()).setEnabled(false);
+                break;
+            }
+        }
+    }
+
+    private void enableProfilePreferences() {
+        for (VpnProfile p : mVpnProfileList) {
+            mVpnPreferenceMap.get(p.getName()).setEnabled(true);
+        }
+    }
+
+    private String getProfileDir(VpnProfile p) {
+        return PROFILES_ROOT + p.getId();
+    }
+
+    private void saveProfileToStorage(VpnProfile p) throws IOException {
+        File f = new File(getProfileDir(p));
+        if (!f.exists()) f.mkdirs();
+        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
+                new File(f, PROFILE_OBJ_FILE)));
+        oos.writeObject(p);
+        oos.close();
+    }
+
+    private void removeProfileFromStorage(VpnProfile p) {
+        Util.deleteFile(getProfileDir(p));
+    }
+
+    private void retrieveVpnListFromStorage() {
+        mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>();
+        mVpnProfileList = new ArrayList<VpnProfile>();
+
+        File root = new File(PROFILES_ROOT);
+        String[] dirs = root.list();
+        if (dirs == null) return;
+        Arrays.sort(dirs);
+        for (String dir : dirs) {
+            File f = new File(new File(root, dir), PROFILE_OBJ_FILE);
+            if (!f.exists()) continue;
+            try {
+                VpnProfile p = deserialize(f);
+                if (!checkIdConsistency(dir, p)) continue;
+
+                mVpnProfileList.add(p);
+                addPreferenceFor(p);
+            } catch (IOException e) {
+                Log.e(TAG, "retrieveVpnListFromStorage()", e);
+            }
+        }
+        disableProfilePreferencesIfOneActive();
+        checkVpnConnectionStatusInBackground();
+    }
+
+    private void checkVpnConnectionStatusInBackground() {
+        new Thread(new Runnable() {
+            public void run() {
+                for (VpnProfile p : mVpnProfileList) {
+                    getActor(p).checkStatus();
+                }
+            }
+        }).start();
+    }
+
+    // A sanity check. Returns true if the profile directory name and profile ID
+    // are consistent.
+    private boolean checkIdConsistency(String dirName, VpnProfile p) {
+        if (!dirName.equals(p.getId())) {
+            Log.v(TAG, "ID inconsistent: " + dirName + " vs " + p.getId());
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private VpnProfile deserialize(File profileObjectFile) throws IOException {
+        try {
+            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
+                    profileObjectFile));
+            VpnProfile p = (VpnProfile) ois.readObject();
+            ois.close();
+            return p;
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private String getProfileSummaryString(VpnProfile p) {
+        switch (p.getState()) {
+        case CONNECTING:
+            return getString(R.string.vpn_connecting);
+        case DISCONNECTING:
+            return getString(R.string.vpn_disconnecting);
+        case CONNECTED:
+            return getString(R.string.vpn_connected);
+        default:
+            return getString(R.string.vpn_connect_hint);
+        }
+    }
+
+    private VpnProfileActor getActor(VpnProfile p) {
+        return new AuthenticationActor(this, p);
+    }
+
+    private VpnProfile createVpnProfile(String type) {
+        return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type));
+    }
+
+    private class VpnPreference extends Preference {
+        VpnProfile mProfile;
+        VpnPreference(Context c, VpnProfile p) {
+            super(c);
+            setProfile(p);
+        }
+
+        void setProfile(VpnProfile p) {
+            mProfile = p;
+            setTitle(p.getName());
+            setSummary(getProfileSummaryString(p));
+        }
+    }
+
+    // to receive vpn connectivity events broadcast by VpnService
+    private class ConnectivityReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String profileName = intent.getStringExtra(
+                    VpnManager.BROADCAST_PROFILE_NAME);
+            if (profileName == null) return;
+
+            VpnState s = (VpnState) intent.getSerializableExtra(
+                    VpnManager.BROADCAST_CONNECTION_STATE);
+            if (s == null) {
+                Log.e(TAG, "received null connectivity state");
+                return;
+            }
+            VpnPreference pref = mVpnPreferenceMap.get(profileName);
+            if (pref != null) {
+                Log.d(TAG, "received connectivity: " + profileName
+                        + ": connected? " + s);
+                changeState(pref.mProfile, s);
+            } else {
+                Log.e(TAG, "received connectivity: " + profileName
+                        + ": connected? " + s + ", but profile does not exist;"
+                        + " just ignore it");
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/vpn/VpnTypeSelection.java b/src/com/android/settings/vpn/VpnTypeSelection.java
new file mode 100644
index 0000000..0448106
--- /dev/null
+++ b/src/com/android/settings/vpn/VpnTypeSelection.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2007 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.vpn;
+
+import com.android.settings.R;
+
+import android.content.Intent;
+import android.net.vpn.VpnManager;
+import android.net.vpn.VpnType;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The activity to select a VPN type.
+ */
+public class VpnTypeSelection extends PreferenceActivity {
+    private Map<String, VpnType> mTypeMap = new HashMap<String, VpnType>();
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        addPreferencesFromResource(R.xml.vpn_type);
+        initTypeList();
+    }
+
+    @Override
+    public boolean onPreferenceTreeClick(PreferenceScreen ps, Preference pref) {
+        setResult(mTypeMap.get(pref.getTitle().toString()));
+        finish();
+        return true;
+    }
+
+    private void initTypeList() {
+        PreferenceScreen root = getPreferenceScreen();
+        for (VpnType t : VpnManager.getSupportedVpnTypes()) {
+            String displayName = t.getDisplayName();
+            mTypeMap.put(displayName, t);
+
+            Preference pref = new Preference(this);
+            pref.setTitle(displayName);
+            root.addPreference(pref);
+        }
+    }
+
+    private void setResult(VpnType type) {
+        Intent intent = new Intent(this, VpnSettings.class);
+        intent.putExtra(VpnSettings.KEY_VPN_TYPE, type.toString());
+        setResult(RESULT_OK, intent);
+    }
+}