• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onCreate(Bundle savedState)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
onCreateOptionsMenu(Menu menu, MenuInflater inflater)105     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
106         super.onCreateOptionsMenu(menu, inflater);
107         inflater.inflate(R.menu.vpn, menu);
108     }
109 
110     @Override
onPrepareOptionsMenu(Menu menu)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
onOptionsItemSelected(MenuItem item)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
onSaveInstanceState(Bundle savedState)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
onResume()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
onPause()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
onDismiss(DialogInterface dialog)230     public void onDismiss(DialogInterface dialog) {
231         // Here is the exit of a dialog.
232         mDialog = null;
233     }
234 
235     @Override
onClick(DialogInterface dialog, int button)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
onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info)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
onContextItemSelected(MenuItem item)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
onPreferenceClick(Preference preference)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
handleMessage(Message message)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 
connect(VpnProfile profile)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 
disconnect(String key)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
getHelpResource()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 
VpnPreference(Context context, VpnProfile profile)404         VpnPreference(Context context, VpnProfile profile) {
405             super(context);
406             setPersistent(false);
407             setOrder(0);
408 
409             mProfile = profile;
410             update();
411         }
412 
getProfile()413         VpnProfile getProfile() {
414             return mProfile;
415         }
416 
update(VpnProfile profile)417         void update(VpnProfile profile) {
418             mProfile = profile;
419             update();
420         }
421 
update(int state)422         void update(int state) {
423             mState = state;
424             update();
425         }
426 
update()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
compareTo(Preference preference)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> {
TitleAdapter(Context context, List<CharSequence> objects)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 
show(VpnSettings parent)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 
getStringOrNull(KeyStore keyStore, String key)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 
initProfiles(KeyStore keyStore, Resources res)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
onCreateDialog(Bundle savedInstanceState)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 
loadVpnProfiles(KeyStore keyStore, int... excludeTypes)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