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
17package com.android.settings.vpn2;
18
19import android.annotation.UiThread;
20import android.annotation.WorkerThread;
21import android.app.AppOpsManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageInfo;
25import android.content.pm.PackageManager;
26import android.net.ConnectivityManager.NetworkCallback;
27import android.net.ConnectivityManager;
28import android.net.IConnectivityManager;
29import android.net.Network;
30import android.net.NetworkCapabilities;
31import android.net.NetworkRequest;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.Message;
35import android.os.RemoteException;
36import android.os.ServiceManager;
37import android.os.UserHandle;
38import android.os.UserManager;
39import android.security.Credentials;
40import android.security.KeyStore;
41import android.support.v7.preference.Preference;
42import android.support.v7.preference.PreferenceGroup;
43import android.support.v7.preference.PreferenceScreen;
44import android.util.ArrayMap;
45import android.util.ArraySet;
46import android.util.Log;
47import android.view.Menu;
48import android.view.MenuInflater;
49import android.view.MenuItem;
50
51import com.android.internal.logging.MetricsProto.MetricsEvent;
52import com.android.internal.net.LegacyVpnInfo;
53import com.android.internal.net.VpnConfig;
54import com.android.internal.net.VpnProfile;
55import com.android.internal.util.ArrayUtils;
56import com.android.settings.GearPreference;
57import com.android.settings.GearPreference.OnGearClickListener;
58import com.android.settings.R;
59import com.android.settings.RestrictedSettingsFragment;
60import com.android.settingslib.RestrictedLockUtils;
61import com.google.android.collect.Lists;
62
63import java.util.ArrayList;
64import java.util.Collections;
65import java.util.List;
66import java.util.Map;
67import java.util.Set;
68
69import 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 */
75public 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
103    public VpnSettings() {
104        super(UserManager.DISALLOW_CONFIG_VPN);
105    }
106
107    @Override
108    protected int getMetricsCategory() {
109        return MetricsEvent.VPN;
110    }
111
112    @Override
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
126    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
127        super.onCreateOptionsMenu(menu, inflater);
128        inflater.inflate(R.menu.vpn, menu);
129    }
130
131    @Override
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
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
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
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
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
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
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
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
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
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
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
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
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
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