blob: 137e42049ca4015393eefbeaf94e949ebc30178b [file] [log] [blame]
/*
* Copyright (C) 2009 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 com.android.settings.SecuritySettings;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.BroadcastReceiver;
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.L2tpIpsecPskProfile;
import android.net.vpn.L2tpProfile;
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.ConditionVariable;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
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.security.Keystore;
import android.text.TextUtils;
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.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The preference activity for configuring VPN settings.
*/
public class VpnSettings extends PreferenceActivity implements
DialogInterface.OnClickListener {
// 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 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 static final int CONNECT_BUTTON = DialogInterface.BUTTON1;
private static final int OK_BUTTON = DialogInterface.BUTTON1;
private static final int DIALOG_CONNECT = 1;
private static final int DIALOG_RECONNECT = 2;
private static final int DIALOG_AUTH_ERROR = 3;
private static final int DIALOG_UNKNOWN_SERVER = 4;
private static final int NO_ERROR = 0;
private static final String NAMESPACE_VPN = "vpn";
private static final String KEY_PREFIX_IPSEC_PSK = "ipsk000";
private static final String KEY_PREFIX_L2TP_SECRET = "lscrt000";
private PreferenceScreen mAddVpn;
private PreferenceCategory mVpnListContainer;
// profile name --> VpnPreference
private Map<String, VpnPreference> mVpnPreferenceMap;
private List<VpnProfile> mVpnProfileList;
// profile engaged in a connection
private VpnProfile mActiveProfile;
// actor engaged in connecting
private VpnProfileActor mConnectingActor;
// states saved for unlocking keystore
private Runnable mUnlockAction;
private VpnManager mVpnManager = new VpnManager(this);
private ConnectivityReceiver mConnectivityReceiver =
new ConnectivityReceiver();
private int mConnectingErrorCode = NO_ERROR;
private Dialog mShowingDialog;
private StatusChecker mStatusChecker = new StatusChecker();
@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);
// 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);
retrieveVpnListFromStorage();
checkVpnConnectionStatusInBackground();
}
@Override
public void onResume() {
super.onResume();
if ((mUnlockAction != null) && isKeystoreUnlocked()) {
Runnable action = mUnlockAction;
mUnlockAction = null;
runOnUiThread(action);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterForContextMenu(getListView());
mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver);
if ((mShowingDialog != null) && mShowingDialog.isShowing()) {
mShowingDialog.dismiss();
}
}
@Override
protected Dialog onCreateDialog (int id) {
switch (id) {
case DIALOG_CONNECT:
return createConnectDialog();
case DIALOG_RECONNECT:
return createReconnectDialog();
case DIALOG_AUTH_ERROR:
return createAuthErrorDialog();
case DIALOG_UNKNOWN_SERVER:
return createUnknownServerDialog();
default:
return super.onCreateDialog(id);
}
}
private Dialog createConnectDialog() {
return new AlertDialog.Builder(this)
.setView(mConnectingActor.createConnectView())
.setTitle(String.format(getString(R.string.vpn_connect_to),
mConnectingActor.getProfile().getName()))
.setPositiveButton(getString(R.string.vpn_connect_button),
this)
.setNegativeButton(getString(android.R.string.cancel),
this)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
removeDialog(DIALOG_CONNECT);
onIdle();
}
})
.create();
}
private Dialog createReconnectDialog() {
return createCommonDialogBuilder()
.setMessage(R.string.vpn_confirm_reconnect)
.create();
}
private Dialog createAuthErrorDialog() {
return createCommonDialogBuilder()
.setMessage(R.string.vpn_auth_error_dialog_msg)
.create();
}
private Dialog createUnknownServerDialog() {
return createCommonDialogBuilder()
.setMessage(R.string.vpn_unknown_server_dialog_msg)
.setPositiveButton(R.string.vpn_yes_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
VpnProfile p = mConnectingActor.getProfile();
onIdle();
startVpnEditor(p);
}
})
.create();
}
private AlertDialog.Builder createCommonDialogBuilder() {
return new AlertDialog.Builder(this)
.setTitle(android.R.string.dialog_alert_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(R.string.vpn_yes_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
connectOrDisconnect(mConnectingActor.getProfile());
}
})
.setNegativeButton(R.string.vpn_no_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
onIdle();
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
onIdle();
}
});
}
@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)
|| (state == VpnState.CANCELLED));
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(state == VpnState.CONNECTED);
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:
startVpnEditor(p);
return true;
case CONTEXT_MENU_DELETE_ID:
deleteProfile(position);
return true;
}
return super.onContextItemSelected(item);
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode,
final Intent data) {
if ((resultCode == RESULT_CANCELED) || (data == null)) {
Log.d(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;
}
int index = getProfileIndexFromId(p.getId());
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;
}
if (needKeystoreToSave(p)) {
Runnable action = new Runnable() {
public void run() {
onActivityResult(requestCode, resultCode, data);
}
};
if (!unlockKeystore(p, action)) return;
}
try {
if (index < 0) {
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);
}
}
// Called when the buttons on the connect dialog are clicked.
//@Override
public synchronized void onClick(DialogInterface dialog, int which) {
if (which == CONNECT_BUTTON) {
Dialog d = (Dialog) dialog;
String error = mConnectingActor.validateInputs(d);
if (error == null) {
changeState(mConnectingActor.getProfile(), VpnState.CONNECTING);
mConnectingActor.connect(d);
removeDialog(DIALOG_CONNECT);
return;
} else {
dismissDialog(DIALOG_CONNECT);
// show error dialog
mShowingDialog = new AlertDialog.Builder(this)
.setTitle(android.R.string.dialog_alert_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(String.format(getString(
R.string.vpn_error_miss_entering), error))
.setPositiveButton(R.string.vpn_back_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
showDialog(DIALOG_CONNECT);
}
})
.create();
mShowingDialog.show();
}
} else {
removeDialog(DIALOG_CONNECT);
onIdle();
}
}
private int getProfileIndexFromId(String id) {
int index = 0;
for (VpnProfile p : mVpnProfileList) {
if (p.getId().equals(id)) {
return index;
} else {
index++;
}
}
return -1;
}
// 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(final int position) {
if ((position < 0) || (position >= mVpnProfileList.size())) return;
DialogInterface.OnClickListener onClickListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
if (which == OK_BUTTON) {
VpnProfile p = mVpnProfileList.remove(position);
VpnPreference pref =
mVpnPreferenceMap.remove(p.getName());
mVpnListContainer.removePreference(pref);
removeProfileFromStorage(p);
}
}
};
mShowingDialog = new AlertDialog.Builder(this)
.setTitle(android.R.string.dialog_alert_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.vpn_confirm_profile_deletion)
.setPositiveButton(android.R.string.ok, onClickListener)
.setNegativeButton(R.string.vpn_no_button, onClickListener)
.create();
mShowingDialog.show();
}
// Randomly generates an ID for the profile.
// The ID is unique and only set once when the profile is created.
private void setProfileId(VpnProfile profile) {
String id;
while (true) {
id = String.valueOf(Math.abs(
Double.doubleToLongBits(Math.random())));
if (id.length() >= 8) break;
}
for (VpnProfile p : mVpnProfileList) {
if (p.getId().equals(id)) {
setProfileId(profile);
return;
}
}
profile.setId(id);
}
private void addProfile(VpnProfile p) throws IOException {
setProfileId(p);
processSecrets(p);
saveProfileToStorage(p);
mVpnProfileList.add(p);
addPreferenceFor(p);
disableProfilePreferencesIfOneActive();
}
private VpnPreference addPreferenceFor(VpnProfile p) {
return addPreferenceFor(p, true);
}
// Adds a preference in mVpnListContainer
private VpnPreference addPreferenceFor(
VpnProfile p, boolean addToContainer) {
VpnPreference pref = new VpnPreference(this, p);
mVpnPreferenceMap.put(p.getName(), pref);
if (addToContainer) mVpnListContainer.addPreference(pref);
pref.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference pref) {
connectOrDisconnect(((VpnPreference) pref).mProfile);
return true;
}
});
return pref;
}
// 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!");
}
p.setId(oldProfile.getId());
processSecrets(p);
// TODO: remove copyFiles once the setId() code propagates.
// 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 boolean isKeystoreUnlocked() {
return (Keystore.getInstance().getState() == Keystore.UNLOCKED);
}
// Returns true if the profile needs to access keystore
private boolean needKeystoreToSave(VpnProfile p) {
return needKeystoreToConnect(p);
}
// Returns true if the profile needs to access keystore
private boolean needKeystoreToEdit(VpnProfile p) {
switch (p.getType()) {
case L2TP_IPSEC:
case L2TP_IPSEC_PSK:
return true;
default:
return false;
}
}
// Returns true if the profile needs to access keystore
private boolean needKeystoreToConnect(VpnProfile p) {
switch (p.getType()) {
case L2TP_IPSEC:
case L2TP_IPSEC_PSK:
return true;
case L2TP:
return ((L2tpProfile) p).isSecretEnabled();
default:
return false;
}
}
// Returns true if keystore is unlocked or keystore is not a concern
private boolean unlockKeystore(VpnProfile p, Runnable action) {
if (isKeystoreUnlocked()) return true;
mUnlockAction = action;
startActivity(
new Intent(SecuritySettings.ACTION_UNLOCK_CREDENTIAL_STORAGE));
return false;
}
private void startVpnEditor(final VpnProfile profile) {
if (needKeystoreToEdit(profile)) {
Runnable action = new Runnable() {
public void run() {
startVpnEditor(profile);
}
};
if (!unlockKeystore(profile, action)) return;
}
Intent intent = new Intent(this, VpnEditor.class);
intent.putExtra(KEY_VPN_PROFILE, (Parcelable) profile);
startActivityForResult(intent, REQUEST_ADD_OR_EDIT_PROFILE);
}
private synchronized void connect(final VpnProfile p) {
if (needKeystoreToConnect(p)) {
Runnable action = new Runnable() {
public void run() {
connect(p);
}
};
if (!unlockKeystore(p, action)) return;
}
mConnectingActor = getActor(p);
mActiveProfile = p;
if (mConnectingActor.isConnectDialogNeeded()) {
showDialog(DIALOG_CONNECT);
} else {
changeState(p, VpnState.CONNECTING);
mConnectingActor.connect(null);
}
}
// 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:
connect(p);
break;
case CONNECTING:
// do nothing
break;
case CONNECTED:
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;
p.setState(state);
mVpnPreferenceMap.get(p.getName()).setSummary(
getProfileSummaryString(p));
switch (state) {
case CONNECTED:
mConnectingActor = null;
mActiveProfile = p;
disableProfilePreferencesIfOneActive();
break;
case CONNECTING:
case DISCONNECTING:
disableProfilePreferencesIfOneActive();
break;
case CANCELLED:
changeState(p, VpnState.IDLE);
break;
case IDLE:
assert(mActiveProfile == p);
switch (mConnectingErrorCode) {
case NO_ERROR:
onIdle();
break;
case VpnManager.VPN_ERROR_AUTH:
showDialog(DIALOG_AUTH_ERROR);
break;
case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
showDialog(DIALOG_UNKNOWN_SERVER);
break;
default:
showDialog(DIALOG_RECONNECT);
break;
}
mConnectingErrorCode = NO_ERROR;
break;
}
}
private void onIdle() {
Log.d(TAG, " onIdle()");
mActiveProfile = null;
mConnectingActor = null;
enableProfilePreferences();
}
private void disableProfilePreferencesIfOneActive() {
if (mActiveProfile == null) return;
for (VpnProfile p : mVpnProfileList) {
switch (p.getState()) {
case CONNECTING:
case DISCONNECTING:
case IDLE:
mVpnPreferenceMap.get(p.getName()).setEnabled(false);
break;
default:
mVpnPreferenceMap.get(p.getName()).setEnabled(true);
}
}
}
private void enableProfilePreferences() {
for (VpnProfile p : mVpnProfileList) {
mVpnPreferenceMap.get(p.getName()).setEnabled(true);
}
}
static String getProfileDir(VpnProfile p) {
return PROFILES_ROOT + p.getId();
}
static 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 = Collections.synchronizedList(
new ArrayList<VpnProfile>());
mVpnListContainer.removeAll();
File root = new File(PROFILES_ROOT);
String[] dirs = root.list();
if (dirs == null) return;
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 (p == null) continue;
if (!checkIdConsistency(dir, p)) continue;
mVpnProfileList.add(p);
} catch (IOException e) {
Log.e(TAG, "retrieveVpnListFromStorage()", e);
}
}
Collections.sort(mVpnProfileList, new Comparator<VpnProfile>() {
public int compare(VpnProfile p1, VpnProfile p2) {
return p1.getName().compareTo(p2.getName());
}
public boolean equals(VpnProfile p) {
// not used
return false;
}
});
for (VpnProfile p : mVpnProfileList) {
Preference pref = addPreferenceFor(p, false);
}
disableProfilePreferencesIfOneActive();
}
private void checkVpnConnectionStatusInBackground() {
new Thread(new Runnable() {
public void run() {
mStatusChecker.check(mVpnProfileList);
}
}).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.d(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) {
Log.d(TAG, "deserialize a profile", e);
return null;
}
}
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 String keyNameForDaemon(String keyName) {
return NAMESPACE_VPN + "_" + keyName;
}
private void processSecrets(VpnProfile p) {
Keystore ks = Keystore.getInstance();
switch (p.getType()) {
case L2TP_IPSEC_PSK:
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
String presharedKey = pskProfile.getPresharedKey();
String keyName = KEY_PREFIX_IPSEC_PSK + p.getId();
if (!TextUtils.isEmpty(presharedKey)) {
int ret = ks.put(NAMESPACE_VPN, keyName, presharedKey);
if (ret != 0) {
Log.e(TAG, "keystore write failed: key=" + keyName);
}
}
pskProfile.setPresharedKey(keyNameForDaemon(keyName));
// pass through
case L2TP:
L2tpProfile l2tpProfile = (L2tpProfile) p;
keyName = KEY_PREFIX_L2TP_SECRET + p.getId();
if (l2tpProfile.isSecretEnabled()) {
String secret = l2tpProfile.getSecretString();
if (!TextUtils.isEmpty(secret)) {
int ret = ks.put(NAMESPACE_VPN, keyName, secret);
if (ret != 0) {
Log.e(TAG, "keystore write failed: key=" + keyName);
}
}
l2tpProfile.setSecretString(keyNameForDaemon(keyName));
} else {
ks.remove(NAMESPACE_VPN, keyName);
}
break;
}
}
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;
}
mConnectingErrorCode = intent.getIntExtra(
VpnManager.BROADCAST_ERROR_CODE, NO_ERROR);
VpnPreference pref = mVpnPreferenceMap.get(profileName);
if (pref != null) {
Log.d(TAG, "received connectivity: " + profileName
+ ": connected? " + s
+ " err=" + mConnectingErrorCode);
changeState(pref.mProfile, s);
} else {
Log.e(TAG, "received connectivity: " + profileName
+ ": connected? " + s + ", but profile does not exist;"
+ " just ignore it");
}
}
}
// managing status check in a background thread
private class StatusChecker {
private List<VpnProfile> mList;
synchronized void check(final List<VpnProfile> list) {
final ConditionVariable cv = new ConditionVariable();
cv.close();
mVpnManager.startVpnService();
ServiceConnection c = new ServiceConnection() {
public synchronized void onServiceConnected(
ComponentName className, IBinder binder) {
cv.open();
IVpnService service = IVpnService.Stub.asInterface(binder);
for (VpnProfile p : list) {
try {
service.checkStatus(p);
} catch (Throwable e) {
Log.e(TAG, " --- checkStatus(): " + p.getName(), e);
changeState(p, VpnState.IDLE);
}
}
VpnSettings.this.unbindService(this);
showPreferences();
}
public void onServiceDisconnected(ComponentName className) {
cv.open();
setDefaultState(list);
VpnSettings.this.unbindService(this);
showPreferences();
}
};
if (mVpnManager.bindVpnService(c)) {
if (!cv.block(1000)) {
Log.d(TAG, "checkStatus() bindService failed");
setDefaultState(list);
}
} else {
setDefaultState(list);
}
}
private void showPreferences() {
for (VpnProfile p : mVpnProfileList) {
VpnPreference pref = mVpnPreferenceMap.get(p.getName());
mVpnListContainer.addPreference(pref);
}
}
private void setDefaultState(List<VpnProfile> list) {
for (VpnProfile p : list) changeState(p, VpnState.IDLE);
showPreferences();
}
}
}