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