1 /* 2 * Copyright (C) 2011 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 17 package com.android.settings.vpn2; 18 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.app.DialogFragment; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.res.Resources; 25 import android.net.ConnectivityManager; 26 import android.net.IConnectivityManager; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.os.ServiceManager; 31 import android.os.SystemProperties; 32 import android.os.UserManager; 33 import android.preference.Preference; 34 import android.preference.PreferenceGroup; 35 import android.preference.PreferenceScreen; 36 import android.security.Credentials; 37 import android.security.KeyStore; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.view.ContextMenu; 41 import android.view.ContextMenu.ContextMenuInfo; 42 import android.view.LayoutInflater; 43 import android.view.Menu; 44 import android.view.MenuInflater; 45 import android.view.MenuItem; 46 import android.view.View; 47 import android.widget.AdapterView.AdapterContextMenuInfo; 48 import android.widget.AdapterView.OnItemSelectedListener; 49 import android.widget.ArrayAdapter; 50 import android.widget.ListView; 51 import android.widget.Spinner; 52 import android.widget.TextView; 53 import android.widget.Toast; 54 55 import com.android.internal.net.LegacyVpnInfo; 56 import com.android.internal.net.VpnConfig; 57 import com.android.internal.net.VpnProfile; 58 import com.android.internal.util.ArrayUtils; 59 import com.android.settings.R; 60 import com.android.settings.SettingsPreferenceFragment; 61 import com.google.android.collect.Lists; 62 63 import java.util.ArrayList; 64 import java.util.HashMap; 65 import java.util.List; 66 67 public class VpnSettings extends SettingsPreferenceFragment implements 68 Handler.Callback, Preference.OnPreferenceClickListener, 69 DialogInterface.OnClickListener, DialogInterface.OnDismissListener { 70 private static final String TAG = "VpnSettings"; 71 72 private static final String TAG_LOCKDOWN = "lockdown"; 73 74 private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN"; 75 76 // TODO: migrate to using DialogFragment when editing 77 78 private final IConnectivityManager mService = IConnectivityManager.Stub 79 .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); 80 private final KeyStore mKeyStore = KeyStore.getInstance(); 81 private boolean mUnlocking = false; 82 83 private HashMap<String, VpnPreference> mPreferences = new HashMap<String, VpnPreference>(); 84 private VpnDialog mDialog; 85 86 private Handler mUpdater; 87 private LegacyVpnInfo mInfo; 88 private UserManager mUm; 89 90 // The key of the profile for the current ContextMenu. 91 private String mSelectedKey; 92 93 private boolean mUnavailable; 94 95 @Override 96 public void onCreate(Bundle savedState) { 97 super.onCreate(savedState); 98 99 mUm = (UserManager) getSystemService(Context.USER_SERVICE); 100 101 if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) { 102 mUnavailable = true; 103 setPreferenceScreen(new PreferenceScreen(getActivity(), null)); 104 return; 105 } 106 107 setHasOptionsMenu(true); 108 addPreferencesFromResource(R.xml.vpn_settings2); 109 110 if (savedState != null) { 111 VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"), 112 savedState.getByteArray("VpnProfile")); 113 if (profile != null) { 114 mDialog = new VpnDialog(getActivity(), this, profile, 115 savedState.getBoolean("VpnEditing")); 116 } 117 } 118 } 119 120 @Override 121 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 122 super.onCreateOptionsMenu(menu, inflater); 123 inflater.inflate(R.menu.vpn, menu); 124 } 125 126 @Override 127 public void onPrepareOptionsMenu(Menu menu) { 128 super.onPrepareOptionsMenu(menu); 129 130 // Hide lockdown VPN on devices that require IMS authentication 131 if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) { 132 menu.findItem(R.id.vpn_lockdown).setVisible(false); 133 } 134 } 135 136 @Override 137 public boolean onOptionsItemSelected(MenuItem item) { 138 switch (item.getItemId()) { 139 case R.id.vpn_create: { 140 // Generate a new key. Here we just use the current time. 141 long millis = System.currentTimeMillis(); 142 while (mPreferences.containsKey(Long.toHexString(millis))) { 143 ++millis; 144 } 145 mDialog = new VpnDialog( 146 getActivity(), this, new VpnProfile(Long.toHexString(millis)), true); 147 mDialog.setOnDismissListener(this); 148 mDialog.show(); 149 return true; 150 } 151 case R.id.vpn_lockdown: { 152 LockdownConfigFragment.show(this); 153 return true; 154 } 155 } 156 return super.onOptionsItemSelected(item); 157 } 158 159 @Override 160 public void onSaveInstanceState(Bundle savedState) { 161 // We do not save view hierarchy, as they are just profiles. 162 if (mDialog != null) { 163 VpnProfile profile = mDialog.getProfile(); 164 savedState.putString("VpnKey", profile.key); 165 savedState.putByteArray("VpnProfile", profile.encode()); 166 savedState.putBoolean("VpnEditing", mDialog.isEditing()); 167 } 168 // else? 169 } 170 171 @Override 172 public void onResume() { 173 super.onResume(); 174 175 if (mUnavailable) { 176 TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); 177 getListView().setEmptyView(emptyView); 178 if (emptyView != null) { 179 emptyView.setText(R.string.vpn_settings_not_available); 180 } 181 return; 182 } 183 184 final boolean pickLockdown = getActivity() 185 .getIntent().getBooleanExtra(EXTRA_PICK_LOCKDOWN, false); 186 if (pickLockdown) { 187 LockdownConfigFragment.show(this); 188 } 189 190 // Check KeyStore here, so others do not need to deal with it. 191 if (!mKeyStore.isUnlocked()) { 192 if (!mUnlocking) { 193 // Let us unlock KeyStore. See you later! 194 Credentials.getInstance().unlock(getActivity()); 195 } else { 196 // We already tried, but it is still not working! 197 finishFragment(); 198 } 199 mUnlocking = !mUnlocking; 200 return; 201 } 202 203 // Now KeyStore is always unlocked. Reset the flag. 204 mUnlocking = false; 205 206 // Currently we are the only user of profiles in KeyStore. 207 // Assuming KeyStore and KeyGuard do the right thing, we can 208 // safely cache profiles in the memory. 209 if (mPreferences.size() == 0) { 210 PreferenceGroup group = getPreferenceScreen(); 211 212 final Context context = getActivity(); 213 final List<VpnProfile> profiles = loadVpnProfiles(mKeyStore); 214 for (VpnProfile profile : profiles) { 215 final VpnPreference pref = new VpnPreference(context, profile); 216 pref.setOnPreferenceClickListener(this); 217 mPreferences.put(profile.key, pref); 218 group.addPreference(pref); 219 } 220 } 221 222 // Show the dialog if there is one. 223 if (mDialog != null) { 224 mDialog.setOnDismissListener(this); 225 mDialog.show(); 226 } 227 228 // Start monitoring. 229 if (mUpdater == null) { 230 mUpdater = new Handler(this); 231 } 232 mUpdater.sendEmptyMessage(0); 233 234 // Register for context menu. Hmmm, getListView() is hidden? 235 registerForContextMenu(getListView()); 236 } 237 238 @Override 239 public void onPause() { 240 super.onPause(); 241 242 if (mUnavailable) { 243 return; 244 } 245 246 // Hide the dialog if there is one. 247 if (mDialog != null) { 248 mDialog.setOnDismissListener(null); 249 mDialog.dismiss(); 250 } 251 252 // Unregister for context menu. 253 if (getView() != null) { 254 unregisterForContextMenu(getListView()); 255 } 256 } 257 258 @Override 259 public void onDismiss(DialogInterface dialog) { 260 // Here is the exit of a dialog. 261 mDialog = null; 262 } 263 264 @Override 265 public void onClick(DialogInterface dialog, int button) { 266 if (button == DialogInterface.BUTTON_POSITIVE) { 267 // Always save the profile. 268 VpnProfile profile = mDialog.getProfile(); 269 mKeyStore.put(Credentials.VPN + profile.key, profile.encode(), KeyStore.UID_SELF, 270 KeyStore.FLAG_ENCRYPTED); 271 272 // Update the preference. 273 VpnPreference preference = mPreferences.get(profile.key); 274 if (preference != null) { 275 disconnect(profile.key); 276 preference.update(profile); 277 } else { 278 preference = new VpnPreference(getActivity(), profile); 279 preference.setOnPreferenceClickListener(this); 280 mPreferences.put(profile.key, preference); 281 getPreferenceScreen().addPreference(preference); 282 } 283 284 // If we are not editing, connect! 285 if (!mDialog.isEditing()) { 286 try { 287 connect(profile); 288 } catch (Exception e) { 289 Log.e(TAG, "connect", e); 290 } 291 } 292 } 293 } 294 295 @Override 296 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) { 297 if (mDialog != null) { 298 Log.v(TAG, "onCreateContextMenu() is called when mDialog != null"); 299 return; 300 } 301 302 if (info instanceof AdapterContextMenuInfo) { 303 Preference preference = (Preference) getListView().getItemAtPosition( 304 ((AdapterContextMenuInfo) info).position); 305 if (preference instanceof VpnPreference) { 306 VpnProfile profile = ((VpnPreference) preference).getProfile(); 307 mSelectedKey = profile.key; 308 menu.setHeaderTitle(profile.name); 309 menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit); 310 menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete); 311 } 312 } 313 } 314 315 @Override 316 public boolean onContextItemSelected(MenuItem item) { 317 if (mDialog != null) { 318 Log.v(TAG, "onContextItemSelected() is called when mDialog != null"); 319 return false; 320 } 321 322 VpnPreference preference = mPreferences.get(mSelectedKey); 323 if (preference == null) { 324 Log.v(TAG, "onContextItemSelected() is called but no preference is found"); 325 return false; 326 } 327 328 switch (item.getItemId()) { 329 case R.string.vpn_menu_edit: 330 mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true); 331 mDialog.setOnDismissListener(this); 332 mDialog.show(); 333 return true; 334 case R.string.vpn_menu_delete: 335 disconnect(mSelectedKey); 336 getPreferenceScreen().removePreference(preference); 337 mPreferences.remove(mSelectedKey); 338 mKeyStore.delete(Credentials.VPN + mSelectedKey); 339 return true; 340 } 341 return false; 342 } 343 344 @Override 345 public boolean onPreferenceClick(Preference preference) { 346 if (mDialog != null) { 347 Log.v(TAG, "onPreferenceClick() is called when mDialog != null"); 348 return true; 349 } 350 351 if (preference instanceof VpnPreference) { 352 VpnProfile profile = ((VpnPreference) preference).getProfile(); 353 if (mInfo != null && profile.key.equals(mInfo.key) && 354 mInfo.state == LegacyVpnInfo.STATE_CONNECTED) { 355 try { 356 mInfo.intent.send(); 357 return true; 358 } catch (Exception e) { 359 // ignore 360 } 361 } 362 mDialog = new VpnDialog(getActivity(), this, profile, false); 363 } else { 364 // Generate a new key. Here we just use the current time. 365 long millis = System.currentTimeMillis(); 366 while (mPreferences.containsKey(Long.toHexString(millis))) { 367 ++millis; 368 } 369 mDialog = new VpnDialog(getActivity(), this, 370 new VpnProfile(Long.toHexString(millis)), true); 371 } 372 mDialog.setOnDismissListener(this); 373 mDialog.show(); 374 return true; 375 } 376 377 @Override 378 public boolean handleMessage(Message message) { 379 mUpdater.removeMessages(0); 380 381 if (isResumed()) { 382 try { 383 LegacyVpnInfo info = mService.getLegacyVpnInfo(); 384 if (mInfo != null) { 385 VpnPreference preference = mPreferences.get(mInfo.key); 386 if (preference != null) { 387 preference.update(-1); 388 } 389 mInfo = null; 390 } 391 if (info != null) { 392 VpnPreference preference = mPreferences.get(info.key); 393 if (preference != null) { 394 preference.update(info.state); 395 mInfo = info; 396 } 397 } 398 } catch (Exception e) { 399 // ignore 400 } 401 mUpdater.sendEmptyMessageDelayed(0, 1000); 402 } 403 return true; 404 } 405 406 private void connect(VpnProfile profile) throws Exception { 407 try { 408 mService.startLegacyVpn(profile); 409 } catch (IllegalStateException e) { 410 Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); 411 } 412 } 413 414 private void disconnect(String key) { 415 if (mInfo != null && key.equals(mInfo.key)) { 416 try { 417 mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN); 418 } catch (Exception e) { 419 // ignore 420 } 421 } 422 } 423 424 @Override 425 protected int getHelpResource() { 426 return R.string.help_url_vpn; 427 } 428 429 private static class VpnPreference extends Preference { 430 private VpnProfile mProfile; 431 private int mState = -1; 432 433 VpnPreference(Context context, VpnProfile profile) { 434 super(context); 435 setPersistent(false); 436 setOrder(0); 437 438 mProfile = profile; 439 update(); 440 } 441 442 VpnProfile getProfile() { 443 return mProfile; 444 } 445 446 void update(VpnProfile profile) { 447 mProfile = profile; 448 update(); 449 } 450 451 void update(int state) { 452 mState = state; 453 update(); 454 } 455 456 void update() { 457 if (mState < 0) { 458 String[] types = getContext().getResources() 459 .getStringArray(R.array.vpn_types_long); 460 setSummary(types[mProfile.type]); 461 } else { 462 String[] states = getContext().getResources() 463 .getStringArray(R.array.vpn_states); 464 setSummary(states[mState]); 465 } 466 setTitle(mProfile.name); 467 notifyHierarchyChanged(); 468 } 469 470 @Override 471 public int compareTo(Preference preference) { 472 int result = -1; 473 if (preference instanceof VpnPreference) { 474 VpnPreference another = (VpnPreference) preference; 475 if ((result = another.mState - mState) == 0 && 476 (result = mProfile.name.compareTo(another.mProfile.name)) == 0 && 477 (result = mProfile.type - another.mProfile.type) == 0) { 478 result = mProfile.key.compareTo(another.mProfile.key); 479 } 480 } 481 return result; 482 } 483 } 484 485 /** 486 * Dialog to configure always-on VPN. 487 */ 488 public static class LockdownConfigFragment extends DialogFragment { 489 private List<VpnProfile> mProfiles; 490 private List<CharSequence> mTitles; 491 private int mCurrentIndex; 492 493 private static class TitleAdapter extends ArrayAdapter<CharSequence> { 494 public TitleAdapter(Context context, List<CharSequence> objects) { 495 super(context, com.android.internal.R.layout.select_dialog_singlechoice_material, 496 android.R.id.text1, objects); 497 } 498 } 499 500 public static void show(VpnSettings parent) { 501 if (!parent.isAdded()) return; 502 503 final LockdownConfigFragment dialog = new LockdownConfigFragment(); 504 dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN); 505 } 506 507 private static String getStringOrNull(KeyStore keyStore, String key) { 508 final byte[] value = keyStore.get(Credentials.LOCKDOWN_VPN); 509 return value == null ? null : new String(value); 510 } 511 512 private void initProfiles(KeyStore keyStore, Resources res) { 513 final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN); 514 515 mProfiles = loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP); 516 mTitles = Lists.newArrayList(); 517 mTitles.add(res.getText(R.string.vpn_lockdown_none)); 518 mCurrentIndex = 0; 519 520 for (VpnProfile profile : mProfiles) { 521 if (TextUtils.equals(profile.key, lockdownKey)) { 522 mCurrentIndex = mTitles.size(); 523 } 524 mTitles.add(profile.name); 525 } 526 } 527 528 @Override 529 public Dialog onCreateDialog(Bundle savedInstanceState) { 530 final Context context = getActivity(); 531 final KeyStore keyStore = KeyStore.getInstance(); 532 533 initProfiles(keyStore, context.getResources()); 534 535 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 536 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); 537 538 builder.setTitle(R.string.vpn_menu_lockdown); 539 540 final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false); 541 final ListView listView = (ListView) view.findViewById(android.R.id.list); 542 listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 543 listView.setAdapter(new TitleAdapter(context, mTitles)); 544 listView.setItemChecked(mCurrentIndex, true); 545 builder.setView(view); 546 547 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 548 @Override 549 public void onClick(DialogInterface dialog, int which) { 550 final int newIndex = listView.getCheckedItemPosition(); 551 if (mCurrentIndex == newIndex) return; 552 553 if (newIndex == 0) { 554 keyStore.delete(Credentials.LOCKDOWN_VPN); 555 556 } else { 557 final VpnProfile profile = mProfiles.get(newIndex - 1); 558 if (!profile.isValidLockdownProfile()) { 559 Toast.makeText(context, R.string.vpn_lockdown_config_error, 560 Toast.LENGTH_LONG).show(); 561 return; 562 } 563 keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes(), 564 KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED); 565 } 566 567 // kick profiles since we changed them 568 ConnectivityManager.from(getActivity()).updateLockdownVpn(); 569 } 570 }); 571 572 return builder.create(); 573 } 574 } 575 576 private static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) { 577 final ArrayList<VpnProfile> result = Lists.newArrayList(); 578 final String[] keys = keyStore.saw(Credentials.VPN); 579 if (keys != null) { 580 for (String key : keys) { 581 final VpnProfile profile = VpnProfile.decode( 582 key, keyStore.get(Credentials.VPN + key)); 583 if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) { 584 result.add(profile); 585 } 586 } 587 } 588 return result; 589 } 590 } 591