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