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     // 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