Home | History | Annotate | Download | only in vpn2
      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