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.
This commit is contained in:
Hung-ying Tyan
2009-06-10 22:52:44 +08:00
parent 1d09759798
commit 1617706d25
18 changed files with 1867 additions and 0 deletions

View File

@@ -24,6 +24,7 @@ import android.content.Intent;
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 @@ public class SecuritySettings extends PreferenceActivity {
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;
}

View File

@@ -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) {}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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() {
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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");
}
}
}
}

View File

@@ -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);
}
}