OSDN Git Service

931f0c2106b73eab47bf4d69e83bedc0f7cc9ece
[android-x86/packages-apps-Settings.git] / src / com / android / settings / vpn2 / VpnSettings.java
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.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;
48
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;
56
57 import java.util.ArrayList;
58 import java.util.HashMap;
59 import java.util.List;
60
61 public class VpnSettings extends SettingsPreferenceFragment implements
62         Handler.Callback, Preference.OnPreferenceClickListener,
63         DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
64     private static final String TAG = "VpnSettings";
65
66     private static final String TAG_LOCKDOWN = "lockdown";
67
68     // TODO: migrate to using DialogFragment when editing
69
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;
74
75     private HashMap<String, VpnPreference> mPreferences;
76     private VpnDialog mDialog;
77
78     private Handler mUpdater;
79     private LegacyVpnInfo mInfo;
80
81     // The key of the profile for the current ContextMenu.
82     private String mSelectedKey;
83
84     @Override
85     public void onCreate(Bundle savedState) {
86         super.onCreate(savedState);
87
88         setHasOptionsMenu(true);
89         addPreferencesFromResource(R.xml.vpn_settings2);
90
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"));
97             }
98         }
99     }
100
101     @Override
102     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
103         super.onCreateOptionsMenu(menu, inflater);
104         inflater.inflate(R.menu.vpn, menu);
105     }
106
107     @Override
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))) {
114                     ++millis;
115                 }
116                 mDialog = new VpnDialog(
117                         getActivity(), this, new VpnProfile(Long.toHexString(millis)), true);
118                 mDialog.setOnDismissListener(this);
119                 mDialog.show();
120                 return true;
121             }
122             case R.id.vpn_lockdown: {
123                 LockdownConfigFragment.show(this);
124                 return true;
125             }
126         }
127         return super.onOptionsItemSelected(item);
128     }
129
130     @Override
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());
138         }
139         // else?
140     }
141
142     @Override
143     public void onResume() {
144         super.onResume();
145
146         // Check KeyStore here, so others do not need to deal with it.
147         if (mKeyStore.state() != KeyStore.State.UNLOCKED) {
148             if (!mUnlocking) {
149                 // Let us unlock KeyStore. See you later!
150                 Credentials.getInstance().unlock(getActivity());
151             } else {
152                 // We already tried, but it is still not working!
153                 finishFragment();
154             }
155             mUnlocking = !mUnlocking;
156             return;
157         }
158
159         // Now KeyStore is always unlocked. Reset the flag.
160         mUnlocking = false;
161
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();
168
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);
176             }
177         }
178
179         // Show the dialog if there is one.
180         if (mDialog != null) {
181             mDialog.setOnDismissListener(this);
182             mDialog.show();
183         }
184
185         // Start monitoring.
186         if (mUpdater == null) {
187             mUpdater = new Handler(this);
188         }
189         mUpdater.sendEmptyMessage(0);
190
191         // Register for context menu. Hmmm, getListView() is hidden?
192         registerForContextMenu(getListView());
193     }
194
195     @Override
196     public void onPause() {
197         super.onPause();
198
199         // Hide the dialog if there is one.
200         if (mDialog != null) {
201             mDialog.setOnDismissListener(null);
202             mDialog.dismiss();
203         }
204
205         // Unregister for context menu.
206         if (getView() != null) {
207             unregisterForContextMenu(getListView());
208         }
209     }
210
211     @Override
212     public void onDismiss(DialogInterface dialog) {
213         // Here is the exit of a dialog.
214         mDialog = null;
215     }
216
217     @Override
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());
223
224             // Update the preference.
225             VpnPreference preference = mPreferences.get(profile.key);
226             if (preference != null) {
227                 disconnect(profile.key);
228                 preference.update(profile);
229             } else {
230                 preference = new VpnPreference(getActivity(), profile);
231                 preference.setOnPreferenceClickListener(this);
232                 mPreferences.put(profile.key, preference);
233                 getPreferenceScreen().addPreference(preference);
234             }
235
236             // If we are not editing, connect!
237             if (!mDialog.isEditing()) {
238                 try {
239                     connect(profile);
240                 } catch (Exception e) {
241                     Log.e(TAG, "connect", e);
242                 }
243             }
244         }
245     }
246
247     @Override
248     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
249         if (mDialog != null) {
250             Log.v(TAG, "onCreateContextMenu() is called when mDialog != null");
251             return;
252         }
253
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);
263             }
264         }
265     }
266
267     @Override
268     public boolean onContextItemSelected(MenuItem item) {
269         if (mDialog != null) {
270             Log.v(TAG, "onContextItemSelected() is called when mDialog != null");
271             return false;
272         }
273
274         VpnPreference preference = mPreferences.get(mSelectedKey);
275         if (preference == null) {
276             Log.v(TAG, "onContextItemSelected() is called but no preference is found");
277             return false;
278         }
279
280         switch (item.getItemId()) {
281             case R.string.vpn_menu_edit:
282                 mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true);
283                 mDialog.setOnDismissListener(this);
284                 mDialog.show();
285                 return true;
286             case R.string.vpn_menu_delete:
287                 disconnect(mSelectedKey);
288                 getPreferenceScreen().removePreference(preference);
289                 mPreferences.remove(mSelectedKey);
290                 mKeyStore.delete(Credentials.VPN + mSelectedKey);
291                 return true;
292         }
293         return false;
294     }
295
296     @Override
297     public boolean onPreferenceClick(Preference preference) {
298         if (mDialog != null) {
299             Log.v(TAG, "onPreferenceClick() is called when mDialog != null");
300             return true;
301         }
302
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) {
307                 try {
308                     mInfo.intent.send();
309                     return true;
310                 } catch (Exception e) {
311                     // ignore
312                 }
313             }
314             mDialog = new VpnDialog(getActivity(), this, profile, false);
315         } else {
316             // Generate a new key. Here we just use the current time.
317             long millis = System.currentTimeMillis();
318             while (mPreferences.containsKey(Long.toHexString(millis))) {
319                 ++millis;
320             }
321             mDialog = new VpnDialog(getActivity(), this,
322                     new VpnProfile(Long.toHexString(millis)), true);
323         }
324         mDialog.setOnDismissListener(this);
325         mDialog.show();
326         return true;
327     }
328
329     @Override
330     public boolean handleMessage(Message message) {
331         mUpdater.removeMessages(0);
332
333         if (isResumed()) {
334             try {
335                 LegacyVpnInfo info = mService.getLegacyVpnInfo();
336                 if (mInfo != null) {
337                     VpnPreference preference = mPreferences.get(mInfo.key);
338                     if (preference != null) {
339                         preference.update(-1);
340                     }
341                     mInfo = null;
342                 }
343                 if (info != null) {
344                     VpnPreference preference = mPreferences.get(info.key);
345                     if (preference != null) {
346                         preference.update(info.state);
347                         mInfo = info;
348                     }
349                 }
350             } catch (Exception e) {
351                 // ignore
352             }
353             mUpdater.sendEmptyMessageDelayed(0, 1000);
354         }
355         return true;
356     }
357
358     private void connect(VpnProfile profile) throws Exception {
359         try {
360             mService.startLegacyVpn(profile);
361         } catch (IllegalStateException e) {
362             Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show();
363         }
364     }
365
366     private void disconnect(String key) {
367         if (mInfo != null && key.equals(mInfo.key)) {
368             try {
369                 mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
370             } catch (Exception e) {
371                 // ignore
372             }
373         }
374     }
375
376     @Override
377     protected int getHelpResource() {
378         return R.string.help_url_vpn;
379     }
380
381     private static class VpnPreference extends Preference {
382         private VpnProfile mProfile;
383         private int mState = -1;
384
385         VpnPreference(Context context, VpnProfile profile) {
386             super(context);
387             setPersistent(false);
388             setOrder(0);
389
390             mProfile = profile;
391             update();
392         }
393
394         VpnProfile getProfile() {
395             return mProfile;
396         }
397
398         void update(VpnProfile profile) {
399             mProfile = profile;
400             update();
401         }
402
403         void update(int state) {
404             mState = state;
405             update();
406         }
407
408         void update() {
409             if (mState < 0) {
410                 String[] types = getContext().getResources()
411                         .getStringArray(R.array.vpn_types_long);
412                 setSummary(types[mProfile.type]);
413             } else {
414                 String[] states = getContext().getResources()
415                         .getStringArray(R.array.vpn_states);
416                 setSummary(states[mState]);
417             }
418             setTitle(mProfile.name);
419             notifyHierarchyChanged();
420         }
421
422         @Override
423         public int compareTo(Preference preference) {
424             int result = -1;
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);
431                 }
432             }
433             return result;
434         }
435     }
436
437     /**
438      * Dialog to configure always-on VPN.
439      */
440     public static class LockdownConfigFragment extends DialogFragment {
441         private List<VpnProfile> mProfiles;
442         private List<CharSequence> mTitles;
443         private int mCurrentIndex;
444
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);
449             }
450         }
451
452         public static void show(VpnSettings parent) {
453             if (!parent.isAdded()) return;
454
455             final LockdownConfigFragment dialog = new LockdownConfigFragment();
456             dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN);
457         }
458
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);
462         }
463
464         private void initProfiles(KeyStore keyStore, Resources res) {
465             final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN);
466
467             mProfiles = loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP);
468             mTitles = Lists.newArrayList();
469             mTitles.add(res.getText(R.string.vpn_lockdown_none));
470             mCurrentIndex = 0;
471
472             for (VpnProfile profile : mProfiles) {
473                 if (TextUtils.equals(profile.key, lockdownKey)) {
474                     mCurrentIndex = mTitles.size();
475                 }
476                 mTitles.add(profile.name);
477             }
478         }
479
480         @Override
481         public Dialog onCreateDialog(Bundle savedInstanceState) {
482             final Context context = getActivity();
483             final KeyStore keyStore = KeyStore.getInstance();
484
485             initProfiles(keyStore, context.getResources());
486
487             final AlertDialog.Builder builder = new AlertDialog.Builder(context);
488             final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
489
490             builder.setTitle(R.string.vpn_menu_lockdown);
491
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);
498
499             builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
500                 @Override
501                 public void onClick(DialogInterface dialog, int which) {
502                     final int newIndex = listView.getCheckedItemPosition();
503                     if (mCurrentIndex == newIndex) return;
504
505                     if (newIndex == 0) {
506                         keyStore.delete(Credentials.LOCKDOWN_VPN);
507
508                     } else {
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();
513                             return;
514                         }
515                         keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes());
516                     }
517
518                     // kick profiles since we changed them
519                     ConnectivityManager.from(getActivity()).updateLockdownVpn();
520                 }
521             });
522
523             return builder.create();
524         }
525     }
526
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);
530         if (keys != null) {
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)) {
535                     result.add(profile);
536                 }
537             }
538         }
539         return result;
540     }
541 }