/* * 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.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.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.Credentials; 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.getProfilePath() + "/"; 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 = 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 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 KeyStore mKeyStore = KeyStore.getInstance(); 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_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() { return new AlertDialog.Builder(this) .setView(mConnectingActor.createConnectView()) .setTitle(String.format(getString(R.string.vpn_connect_to), mActiveProfile.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); changeState(mActiveProfile, VpnState.IDLE); } }) .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); } }) .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); } }); } 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(mActiveProfile); } }) .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) { 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); changeState(mActiveProfile, VpnState.IDLE); } } 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 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; // pass through case L2TP: L2tpProfile l2tpProfile = (L2tpProfile) p; if (l2tpProfile.isSecretEnabled() && !TextUtils.isEmpty(l2tpProfile.getSecretString())) { return true; } // pass 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(this); return false; } private void startVpnEditor(final VpnProfile profile) { 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; } 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(); 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: mConnectingActor = getActor(p); // pass 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() { 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 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); // pass 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); 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(); } } }