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