| /* |
| * 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.app.Fragment; |
| import android.content.BroadcastReceiver; |
| 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.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.preference.Preference; |
| import android.preference.Preference.OnPreferenceClickListener; |
| import android.preference.PreferenceCategory; |
| import android.preference.PreferenceScreen; |
| 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.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.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * The preference activity for configuring VPN settings. |
| */ |
| public class VpnSettings extends SettingsPreferenceFragment |
| 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.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 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; |
| |
| 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); |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| |
| 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; |
| } |
| }); |
| |
| // 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; |
| getActivity().runOnUiThread(action); |
| } |
| } |
| |
| @Override |
| public void onDestroyView() { |
| unregisterForContextMenu(getListView()); |
| mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver); |
| if ((mShowingDialog != null) && mShowingDialog.isShowing()) { |
| mShowingDialog.dismiss(); |
| } |
| // This should be called after the procedure above as ListView inside this Fragment |
| // will be deleted here. |
| super.onDestroyView(); |
| } |
| |
| @Override |
| public 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() { |
| final Activity activity = getActivity(); |
| return new AlertDialog.Builder(activity) |
| .setView(mConnectingActor.createConnectView()) |
| .setTitle(String.format(activity.getString(R.string.vpn_connect_to), |
| mActiveProfile.getName())) |
| .setPositiveButton(activity.getString(R.string.vpn_connect_button), |
| this) |
| .setNegativeButton(activity.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(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(); |
| } |
| }) |
| .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 |
| public void onActivityResult(final int requestCode, final int resultCode, |
| final Intent data) { |
| if ((resultCode == Activity.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; |
| } |
| |
| 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); |
| } |
| }); |
| 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); |
| } |
| }); |
| } |
| |
| // 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); |
| removeDialog(DIALOG_CONNECT); |
| return; |
| } else { |
| // dismissDialog(DIALOG_CONNECT); |
| removeDialog(DIALOG_CONNECT); |
| |
| final Activity activity = getActivity(); |
| // show error dialog |
| 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(); |
| 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(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 : 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(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 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() { |
| startFragment(this, VpnTypeSelection.class.getCanonicalName(), |
| REQUEST_SELECT_VPN_TYPE, null); |
| } |
| |
| 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) { |
| Bundle args = new Bundle(); |
| args.putParcelable(KEY_VPN_PROFILE, profile); |
| startFragment(this, VpnEditor.class.getCanonicalName(), |
| REQUEST_ADD_OR_EDIT_PROFILE, args); |
| } |
| |
| 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); |
| // $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() { |
| 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()); |
| } |
| }); |
| 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) { |
| 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); |
| 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 { |
| 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); |
| } |
| } |
| getActivity().unbindService(this); |
| showPreferences(); |
| } |
| |
| public void onServiceDisconnected(ComponentName className) { |
| cv.open(); |
| |
| setDefaultState(list); |
| getActivity().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(); |
| } |
| } |
| } |