diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f9758f33a21..2714457e8d4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -110,6 +110,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/vpn_connect_dialog_view.xml b/res/layout/vpn_connect_dialog_view.xml
new file mode 100644
index 00000000000..540b404b21f
--- /dev/null
+++ b/res/layout/vpn_connect_dialog_view.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100755
index 00000000000..56bd60cef64
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,9 @@
+
+
+ 5sp
+ 5sp
+ 16sp
+ 90sp
+ 200sp
+ 5sp
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c2f53b8ac72..3b91ef66fbd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1726,4 +1726,73 @@ found in the list of installed applications.
Power Control
+
+ VPN settings
+
+ User name:
+ Password:
+ User name
+ Password
+ You missed a field!
+ Please fill up \"%s\".
+
+ Connect
+ Cancel
+ Yes
+ No
+ Back
+ No, it's a mistake
+
+ Save
+
+ Discard
+ Connect
+ Disconnect
+ Edit
+ Delete
+
+
+ Attention
+ VPN Name cannot be empty.
+ The Server Name field cannot be empty.
+ The VPN Name \'%s\' already exists. Find another name.
+ Need to select a user certificate.
+ Need to select a CA certificate.
+ Need to select a userkey.
+ Are you sure you don\'t want to create this profile?
+ The previous connection attempt failed. Do you want to try again?
+
+ Add new VPN
+ Add new %s VPN
+ Edit %s VPN
+ Select VPN type
+ VPN networks
+
+ Click to set the value
+
+ Connecting...
+
+ Disconnecting...
+
+ Connected
+
+ Select to connect
+
+ Connect to
+
+ VPN Name
+ Give a name to this VPN;
+
+ '%s' is added
+ Changes are made to '%s'
+
+ User Certificate
+ CA Certificate
+ User Key
+ Server Name
+ DNS Search List
+
+ VPN
+ VPN
+ Set up and manage VPN configurations, connections
diff --git a/res/xml/vpn_edit.xml b/res/xml/vpn_edit.xml
new file mode 100644
index 00000000000..7976242160d
--- /dev/null
+++ b/res/xml/vpn_edit.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/res/xml/vpn_settings.xml b/res/xml/vpn_settings.xml
new file mode 100644
index 00000000000..a1a4bf9a61f
--- /dev/null
+++ b/res/xml/vpn_settings.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/xml/vpn_type.xml b/res/xml/vpn_type.xml
new file mode 100644
index 00000000000..c59b54a5d64
--- /dev/null
+++ b/res/xml/vpn_type.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/res/xml/wireless_settings.xml b/res/xml/wireless_settings.xml
index f81fb41637e..4731fb3f8fd 100644
--- a/res/xml/wireless_settings.xml
+++ b/res/xml/wireless_settings.xml
@@ -52,6 +52,16 @@
android:targetClass="com.android.settings.bluetooth.BluetoothSettings" />
+
+
+
+
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 00000000000..d7ba1f752d6
--- /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 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 00000000000..a37b3355d12
--- /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 00000000000..fb0e2781001
--- /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 00000000000..686e513fe23
--- /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 00000000000..97a14404a6a
--- /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 mVpnPreferenceMap;
+ private List 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 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 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();
+ mVpnProfileList = new ArrayList();
+
+ 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 00000000000..044810664cf
--- /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 mTypeMap = new HashMap();
+
+ @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);
+ }
+}