2 * Copyright (C) 2011 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.vpn2;
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.preference.Preference;
32 import android.preference.PreferenceGroup;
33 import android.security.Credentials;
34 import android.security.KeyStore;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.view.ContextMenu;
38 import android.view.ContextMenu.ContextMenuInfo;
39 import android.view.LayoutInflater;
40 import android.view.Menu;
41 import android.view.MenuInflater;
42 import android.view.MenuItem;
43 import android.view.View;
44 import android.widget.AdapterView.AdapterContextMenuInfo;
45 import android.widget.ArrayAdapter;
46 import android.widget.ListView;
47 import android.widget.Toast;
49 import com.android.internal.net.LegacyVpnInfo;
50 import com.android.internal.net.VpnConfig;
51 import com.android.internal.net.VpnProfile;
52 import com.android.internal.util.ArrayUtils;
53 import com.android.settings.R;
54 import com.android.settings.SettingsPreferenceFragment;
55 import com.google.android.collect.Lists;
57 import java.util.ArrayList;
58 import java.util.HashMap;
59 import java.util.List;
61 public class VpnSettings extends SettingsPreferenceFragment implements
62 Handler.Callback, Preference.OnPreferenceClickListener,
63 DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
64 private static final String TAG = "VpnSettings";
66 private static final String TAG_LOCKDOWN = "lockdown";
68 // TODO: migrate to using DialogFragment when editing
70 private final IConnectivityManager mService = IConnectivityManager.Stub
71 .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
72 private final KeyStore mKeyStore = KeyStore.getInstance();
73 private boolean mUnlocking = false;
75 private HashMap<String, VpnPreference> mPreferences;
76 private VpnDialog mDialog;
78 private Handler mUpdater;
79 private LegacyVpnInfo mInfo;
81 // The key of the profile for the current ContextMenu.
82 private String mSelectedKey;
85 public void onCreate(Bundle savedState) {
86 super.onCreate(savedState);
88 setHasOptionsMenu(true);
89 addPreferencesFromResource(R.xml.vpn_settings2);
91 if (savedState != null) {
92 VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"),
93 savedState.getByteArray("VpnProfile"));
94 if (profile != null) {
95 mDialog = new VpnDialog(getActivity(), this, profile,
96 savedState.getBoolean("VpnEditing"));
102 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
103 super.onCreateOptionsMenu(menu, inflater);
104 inflater.inflate(R.menu.vpn, menu);
108 public boolean onOptionsItemSelected(MenuItem item) {
109 switch (item.getItemId()) {
110 case R.id.vpn_create: {
111 // Generate a new key. Here we just use the current time.
112 long millis = System.currentTimeMillis();
113 while (mPreferences.containsKey(Long.toHexString(millis))) {
116 mDialog = new VpnDialog(
117 getActivity(), this, new VpnProfile(Long.toHexString(millis)), true);
118 mDialog.setOnDismissListener(this);
122 case R.id.vpn_lockdown: {
123 LockdownConfigFragment.show(this);
127 return super.onOptionsItemSelected(item);
131 public void onSaveInstanceState(Bundle savedState) {
132 // We do not save view hierarchy, as they are just profiles.
133 if (mDialog != null) {
134 VpnProfile profile = mDialog.getProfile();
135 savedState.putString("VpnKey", profile.key);
136 savedState.putByteArray("VpnProfile", profile.encode());
137 savedState.putBoolean("VpnEditing", mDialog.isEditing());
143 public void onResume() {
146 // Check KeyStore here, so others do not need to deal with it.
147 if (mKeyStore.state() != KeyStore.State.UNLOCKED) {
149 // Let us unlock KeyStore. See you later!
150 Credentials.getInstance().unlock(getActivity());
152 // We already tried, but it is still not working!
155 mUnlocking = !mUnlocking;
159 // Now KeyStore is always unlocked. Reset the flag.
162 // Currently we are the only user of profiles in KeyStore.
163 // Assuming KeyStore and KeyGuard do the right thing, we can
164 // safely cache profiles in the memory.
165 if (mPreferences == null) {
166 mPreferences = new HashMap<String, VpnPreference>();
167 PreferenceGroup group = getPreferenceScreen();
169 final Context context = getActivity();
170 final List<VpnProfile> profiles = loadVpnProfiles(mKeyStore);
171 for (VpnProfile profile : profiles) {
172 final VpnPreference pref = new VpnPreference(context, profile);
173 pref.setOnPreferenceClickListener(this);
174 mPreferences.put(profile.key, pref);
175 group.addPreference(pref);
179 // Show the dialog if there is one.
180 if (mDialog != null) {
181 mDialog.setOnDismissListener(this);
186 if (mUpdater == null) {
187 mUpdater = new Handler(this);
189 mUpdater.sendEmptyMessage(0);
191 // Register for context menu. Hmmm, getListView() is hidden?
192 registerForContextMenu(getListView());
196 public void onPause() {
199 // Hide the dialog if there is one.
200 if (mDialog != null) {
201 mDialog.setOnDismissListener(null);
205 // Unregister for context menu.
206 if (getView() != null) {
207 unregisterForContextMenu(getListView());
212 public void onDismiss(DialogInterface dialog) {
213 // Here is the exit of a dialog.
218 public void onClick(DialogInterface dialog, int button) {
219 if (button == DialogInterface.BUTTON_POSITIVE) {
220 // Always save the profile.
221 VpnProfile profile = mDialog.getProfile();
222 mKeyStore.put(Credentials.VPN + profile.key, profile.encode());
224 // Update the preference.
225 VpnPreference preference = mPreferences.get(profile.key);
226 if (preference != null) {
227 disconnect(profile.key);
228 preference.update(profile);
230 preference = new VpnPreference(getActivity(), profile);
231 preference.setOnPreferenceClickListener(this);
232 mPreferences.put(profile.key, preference);
233 getPreferenceScreen().addPreference(preference);
236 // If we are not editing, connect!
237 if (!mDialog.isEditing()) {
240 } catch (Exception e) {
241 Log.e(TAG, "connect", e);
248 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
249 if (mDialog != null) {
250 Log.v(TAG, "onCreateContextMenu() is called when mDialog != null");
254 if (info instanceof AdapterContextMenuInfo) {
255 Preference preference = (Preference) getListView().getItemAtPosition(
256 ((AdapterContextMenuInfo) info).position);
257 if (preference instanceof VpnPreference) {
258 VpnProfile profile = ((VpnPreference) preference).getProfile();
259 mSelectedKey = profile.key;
260 menu.setHeaderTitle(profile.name);
261 menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit);
262 menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete);
268 public boolean onContextItemSelected(MenuItem item) {
269 if (mDialog != null) {
270 Log.v(TAG, "onContextItemSelected() is called when mDialog != null");
274 VpnPreference preference = mPreferences.get(mSelectedKey);
275 if (preference == null) {
276 Log.v(TAG, "onContextItemSelected() is called but no preference is found");
280 switch (item.getItemId()) {
281 case R.string.vpn_menu_edit:
282 mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true);
283 mDialog.setOnDismissListener(this);
286 case R.string.vpn_menu_delete:
287 disconnect(mSelectedKey);
288 getPreferenceScreen().removePreference(preference);
289 mPreferences.remove(mSelectedKey);
290 mKeyStore.delete(Credentials.VPN + mSelectedKey);
297 public boolean onPreferenceClick(Preference preference) {
298 if (mDialog != null) {
299 Log.v(TAG, "onPreferenceClick() is called when mDialog != null");
303 if (preference instanceof VpnPreference) {
304 VpnProfile profile = ((VpnPreference) preference).getProfile();
305 if (mInfo != null && profile.key.equals(mInfo.key) &&
306 mInfo.state == LegacyVpnInfo.STATE_CONNECTED) {
310 } catch (Exception e) {
314 mDialog = new VpnDialog(getActivity(), this, profile, false);
316 // Generate a new key. Here we just use the current time.
317 long millis = System.currentTimeMillis();
318 while (mPreferences.containsKey(Long.toHexString(millis))) {
321 mDialog = new VpnDialog(getActivity(), this,
322 new VpnProfile(Long.toHexString(millis)), true);
324 mDialog.setOnDismissListener(this);
330 public boolean handleMessage(Message message) {
331 mUpdater.removeMessages(0);
335 LegacyVpnInfo info = mService.getLegacyVpnInfo();
337 VpnPreference preference = mPreferences.get(mInfo.key);
338 if (preference != null) {
339 preference.update(-1);
344 VpnPreference preference = mPreferences.get(info.key);
345 if (preference != null) {
346 preference.update(info.state);
350 } catch (Exception e) {
353 mUpdater.sendEmptyMessageDelayed(0, 1000);
358 private void connect(VpnProfile profile) throws Exception {
360 mService.startLegacyVpn(profile);
361 } catch (IllegalStateException e) {
362 Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show();
366 private void disconnect(String key) {
367 if (mInfo != null && key.equals(mInfo.key)) {
369 mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
370 } catch (Exception e) {
377 protected int getHelpResource() {
378 return R.string.help_url_vpn;
381 private static class VpnPreference extends Preference {
382 private VpnProfile mProfile;
383 private int mState = -1;
385 VpnPreference(Context context, VpnProfile profile) {
387 setPersistent(false);
394 VpnProfile getProfile() {
398 void update(VpnProfile profile) {
403 void update(int state) {
410 String[] types = getContext().getResources()
411 .getStringArray(R.array.vpn_types_long);
412 setSummary(types[mProfile.type]);
414 String[] states = getContext().getResources()
415 .getStringArray(R.array.vpn_states);
416 setSummary(states[mState]);
418 setTitle(mProfile.name);
419 notifyHierarchyChanged();
423 public int compareTo(Preference preference) {
425 if (preference instanceof VpnPreference) {
426 VpnPreference another = (VpnPreference) preference;
427 if ((result = another.mState - mState) == 0 &&
428 (result = mProfile.name.compareTo(another.mProfile.name)) == 0 &&
429 (result = mProfile.type - another.mProfile.type) == 0) {
430 result = mProfile.key.compareTo(another.mProfile.key);
438 * Dialog to configure always-on VPN.
440 public static class LockdownConfigFragment extends DialogFragment {
441 private List<VpnProfile> mProfiles;
442 private List<CharSequence> mTitles;
443 private int mCurrentIndex;
445 private static class TitleAdapter extends ArrayAdapter<CharSequence> {
446 public TitleAdapter(Context context, List<CharSequence> objects) {
447 super(context, com.android.internal.R.layout.select_dialog_singlechoice_holo,
448 android.R.id.text1, objects);
452 public static void show(VpnSettings parent) {
453 if (!parent.isAdded()) return;
455 final LockdownConfigFragment dialog = new LockdownConfigFragment();
456 dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN);
459 private static String getStringOrNull(KeyStore keyStore, String key) {
460 final byte[] value = keyStore.get(Credentials.LOCKDOWN_VPN);
461 return value == null ? null : new String(value);
464 private void initProfiles(KeyStore keyStore, Resources res) {
465 final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN);
467 mProfiles = loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP);
468 mTitles = Lists.newArrayList();
469 mTitles.add(res.getText(R.string.vpn_lockdown_none));
472 for (VpnProfile profile : mProfiles) {
473 if (TextUtils.equals(profile.key, lockdownKey)) {
474 mCurrentIndex = mTitles.size();
476 mTitles.add(profile.name);
481 public Dialog onCreateDialog(Bundle savedInstanceState) {
482 final Context context = getActivity();
483 final KeyStore keyStore = KeyStore.getInstance();
485 initProfiles(keyStore, context.getResources());
487 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
488 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
490 builder.setTitle(R.string.vpn_menu_lockdown);
492 final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false);
493 final ListView listView = (ListView) view.findViewById(android.R.id.list);
494 listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
495 listView.setAdapter(new TitleAdapter(context, mTitles));
496 listView.setItemChecked(mCurrentIndex, true);
497 builder.setView(view);
499 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
501 public void onClick(DialogInterface dialog, int which) {
502 final int newIndex = listView.getCheckedItemPosition();
503 if (mCurrentIndex == newIndex) return;
506 keyStore.delete(Credentials.LOCKDOWN_VPN);
509 final VpnProfile profile = mProfiles.get(newIndex - 1);
510 if (!profile.isValidLockdownProfile()) {
511 Toast.makeText(context, R.string.vpn_lockdown_config_error,
512 Toast.LENGTH_LONG).show();
515 keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes());
518 // kick profiles since we changed them
519 ConnectivityManager.from(getActivity()).updateLockdownVpn();
523 return builder.create();
527 private static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
528 final ArrayList<VpnProfile> result = Lists.newArrayList();
529 final String[] keys = keyStore.saw(Credentials.VPN);
531 for (String key : keys) {
532 final VpnProfile profile = VpnProfile.decode(
533 key, keyStore.get(Credentials.VPN + key));
534 if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {