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.os.UserManager;
     33 import android.preference.Preference;
     34 import android.preference.PreferenceGroup;
     35 import android.preference.PreferenceScreen;
     36 import android.security.Credentials;
     37 import android.security.KeyStore;
     38 import android.text.TextUtils;
     39 import android.util.Log;
     40 import android.view.ContextMenu;
     41 import android.view.ContextMenu.ContextMenuInfo;
     42 import android.view.LayoutInflater;
     43 import android.view.Menu;
     44 import android.view.MenuInflater;
     45 import android.view.MenuItem;
     46 import android.view.View;
     47 import android.widget.AdapterView.AdapterContextMenuInfo;
     48 import android.widget.AdapterView.OnItemSelectedListener;
     49 import android.widget.ArrayAdapter;
     50 import android.widget.ListView;
     51 import android.widget.Spinner;
     52 import android.widget.TextView;
     53 import android.widget.Toast;
     54 
     55 import com.android.internal.net.LegacyVpnInfo;
     56 import com.android.internal.net.VpnConfig;
     57 import com.android.internal.net.VpnProfile;
     58 import com.android.internal.util.ArrayUtils;
     59 import com.android.settings.R;
     60 import com.android.settings.SettingsPreferenceFragment;
     61 import com.google.android.collect.Lists;
     62 
     63 import java.util.ArrayList;
     64 import java.util.HashMap;
     65 import java.util.List;
     66 
     67 public class VpnSettings extends SettingsPreferenceFragment implements
     68         Handler.Callback, Preference.OnPreferenceClickListener,
     69         DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
     70     private static final String TAG = "VpnSettings";
     71 
     72     private static final String TAG_LOCKDOWN = "lockdown";
     73 
     74     private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN";
     75 
     76     // TODO: migrate to using DialogFragment when editing
     77 
     78     private final IConnectivityManager mService = IConnectivityManager.Stub
     79             .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
     80     private final KeyStore mKeyStore = KeyStore.getInstance();
     81     private boolean mUnlocking = false;
     82 
     83     private HashMap<String, VpnPreference> mPreferences = new HashMap<String, VpnPreference>();
     84     private VpnDialog mDialog;
     85 
     86     private Handler mUpdater;
     87     private LegacyVpnInfo mInfo;
     88     private UserManager mUm;
     89 
     90     // The key of the profile for the current ContextMenu.
     91     private String mSelectedKey;
     92 
     93     private boolean mUnavailable;
     94 
     95     @Override
     96     public void onCreate(Bundle savedState) {
     97         super.onCreate(savedState);
     98 
     99         mUm = (UserManager) getSystemService(Context.USER_SERVICE);
    100 
    101         if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) {
    102             mUnavailable = true;
    103             setPreferenceScreen(new PreferenceScreen(getActivity(), null));
    104             return;
    105         }
    106 
    107         setHasOptionsMenu(true);
    108         addPreferencesFromResource(R.xml.vpn_settings2);
    109 
    110         if (savedState != null) {
    111             VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"),
    112                     savedState.getByteArray("VpnProfile"));
    113             if (profile != null) {
    114                 mDialog = new VpnDialog(getActivity(), this, profile,
    115                         savedState.getBoolean("VpnEditing"));
    116             }
    117         }
    118     }
    119 
    120     @Override
    121     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    122         super.onCreateOptionsMenu(menu, inflater);
    123         inflater.inflate(R.menu.vpn, menu);
    124     }
    125 
    126     @Override
    127     public void onPrepareOptionsMenu(Menu menu) {
    128         super.onPrepareOptionsMenu(menu);
    129 
    130         // Hide lockdown VPN on devices that require IMS authentication
    131         if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
    132             menu.findItem(R.id.vpn_lockdown).setVisible(false);
    133         }
    134     }
    135 
    136     @Override
    137     public boolean onOptionsItemSelected(MenuItem item) {
    138         switch (item.getItemId()) {
    139             case R.id.vpn_create: {
    140                 // Generate a new key. Here we just use the current time.
    141                 long millis = System.currentTimeMillis();
    142                 while (mPreferences.containsKey(Long.toHexString(millis))) {
    143                     ++millis;
    144                 }
    145                 mDialog = new VpnDialog(
    146                         getActivity(), this, new VpnProfile(Long.toHexString(millis)), true);
    147                 mDialog.setOnDismissListener(this);
    148                 mDialog.show();
    149                 return true;
    150             }
    151             case R.id.vpn_lockdown: {
    152                 LockdownConfigFragment.show(this);
    153                 return true;
    154             }
    155         }
    156         return super.onOptionsItemSelected(item);
    157     }
    158 
    159     @Override
    160     public void onSaveInstanceState(Bundle savedState) {
    161         // We do not save view hierarchy, as they are just profiles.
    162         if (mDialog != null) {
    163             VpnProfile profile = mDialog.getProfile();
    164             savedState.putString("VpnKey", profile.key);
    165             savedState.putByteArray("VpnProfile", profile.encode());
    166             savedState.putBoolean("VpnEditing", mDialog.isEditing());
    167         }
    168         // else?
    169     }
    170 
    171     @Override
    172     public void onResume() {
    173         super.onResume();
    174 
    175         if (mUnavailable) {
    176             TextView emptyView = (TextView) getView().findViewById(android.R.id.empty);
    177             getListView().setEmptyView(emptyView);
    178             if (emptyView != null) {
    179                 emptyView.setText(R.string.vpn_settings_not_available);
    180             }
    181             return;
    182         }
    183 
    184         final boolean pickLockdown = getActivity()
    185                 .getIntent().getBooleanExtra(EXTRA_PICK_LOCKDOWN, false);
    186         if (pickLockdown) {
    187             LockdownConfigFragment.show(this);
    188         }
    189 
    190         // Check KeyStore here, so others do not need to deal with it.
    191         if (!mKeyStore.isUnlocked()) {
    192             if (!mUnlocking) {
    193                 // Let us unlock KeyStore. See you later!
    194                 Credentials.getInstance().unlock(getActivity());
    195             } else {
    196                 // We already tried, but it is still not working!
    197                 finishFragment();
    198             }
    199             mUnlocking = !mUnlocking;
    200             return;
    201         }
    202 
    203         // Now KeyStore is always unlocked. Reset the flag.
    204         mUnlocking = false;
    205 
    206         // Currently we are the only user of profiles in KeyStore.
    207         // Assuming KeyStore and KeyGuard do the right thing, we can
    208         // safely cache profiles in the memory.
    209         if (mPreferences.size() == 0) {
    210             PreferenceGroup group = getPreferenceScreen();
    211 
    212             final Context context = getActivity();
    213             final List<VpnProfile> profiles = loadVpnProfiles(mKeyStore);
    214             for (VpnProfile profile : profiles) {
    215                 final VpnPreference pref = new VpnPreference(context, profile);
    216                 pref.setOnPreferenceClickListener(this);
    217                 mPreferences.put(profile.key, pref);
    218                 group.addPreference(pref);
    219             }
    220         }
    221 
    222         // Show the dialog if there is one.
    223         if (mDialog != null) {
    224             mDialog.setOnDismissListener(this);
    225             mDialog.show();
    226         }
    227 
    228         // Start monitoring.
    229         if (mUpdater == null) {
    230             mUpdater = new Handler(this);
    231         }
    232         mUpdater.sendEmptyMessage(0);
    233 
    234         // Register for context menu. Hmmm, getListView() is hidden?
    235         registerForContextMenu(getListView());
    236     }
    237 
    238     @Override
    239     public void onPause() {
    240         super.onPause();
    241 
    242         if (mUnavailable) {
    243             return;
    244         }
    245 
    246         // Hide the dialog if there is one.
    247         if (mDialog != null) {
    248             mDialog.setOnDismissListener(null);
    249             mDialog.dismiss();
    250         }
    251 
    252         // Unregister for context menu.
    253         if (getView() != null) {
    254             unregisterForContextMenu(getListView());
    255         }
    256     }
    257 
    258     @Override
    259     public void onDismiss(DialogInterface dialog) {
    260         // Here is the exit of a dialog.
    261         mDialog = null;
    262     }
    263 
    264     @Override
    265     public void onClick(DialogInterface dialog, int button) {
    266         if (button == DialogInterface.BUTTON_POSITIVE) {
    267             // Always save the profile.
    268             VpnProfile profile = mDialog.getProfile();
    269             mKeyStore.put(Credentials.VPN + profile.key, profile.encode(), KeyStore.UID_SELF,
    270                     KeyStore.FLAG_ENCRYPTED);
    271 
    272             // Update the preference.
    273             VpnPreference preference = mPreferences.get(profile.key);
    274             if (preference != null) {
    275                 disconnect(profile.key);
    276                 preference.update(profile);
    277             } else {
    278                 preference = new VpnPreference(getActivity(), profile);
    279                 preference.setOnPreferenceClickListener(this);
    280                 mPreferences.put(profile.key, preference);
    281                 getPreferenceScreen().addPreference(preference);
    282             }
    283 
    284             // If we are not editing, connect!
    285             if (!mDialog.isEditing()) {
    286                 try {
    287                     connect(profile);
    288                 } catch (Exception e) {
    289                     Log.e(TAG, "connect", e);
    290                 }
    291             }
    292         }
    293     }
    294 
    295     @Override
    296     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
    297         if (mDialog != null) {
    298             Log.v(TAG, "onCreateContextMenu() is called when mDialog != null");
    299             return;
    300         }
    301 
    302         if (info instanceof AdapterContextMenuInfo) {
    303             Preference preference = (Preference) getListView().getItemAtPosition(
    304                     ((AdapterContextMenuInfo) info).position);
    305             if (preference instanceof VpnPreference) {
    306                 VpnProfile profile = ((VpnPreference) preference).getProfile();
    307                 mSelectedKey = profile.key;
    308                 menu.setHeaderTitle(profile.name);
    309                 menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit);
    310                 menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete);
    311             }
    312         }
    313     }
    314 
    315     @Override
    316     public boolean onContextItemSelected(MenuItem item) {
    317         if (mDialog != null) {
    318             Log.v(TAG, "onContextItemSelected() is called when mDialog != null");
    319             return false;
    320         }
    321 
    322         VpnPreference preference = mPreferences.get(mSelectedKey);
    323         if (preference == null) {
    324             Log.v(TAG, "onContextItemSelected() is called but no preference is found");
    325             return false;
    326         }
    327 
    328         switch (item.getItemId()) {
    329             case R.string.vpn_menu_edit:
    330                 mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true);
    331                 mDialog.setOnDismissListener(this);
    332                 mDialog.show();
    333                 return true;
    334             case R.string.vpn_menu_delete:
    335                 disconnect(mSelectedKey);
    336                 getPreferenceScreen().removePreference(preference);
    337                 mPreferences.remove(mSelectedKey);
    338                 mKeyStore.delete(Credentials.VPN + mSelectedKey);
    339                 return true;
    340         }
    341         return false;
    342     }
    343 
    344     @Override
    345     public boolean onPreferenceClick(Preference preference) {
    346         if (mDialog != null) {
    347             Log.v(TAG, "onPreferenceClick() is called when mDialog != null");
    348             return true;
    349         }
    350 
    351         if (preference instanceof VpnPreference) {
    352             VpnProfile profile = ((VpnPreference) preference).getProfile();
    353             if (mInfo != null && profile.key.equals(mInfo.key) &&
    354                     mInfo.state == LegacyVpnInfo.STATE_CONNECTED) {
    355                 try {
    356                     mInfo.intent.send();
    357                     return true;
    358                 } catch (Exception e) {
    359                     // ignore
    360                 }
    361             }
    362             mDialog = new VpnDialog(getActivity(), this, profile, false);
    363         } else {
    364             // Generate a new key. Here we just use the current time.
    365             long millis = System.currentTimeMillis();
    366             while (mPreferences.containsKey(Long.toHexString(millis))) {
    367                 ++millis;
    368             }
    369             mDialog = new VpnDialog(getActivity(), this,
    370                     new VpnProfile(Long.toHexString(millis)), true);
    371         }
    372         mDialog.setOnDismissListener(this);
    373         mDialog.show();
    374         return true;
    375     }
    376 
    377     @Override
    378     public boolean handleMessage(Message message) {
    379         mUpdater.removeMessages(0);
    380 
    381         if (isResumed()) {
    382             try {
    383                 LegacyVpnInfo info = mService.getLegacyVpnInfo();
    384                 if (mInfo != null) {
    385                     VpnPreference preference = mPreferences.get(mInfo.key);
    386                     if (preference != null) {
    387                         preference.update(-1);
    388                     }
    389                     mInfo = null;
    390                 }
    391                 if (info != null) {
    392                     VpnPreference preference = mPreferences.get(info.key);
    393                     if (preference != null) {
    394                         preference.update(info.state);
    395                         mInfo = info;
    396                     }
    397                 }
    398             } catch (Exception e) {
    399                 // ignore
    400             }
    401             mUpdater.sendEmptyMessageDelayed(0, 1000);
    402         }
    403         return true;
    404     }
    405 
    406     private void connect(VpnProfile profile) throws Exception {
    407         try {
    408             mService.startLegacyVpn(profile);
    409         } catch (IllegalStateException e) {
    410             Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show();
    411         }
    412     }
    413 
    414     private void disconnect(String key) {
    415         if (mInfo != null && key.equals(mInfo.key)) {
    416             try {
    417                 mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
    418             } catch (Exception e) {
    419                 // ignore
    420             }
    421         }
    422     }
    423 
    424     @Override
    425     protected int getHelpResource() {
    426         return R.string.help_url_vpn;
    427     }
    428 
    429     private static class VpnPreference extends Preference {
    430         private VpnProfile mProfile;
    431         private int mState = -1;
    432 
    433         VpnPreference(Context context, VpnProfile profile) {
    434             super(context);
    435             setPersistent(false);
    436             setOrder(0);
    437 
    438             mProfile = profile;
    439             update();
    440         }
    441 
    442         VpnProfile getProfile() {
    443             return mProfile;
    444         }
    445 
    446         void update(VpnProfile profile) {
    447             mProfile = profile;
    448             update();
    449         }
    450 
    451         void update(int state) {
    452             mState = state;
    453             update();
    454         }
    455 
    456         void update() {
    457             if (mState < 0) {
    458                 String[] types = getContext().getResources()
    459                         .getStringArray(R.array.vpn_types_long);
    460                 setSummary(types[mProfile.type]);
    461             } else {
    462                 String[] states = getContext().getResources()
    463                         .getStringArray(R.array.vpn_states);
    464                 setSummary(states[mState]);
    465             }
    466             setTitle(mProfile.name);
    467             notifyHierarchyChanged();
    468         }
    469 
    470         @Override
    471         public int compareTo(Preference preference) {
    472             int result = -1;
    473             if (preference instanceof VpnPreference) {
    474                 VpnPreference another = (VpnPreference) preference;
    475                 if ((result = another.mState - mState) == 0 &&
    476                         (result = mProfile.name.compareTo(another.mProfile.name)) == 0 &&
    477                         (result = mProfile.type - another.mProfile.type) == 0) {
    478                     result = mProfile.key.compareTo(another.mProfile.key);
    479                 }
    480             }
    481             return result;
    482         }
    483     }
    484 
    485     /**
    486      * Dialog to configure always-on VPN.
    487      */
    488     public static class LockdownConfigFragment extends DialogFragment {
    489         private List<VpnProfile> mProfiles;
    490         private List<CharSequence> mTitles;
    491         private int mCurrentIndex;
    492 
    493         private static class TitleAdapter extends ArrayAdapter<CharSequence> {
    494             public TitleAdapter(Context context, List<CharSequence> objects) {
    495                 super(context, com.android.internal.R.layout.select_dialog_singlechoice_material,
    496                         android.R.id.text1, objects);
    497             }
    498         }
    499 
    500         public static void show(VpnSettings parent) {
    501             if (!parent.isAdded()) return;
    502 
    503             final LockdownConfigFragment dialog = new LockdownConfigFragment();
    504             dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN);
    505         }
    506 
    507         private static String getStringOrNull(KeyStore keyStore, String key) {
    508             final byte[] value = keyStore.get(Credentials.LOCKDOWN_VPN);
    509             return value == null ? null : new String(value);
    510         }
    511 
    512         private void initProfiles(KeyStore keyStore, Resources res) {
    513             final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN);
    514 
    515             mProfiles = loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP);
    516             mTitles = Lists.newArrayList();
    517             mTitles.add(res.getText(R.string.vpn_lockdown_none));
    518             mCurrentIndex = 0;
    519 
    520             for (VpnProfile profile : mProfiles) {
    521                 if (TextUtils.equals(profile.key, lockdownKey)) {
    522                     mCurrentIndex = mTitles.size();
    523                 }
    524                 mTitles.add(profile.name);
    525             }
    526         }
    527 
    528         @Override
    529         public Dialog onCreateDialog(Bundle savedInstanceState) {
    530             final Context context = getActivity();
    531             final KeyStore keyStore = KeyStore.getInstance();
    532 
    533             initProfiles(keyStore, context.getResources());
    534 
    535             final AlertDialog.Builder builder = new AlertDialog.Builder(context);
    536             final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
    537 
    538             builder.setTitle(R.string.vpn_menu_lockdown);
    539 
    540             final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false);
    541             final ListView listView = (ListView) view.findViewById(android.R.id.list);
    542             listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
    543             listView.setAdapter(new TitleAdapter(context, mTitles));
    544             listView.setItemChecked(mCurrentIndex, true);
    545             builder.setView(view);
    546 
    547             builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    548                 @Override
    549                 public void onClick(DialogInterface dialog, int which) {
    550                     final int newIndex = listView.getCheckedItemPosition();
    551                     if (mCurrentIndex == newIndex) return;
    552 
    553                     if (newIndex == 0) {
    554                         keyStore.delete(Credentials.LOCKDOWN_VPN);
    555 
    556                     } else {
    557                         final VpnProfile profile = mProfiles.get(newIndex - 1);
    558                         if (!profile.isValidLockdownProfile()) {
    559                             Toast.makeText(context, R.string.vpn_lockdown_config_error,
    560                                     Toast.LENGTH_LONG).show();
    561                             return;
    562                         }
    563                         keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes(),
    564                                 KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED);
    565                     }
    566 
    567                     // kick profiles since we changed them
    568                     ConnectivityManager.from(getActivity()).updateLockdownVpn();
    569                 }
    570             });
    571 
    572             return builder.create();
    573         }
    574     }
    575 
    576     private static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
    577         final ArrayList<VpnProfile> result = Lists.newArrayList();
    578         final String[] keys = keyStore.saw(Credentials.VPN);
    579         if (keys != null) {
    580             for (String key : keys) {
    581                 final VpnProfile profile = VpnProfile.decode(
    582                         key, keyStore.get(Credentials.VPN + key));
    583                 if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {
    584                     result.add(profile);
    585                 }
    586             }
    587         }
    588         return result;
    589     }
    590 }
    591