Files
packages_apps_Settings/src/com/android/settings/vpn/VpnSettings.java
Hung-ying Tyan 18eb39d085 Clear mDialogFragment when it's detached
and re-associate it when it's re-created.

Before this CL, the association is gone when fragment goes through the pause-resume
cycle.

Similarly, restore onDismiss and onCancel listeners in VpnSettings.onCreateDialog(),
restore states in onCreate() instead of onActivityCreated() so that screen rotation
can be handled correctly.

Now that profiles are shared between Settings instances, always handle state change
in VpnSettings.changeState() so that state changed in one instance can be conveyed
to the other and preferences can be correctly enabled/disabled.

In additions, fix some trivial mistakes in VpnSettings.

Bug: 3396394
Change-Id: I242e1ed6c6d410b4dfefb373d8f98266fc9b46d0
2011-01-28 23:39:45 +08:00

1102 lines
40 KiB
Java

/*
* 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.SettingsPreferenceFragment;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.vpn.L2tpIpsecProfile;
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.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
import android.preference.Preference.OnPreferenceClickListener;
import android.security.Credentials;
import android.security.KeyStore;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* The preference activity for configuring VPN settings.
*/
public class VpnSettings extends SettingsPreferenceFragment
implements DialogInterface.OnClickListener {
private static final boolean DEBUG = false;
// 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.getProfilePath() + "/";
private static final String PROFILE_OBJ_FILE = ".pobj";
private static final String KEY_ACTIVE_PROFILE = "ActiveProfile";
private static final String KEY_PROFILE_CONNECTING = "ProfileConnecting";
private static final String KEY_CONNECT_DIALOG_SHOWING = "ConnectDialogShowing";
private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1;
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.BUTTON_POSITIVE;
private static final int OK_BUTTON = DialogInterface.BUTTON_POSITIVE;
private static final int DIALOG_CONNECT = VpnManager.VPN_ERROR_LARGEST + 1;
private static final int DIALOG_SECRET_NOT_SET = DIALOG_CONNECT + 1;
private static final int NO_ERROR = VpnManager.VPN_ERROR_NO_ERROR;
private static final String KEY_PREFIX_IPSEC_PSK = Credentials.VPN + 'i';
private static final String KEY_PREFIX_L2TP_SECRET = Credentials.VPN + 'l';
private static List<VpnProfile> sVpnProfileList = new ArrayList<VpnProfile>();
private PreferenceScreen mAddVpn;
private PreferenceCategory mVpnListContainer;
// profile name --> VpnPreference
private Map<String, VpnPreference> mVpnPreferenceMap;
// profile engaged in a connection
private VpnProfile mActiveProfile;
// actor engaged in connecting
private VpnProfileActor mConnectingActor;
// states saved for unlocking keystore
private Runnable mUnlockAction;
private KeyStore mKeyStore = KeyStore.getInstance();
private VpnManager mVpnManager;
private ConnectivityReceiver mConnectivityReceiver =
new ConnectivityReceiver();
private int mConnectingErrorCode = NO_ERROR;
private Dialog mShowingDialog;
private boolean mConnectDialogShowing = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.vpn_settings);
mVpnManager = new VpnManager(getActivity());
// 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;
}
});
retrieveVpnListFromStorage();
restoreInstanceState(savedInstanceState);
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
if (mActiveProfile != null) {
savedInstanceState.putString(KEY_ACTIVE_PROFILE,
mActiveProfile.getId());
savedInstanceState.putBoolean(KEY_PROFILE_CONNECTING,
(mConnectingActor != null));
savedInstanceState.putBoolean(KEY_CONNECT_DIALOG_SHOWING,
mConnectDialogShowing);
}
super.onSaveInstanceState(savedInstanceState);
}
private void restoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState == null) return;
String profileId = savedInstanceState.getString(KEY_ACTIVE_PROFILE);
if (profileId != null) {
mActiveProfile = getProfile(getProfileIndexFromId(profileId));
if (savedInstanceState.getBoolean(KEY_PROFILE_CONNECTING)) {
mConnectingActor = getActor(mActiveProfile);
}
mConnectDialogShowing = savedInstanceState.getBoolean(KEY_CONNECT_DIALOG_SHOWING);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// for long-press gesture on a profile preference
registerForContextMenu(getListView());
}
@Override
public void onPause() {
// ignore vpn connectivity event
mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver);
if ((mShowingDialog != null) && mShowingDialog.isShowing()) {
mShowingDialog.dismiss();
mShowingDialog = null;
}
super.onPause();
}
@Override
public void onResume() {
super.onResume();
updatePreferenceMap();
if (DEBUG) Log.d(TAG, "onResume");
// listen to vpn connectivity event
mVpnManager.registerConnectivityReceiver(mConnectivityReceiver);
if ((mUnlockAction != null) && isKeyStoreUnlocked()) {
Runnable action = mUnlockAction;
mUnlockAction = null;
getActivity().runOnUiThread(action);
}
if (!mConnectDialogShowing) {
checkVpnConnectionStatus();
} else {
// Dismiss the connect dialog in case there is another instance
// trying to operate a vpn connection.
if (!mVpnManager.isIdle() || (mActiveProfile == null)) {
removeDialog(DIALOG_CONNECT);
checkVpnConnectionStatus();
}
}
}
@Override
public void onDestroyView() {
unregisterForContextMenu(getListView());
// This should be called after the procedure above as ListView inside this Fragment
// will be deleted here.
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
// Remove any onClick listeners
if (mVpnListContainer != null) {
for (int i = 0; i < mVpnListContainer.getPreferenceCount(); i++) {
mVpnListContainer.getPreference(i).setOnPreferenceClickListener(null);
}
}
}
@Override
public Dialog onCreateDialog (int id) {
setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (mActiveProfile != null) {
changeState(mActiveProfile, VpnState.IDLE);
}
// Make sure onIdle() is called as the above changeState()
// may not be effective if the state is already IDLE.
// XXX: VpnService should broadcast non-IDLE state, say UNUSABLE,
// when an error occurs.
onIdle();
}
});
switch (id) {
case DIALOG_CONNECT:
mConnectDialogShowing = true;
setOnDismissListener(new DialogInterface.OnDismissListener() {
public void onDismiss(DialogInterface dialog) {
mConnectDialogShowing = false;
}
});
return createConnectDialog();
case DIALOG_SECRET_NOT_SET:
return createSecretNotSetDialog();
case VpnManager.VPN_ERROR_CHALLENGE:
case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
return createEditDialog(id);
default:
Log.d(TAG, "create reconnect dialog for event " + id);
return createReconnectDialog(id);
}
}
private Dialog createConnectDialog() {
final Activity activity = getActivity();
return new AlertDialog.Builder(activity)
.setView(mConnectingActor.createConnectView())
.setTitle(String.format(activity.getString(R.string.vpn_connect_to),
mConnectingActor.getProfile().getName()))
.setPositiveButton(activity.getString(R.string.vpn_connect_button),
this)
.setNegativeButton(activity.getString(android.R.string.cancel),
this)
.create();
}
private Dialog createReconnectDialog(int id) {
int msgId;
switch (id) {
case VpnManager.VPN_ERROR_AUTH:
msgId = R.string.vpn_auth_error_dialog_msg;
break;
case VpnManager.VPN_ERROR_REMOTE_HUNG_UP:
msgId = R.string.vpn_remote_hung_up_error_dialog_msg;
break;
case VpnManager.VPN_ERROR_CONNECTION_LOST:
msgId = R.string.vpn_reconnect_from_lost;
break;
case VpnManager.VPN_ERROR_REMOTE_PPP_HUNG_UP:
msgId = R.string.vpn_remote_ppp_hung_up_error_dialog_msg;
break;
default:
msgId = R.string.vpn_confirm_reconnect;
}
return createCommonDialogBuilder().setMessage(msgId).create();
}
private Dialog createEditDialog(int id) {
int msgId;
switch (id) {
case VpnManager.VPN_ERROR_CHALLENGE:
msgId = R.string.vpn_challenge_error_dialog_msg;
break;
case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
msgId = R.string.vpn_unknown_server_dialog_msg;
break;
case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
msgId = R.string.vpn_ppp_negotiation_failed_dialog_msg;
break;
default:
return null;
}
return createCommonEditDialogBuilder().setMessage(msgId).create();
}
private Dialog createSecretNotSetDialog() {
return createCommonDialogBuilder()
.setMessage(R.string.vpn_secret_not_set_dialog_msg)
.setPositiveButton(R.string.vpn_yes_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
startVpnEditor(mActiveProfile, false);
}
})
.create();
}
private AlertDialog.Builder createCommonEditDialogBuilder() {
return createCommonDialogBuilder()
.setPositiveButton(R.string.vpn_yes_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
VpnProfile p = mActiveProfile;
onIdle();
startVpnEditor(p, false);
}
});
}
private AlertDialog.Builder createCommonDialogBuilder() {
return new AlertDialog.Builder(getActivity())
.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(mActiveProfile);
}
})
.setNegativeButton(R.string.vpn_no_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
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, false);
return true;
case CONTEXT_MENU_DELETE_ID:
deleteProfile(position);
return true;
}
return super.onContextItemSelected(item);
}
@Override
public void onActivityResult(final int requestCode, final int resultCode,
final Intent data) {
if (DEBUG) Log.d(TAG, "onActivityResult , result = " + resultCode + ", data = " + data);
if ((resultCode == Activity.RESULT_CANCELED) || (data == null)) {
Log.d(TAG, "no result returned by editor");
return;
}
if (requestCode == REQUEST_SELECT_VPN_TYPE) {
final String typeName = data.getStringExtra(KEY_VPN_TYPE);
startVpnEditor(createVpnProfile(typeName), true);
} 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;
}
final Activity activity = getActivity();
int index = getProfileIndexFromId(p.getId());
if (checkDuplicateName(p, index)) {
final VpnProfile profile = p;
Util.showErrorMessage(activity, String.format(
activity.getString(R.string.vpn_error_duplicate_name),
p.getName()),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
startVpnEditor(profile, false);
}
});
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(activity, String.format(
activity.getString(R.string.vpn_profile_added), p.getName()));
} else {
replaceProfile(index, p);
Util.showShortToastMessage(activity, String.format(
activity.getString(R.string.vpn_profile_replaced),
p.getName()));
}
} catch (IOException e) {
final VpnProfile profile = p;
Util.showErrorMessage(activity, e + ": " + e.getMessage(),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
startVpnEditor(profile, false);
}
});
}
// Remove cached VpnEditor as it is needless anymore.
} 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) {
mConnectingActor.connect(d);
} else {
// show error dialog
final Activity activity = getActivity();
mShowingDialog = new AlertDialog.Builder(activity)
.setTitle(android.R.string.dialog_alert_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(String.format(activity.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();
// The profile state is "connecting". If we allow the dialog to
// be cancelable, then we need to clear the state in the
// onCancel handler.
mShowingDialog.setCancelable(false);
mShowingDialog.show();
}
} else {
changeState(mActiveProfile, VpnState.IDLE);
}
}
private int getProfileIndexFromId(String id) {
int index = 0;
for (VpnProfile p : sVpnProfileList) {
if (p.getId().equals(id)) {
return index;
} else {
index++;
}
}
return -1;
}
// Replaces the profile at index in sVpnProfileList with p.
// Returns true if p's name is a duplicate.
private boolean checkDuplicateName(VpnProfile p, int index) {
List<VpnProfile> list = sVpnProfileList;
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 sVpnProfileList
private VpnProfile getProfile(int position) {
return ((position >= 0) ? sVpnProfileList.get(position) : null);
}
// position: position in sVpnProfileList
private void deleteProfile(final int position) {
if ((position < 0) || (position >= sVpnProfileList.size())) return;
final VpnProfile target = sVpnProfileList.get(position);
DialogInterface.OnClickListener onClickListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// Double check if the target is still the one we want
// to remove.
VpnProfile p = sVpnProfileList.get(position);
if (p != target) return;
if (which == OK_BUTTON) {
sVpnProfileList.remove(position);
VpnPreference pref =
mVpnPreferenceMap.remove(p.getName());
mVpnListContainer.removePreference(pref);
removeProfileFromStorage(p);
}
}
};
mShowingDialog = new AlertDialog.Builder(getActivity())
.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 : sVpnProfileList) {
if (p.getId().equals(id)) {
setProfileId(profile);
return;
}
}
profile.setId(id);
}
private void addProfile(VpnProfile p) throws IOException {
setProfileId(p);
processSecrets(p);
saveProfileToStorage(p);
sVpnProfileList.add(p);
addPreferenceFor(p, true);
disableProfilePreferencesIfOneActive();
}
// Adds a preference in mVpnListContainer
private VpnPreference addPreferenceFor(
VpnProfile p, boolean addToContainer) {
VpnPreference pref = new VpnPreference(getActivity(), 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 sVpnProfileList
private void replaceProfile(int index, VpnProfile p) throws IOException {
Map<String, VpnPreference> map = mVpnPreferenceMap;
VpnProfile oldProfile = sVpnProfileList.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() {
if (getActivity() == null) return;
((PreferenceActivity) getActivity()).startPreferencePanel(
VpnTypeSelection.class.getCanonicalName(), null, R.string.vpn_type_title, null,
this, REQUEST_SELECT_VPN_TYPE);
}
private boolean isKeyStoreUnlocked() {
return mKeyStore.test() == KeyStore.NO_ERROR;
}
// Returns true if the profile needs to access keystore
private boolean needKeyStoreToSave(VpnProfile p) {
switch (p.getType()) {
case L2TP_IPSEC_PSK:
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
String presharedKey = pskProfile.getPresharedKey();
if (!TextUtils.isEmpty(presharedKey)) return true;
// $FALL-THROUGH$
case L2TP:
L2tpProfile l2tpProfile = (L2tpProfile) p;
if (l2tpProfile.isSecretEnabled() &&
!TextUtils.isEmpty(l2tpProfile.getSecretString())) {
return true;
}
// $FALL-THROUGH$
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;
Credentials.getInstance().unlock(getActivity());
return false;
}
private void startVpnEditor(final VpnProfile profile, boolean add) {
if (getActivity() == null) return;
Bundle args = new Bundle();
args.putParcelable(KEY_VPN_PROFILE, profile);
// TODO: Show different titles for add and edit.
((PreferenceActivity)getActivity()).startPreferencePanel(
VpnEditor.class.getCanonicalName(), args,
add ? R.string.vpn_details_title : R.string.vpn_details_title, null,
this, 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;
}
if (!checkSecrets(p)) return;
changeState(p, VpnState.CONNECTING);
if (mConnectingActor.isConnectDialogNeeded()) {
showDialog(DIALOG_CONNECT);
} else {
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();
p.setState(state);
mVpnPreferenceMap.get(p.getName()).setSummary(
getProfileSummaryString(p));
switch (state) {
case CONNECTED:
mConnectingActor = null;
mActiveProfile = p;
disableProfilePreferencesIfOneActive();
break;
case CONNECTING:
if (mConnectingActor == null) {
mConnectingActor = getActor(p);
}
// $FALL-THROUGH$
case DISCONNECTING:
mActiveProfile = p;
disableProfilePreferencesIfOneActive();
break;
case CANCELLED:
changeState(p, VpnState.IDLE);
break;
case IDLE:
assert(mActiveProfile == p);
if (mConnectingErrorCode == NO_ERROR) {
onIdle();
} else {
showDialog(mConnectingErrorCode);
mConnectingErrorCode = NO_ERROR;
}
break;
}
}
private void onIdle() {
if (DEBUG) Log.d(TAG, " onIdle()");
mActiveProfile = null;
mConnectingActor = null;
enableProfilePreferences();
}
private void disableProfilePreferencesIfOneActive() {
if (mActiveProfile == null) return;
for (VpnProfile p : sVpnProfileList) {
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 : sVpnProfileList) {
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 updatePreferenceMap() {
mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>();
mVpnListContainer.removeAll();
for (VpnProfile p : sVpnProfileList) {
addPreferenceFor(p, true);
}
// reset the mActiveProfile if the profile has been removed from the
// other instance.
if ((mActiveProfile != null)
&& !mVpnPreferenceMap.containsKey(mActiveProfile.getName())) {
onIdle();
}
}
private void retrieveVpnListFromStorage() {
// skip the loop if the profile is loaded already.
if (sVpnProfileList.size() > 0) return;
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;
sVpnProfileList.add(p);
} catch (IOException e) {
Log.e(TAG, "retrieveVpnListFromStorage()", e);
}
}
Collections.sort(sVpnProfileList, new Comparator<VpnProfile>() {
public int compare(VpnProfile p1, VpnProfile p2) {
return p1.getName().compareTo(p2.getName());
}
});
disableProfilePreferencesIfOneActive();
}
private void checkVpnConnectionStatus() {
for (VpnProfile p : sVpnProfileList) {
changeState(p, mVpnManager.getState(p));
}
}
// 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) {
final Activity activity = getActivity();
switch (p.getState()) {
case CONNECTING:
return activity.getString(R.string.vpn_connecting);
case DISCONNECTING:
return activity.getString(R.string.vpn_disconnecting);
case CONNECTED:
return activity.getString(R.string.vpn_connected);
default:
return activity.getString(R.string.vpn_connect_hint);
}
}
private VpnProfileActor getActor(VpnProfile p) {
return new AuthenticationActor(getActivity(), p);
}
private VpnProfile createVpnProfile(String type) {
return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type));
}
private boolean checkSecrets(VpnProfile p) {
boolean secretMissing = false;
if (p instanceof L2tpIpsecProfile) {
L2tpIpsecProfile certProfile = (L2tpIpsecProfile) p;
String cert = certProfile.getCaCertificate();
if (TextUtils.isEmpty(cert) ||
!mKeyStore.contains(Credentials.CA_CERTIFICATE + cert)) {
certProfile.setCaCertificate(null);
secretMissing = true;
}
cert = certProfile.getUserCertificate();
if (TextUtils.isEmpty(cert) ||
!mKeyStore.contains(Credentials.USER_CERTIFICATE + cert)) {
certProfile.setUserCertificate(null);
secretMissing = true;
}
}
if (p instanceof L2tpIpsecPskProfile) {
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
String presharedKey = pskProfile.getPresharedKey();
String key = KEY_PREFIX_IPSEC_PSK + p.getId();
if (TextUtils.isEmpty(presharedKey) || !mKeyStore.contains(key)) {
pskProfile.setPresharedKey(null);
secretMissing = true;
}
}
if (p instanceof L2tpProfile) {
L2tpProfile l2tpProfile = (L2tpProfile) p;
if (l2tpProfile.isSecretEnabled()) {
String secret = l2tpProfile.getSecretString();
String key = KEY_PREFIX_L2TP_SECRET + p.getId();
if (TextUtils.isEmpty(secret) || !mKeyStore.contains(key)) {
l2tpProfile.setSecretString(null);
secretMissing = true;
}
}
}
if (secretMissing) {
mActiveProfile = p;
showDialog(DIALOG_SECRET_NOT_SET);
return false;
} else {
return true;
}
}
private void processSecrets(VpnProfile p) {
switch (p.getType()) {
case L2TP_IPSEC_PSK:
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
String presharedKey = pskProfile.getPresharedKey();
String key = KEY_PREFIX_IPSEC_PSK + p.getId();
if (!TextUtils.isEmpty(presharedKey) &&
!mKeyStore.put(key, presharedKey)) {
Log.e(TAG, "keystore write failed: key=" + key);
}
pskProfile.setPresharedKey(key);
// $FALL-THROUGH$
case L2TP_IPSEC:
case L2TP:
L2tpProfile l2tpProfile = (L2tpProfile) p;
key = KEY_PREFIX_L2TP_SECRET + p.getId();
if (l2tpProfile.isSecretEnabled()) {
String secret = l2tpProfile.getSecretString();
if (!TextUtils.isEmpty(secret) &&
!mKeyStore.put(key, secret)) {
Log.e(TAG, "keystore write failed: key=" + key);
}
l2tpProfile.setSecretString(key);
} else {
mKeyStore.delete(key);
}
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);
// XXX: VpnService should broadcast non-IDLE state, say UNUSABLE,
// when an error occurs.
changeState(pref.mProfile, s);
} else {
Log.e(TAG, "received connectivity: " + profileName
+ ": connected? " + s + ", but profile does not exist;"
+ " just ignore it");
}
}
}
}