1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17package com.android.settings.vpn; 18 19import com.android.settings.R; 20 21import android.app.AlertDialog; 22import android.app.Dialog; 23import android.content.ComponentName; 24import android.content.BroadcastReceiver; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.Intent; 28import android.content.ServiceConnection; 29import android.net.vpn.IVpnService; 30import android.net.vpn.L2tpIpsecProfile; 31import android.net.vpn.L2tpIpsecPskProfile; 32import android.net.vpn.L2tpProfile; 33import android.net.vpn.VpnManager; 34import android.net.vpn.VpnProfile; 35import android.net.vpn.VpnState; 36import android.net.vpn.VpnType; 37import android.os.Bundle; 38import android.os.ConditionVariable; 39import android.os.IBinder; 40import android.os.Parcelable; 41import android.preference.Preference; 42import android.preference.PreferenceActivity; 43import android.preference.PreferenceCategory; 44import android.preference.PreferenceScreen; 45import android.preference.Preference.OnPreferenceClickListener; 46import android.security.Credentials; 47import android.security.KeyStore; 48import android.text.TextUtils; 49import android.util.Log; 50import android.view.ContextMenu; 51import android.view.ContextMenu.ContextMenuInfo; 52import android.view.MenuItem; 53import android.view.View; 54import android.widget.AdapterView.AdapterContextMenuInfo; 55 56import java.io.File; 57import java.io.FileInputStream; 58import java.io.FileOutputStream; 59import java.io.IOException; 60import java.io.ObjectInputStream; 61import java.io.ObjectOutputStream; 62import java.util.ArrayList; 63import java.util.Collections; 64import java.util.Comparator; 65import java.util.LinkedHashMap; 66import java.util.List; 67import java.util.Map; 68 69/** 70 * The preference activity for configuring VPN settings. 71 */ 72public class VpnSettings extends PreferenceActivity implements 73 DialogInterface.OnClickListener { 74 // Key to the field exchanged for profile editing. 75 static final String KEY_VPN_PROFILE = "vpn_profile"; 76 77 // Key to the field exchanged for VPN type selection. 78 static final String KEY_VPN_TYPE = "vpn_type"; 79 80 private static final String TAG = VpnSettings.class.getSimpleName(); 81 82 private static final String PREF_ADD_VPN = "add_new_vpn"; 83 private static final String PREF_VPN_LIST = "vpn_list"; 84 85 private static final String PROFILES_ROOT = VpnManager.getProfilePath() + "/"; 86 private static final String PROFILE_OBJ_FILE = ".pobj"; 87 88 private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1; 89 private static final int REQUEST_SELECT_VPN_TYPE = 2; 90 91 private static final int CONTEXT_MENU_CONNECT_ID = ContextMenu.FIRST + 0; 92 private static final int CONTEXT_MENU_DISCONNECT_ID = ContextMenu.FIRST + 1; 93 private static final int CONTEXT_MENU_EDIT_ID = ContextMenu.FIRST + 2; 94 private static final int CONTEXT_MENU_DELETE_ID = ContextMenu.FIRST + 3; 95 96 private static final int CONNECT_BUTTON = DialogInterface.BUTTON_POSITIVE; 97 private static final int OK_BUTTON = DialogInterface.BUTTON_POSITIVE; 98 99 private static final int DIALOG_CONNECT = VpnManager.VPN_ERROR_LARGEST + 1; 100 private static final int DIALOG_SECRET_NOT_SET = DIALOG_CONNECT + 1; 101 102 private static final int NO_ERROR = VpnManager.VPN_ERROR_NO_ERROR; 103 104 private static final String KEY_PREFIX_IPSEC_PSK = Credentials.VPN + 'i'; 105 private static final String KEY_PREFIX_L2TP_SECRET = Credentials.VPN + 'l'; 106 107 private PreferenceScreen mAddVpn; 108 private PreferenceCategory mVpnListContainer; 109 110 // profile name --> VpnPreference 111 private Map<String, VpnPreference> mVpnPreferenceMap; 112 private List<VpnProfile> mVpnProfileList; 113 114 // profile engaged in a connection 115 private VpnProfile mActiveProfile; 116 117 // actor engaged in connecting 118 private VpnProfileActor mConnectingActor; 119 120 // states saved for unlocking keystore 121 private Runnable mUnlockAction; 122 123 private KeyStore mKeyStore = KeyStore.getInstance(); 124 125 private VpnManager mVpnManager = new VpnManager(this); 126 127 private ConnectivityReceiver mConnectivityReceiver = 128 new ConnectivityReceiver(); 129 130 private int mConnectingErrorCode = NO_ERROR; 131 132 private Dialog mShowingDialog; 133 134 private StatusChecker mStatusChecker = new StatusChecker(); 135 136 @Override 137 public void onCreate(Bundle savedInstanceState) { 138 super.onCreate(savedInstanceState); 139 addPreferencesFromResource(R.xml.vpn_settings); 140 141 // restore VpnProfile list and construct VpnPreference map 142 mVpnListContainer = (PreferenceCategory) findPreference(PREF_VPN_LIST); 143 144 // set up the "add vpn" preference 145 mAddVpn = (PreferenceScreen) findPreference(PREF_ADD_VPN); 146 mAddVpn.setOnPreferenceClickListener( 147 new OnPreferenceClickListener() { 148 public boolean onPreferenceClick(Preference preference) { 149 startVpnTypeSelection(); 150 return true; 151 } 152 }); 153 154 // for long-press gesture on a profile preference 155 registerForContextMenu(getListView()); 156 157 // listen to vpn connectivity event 158 mVpnManager.registerConnectivityReceiver(mConnectivityReceiver); 159 160 retrieveVpnListFromStorage(); 161 checkVpnConnectionStatusInBackground(); 162 } 163 164 @Override 165 public void onResume() { 166 super.onResume(); 167 168 if ((mUnlockAction != null) && isKeyStoreUnlocked()) { 169 Runnable action = mUnlockAction; 170 mUnlockAction = null; 171 runOnUiThread(action); 172 } 173 } 174 175 @Override 176 protected void onDestroy() { 177 super.onDestroy(); 178 unregisterForContextMenu(getListView()); 179 mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver); 180 if ((mShowingDialog != null) && mShowingDialog.isShowing()) { 181 mShowingDialog.dismiss(); 182 } 183 } 184 185 @Override 186 protected Dialog onCreateDialog (int id) { 187 switch (id) { 188 case DIALOG_CONNECT: 189 return createConnectDialog(); 190 191 case DIALOG_SECRET_NOT_SET: 192 return createSecretNotSetDialog(); 193 194 case VpnManager.VPN_ERROR_CHALLENGE: 195 case VpnManager.VPN_ERROR_UNKNOWN_SERVER: 196 case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED: 197 return createEditDialog(id); 198 199 default: 200 Log.d(TAG, "create reconnect dialog for event " + id); 201 return createReconnectDialog(id); 202 } 203 } 204 205 private Dialog createConnectDialog() { 206 return new AlertDialog.Builder(this) 207 .setView(mConnectingActor.createConnectView()) 208 .setTitle(String.format(getString(R.string.vpn_connect_to), 209 mActiveProfile.getName())) 210 .setPositiveButton(getString(R.string.vpn_connect_button), 211 this) 212 .setNegativeButton(getString(android.R.string.cancel), 213 this) 214 .setOnCancelListener(new DialogInterface.OnCancelListener() { 215 public void onCancel(DialogInterface dialog) { 216 removeDialog(DIALOG_CONNECT); 217 changeState(mActiveProfile, VpnState.IDLE); 218 } 219 }) 220 .create(); 221 } 222 223 private Dialog createReconnectDialog(int id) { 224 int msgId; 225 switch (id) { 226 case VpnManager.VPN_ERROR_AUTH: 227 msgId = R.string.vpn_auth_error_dialog_msg; 228 break; 229 230 case VpnManager.VPN_ERROR_REMOTE_HUNG_UP: 231 msgId = R.string.vpn_remote_hung_up_error_dialog_msg; 232 break; 233 234 case VpnManager.VPN_ERROR_CONNECTION_LOST: 235 msgId = R.string.vpn_reconnect_from_lost; 236 break; 237 238 case VpnManager.VPN_ERROR_REMOTE_PPP_HUNG_UP: 239 msgId = R.string.vpn_remote_ppp_hung_up_error_dialog_msg; 240 break; 241 242 default: 243 msgId = R.string.vpn_confirm_reconnect; 244 } 245 return createCommonDialogBuilder().setMessage(msgId).create(); 246 } 247 248 private Dialog createEditDialog(int id) { 249 int msgId; 250 switch (id) { 251 case VpnManager.VPN_ERROR_CHALLENGE: 252 msgId = R.string.vpn_challenge_error_dialog_msg; 253 break; 254 255 case VpnManager.VPN_ERROR_UNKNOWN_SERVER: 256 msgId = R.string.vpn_unknown_server_dialog_msg; 257 break; 258 259 case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED: 260 msgId = R.string.vpn_ppp_negotiation_failed_dialog_msg; 261 break; 262 263 default: 264 return null; 265 } 266 return createCommonEditDialogBuilder().setMessage(msgId).create(); 267 } 268 269 private Dialog createSecretNotSetDialog() { 270 return createCommonDialogBuilder() 271 .setMessage(R.string.vpn_secret_not_set_dialog_msg) 272 .setPositiveButton(R.string.vpn_yes_button, 273 new DialogInterface.OnClickListener() { 274 public void onClick(DialogInterface dialog, int w) { 275 startVpnEditor(mActiveProfile); 276 } 277 }) 278 .create(); 279 } 280 281 private AlertDialog.Builder createCommonEditDialogBuilder() { 282 return createCommonDialogBuilder() 283 .setPositiveButton(R.string.vpn_yes_button, 284 new DialogInterface.OnClickListener() { 285 public void onClick(DialogInterface dialog, int w) { 286 VpnProfile p = mActiveProfile; 287 onIdle(); 288 startVpnEditor(p); 289 } 290 }); 291 } 292 293 private AlertDialog.Builder createCommonDialogBuilder() { 294 return new AlertDialog.Builder(this) 295 .setTitle(android.R.string.dialog_alert_title) 296 .setIcon(android.R.drawable.ic_dialog_alert) 297 .setPositiveButton(R.string.vpn_yes_button, 298 new DialogInterface.OnClickListener() { 299 public void onClick(DialogInterface dialog, int w) { 300 connectOrDisconnect(mActiveProfile); 301 } 302 }) 303 .setNegativeButton(R.string.vpn_no_button, 304 new DialogInterface.OnClickListener() { 305 public void onClick(DialogInterface dialog, int w) { 306 onIdle(); 307 } 308 }) 309 .setOnCancelListener(new DialogInterface.OnCancelListener() { 310 public void onCancel(DialogInterface dialog) { 311 onIdle(); 312 } 313 }); 314 } 315 316 @Override 317 public void onCreateContextMenu(ContextMenu menu, View v, 318 ContextMenuInfo menuInfo) { 319 super.onCreateContextMenu(menu, v, menuInfo); 320 321 VpnProfile p = getProfile(getProfilePositionFrom( 322 (AdapterContextMenuInfo) menuInfo)); 323 if (p != null) { 324 VpnState state = p.getState(); 325 menu.setHeaderTitle(p.getName()); 326 327 boolean isIdle = (state == VpnState.IDLE); 328 boolean isNotConnect = (isIdle || (state == VpnState.DISCONNECTING) 329 || (state == VpnState.CANCELLED)); 330 menu.add(0, CONTEXT_MENU_CONNECT_ID, 0, R.string.vpn_menu_connect) 331 .setEnabled(isIdle && (mActiveProfile == null)); 332 menu.add(0, CONTEXT_MENU_DISCONNECT_ID, 0, 333 R.string.vpn_menu_disconnect) 334 .setEnabled(state == VpnState.CONNECTED); 335 menu.add(0, CONTEXT_MENU_EDIT_ID, 0, R.string.vpn_menu_edit) 336 .setEnabled(isNotConnect); 337 menu.add(0, CONTEXT_MENU_DELETE_ID, 0, R.string.vpn_menu_delete) 338 .setEnabled(isNotConnect); 339 } 340 } 341 342 @Override 343 public boolean onContextItemSelected(MenuItem item) { 344 int position = getProfilePositionFrom( 345 (AdapterContextMenuInfo) item.getMenuInfo()); 346 VpnProfile p = getProfile(position); 347 348 switch(item.getItemId()) { 349 case CONTEXT_MENU_CONNECT_ID: 350 case CONTEXT_MENU_DISCONNECT_ID: 351 connectOrDisconnect(p); 352 return true; 353 354 case CONTEXT_MENU_EDIT_ID: 355 startVpnEditor(p); 356 return true; 357 358 case CONTEXT_MENU_DELETE_ID: 359 deleteProfile(position); 360 return true; 361 } 362 363 return super.onContextItemSelected(item); 364 } 365 366 @Override 367 protected void onActivityResult(final int requestCode, final int resultCode, 368 final Intent data) { 369 if ((resultCode == RESULT_CANCELED) || (data == null)) { 370 Log.d(TAG, "no result returned by editor"); 371 return; 372 } 373 374 if (requestCode == REQUEST_SELECT_VPN_TYPE) { 375 String typeName = data.getStringExtra(KEY_VPN_TYPE); 376 startVpnEditor(createVpnProfile(typeName)); 377 } else if (requestCode == REQUEST_ADD_OR_EDIT_PROFILE) { 378 VpnProfile p = data.getParcelableExtra(KEY_VPN_PROFILE); 379 if (p == null) { 380 Log.e(TAG, "null object returned by editor"); 381 return; 382 } 383 384 int index = getProfileIndexFromId(p.getId()); 385 if (checkDuplicateName(p, index)) { 386 final VpnProfile profile = p; 387 Util.showErrorMessage(this, String.format( 388 getString(R.string.vpn_error_duplicate_name), 389 p.getName()), 390 new DialogInterface.OnClickListener() { 391 public void onClick(DialogInterface dialog, int w) { 392 startVpnEditor(profile); 393 } 394 }); 395 return; 396 } 397 398 if (needKeyStoreToSave(p)) { 399 Runnable action = new Runnable() { 400 public void run() { 401 onActivityResult(requestCode, resultCode, data); 402 } 403 }; 404 if (!unlockKeyStore(p, action)) return; 405 } 406 407 try { 408 if (index < 0) { 409 addProfile(p); 410 Util.showShortToastMessage(this, String.format( 411 getString(R.string.vpn_profile_added), p.getName())); 412 } else { 413 replaceProfile(index, p); 414 Util.showShortToastMessage(this, String.format( 415 getString(R.string.vpn_profile_replaced), 416 p.getName())); 417 } 418 } catch (IOException e) { 419 final VpnProfile profile = p; 420 Util.showErrorMessage(this, e + ": " + e.getMessage(), 421 new DialogInterface.OnClickListener() { 422 public void onClick(DialogInterface dialog, int w) { 423 startVpnEditor(profile); 424 } 425 }); 426 } 427 } else { 428 throw new RuntimeException("unknown request code: " + requestCode); 429 } 430 } 431 432 // Called when the buttons on the connect dialog are clicked. 433 //@Override 434 public synchronized void onClick(DialogInterface dialog, int which) { 435 if (which == CONNECT_BUTTON) { 436 Dialog d = (Dialog) dialog; 437 String error = mConnectingActor.validateInputs(d); 438 if (error == null) { 439 mConnectingActor.connect(d); 440 removeDialog(DIALOG_CONNECT); 441 return; 442 } else { 443 dismissDialog(DIALOG_CONNECT); 444 // show error dialog 445 mShowingDialog = new AlertDialog.Builder(this) 446 .setTitle(android.R.string.dialog_alert_title) 447 .setIcon(android.R.drawable.ic_dialog_alert) 448 .setMessage(String.format(getString( 449 R.string.vpn_error_miss_entering), error)) 450 .setPositiveButton(R.string.vpn_back_button, 451 new DialogInterface.OnClickListener() { 452 public void onClick(DialogInterface dialog, 453 int which) { 454 showDialog(DIALOG_CONNECT); 455 } 456 }) 457 .create(); 458 mShowingDialog.show(); 459 } 460 } else { 461 removeDialog(DIALOG_CONNECT); 462 changeState(mActiveProfile, VpnState.IDLE); 463 } 464 } 465 466 private int getProfileIndexFromId(String id) { 467 int index = 0; 468 for (VpnProfile p : mVpnProfileList) { 469 if (p.getId().equals(id)) { 470 return index; 471 } else { 472 index++; 473 } 474 } 475 return -1; 476 } 477 478 // Replaces the profile at index in mVpnProfileList with p. 479 // Returns true if p's name is a duplicate. 480 private boolean checkDuplicateName(VpnProfile p, int index) { 481 List<VpnProfile> list = mVpnProfileList; 482 VpnPreference pref = mVpnPreferenceMap.get(p.getName()); 483 if ((pref != null) && (index >= 0) && (index < list.size())) { 484 // not a duplicate if p is to replace the profile at index 485 if (pref.mProfile == list.get(index)) pref = null; 486 } 487 return (pref != null); 488 } 489 490 private int getProfilePositionFrom(AdapterContextMenuInfo menuInfo) { 491 // excludes mVpnListContainer and the preferences above it 492 return menuInfo.position - mVpnListContainer.getOrder() - 1; 493 } 494 495 // position: position in mVpnProfileList 496 private VpnProfile getProfile(int position) { 497 return ((position >= 0) ? mVpnProfileList.get(position) : null); 498 } 499 500 // position: position in mVpnProfileList 501 private void deleteProfile(final int position) { 502 if ((position < 0) || (position >= mVpnProfileList.size())) return; 503 DialogInterface.OnClickListener onClickListener = 504 new DialogInterface.OnClickListener() { 505 public void onClick(DialogInterface dialog, int which) { 506 dialog.dismiss(); 507 if (which == OK_BUTTON) { 508 VpnProfile p = mVpnProfileList.remove(position); 509 VpnPreference pref = 510 mVpnPreferenceMap.remove(p.getName()); 511 mVpnListContainer.removePreference(pref); 512 removeProfileFromStorage(p); 513 } 514 } 515 }; 516 mShowingDialog = new AlertDialog.Builder(this) 517 .setTitle(android.R.string.dialog_alert_title) 518 .setIcon(android.R.drawable.ic_dialog_alert) 519 .setMessage(R.string.vpn_confirm_profile_deletion) 520 .setPositiveButton(android.R.string.ok, onClickListener) 521 .setNegativeButton(R.string.vpn_no_button, onClickListener) 522 .create(); 523 mShowingDialog.show(); 524 } 525 526 // Randomly generates an ID for the profile. 527 // The ID is unique and only set once when the profile is created. 528 private void setProfileId(VpnProfile profile) { 529 String id; 530 531 while (true) { 532 id = String.valueOf(Math.abs( 533 Double.doubleToLongBits(Math.random()))); 534 if (id.length() >= 8) break; 535 } 536 for (VpnProfile p : mVpnProfileList) { 537 if (p.getId().equals(id)) { 538 setProfileId(profile); 539 return; 540 } 541 } 542 profile.setId(id); 543 } 544 545 private void addProfile(VpnProfile p) throws IOException { 546 setProfileId(p); 547 processSecrets(p); 548 saveProfileToStorage(p); 549 550 mVpnProfileList.add(p); 551 addPreferenceFor(p); 552 disableProfilePreferencesIfOneActive(); 553 } 554 555 private VpnPreference addPreferenceFor(VpnProfile p) { 556 return addPreferenceFor(p, true); 557 } 558 559 // Adds a preference in mVpnListContainer 560 private VpnPreference addPreferenceFor( 561 VpnProfile p, boolean addToContainer) { 562 VpnPreference pref = new VpnPreference(this, p); 563 mVpnPreferenceMap.put(p.getName(), pref); 564 if (addToContainer) mVpnListContainer.addPreference(pref); 565 566 pref.setOnPreferenceClickListener( 567 new Preference.OnPreferenceClickListener() { 568 public boolean onPreferenceClick(Preference pref) { 569 connectOrDisconnect(((VpnPreference) pref).mProfile); 570 return true; 571 } 572 }); 573 return pref; 574 } 575 576 // index: index to mVpnProfileList 577 private void replaceProfile(int index, VpnProfile p) throws IOException { 578 Map<String, VpnPreference> map = mVpnPreferenceMap; 579 VpnProfile oldProfile = mVpnProfileList.set(index, p); 580 VpnPreference pref = map.remove(oldProfile.getName()); 581 if (pref.mProfile != oldProfile) { 582 throw new RuntimeException("inconsistent state!"); 583 } 584 585 p.setId(oldProfile.getId()); 586 587 processSecrets(p); 588 589 // TODO: remove copyFiles once the setId() code propagates. 590 // Copy config files and remove the old ones if they are in different 591 // directories. 592 if (Util.copyFiles(getProfileDir(oldProfile), getProfileDir(p))) { 593 removeProfileFromStorage(oldProfile); 594 } 595 saveProfileToStorage(p); 596 597 pref.setProfile(p); 598 map.put(p.getName(), pref); 599 } 600 601 private void startVpnTypeSelection() { 602 Intent intent = new Intent(this, VpnTypeSelection.class); 603 startActivityForResult(intent, REQUEST_SELECT_VPN_TYPE); 604 } 605 606 private boolean isKeyStoreUnlocked() { 607 return mKeyStore.test() == KeyStore.NO_ERROR; 608 } 609 610 // Returns true if the profile needs to access keystore 611 private boolean needKeyStoreToSave(VpnProfile p) { 612 switch (p.getType()) { 613 case L2TP_IPSEC_PSK: 614 L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p; 615 String presharedKey = pskProfile.getPresharedKey(); 616 if (!TextUtils.isEmpty(presharedKey)) return true; 617 // pass through 618 619 case L2TP: 620 L2tpProfile l2tpProfile = (L2tpProfile) p; 621 if (l2tpProfile.isSecretEnabled() && 622 !TextUtils.isEmpty(l2tpProfile.getSecretString())) { 623 return true; 624 } 625 // pass through 626 627 default: 628 return false; 629 } 630 } 631 632 // Returns true if the profile needs to access keystore 633 private boolean needKeyStoreToConnect(VpnProfile p) { 634 switch (p.getType()) { 635 case L2TP_IPSEC: 636 case L2TP_IPSEC_PSK: 637 return true; 638 639 case L2TP: 640 return ((L2tpProfile) p).isSecretEnabled(); 641 642 default: 643 return false; 644 } 645 } 646 647 // Returns true if keystore is unlocked or keystore is not a concern 648 private boolean unlockKeyStore(VpnProfile p, Runnable action) { 649 if (isKeyStoreUnlocked()) return true; 650 mUnlockAction = action; 651 Credentials.getInstance().unlock(this); 652 return false; 653 } 654 655 private void startVpnEditor(final VpnProfile profile) { 656 Intent intent = new Intent(this, VpnEditor.class); 657 intent.putExtra(KEY_VPN_PROFILE, (Parcelable) profile); 658 startActivityForResult(intent, REQUEST_ADD_OR_EDIT_PROFILE); 659 } 660 661 private synchronized void connect(final VpnProfile p) { 662 if (needKeyStoreToConnect(p)) { 663 Runnable action = new Runnable() { 664 public void run() { 665 connect(p); 666 } 667 }; 668 if (!unlockKeyStore(p, action)) return; 669 } 670 671 if (!checkSecrets(p)) return; 672 changeState(p, VpnState.CONNECTING); 673 if (mConnectingActor.isConnectDialogNeeded()) { 674 showDialog(DIALOG_CONNECT); 675 } else { 676 mConnectingActor.connect(null); 677 } 678 } 679 680 // Do connect or disconnect based on the current state. 681 private synchronized void connectOrDisconnect(VpnProfile p) { 682 VpnPreference pref = mVpnPreferenceMap.get(p.getName()); 683 switch (p.getState()) { 684 case IDLE: 685 connect(p); 686 break; 687 688 case CONNECTING: 689 // do nothing 690 break; 691 692 case CONNECTED: 693 case DISCONNECTING: 694 changeState(p, VpnState.DISCONNECTING); 695 getActor(p).disconnect(); 696 break; 697 } 698 } 699 700 private void changeState(VpnProfile p, VpnState state) { 701 VpnState oldState = p.getState(); 702 if (oldState == state) return; 703 704 p.setState(state); 705 mVpnPreferenceMap.get(p.getName()).setSummary( 706 getProfileSummaryString(p)); 707 708 switch (state) { 709 case CONNECTED: 710 mConnectingActor = null; 711 mActiveProfile = p; 712 disableProfilePreferencesIfOneActive(); 713 break; 714 715 case CONNECTING: 716 mConnectingActor = getActor(p); 717 // pass through 718 case DISCONNECTING: 719 mActiveProfile = p; 720 disableProfilePreferencesIfOneActive(); 721 break; 722 723 case CANCELLED: 724 changeState(p, VpnState.IDLE); 725 break; 726 727 case IDLE: 728 assert(mActiveProfile == p); 729 730 if (mConnectingErrorCode == NO_ERROR) { 731 onIdle(); 732 } else { 733 showDialog(mConnectingErrorCode); 734 mConnectingErrorCode = NO_ERROR; 735 } 736 break; 737 } 738 } 739 740 private void onIdle() { 741 Log.d(TAG, " onIdle()"); 742 mActiveProfile = null; 743 mConnectingActor = null; 744 enableProfilePreferences(); 745 } 746 747 private void disableProfilePreferencesIfOneActive() { 748 if (mActiveProfile == null) return; 749 750 for (VpnProfile p : mVpnProfileList) { 751 switch (p.getState()) { 752 case CONNECTING: 753 case DISCONNECTING: 754 case IDLE: 755 mVpnPreferenceMap.get(p.getName()).setEnabled(false); 756 break; 757 758 default: 759 mVpnPreferenceMap.get(p.getName()).setEnabled(true); 760 } 761 } 762 } 763 764 private void enableProfilePreferences() { 765 for (VpnProfile p : mVpnProfileList) { 766 mVpnPreferenceMap.get(p.getName()).setEnabled(true); 767 } 768 } 769 770 static String getProfileDir(VpnProfile p) { 771 return PROFILES_ROOT + p.getId(); 772 } 773 774 static void saveProfileToStorage(VpnProfile p) throws IOException { 775 File f = new File(getProfileDir(p)); 776 if (!f.exists()) f.mkdirs(); 777 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream( 778 new File(f, PROFILE_OBJ_FILE))); 779 oos.writeObject(p); 780 oos.close(); 781 } 782 783 private void removeProfileFromStorage(VpnProfile p) { 784 Util.deleteFile(getProfileDir(p)); 785 } 786 787 private void retrieveVpnListFromStorage() { 788 mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>(); 789 mVpnProfileList = Collections.synchronizedList( 790 new ArrayList<VpnProfile>()); 791 mVpnListContainer.removeAll(); 792 793 File root = new File(PROFILES_ROOT); 794 String[] dirs = root.list(); 795 if (dirs == null) return; 796 for (String dir : dirs) { 797 File f = new File(new File(root, dir), PROFILE_OBJ_FILE); 798 if (!f.exists()) continue; 799 try { 800 VpnProfile p = deserialize(f); 801 if (p == null) continue; 802 if (!checkIdConsistency(dir, p)) continue; 803 804 mVpnProfileList.add(p); 805 } catch (IOException e) { 806 Log.e(TAG, "retrieveVpnListFromStorage()", e); 807 } 808 } 809 Collections.sort(mVpnProfileList, new Comparator<VpnProfile>() { 810 public int compare(VpnProfile p1, VpnProfile p2) { 811 return p1.getName().compareTo(p2.getName()); 812 } 813 814 public boolean equals(VpnProfile p) { 815 // not used 816 return false; 817 } 818 }); 819 for (VpnProfile p : mVpnProfileList) { 820 Preference pref = addPreferenceFor(p, false); 821 } 822 disableProfilePreferencesIfOneActive(); 823 } 824 825 private void checkVpnConnectionStatusInBackground() { 826 new Thread(new Runnable() { 827 public void run() { 828 mStatusChecker.check(mVpnProfileList); 829 } 830 }).start(); 831 } 832 833 // A sanity check. Returns true if the profile directory name and profile ID 834 // are consistent. 835 private boolean checkIdConsistency(String dirName, VpnProfile p) { 836 if (!dirName.equals(p.getId())) { 837 Log.d(TAG, "ID inconsistent: " + dirName + " vs " + p.getId()); 838 return false; 839 } else { 840 return true; 841 } 842 } 843 844 private VpnProfile deserialize(File profileObjectFile) throws IOException { 845 try { 846 ObjectInputStream ois = new ObjectInputStream(new FileInputStream( 847 profileObjectFile)); 848 VpnProfile p = (VpnProfile) ois.readObject(); 849 ois.close(); 850 return p; 851 } catch (ClassNotFoundException e) { 852 Log.d(TAG, "deserialize a profile", e); 853 return null; 854 } 855 } 856 857 private String getProfileSummaryString(VpnProfile p) { 858 switch (p.getState()) { 859 case CONNECTING: 860 return getString(R.string.vpn_connecting); 861 case DISCONNECTING: 862 return getString(R.string.vpn_disconnecting); 863 case CONNECTED: 864 return getString(R.string.vpn_connected); 865 default: 866 return getString(R.string.vpn_connect_hint); 867 } 868 } 869 870 private VpnProfileActor getActor(VpnProfile p) { 871 return new AuthenticationActor(this, p); 872 } 873 874 private VpnProfile createVpnProfile(String type) { 875 return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type)); 876 } 877 878 private boolean checkSecrets(VpnProfile p) { 879 boolean secretMissing = false; 880 881 if (p instanceof L2tpIpsecProfile) { 882 L2tpIpsecProfile certProfile = (L2tpIpsecProfile) p; 883 884 String cert = certProfile.getCaCertificate(); 885 if (TextUtils.isEmpty(cert) || 886 !mKeyStore.contains(Credentials.CA_CERTIFICATE + cert)) { 887 certProfile.setCaCertificate(null); 888 secretMissing = true; 889 } 890 891 cert = certProfile.getUserCertificate(); 892 if (TextUtils.isEmpty(cert) || 893 !mKeyStore.contains(Credentials.USER_CERTIFICATE + cert)) { 894 certProfile.setUserCertificate(null); 895 secretMissing = true; 896 } 897 } 898 899 if (p instanceof L2tpIpsecPskProfile) { 900 L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p; 901 String presharedKey = pskProfile.getPresharedKey(); 902 String key = KEY_PREFIX_IPSEC_PSK + p.getId(); 903 if (TextUtils.isEmpty(presharedKey) || !mKeyStore.contains(key)) { 904 pskProfile.setPresharedKey(null); 905 secretMissing = true; 906 } 907 } 908 909 if (p instanceof L2tpProfile) { 910 L2tpProfile l2tpProfile = (L2tpProfile) p; 911 if (l2tpProfile.isSecretEnabled()) { 912 String secret = l2tpProfile.getSecretString(); 913 String key = KEY_PREFIX_L2TP_SECRET + p.getId(); 914 if (TextUtils.isEmpty(secret) || !mKeyStore.contains(key)) { 915 l2tpProfile.setSecretString(null); 916 secretMissing = true; 917 } 918 } 919 } 920 921 if (secretMissing) { 922 mActiveProfile = p; 923 showDialog(DIALOG_SECRET_NOT_SET); 924 return false; 925 } else { 926 return true; 927 } 928 } 929 930 private void processSecrets(VpnProfile p) { 931 switch (p.getType()) { 932 case L2TP_IPSEC_PSK: 933 L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p; 934 String presharedKey = pskProfile.getPresharedKey(); 935 String key = KEY_PREFIX_IPSEC_PSK + p.getId(); 936 if (!TextUtils.isEmpty(presharedKey) && 937 !mKeyStore.put(key, presharedKey)) { 938 Log.e(TAG, "keystore write failed: key=" + key); 939 } 940 pskProfile.setPresharedKey(key); 941 // pass through 942 943 case L2TP_IPSEC: 944 case L2TP: 945 L2tpProfile l2tpProfile = (L2tpProfile) p; 946 key = KEY_PREFIX_L2TP_SECRET + p.getId(); 947 if (l2tpProfile.isSecretEnabled()) { 948 String secret = l2tpProfile.getSecretString(); 949 if (!TextUtils.isEmpty(secret) && 950 !mKeyStore.put(key, secret)) { 951 Log.e(TAG, "keystore write failed: key=" + key); 952 } 953 l2tpProfile.setSecretString(key); 954 } else { 955 mKeyStore.delete(key); 956 } 957 break; 958 } 959 } 960 961 private class VpnPreference extends Preference { 962 VpnProfile mProfile; 963 VpnPreference(Context c, VpnProfile p) { 964 super(c); 965 setProfile(p); 966 } 967 968 void setProfile(VpnProfile p) { 969 mProfile = p; 970 setTitle(p.getName()); 971 setSummary(getProfileSummaryString(p)); 972 } 973 } 974 975 // to receive vpn connectivity events broadcast by VpnService 976 private class ConnectivityReceiver extends BroadcastReceiver { 977 @Override 978 public void onReceive(Context context, Intent intent) { 979 String profileName = intent.getStringExtra( 980 VpnManager.BROADCAST_PROFILE_NAME); 981 if (profileName == null) return; 982 983 VpnState s = (VpnState) intent.getSerializableExtra( 984 VpnManager.BROADCAST_CONNECTION_STATE); 985 986 if (s == null) { 987 Log.e(TAG, "received null connectivity state"); 988 return; 989 } 990 991 mConnectingErrorCode = intent.getIntExtra( 992 VpnManager.BROADCAST_ERROR_CODE, NO_ERROR); 993 994 VpnPreference pref = mVpnPreferenceMap.get(profileName); 995 if (pref != null) { 996 Log.d(TAG, "received connectivity: " + profileName 997 + ": connected? " + s 998 + " err=" + mConnectingErrorCode); 999 changeState(pref.mProfile, s); 1000 } else { 1001 Log.e(TAG, "received connectivity: " + profileName 1002 + ": connected? " + s + ", but profile does not exist;" 1003 + " just ignore it"); 1004 } 1005 } 1006 } 1007 1008 // managing status check in a background thread 1009 private class StatusChecker { 1010 private List<VpnProfile> mList; 1011 1012 synchronized void check(final List<VpnProfile> list) { 1013 final ConditionVariable cv = new ConditionVariable(); 1014 cv.close(); 1015 mVpnManager.startVpnService(); 1016 ServiceConnection c = new ServiceConnection() { 1017 public synchronized void onServiceConnected( 1018 ComponentName className, IBinder binder) { 1019 cv.open(); 1020 1021 IVpnService service = IVpnService.Stub.asInterface(binder); 1022 for (VpnProfile p : list) { 1023 try { 1024 service.checkStatus(p); 1025 } catch (Throwable e) { 1026 Log.e(TAG, " --- checkStatus(): " + p.getName(), e); 1027 changeState(p, VpnState.IDLE); 1028 } 1029 } 1030 VpnSettings.this.unbindService(this); 1031 showPreferences(); 1032 } 1033 1034 public void onServiceDisconnected(ComponentName className) { 1035 cv.open(); 1036 1037 setDefaultState(list); 1038 VpnSettings.this.unbindService(this); 1039 showPreferences(); 1040 } 1041 }; 1042 if (mVpnManager.bindVpnService(c)) { 1043 if (!cv.block(1000)) { 1044 Log.d(TAG, "checkStatus() bindService failed"); 1045 setDefaultState(list); 1046 } 1047 } else { 1048 setDefaultState(list); 1049 } 1050 } 1051 1052 private void showPreferences() { 1053 for (VpnProfile p : mVpnProfileList) { 1054 VpnPreference pref = mVpnPreferenceMap.get(p.getName()); 1055 mVpnListContainer.addPreference(pref); 1056 } 1057 } 1058 1059 private void setDefaultState(List<VpnProfile> list) { 1060 for (VpnProfile p : list) changeState(p, VpnState.IDLE); 1061 showPreferences(); 1062 } 1063 } 1064} 1065