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