2 * Copyright (C) 2009 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.settings.vpn;
19 import com.android.settings.R;
20 import com.android.settings.SettingsPreferenceFragment;
22 import android.app.Activity;
23 import android.app.AlertDialog;
24 import android.app.Dialog;
25 import android.content.BroadcastReceiver;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.ServiceConnection;
31 import android.net.vpn.IVpnService;
32 import android.net.vpn.L2tpIpsecProfile;
33 import android.net.vpn.L2tpIpsecPskProfile;
34 import android.net.vpn.L2tpProfile;
35 import android.net.vpn.VpnManager;
36 import android.net.vpn.VpnProfile;
37 import android.net.vpn.VpnState;
38 import android.net.vpn.VpnType;
39 import android.os.Bundle;
40 import android.os.ConditionVariable;
41 import android.os.Handler;
42 import android.os.IBinder;
43 import android.preference.Preference;
44 import android.preference.PreferenceActivity;
45 import android.preference.PreferenceCategory;
46 import android.preference.PreferenceScreen;
47 import android.preference.Preference.OnPreferenceClickListener;
48 import android.security.Credentials;
49 import android.security.KeyStore;
50 import android.text.TextUtils;
51 import android.util.Log;
52 import android.view.ContextMenu;
53 import android.view.MenuItem;
54 import android.view.View;
55 import android.view.ContextMenu.ContextMenuInfo;
56 import android.widget.AdapterView.AdapterContextMenuInfo;
59 import java.io.FileInputStream;
60 import java.io.FileOutputStream;
61 import java.io.IOException;
62 import java.io.ObjectInputStream;
63 import java.io.ObjectOutputStream;
64 import java.util.ArrayList;
65 import java.util.Collections;
66 import java.util.Comparator;
67 import java.util.LinkedHashMap;
68 import java.util.List;
72 * The preference activity for configuring VPN settings.
74 public class VpnSettings extends SettingsPreferenceFragment
75 implements DialogInterface.OnClickListener {
77 private static final boolean DEBUG = false;
79 // Key to the field exchanged for profile editing.
80 static final String KEY_VPN_PROFILE = "vpn_profile";
82 // Key to the field exchanged for VPN type selection.
83 static final String KEY_VPN_TYPE = "vpn_type";
85 private static final String TAG = VpnSettings.class.getSimpleName();
87 private static final String PREF_ADD_VPN = "add_new_vpn";
88 private static final String PREF_VPN_LIST = "vpn_list";
90 private static final String PROFILES_ROOT = VpnManager.getProfilePath() + "/";
91 private static final String PROFILE_OBJ_FILE = ".pobj";
93 private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1;
94 static final int REQUEST_SELECT_VPN_TYPE = 2;
96 private static final int CONTEXT_MENU_CONNECT_ID = ContextMenu.FIRST + 0;
97 private static final int CONTEXT_MENU_DISCONNECT_ID = ContextMenu.FIRST + 1;
98 private static final int CONTEXT_MENU_EDIT_ID = ContextMenu.FIRST + 2;
99 private static final int CONTEXT_MENU_DELETE_ID = ContextMenu.FIRST + 3;
101 private static final int CONNECT_BUTTON = DialogInterface.BUTTON_POSITIVE;
102 private static final int OK_BUTTON = DialogInterface.BUTTON_POSITIVE;
104 private static final int DIALOG_CONNECT = VpnManager.VPN_ERROR_LARGEST + 1;
105 private static final int DIALOG_SECRET_NOT_SET = DIALOG_CONNECT + 1;
107 private static final int NO_ERROR = VpnManager.VPN_ERROR_NO_ERROR;
109 private static final String KEY_PREFIX_IPSEC_PSK = Credentials.VPN + 'i';
110 private static final String KEY_PREFIX_L2TP_SECRET = Credentials.VPN + 'l';
112 private PreferenceScreen mAddVpn;
113 private PreferenceCategory mVpnListContainer;
115 // profile name --> VpnPreference
116 private Map<String, VpnPreference> mVpnPreferenceMap;
117 private List<VpnProfile> mVpnProfileList;
119 // profile engaged in a connection
120 private VpnProfile mActiveProfile;
122 // actor engaged in connecting
123 private VpnProfileActor mConnectingActor;
125 // states saved for unlocking keystore
126 private Runnable mUnlockAction;
128 private KeyStore mKeyStore = KeyStore.getInstance();
130 private VpnManager mVpnManager;
132 private ConnectivityReceiver mConnectivityReceiver =
133 new ConnectivityReceiver();
135 private int mConnectingErrorCode = NO_ERROR;
137 private Dialog mShowingDialog;
139 private StatusChecker mStatusChecker = new StatusChecker();
141 private Handler mHandler = new Handler();
144 public void onCreate(Bundle savedInstanceState) {
145 super.onCreate(savedInstanceState);
146 addPreferencesFromResource(R.xml.vpn_settings);
150 public void onActivityCreated(Bundle savedInstanceState) {
151 super.onActivityCreated(savedInstanceState);
153 mVpnManager = new VpnManager(getActivity());
154 // restore VpnProfile list and construct VpnPreference map
155 mVpnListContainer = (PreferenceCategory) findPreference(PREF_VPN_LIST);
157 // set up the "add vpn" preference
158 mAddVpn = (PreferenceScreen) findPreference(PREF_ADD_VPN);
159 mAddVpn.setOnPreferenceClickListener(
160 new OnPreferenceClickListener() {
161 public boolean onPreferenceClick(Preference preference) {
162 startVpnTypeSelection();
167 // for long-press gesture on a profile preference
168 registerForContextMenu(getListView());
170 // listen to vpn connectivity event
171 mVpnManager.registerConnectivityReceiver(mConnectivityReceiver);
173 retrieveVpnListFromStorage();
174 checkVpnConnectionStatusInBackground();
178 public void onResume() {
181 Log.d(TAG, "onResume");
182 if ((mUnlockAction != null) && isKeyStoreUnlocked()) {
183 Runnable action = mUnlockAction;
184 mUnlockAction = null;
185 getActivity().runOnUiThread(action);
190 public void onDestroyView() {
191 unregisterForContextMenu(getListView());
192 mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver);
193 if ((mShowingDialog != null) && mShowingDialog.isShowing()) {
194 mShowingDialog.dismiss();
196 // This should be called after the procedure above as ListView inside this Fragment
197 // will be deleted here.
198 super.onDestroyView();
202 public Dialog onCreateDialog (int id) {
205 return createConnectDialog();
207 case DIALOG_SECRET_NOT_SET:
208 return createSecretNotSetDialog();
210 case VpnManager.VPN_ERROR_CHALLENGE:
211 case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
212 case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
213 return createEditDialog(id);
216 Log.d(TAG, "create reconnect dialog for event " + id);
217 return createReconnectDialog(id);
221 private Dialog createConnectDialog() {
222 final Activity activity = getActivity();
223 return new AlertDialog.Builder(activity)
224 .setView(mConnectingActor.createConnectView())
225 .setTitle(String.format(activity.getString(R.string.vpn_connect_to),
226 mActiveProfile.getName()))
227 .setPositiveButton(activity.getString(R.string.vpn_connect_button),
229 .setNegativeButton(activity.getString(android.R.string.cancel),
231 .setOnCancelListener(new DialogInterface.OnCancelListener() {
232 public void onCancel(DialogInterface dialog) {
233 removeDialog(DIALOG_CONNECT);
234 changeState(mActiveProfile, VpnState.IDLE);
240 private Dialog createReconnectDialog(int id) {
243 case VpnManager.VPN_ERROR_AUTH:
244 msgId = R.string.vpn_auth_error_dialog_msg;
247 case VpnManager.VPN_ERROR_REMOTE_HUNG_UP:
248 msgId = R.string.vpn_remote_hung_up_error_dialog_msg;
251 case VpnManager.VPN_ERROR_CONNECTION_LOST:
252 msgId = R.string.vpn_reconnect_from_lost;
255 case VpnManager.VPN_ERROR_REMOTE_PPP_HUNG_UP:
256 msgId = R.string.vpn_remote_ppp_hung_up_error_dialog_msg;
260 msgId = R.string.vpn_confirm_reconnect;
262 return createCommonDialogBuilder().setMessage(msgId).create();
265 private Dialog createEditDialog(int id) {
268 case VpnManager.VPN_ERROR_CHALLENGE:
269 msgId = R.string.vpn_challenge_error_dialog_msg;
272 case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
273 msgId = R.string.vpn_unknown_server_dialog_msg;
276 case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
277 msgId = R.string.vpn_ppp_negotiation_failed_dialog_msg;
283 return createCommonEditDialogBuilder().setMessage(msgId).create();
286 private Dialog createSecretNotSetDialog() {
287 return createCommonDialogBuilder()
288 .setMessage(R.string.vpn_secret_not_set_dialog_msg)
289 .setPositiveButton(R.string.vpn_yes_button,
290 new DialogInterface.OnClickListener() {
291 public void onClick(DialogInterface dialog, int w) {
292 startVpnEditor(mActiveProfile, false);
298 private AlertDialog.Builder createCommonEditDialogBuilder() {
299 return createCommonDialogBuilder()
300 .setPositiveButton(R.string.vpn_yes_button,
301 new DialogInterface.OnClickListener() {
302 public void onClick(DialogInterface dialog, int w) {
303 VpnProfile p = mActiveProfile;
305 startVpnEditor(p, false);
310 private AlertDialog.Builder createCommonDialogBuilder() {
311 return new AlertDialog.Builder(getActivity())
312 .setTitle(android.R.string.dialog_alert_title)
313 .setIcon(android.R.drawable.ic_dialog_alert)
314 .setPositiveButton(R.string.vpn_yes_button,
315 new DialogInterface.OnClickListener() {
316 public void onClick(DialogInterface dialog, int w) {
317 connectOrDisconnect(mActiveProfile);
320 .setNegativeButton(R.string.vpn_no_button,
321 new DialogInterface.OnClickListener() {
322 public void onClick(DialogInterface dialog, int w) {
326 .setOnCancelListener(new DialogInterface.OnCancelListener() {
327 public void onCancel(DialogInterface dialog) {
334 public void onCreateContextMenu(ContextMenu menu, View v,
335 ContextMenuInfo menuInfo) {
336 super.onCreateContextMenu(menu, v, menuInfo);
338 VpnProfile p = getProfile(getProfilePositionFrom(
339 (AdapterContextMenuInfo) menuInfo));
341 VpnState state = p.getState();
342 menu.setHeaderTitle(p.getName());
344 boolean isIdle = (state == VpnState.IDLE);
345 boolean isNotConnect = (isIdle || (state == VpnState.DISCONNECTING)
346 || (state == VpnState.CANCELLED));
347 menu.add(0, CONTEXT_MENU_CONNECT_ID, 0, R.string.vpn_menu_connect)
348 .setEnabled(isIdle && (mActiveProfile == null));
349 menu.add(0, CONTEXT_MENU_DISCONNECT_ID, 0,
350 R.string.vpn_menu_disconnect)
351 .setEnabled(state == VpnState.CONNECTED);
352 menu.add(0, CONTEXT_MENU_EDIT_ID, 0, R.string.vpn_menu_edit)
353 .setEnabled(isNotConnect);
354 menu.add(0, CONTEXT_MENU_DELETE_ID, 0, R.string.vpn_menu_delete)
355 .setEnabled(isNotConnect);
360 public boolean onContextItemSelected(MenuItem item) {
361 int position = getProfilePositionFrom(
362 (AdapterContextMenuInfo) item.getMenuInfo());
363 VpnProfile p = getProfile(position);
365 switch(item.getItemId()) {
366 case CONTEXT_MENU_CONNECT_ID:
367 case CONTEXT_MENU_DISCONNECT_ID:
368 connectOrDisconnect(p);
371 case CONTEXT_MENU_EDIT_ID:
372 startVpnEditor(p, false);
375 case CONTEXT_MENU_DELETE_ID:
376 deleteProfile(position);
380 return super.onContextItemSelected(item);
384 public void onActivityResult(final int requestCode, final int resultCode,
387 if (DEBUG) Log.d(TAG, "onActivityResult , result = " + resultCode + ", data = " + data);
388 if ((resultCode == Activity.RESULT_CANCELED) || (data == null)) {
389 Log.d(TAG, "no result returned by editor");
393 if (requestCode == REQUEST_SELECT_VPN_TYPE) {
394 final String typeName = data.getStringExtra(KEY_VPN_TYPE);
395 mHandler.post(new Runnable() {
398 startVpnEditor(createVpnProfile(typeName), true);
401 } else if (requestCode == REQUEST_ADD_OR_EDIT_PROFILE) {
402 VpnProfile p = data.getParcelableExtra(KEY_VPN_PROFILE);
404 Log.e(TAG, "null object returned by editor");
408 final Activity activity = getActivity();
409 int index = getProfileIndexFromId(p.getId());
410 if (checkDuplicateName(p, index)) {
411 final VpnProfile profile = p;
412 Util.showErrorMessage(activity, String.format(
413 activity.getString(R.string.vpn_error_duplicate_name),
415 new DialogInterface.OnClickListener() {
416 public void onClick(DialogInterface dialog, int w) {
417 startVpnEditor(profile, false);
423 if (needKeyStoreToSave(p)) {
424 Runnable action = new Runnable() {
426 onActivityResult(requestCode, resultCode, data);
429 if (!unlockKeyStore(p, action)) return;
435 Util.showShortToastMessage(activity, String.format(
436 activity.getString(R.string.vpn_profile_added), p.getName()));
438 replaceProfile(index, p);
439 Util.showShortToastMessage(activity, String.format(
440 activity.getString(R.string.vpn_profile_replaced),
443 } catch (IOException e) {
444 final VpnProfile profile = p;
445 Util.showErrorMessage(activity, e + ": " + e.getMessage(),
446 new DialogInterface.OnClickListener() {
447 public void onClick(DialogInterface dialog, int w) {
448 startVpnEditor(profile, false);
453 // Remove cached VpnEditor as it is needless anymore.
455 throw new RuntimeException("unknown request code: " + requestCode);
459 // Called when the buttons on the connect dialog are clicked.
461 public synchronized void onClick(DialogInterface dialog, int which) {
462 if (which == CONNECT_BUTTON) {
463 Dialog d = (Dialog) dialog;
464 String error = mConnectingActor.validateInputs(d);
466 mConnectingActor.connect(d);
467 removeDialog(DIALOG_CONNECT);
470 // dismissDialog(DIALOG_CONNECT);
471 removeDialog(DIALOG_CONNECT);
473 final Activity activity = getActivity();
475 mShowingDialog = new AlertDialog.Builder(activity)
476 .setTitle(android.R.string.dialog_alert_title)
477 .setIcon(android.R.drawable.ic_dialog_alert)
478 .setMessage(String.format(activity.getString(
479 R.string.vpn_error_miss_entering), error))
480 .setPositiveButton(R.string.vpn_back_button,
481 new DialogInterface.OnClickListener() {
482 public void onClick(DialogInterface dialog,
484 showDialog(DIALOG_CONNECT);
488 mShowingDialog.show();
491 removeDialog(DIALOG_CONNECT);
492 changeState(mActiveProfile, VpnState.IDLE);
496 private int getProfileIndexFromId(String id) {
498 for (VpnProfile p : mVpnProfileList) {
499 if (p.getId().equals(id)) {
508 // Replaces the profile at index in mVpnProfileList with p.
509 // Returns true if p's name is a duplicate.
510 private boolean checkDuplicateName(VpnProfile p, int index) {
511 List<VpnProfile> list = mVpnProfileList;
512 VpnPreference pref = mVpnPreferenceMap.get(p.getName());
513 if ((pref != null) && (index >= 0) && (index < list.size())) {
514 // not a duplicate if p is to replace the profile at index
515 if (pref.mProfile == list.get(index)) pref = null;
517 return (pref != null);
520 private int getProfilePositionFrom(AdapterContextMenuInfo menuInfo) {
521 // excludes mVpnListContainer and the preferences above it
522 return menuInfo.position - mVpnListContainer.getOrder() - 1;
525 // position: position in mVpnProfileList
526 private VpnProfile getProfile(int position) {
527 return ((position >= 0) ? mVpnProfileList.get(position) : null);
530 // position: position in mVpnProfileList
531 private void deleteProfile(final int position) {
532 if ((position < 0) || (position >= mVpnProfileList.size())) return;
533 DialogInterface.OnClickListener onClickListener =
534 new DialogInterface.OnClickListener() {
535 public void onClick(DialogInterface dialog, int which) {
537 if (which == OK_BUTTON) {
538 VpnProfile p = mVpnProfileList.remove(position);
540 mVpnPreferenceMap.remove(p.getName());
541 mVpnListContainer.removePreference(pref);
542 removeProfileFromStorage(p);
546 mShowingDialog = new AlertDialog.Builder(getActivity())
547 .setTitle(android.R.string.dialog_alert_title)
548 .setIcon(android.R.drawable.ic_dialog_alert)
549 .setMessage(R.string.vpn_confirm_profile_deletion)
550 .setPositiveButton(android.R.string.ok, onClickListener)
551 .setNegativeButton(R.string.vpn_no_button, onClickListener)
553 mShowingDialog.show();
556 // Randomly generates an ID for the profile.
557 // The ID is unique and only set once when the profile is created.
558 private void setProfileId(VpnProfile profile) {
562 id = String.valueOf(Math.abs(
563 Double.doubleToLongBits(Math.random())));
564 if (id.length() >= 8) break;
566 for (VpnProfile p : mVpnProfileList) {
567 if (p.getId().equals(id)) {
568 setProfileId(profile);
575 private void addProfile(VpnProfile p) throws IOException {
578 saveProfileToStorage(p);
580 mVpnProfileList.add(p);
582 disableProfilePreferencesIfOneActive();
585 private VpnPreference addPreferenceFor(VpnProfile p) {
586 return addPreferenceFor(p, true);
589 // Adds a preference in mVpnListContainer
590 private VpnPreference addPreferenceFor(
591 VpnProfile p, boolean addToContainer) {
592 VpnPreference pref = new VpnPreference(getActivity(), p);
593 mVpnPreferenceMap.put(p.getName(), pref);
594 if (addToContainer) mVpnListContainer.addPreference(pref);
596 pref.setOnPreferenceClickListener(
597 new Preference.OnPreferenceClickListener() {
598 public boolean onPreferenceClick(Preference pref) {
599 connectOrDisconnect(((VpnPreference) pref).mProfile);
606 // index: index to mVpnProfileList
607 private void replaceProfile(int index, VpnProfile p) throws IOException {
608 Map<String, VpnPreference> map = mVpnPreferenceMap;
609 VpnProfile oldProfile = mVpnProfileList.set(index, p);
610 VpnPreference pref = map.remove(oldProfile.getName());
611 if (pref.mProfile != oldProfile) {
612 throw new RuntimeException("inconsistent state!");
615 p.setId(oldProfile.getId());
619 // TODO: remove copyFiles once the setId() code propagates.
620 // Copy config files and remove the old ones if they are in different
622 if (Util.copyFiles(getProfileDir(oldProfile), getProfileDir(p))) {
623 removeProfileFromStorage(oldProfile);
625 saveProfileToStorage(p);
628 map.put(p.getName(), pref);
631 private void startVpnTypeSelection() {
632 ((PreferenceActivity)getActivity()).startPreferencePanel(
633 VpnTypeSelection.class.getCanonicalName(), null, R.string.vpn_type_title, null,
634 this, REQUEST_SELECT_VPN_TYPE);
637 private boolean isKeyStoreUnlocked() {
638 return mKeyStore.test() == KeyStore.NO_ERROR;
641 // Returns true if the profile needs to access keystore
642 private boolean needKeyStoreToSave(VpnProfile p) {
643 switch (p.getType()) {
645 L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
646 String presharedKey = pskProfile.getPresharedKey();
647 if (!TextUtils.isEmpty(presharedKey)) return true;
650 L2tpProfile l2tpProfile = (L2tpProfile) p;
651 if (l2tpProfile.isSecretEnabled() &&
652 !TextUtils.isEmpty(l2tpProfile.getSecretString())) {
661 // Returns true if the profile needs to access keystore
662 private boolean needKeyStoreToConnect(VpnProfile p) {
663 switch (p.getType()) {
669 return ((L2tpProfile) p).isSecretEnabled();
676 // Returns true if keystore is unlocked or keystore is not a concern
677 private boolean unlockKeyStore(VpnProfile p, Runnable action) {
678 if (isKeyStoreUnlocked()) return true;
679 mUnlockAction = action;
680 Credentials.getInstance().unlock(getActivity());
684 private void startVpnEditor(final VpnProfile profile, boolean add) {
685 Bundle args = new Bundle();
686 args.putParcelable(KEY_VPN_PROFILE, profile);
687 // TODO: Show different titles for add and edit.
688 ((PreferenceActivity)getActivity()).startPreferencePanel(
689 VpnEditor.class.getCanonicalName(), args,
690 add ? R.string.vpn_details_title : R.string.vpn_details_title, null,
691 this, REQUEST_ADD_OR_EDIT_PROFILE);
694 private synchronized void connect(final VpnProfile p) {
695 if (needKeyStoreToConnect(p)) {
696 Runnable action = new Runnable() {
701 if (!unlockKeyStore(p, action)) return;
704 if (!checkSecrets(p)) return;
705 changeState(p, VpnState.CONNECTING);
706 if (mConnectingActor.isConnectDialogNeeded()) {
707 showDialog(DIALOG_CONNECT);
709 mConnectingActor.connect(null);
713 // Do connect or disconnect based on the current state.
714 private synchronized void connectOrDisconnect(VpnProfile p) {
715 VpnPreference pref = mVpnPreferenceMap.get(p.getName());
716 switch (p.getState()) {
727 changeState(p, VpnState.DISCONNECTING);
728 getActor(p).disconnect();
733 private void changeState(VpnProfile p, VpnState state) {
734 VpnState oldState = p.getState();
735 if (oldState == state) return;
738 mVpnPreferenceMap.get(p.getName()).setSummary(
739 getProfileSummaryString(p));
743 mConnectingActor = null;
745 disableProfilePreferencesIfOneActive();
749 mConnectingActor = getActor(p);
753 disableProfilePreferencesIfOneActive();
757 changeState(p, VpnState.IDLE);
761 assert(mActiveProfile == p);
763 if (mConnectingErrorCode == NO_ERROR) {
766 showDialog(mConnectingErrorCode);
767 mConnectingErrorCode = NO_ERROR;
773 private void onIdle() {
774 Log.d(TAG, " onIdle()");
775 mActiveProfile = null;
776 mConnectingActor = null;
777 enableProfilePreferences();
780 private void disableProfilePreferencesIfOneActive() {
781 if (mActiveProfile == null) return;
783 for (VpnProfile p : mVpnProfileList) {
784 switch (p.getState()) {
788 mVpnPreferenceMap.get(p.getName()).setEnabled(false);
792 mVpnPreferenceMap.get(p.getName()).setEnabled(true);
797 private void enableProfilePreferences() {
798 for (VpnProfile p : mVpnProfileList) {
799 mVpnPreferenceMap.get(p.getName()).setEnabled(true);
803 static String getProfileDir(VpnProfile p) {
804 return PROFILES_ROOT + p.getId();
807 static void saveProfileToStorage(VpnProfile p) throws IOException {
808 File f = new File(getProfileDir(p));
809 if (!f.exists()) f.mkdirs();
810 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
811 new File(f, PROFILE_OBJ_FILE)));
816 private void removeProfileFromStorage(VpnProfile p) {
817 Util.deleteFile(getProfileDir(p));
820 private void retrieveVpnListFromStorage() {
821 mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>();
822 mVpnProfileList = Collections.synchronizedList(
823 new ArrayList<VpnProfile>());
824 mVpnListContainer.removeAll();
826 File root = new File(PROFILES_ROOT);
827 String[] dirs = root.list();
828 if (dirs == null) return;
829 for (String dir : dirs) {
830 File f = new File(new File(root, dir), PROFILE_OBJ_FILE);
831 if (!f.exists()) continue;
833 VpnProfile p = deserialize(f);
834 if (p == null) continue;
835 if (!checkIdConsistency(dir, p)) continue;
837 mVpnProfileList.add(p);
838 } catch (IOException e) {
839 Log.e(TAG, "retrieveVpnListFromStorage()", e);
842 Collections.sort(mVpnProfileList, new Comparator<VpnProfile>() {
843 public int compare(VpnProfile p1, VpnProfile p2) {
844 return p1.getName().compareTo(p2.getName());
847 for (VpnProfile p : mVpnProfileList) {
848 Preference pref = addPreferenceFor(p, false);
850 disableProfilePreferencesIfOneActive();
853 private void checkVpnConnectionStatusInBackground() {
854 new Thread(new Runnable() {
856 mStatusChecker.check(mVpnProfileList);
861 // A sanity check. Returns true if the profile directory name and profile ID
863 private boolean checkIdConsistency(String dirName, VpnProfile p) {
864 if (!dirName.equals(p.getId())) {
865 Log.d(TAG, "ID inconsistent: " + dirName + " vs " + p.getId());
872 private VpnProfile deserialize(File profileObjectFile) throws IOException {
874 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
876 VpnProfile p = (VpnProfile) ois.readObject();
879 } catch (ClassNotFoundException e) {
880 Log.d(TAG, "deserialize a profile", e);
885 private String getProfileSummaryString(VpnProfile p) {
886 final Activity activity = getActivity();
887 switch (p.getState()) {
889 return activity.getString(R.string.vpn_connecting);
891 return activity.getString(R.string.vpn_disconnecting);
893 return activity.getString(R.string.vpn_connected);
895 return activity.getString(R.string.vpn_connect_hint);
899 private VpnProfileActor getActor(VpnProfile p) {
900 return new AuthenticationActor(getActivity(), p);
903 private VpnProfile createVpnProfile(String type) {
904 return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type));
907 private boolean checkSecrets(VpnProfile p) {
908 boolean secretMissing = false;
910 if (p instanceof L2tpIpsecProfile) {
911 L2tpIpsecProfile certProfile = (L2tpIpsecProfile) p;
913 String cert = certProfile.getCaCertificate();
914 if (TextUtils.isEmpty(cert) ||
915 !mKeyStore.contains(Credentials.CA_CERTIFICATE + cert)) {
916 certProfile.setCaCertificate(null);
917 secretMissing = true;
920 cert = certProfile.getUserCertificate();
921 if (TextUtils.isEmpty(cert) ||
922 !mKeyStore.contains(Credentials.USER_CERTIFICATE + cert)) {
923 certProfile.setUserCertificate(null);
924 secretMissing = true;
928 if (p instanceof L2tpIpsecPskProfile) {
929 L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
930 String presharedKey = pskProfile.getPresharedKey();
931 String key = KEY_PREFIX_IPSEC_PSK + p.getId();
932 if (TextUtils.isEmpty(presharedKey) || !mKeyStore.contains(key)) {
933 pskProfile.setPresharedKey(null);
934 secretMissing = true;
938 if (p instanceof L2tpProfile) {
939 L2tpProfile l2tpProfile = (L2tpProfile) p;
940 if (l2tpProfile.isSecretEnabled()) {
941 String secret = l2tpProfile.getSecretString();
942 String key = KEY_PREFIX_L2TP_SECRET + p.getId();
943 if (TextUtils.isEmpty(secret) || !mKeyStore.contains(key)) {
944 l2tpProfile.setSecretString(null);
945 secretMissing = true;
952 showDialog(DIALOG_SECRET_NOT_SET);
959 private void processSecrets(VpnProfile p) {
960 switch (p.getType()) {
962 L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
963 String presharedKey = pskProfile.getPresharedKey();
964 String key = KEY_PREFIX_IPSEC_PSK + p.getId();
965 if (!TextUtils.isEmpty(presharedKey) &&
966 !mKeyStore.put(key, presharedKey)) {
967 Log.e(TAG, "keystore write failed: key=" + key);
969 pskProfile.setPresharedKey(key);
973 L2tpProfile l2tpProfile = (L2tpProfile) p;
974 key = KEY_PREFIX_L2TP_SECRET + p.getId();
975 if (l2tpProfile.isSecretEnabled()) {
976 String secret = l2tpProfile.getSecretString();
977 if (!TextUtils.isEmpty(secret) &&
978 !mKeyStore.put(key, secret)) {
979 Log.e(TAG, "keystore write failed: key=" + key);
981 l2tpProfile.setSecretString(key);
983 mKeyStore.delete(key);
989 private class VpnPreference extends Preference {
991 VpnPreference(Context c, VpnProfile p) {
996 void setProfile(VpnProfile p) {
998 setTitle(p.getName());
999 setSummary(getProfileSummaryString(p));
1003 // to receive vpn connectivity events broadcast by VpnService
1004 private class ConnectivityReceiver extends BroadcastReceiver {
1006 public void onReceive(Context context, Intent intent) {
1007 String profileName = intent.getStringExtra(
1008 VpnManager.BROADCAST_PROFILE_NAME);
1009 if (profileName == null) return;
1011 VpnState s = (VpnState) intent.getSerializableExtra(
1012 VpnManager.BROADCAST_CONNECTION_STATE);
1015 Log.e(TAG, "received null connectivity state");
1019 mConnectingErrorCode = intent.getIntExtra(
1020 VpnManager.BROADCAST_ERROR_CODE, NO_ERROR);
1022 VpnPreference pref = mVpnPreferenceMap.get(profileName);
1024 Log.d(TAG, "received connectivity: " + profileName
1025 + ": connected? " + s
1026 + " err=" + mConnectingErrorCode);
1027 changeState(pref.mProfile, s);
1029 Log.e(TAG, "received connectivity: " + profileName
1030 + ": connected? " + s + ", but profile does not exist;"
1031 + " just ignore it");
1036 // managing status check in a background thread
1037 private class StatusChecker {
1038 synchronized void check(final List<VpnProfile> list) {
1039 final ConditionVariable cv = new ConditionVariable();
1041 mVpnManager.startVpnService();
1042 ServiceConnection c = new ServiceConnection() {
1043 public synchronized void onServiceConnected(
1044 ComponentName className, IBinder binder) {
1047 IVpnService service = IVpnService.Stub.asInterface(binder);
1048 for (VpnProfile p : list) {
1050 service.checkStatus(p);
1051 } catch (Throwable e) {
1052 Log.e(TAG, " --- checkStatus(): " + p.getName(), e);
1053 changeState(p, VpnState.IDLE);
1056 getActivity().unbindService(this);
1060 public void onServiceDisconnected(ComponentName className) {
1063 setDefaultState(list);
1064 getActivity().unbindService(this);
1068 if (mVpnManager.bindVpnService(c)) {
1069 if (!cv.block(1000)) {
1070 Log.d(TAG, "checkStatus() bindService failed");
1071 setDefaultState(list);
1074 setDefaultState(list);
1078 private void showPreferences() {
1079 for (VpnProfile p : mVpnProfileList) {
1080 VpnPreference pref = mVpnPreferenceMap.get(p.getName());
1081 mVpnListContainer.addPreference(pref);
1085 private void setDefaultState(List<VpnProfile> list) {
1086 for (VpnProfile p : list) changeState(p, VpnState.IDLE);