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.annotation.UiThread;
     20 import android.annotation.WorkerThread;
     21 import android.app.Activity;
     22 import android.app.AppOpsManager;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.pm.PackageInfo;
     26 import android.content.pm.PackageManager;
     27 import android.net.ConnectivityManager;
     28 import android.net.ConnectivityManager.NetworkCallback;
     29 import android.net.IConnectivityManager;
     30 import android.net.Network;
     31 import android.net.NetworkCapabilities;
     32 import android.net.NetworkRequest;
     33 import android.os.Bundle;
     34 import android.os.Handler;
     35 import android.os.HandlerThread;
     36 import android.os.Message;
     37 import android.os.RemoteException;
     38 import android.os.ServiceManager;
     39 import android.os.UserHandle;
     40 import android.os.UserManager;
     41 import android.security.Credentials;
     42 import android.security.KeyStore;
     43 import android.support.v7.preference.Preference;
     44 import android.support.v7.preference.PreferenceGroup;
     45 import android.util.ArrayMap;
     46 import android.util.ArraySet;
     47 import android.util.Log;
     48 import android.view.Menu;
     49 import android.view.MenuInflater;
     50 import android.view.MenuItem;
     51 
     52 import com.android.internal.annotations.GuardedBy;
     53 import com.android.internal.annotations.VisibleForTesting;
     54 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     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.RestrictedSettingsFragment;
     61 import com.android.settings.widget.GearPreference;
     62 import com.android.settings.widget.GearPreference.OnGearClickListener;
     63 import com.android.settingslib.RestrictedLockUtils;
     64 
     65 import com.google.android.collect.Lists;
     66 
     67 import java.util.ArrayList;
     68 import java.util.Collection;
     69 import java.util.Collections;
     70 import java.util.List;
     71 import java.util.Map;
     72 import java.util.Set;
     73 
     74 import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
     75 
     76 /**
     77  * Settings screen listing VPNs. Configured VPNs and networks managed by apps
     78  * are shown in the same list.
     79  */
     80 public class VpnSettings extends RestrictedSettingsFragment implements
     81         Handler.Callback, Preference.OnPreferenceClickListener {
     82     private static final String LOG_TAG = "VpnSettings";
     83 
     84     private static final int RESCAN_MESSAGE = 0;
     85     private static final int RESCAN_INTERVAL_MS = 1000;
     86 
     87     private static final NetworkRequest VPN_REQUEST = new NetworkRequest.Builder()
     88             .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
     89             .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
     90             .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
     91             .build();
     92 
     93     private final IConnectivityManager mConnectivityService = IConnectivityManager.Stub
     94             .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
     95     private ConnectivityManager mConnectivityManager;
     96     private UserManager mUserManager;
     97 
     98     private final KeyStore mKeyStore = KeyStore.getInstance();
     99 
    100     private Map<String, LegacyVpnPreference> mLegacyVpnPreferences = new ArrayMap<>();
    101     private Map<AppVpnInfo, AppPreference> mAppPreferences = new ArrayMap<>();
    102 
    103     @GuardedBy("this")
    104     private Handler mUpdater;
    105     private HandlerThread mUpdaterThread;
    106     private LegacyVpnInfo mConnectedLegacyVpn;
    107 
    108     private boolean mUnavailable;
    109 
    110     public VpnSettings() {
    111         super(UserManager.DISALLOW_CONFIG_VPN);
    112     }
    113 
    114     @Override
    115     public int getMetricsCategory() {
    116         return MetricsEvent.VPN;
    117     }
    118 
    119     @Override
    120     public void onActivityCreated(Bundle savedInstanceState) {
    121         super.onActivityCreated(savedInstanceState);
    122 
    123         mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
    124         mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    125 
    126         mUnavailable = isUiRestricted();
    127         setHasOptionsMenu(!mUnavailable);
    128 
    129         addPreferencesFromResource(R.xml.vpn_settings2);
    130     }
    131 
    132     @Override
    133     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    134         super.onCreateOptionsMenu(menu, inflater);
    135         inflater.inflate(R.menu.vpn, menu);
    136     }
    137 
    138     @Override
    139     public void onPrepareOptionsMenu(Menu menu) {
    140         super.onPrepareOptionsMenu(menu);
    141 
    142         // Disable all actions if VPN configuration has been disallowed
    143         for (int i = 0; i < menu.size(); i++) {
    144             if (isUiRestrictedByOnlyAdmin()) {
    145                 RestrictedLockUtils.setMenuItemAsDisabledByAdmin(getPrefContext(),
    146                         menu.getItem(i), getRestrictionEnforcedAdmin());
    147             } else {
    148                 menu.getItem(i).setEnabled(!mUnavailable);
    149             }
    150         }
    151     }
    152 
    153     @Override
    154     public boolean onOptionsItemSelected(MenuItem item) {
    155         switch (item.getItemId()) {
    156             case R.id.vpn_create: {
    157                 // Generate a new key. Here we just use the current time.
    158                 long millis = System.currentTimeMillis();
    159                 while (mLegacyVpnPreferences.containsKey(Long.toHexString(millis))) {
    160                     ++millis;
    161                 }
    162                 VpnProfile profile = new VpnProfile(Long.toHexString(millis));
    163                 ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */);
    164                 return true;
    165             }
    166         }
    167         return super.onOptionsItemSelected(item);
    168     }
    169 
    170     @Override
    171     public void onResume() {
    172         super.onResume();
    173 
    174         mUnavailable = mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN);
    175         if (mUnavailable) {
    176             // Show a message to explain that VPN settings have been disabled
    177             if (!isUiRestrictedByOnlyAdmin()) {
    178                 getEmptyTextView().setText(R.string.vpn_settings_not_available);
    179             }
    180             getPreferenceScreen().removeAll();
    181             return;
    182         } else {
    183             setEmptyView(getEmptyTextView());
    184             getEmptyTextView().setText(R.string.vpn_no_vpns_added);
    185         }
    186 
    187         // Start monitoring
    188         mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback);
    189 
    190         // Trigger a refresh
    191         mUpdaterThread = new HandlerThread("Refresh VPN list in background");
    192         mUpdaterThread.start();
    193         mUpdater = new Handler(mUpdaterThread.getLooper(), this);
    194         mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
    195     }
    196 
    197     @Override
    198     public void onPause() {
    199         if (mUnavailable) {
    200             super.onPause();
    201             return;
    202         }
    203 
    204         // Stop monitoring
    205         mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
    206 
    207         synchronized (this) {
    208             mUpdater.removeCallbacksAndMessages(null);
    209             mUpdater = null;
    210             mUpdaterThread.quit();
    211             mUpdaterThread = null;
    212         }
    213 
    214         super.onPause();
    215     }
    216 
    217     @Override @WorkerThread
    218     public boolean handleMessage(Message message) {
    219         //Return if activity has been recycled
    220         final Activity activity = getActivity();
    221         if (activity == null) {
    222             return true;
    223         }
    224         final Context context = activity.getApplicationContext();
    225 
    226         // Run heavy RPCs before switching to UI thread
    227         final List<VpnProfile> vpnProfiles = loadVpnProfiles(mKeyStore);
    228         final List<AppVpnInfo> vpnApps = getVpnApps(context, /* includeProfiles */ true);
    229 
    230         final Map<String, LegacyVpnInfo> connectedLegacyVpns = getConnectedLegacyVpns();
    231         final Set<AppVpnInfo> connectedAppVpns = getConnectedAppVpns();
    232 
    233         final Set<AppVpnInfo> alwaysOnAppVpnInfos = getAlwaysOnAppVpnInfos();
    234         final String lockdownVpnKey = VpnUtils.getLockdownVpn();
    235 
    236         // Refresh list of VPNs
    237         activity.runOnUiThread(new UpdatePreferences(this)
    238                 .legacyVpns(vpnProfiles, connectedLegacyVpns, lockdownVpnKey)
    239                 .appVpns(vpnApps, connectedAppVpns, alwaysOnAppVpnInfos));
    240 
    241         synchronized (this) {
    242             if (mUpdater != null) {
    243                 mUpdater.removeMessages(RESCAN_MESSAGE);
    244                 mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS);
    245             }
    246         }
    247         return true;
    248     }
    249 
    250     @VisibleForTesting
    251     static class UpdatePreferences implements Runnable {
    252         private List<VpnProfile> vpnProfiles = Collections.<VpnProfile>emptyList();
    253         private List<AppVpnInfo> vpnApps = Collections.<AppVpnInfo>emptyList();
    254 
    255         private Map<String, LegacyVpnInfo> connectedLegacyVpns =
    256                 Collections.<String, LegacyVpnInfo>emptyMap();
    257         private Set<AppVpnInfo> connectedAppVpns = Collections.<AppVpnInfo>emptySet();
    258 
    259         private Set<AppVpnInfo> alwaysOnAppVpnInfos = Collections.<AppVpnInfo>emptySet();
    260         private String lockdownVpnKey = null;
    261 
    262         private final VpnSettings mSettings;
    263 
    264         public UpdatePreferences(VpnSettings settings) {
    265             mSettings = settings;
    266         }
    267 
    268         public final UpdatePreferences legacyVpns(List<VpnProfile> vpnProfiles,
    269                 Map<String, LegacyVpnInfo> connectedLegacyVpns, String lockdownVpnKey) {
    270             this.vpnProfiles = vpnProfiles;
    271             this.connectedLegacyVpns = connectedLegacyVpns;
    272             this.lockdownVpnKey = lockdownVpnKey;
    273             return this;
    274         }
    275 
    276         public final UpdatePreferences appVpns(List<AppVpnInfo> vpnApps,
    277                 Set<AppVpnInfo> connectedAppVpns, Set<AppVpnInfo> alwaysOnAppVpnInfos) {
    278             this.vpnApps = vpnApps;
    279             this.connectedAppVpns = connectedAppVpns;
    280             this.alwaysOnAppVpnInfos = alwaysOnAppVpnInfos;
    281             return this;
    282         }
    283 
    284         @Override @UiThread
    285         public void run() {
    286             if (!mSettings.canAddPreferences()) {
    287                 return;
    288             }
    289 
    290             // Find new VPNs by subtracting existing ones from the full set
    291             final Set<Preference> updates = new ArraySet<>();
    292 
    293             // Add legacy VPNs
    294             for (VpnProfile profile : vpnProfiles) {
    295                 LegacyVpnPreference p = mSettings.findOrCreatePreference(profile, true);
    296                 if (connectedLegacyVpns.containsKey(profile.key)) {
    297                     p.setState(connectedLegacyVpns.get(profile.key).state);
    298                 } else {
    299                     p.setState(LegacyVpnPreference.STATE_NONE);
    300                 }
    301                 p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(profile.key));
    302                 updates.add(p);
    303             }
    304 
    305             // Show connected VPNs even if the original entry in keystore is gone
    306             for (LegacyVpnInfo vpn : connectedLegacyVpns.values()) {
    307                 final VpnProfile stubProfile = new VpnProfile(vpn.key);
    308                 LegacyVpnPreference p = mSettings.findOrCreatePreference(stubProfile, false);
    309                 p.setState(vpn.state);
    310                 p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(vpn.key));
    311                 updates.add(p);
    312             }
    313 
    314             // Add VpnService VPNs
    315             for (AppVpnInfo app : vpnApps) {
    316                 AppPreference p = mSettings.findOrCreatePreference(app);
    317                 if (connectedAppVpns.contains(app)) {
    318                     p.setState(AppPreference.STATE_CONNECTED);
    319                 } else {
    320                     p.setState(AppPreference.STATE_DISCONNECTED);
    321                 }
    322                 p.setAlwaysOn(alwaysOnAppVpnInfos.contains(app));
    323                 updates.add(p);
    324             }
    325 
    326             // Trim out deleted VPN preferences
    327             mSettings.setShownPreferences(updates);
    328         }
    329     }
    330 
    331     @VisibleForTesting
    332     public boolean canAddPreferences() {
    333         return isAdded();
    334     }
    335 
    336     @VisibleForTesting @UiThread
    337     public void setShownPreferences(final Collection<Preference> updates) {
    338         mLegacyVpnPreferences.values().retainAll(updates);
    339         mAppPreferences.values().retainAll(updates);
    340 
    341         // Change {@param updates} in-place to only contain new preferences that were not already
    342         // added to the preference screen.
    343         final PreferenceGroup vpnGroup = getPreferenceScreen();
    344         for (int i = vpnGroup.getPreferenceCount() - 1; i >= 0; i--) {
    345             Preference p = vpnGroup.getPreference(i);
    346             if (updates.contains(p)) {
    347                 updates.remove(p);
    348             } else {
    349                 vpnGroup.removePreference(p);
    350             }
    351         }
    352 
    353         // Show any new preferences on the screen
    354         for (Preference pref : updates) {
    355             vpnGroup.addPreference(pref);
    356         }
    357     }
    358 
    359     @Override
    360     public boolean onPreferenceClick(Preference preference) {
    361         if (preference instanceof LegacyVpnPreference) {
    362             LegacyVpnPreference pref = (LegacyVpnPreference) preference;
    363             VpnProfile profile = pref.getProfile();
    364             if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) &&
    365                     mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) {
    366                 try {
    367                     mConnectedLegacyVpn.intent.send();
    368                     return true;
    369                 } catch (Exception e) {
    370                     Log.w(LOG_TAG, "Starting config intent failed", e);
    371                 }
    372             }
    373             ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */);
    374             return true;
    375         } else if (preference instanceof AppPreference) {
    376             AppPreference pref = (AppPreference) preference;
    377             boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
    378 
    379             if (!connected) {
    380                 try {
    381                     UserHandle user = UserHandle.of(pref.getUserId());
    382                     Context userContext = getActivity().createPackageContextAsUser(
    383                             getActivity().getPackageName(), 0 /* flags */, user);
    384                     PackageManager pm = userContext.getPackageManager();
    385                     Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName());
    386                     if (appIntent != null) {
    387                         userContext.startActivityAsUser(appIntent, user);
    388                         return true;
    389                     }
    390                 } catch (PackageManager.NameNotFoundException nnfe) {
    391                     Log.w(LOG_TAG, "VPN provider does not exist: " + pref.getPackageName(), nnfe);
    392                 }
    393             }
    394 
    395             // Already connected or no launch intent available - show an info dialog
    396             PackageInfo pkgInfo = pref.getPackageInfo();
    397             AppDialogFragment.show(this, pkgInfo, pref.getLabel(), false /* editing */, connected);
    398             return true;
    399         }
    400         return false;
    401     }
    402 
    403     @Override
    404     protected int getHelpResource() {
    405         return R.string.help_url_vpn;
    406     }
    407 
    408     private OnGearClickListener mGearListener = new OnGearClickListener() {
    409         @Override
    410         public void onGearClick(GearPreference p) {
    411             if (p instanceof LegacyVpnPreference) {
    412                 LegacyVpnPreference pref = (LegacyVpnPreference) p;
    413                 ConfigDialogFragment.show(VpnSettings.this, pref.getProfile(), true /* editing */,
    414                         true /* exists */);
    415             } else if (p instanceof AppPreference) {
    416                 AppPreference pref = (AppPreference) p;
    417                 AppManagementFragment.show(getPrefContext(), pref, getMetricsCategory());
    418             }
    419         }
    420     };
    421 
    422     private NetworkCallback mNetworkCallback = new NetworkCallback() {
    423         @Override
    424         public void onAvailable(Network network) {
    425             if (mUpdater != null) {
    426                 mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
    427             }
    428         }
    429 
    430         @Override
    431         public void onLost(Network network) {
    432             if (mUpdater != null) {
    433                 mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
    434             }
    435         }
    436     };
    437 
    438     @VisibleForTesting @UiThread
    439     public LegacyVpnPreference findOrCreatePreference(VpnProfile profile, boolean update) {
    440         LegacyVpnPreference pref = mLegacyVpnPreferences.get(profile.key);
    441         boolean created = false;
    442         if (pref == null ) {
    443             pref = new LegacyVpnPreference(getPrefContext());
    444             pref.setOnGearClickListener(mGearListener);
    445             pref.setOnPreferenceClickListener(this);
    446             mLegacyVpnPreferences.put(profile.key, pref);
    447             created = true;
    448         }
    449         if (created || update) {
    450             // This can change call-to-call because the profile can update and keep the same key.
    451             pref.setProfile(profile);
    452         }
    453         return pref;
    454     }
    455 
    456     @VisibleForTesting @UiThread
    457     public AppPreference findOrCreatePreference(AppVpnInfo app) {
    458         AppPreference pref = mAppPreferences.get(app);
    459         if (pref == null) {
    460             pref = new AppPreference(getPrefContext(), app.userId, app.packageName);
    461             pref.setOnGearClickListener(mGearListener);
    462             pref.setOnPreferenceClickListener(this);
    463             mAppPreferences.put(app, pref);
    464         }
    465         return pref;
    466     }
    467 
    468     @WorkerThread
    469     private Map<String, LegacyVpnInfo> getConnectedLegacyVpns() {
    470         try {
    471             mConnectedLegacyVpn = mConnectivityService.getLegacyVpnInfo(UserHandle.myUserId());
    472             if (mConnectedLegacyVpn != null) {
    473                 return Collections.singletonMap(mConnectedLegacyVpn.key, mConnectedLegacyVpn);
    474             }
    475         } catch (RemoteException e) {
    476             Log.e(LOG_TAG, "Failure updating VPN list with connected legacy VPNs", e);
    477         }
    478         return Collections.emptyMap();
    479     }
    480 
    481     @WorkerThread
    482     private Set<AppVpnInfo> getConnectedAppVpns() {
    483         // Mark connected third-party services
    484         Set<AppVpnInfo> connections = new ArraySet<>();
    485         try {
    486             for (UserHandle profile : mUserManager.getUserProfiles()) {
    487                 VpnConfig config = mConnectivityService.getVpnConfig(profile.getIdentifier());
    488                 if (config != null && !config.legacy) {
    489                     connections.add(new AppVpnInfo(profile.getIdentifier(), config.user));
    490                 }
    491             }
    492         } catch (RemoteException e) {
    493             Log.e(LOG_TAG, "Failure updating VPN list with connected app VPNs", e);
    494         }
    495         return connections;
    496     }
    497 
    498     @WorkerThread
    499     private Set<AppVpnInfo> getAlwaysOnAppVpnInfos() {
    500         Set<AppVpnInfo> result = new ArraySet<>();
    501         for (UserHandle profile : mUserManager.getUserProfiles()) {
    502             final int profileId = profile.getIdentifier();
    503             final String packageName = mConnectivityManager.getAlwaysOnVpnPackageForUser(profileId);
    504             if (packageName != null) {
    505                 result.add(new AppVpnInfo(profileId, packageName));
    506             }
    507         }
    508         return result;
    509     }
    510 
    511     static List<AppVpnInfo> getVpnApps(Context context, boolean includeProfiles) {
    512         List<AppVpnInfo> result = Lists.newArrayList();
    513 
    514         final Set<Integer> profileIds;
    515         if (includeProfiles) {
    516             profileIds = new ArraySet<>();
    517             for (UserHandle profile : UserManager.get(context).getUserProfiles()) {
    518                 profileIds.add(profile.getIdentifier());
    519             }
    520         } else {
    521             profileIds = Collections.singleton(UserHandle.myUserId());
    522         }
    523 
    524         // Fetch VPN-enabled apps from AppOps.
    525         AppOpsManager aom = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
    526         List<AppOpsManager.PackageOps> apps = aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN});
    527         if (apps != null) {
    528             for (AppOpsManager.PackageOps pkg : apps) {
    529                 int userId = UserHandle.getUserId(pkg.getUid());
    530                 if (!profileIds.contains(userId)) {
    531                     // Skip packages for users outside of our profile group.
    532                     continue;
    533                 }
    534                 // Look for a MODE_ALLOWED permission to activate VPN.
    535                 boolean allowed = false;
    536                 for (AppOpsManager.OpEntry op : pkg.getOps()) {
    537                     if (op.getOp() == OP_ACTIVATE_VPN &&
    538                             op.getMode() == AppOpsManager.MODE_ALLOWED) {
    539                         allowed = true;
    540                     }
    541                 }
    542                 if (allowed) {
    543                     result.add(new AppVpnInfo(userId, pkg.getPackageName()));
    544                 }
    545             }
    546         }
    547 
    548         Collections.sort(result);
    549         return result;
    550     }
    551 
    552     static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
    553         final ArrayList<VpnProfile> result = Lists.newArrayList();
    554 
    555         for (String key : keyStore.list(Credentials.VPN)) {
    556             final VpnProfile profile = VpnProfile.decode(key, keyStore.get(Credentials.VPN + key));
    557             if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {
    558                 result.add(profile);
    559             }
    560         }
    561         return result;
    562     }
    563 }
    564