VpnSettings.java revision a5df4779a04331294c15e6d22d749a734b59400f
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.app.AppOpsManager;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.PackageInfo;
23import android.content.pm.PackageManager;
24import android.net.ConnectivityManager;
25import android.net.ConnectivityManager.NetworkCallback;
26import android.net.IConnectivityManager;
27import android.net.Network;
28import android.net.NetworkCapabilities;
29import android.net.NetworkRequest;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.Message;
33import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.os.SystemProperties;
36import android.os.UserHandle;
37import android.os.UserManager;
38import android.preference.Preference;
39import android.preference.PreferenceGroup;
40import android.preference.PreferenceScreen;
41import android.security.Credentials;
42import android.security.KeyStore;
43import android.util.SparseArray;
44import android.view.Menu;
45import android.view.MenuInflater;
46import android.view.MenuItem;
47import android.view.View;
48import android.widget.TextView;
49
50import com.android.internal.logging.MetricsLogger;
51import com.android.internal.net.LegacyVpnInfo;
52import com.android.internal.net.VpnConfig;
53import com.android.internal.net.VpnProfile;
54import com.android.internal.util.ArrayUtils;
55import com.android.settings.R;
56import com.android.settings.SettingsPreferenceFragment;
57import com.google.android.collect.Lists;
58
59import java.util.ArrayList;
60import java.util.HashMap;
61import java.util.HashSet;
62import java.util.List;
63
64import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
65
66/**
67 * Settings screen listing VPNs. Configured VPNs and networks managed by apps
68 * are shown in the same list.
69 */
70public class VpnSettings extends SettingsPreferenceFragment implements
71        Handler.Callback, Preference.OnPreferenceClickListener {
72    private static final String LOG_TAG = "VpnSettings";
73
74    private static final int RESCAN_MESSAGE = 0;
75    private static final int RESCAN_INTERVAL_MS = 1000;
76
77    private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN";
78    private static final NetworkRequest VPN_REQUEST = new NetworkRequest.Builder()
79            .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
80            .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
81            .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
82            .build();
83
84    private final IConnectivityManager mConnectivityService = IConnectivityManager.Stub
85            .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
86    private ConnectivityManager mConnectivityManager;
87    private UserManager mUserManager;
88
89    private final KeyStore mKeyStore = KeyStore.getInstance();
90
91    private HashMap<String, ConfigPreference> mConfigPreferences = new HashMap<>();
92    private HashMap<String, AppPreference> mAppPreferences = new HashMap<>();
93
94    private Handler mUpdater;
95    private LegacyVpnInfo mConnectedLegacyVpn;
96
97    private boolean mUnavailable;
98
99    @Override
100    protected int getMetricsCategory() {
101        return MetricsLogger.VPN;
102    }
103
104    @Override
105    public void onCreate(Bundle savedState) {
106        super.onCreate(savedState);
107
108        mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
109        if (mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)
110                || UserHandle.myUserId() != UserHandle.USER_OWNER) {
111            mUnavailable = true;
112            setPreferenceScreen(new PreferenceScreen(getActivity(), null));
113            return;
114        }
115
116        mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
117
118        setHasOptionsMenu(true);
119        addPreferencesFromResource(R.xml.vpn_settings2);
120    }
121
122    @Override
123    public void onDestroy() {
124        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
125        super.onDestroy();
126    }
127
128    @Override
129    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
130        super.onCreateOptionsMenu(menu, inflater);
131        inflater.inflate(R.menu.vpn, menu);
132    }
133
134    @Override
135    public void onPrepareOptionsMenu(Menu menu) {
136        super.onPrepareOptionsMenu(menu);
137
138        // Hide lockdown VPN on devices that require IMS authentication
139        if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
140            menu.findItem(R.id.vpn_lockdown).setVisible(false);
141        }
142    }
143
144    @Override
145    public boolean onOptionsItemSelected(MenuItem item) {
146        switch (item.getItemId()) {
147            case R.id.vpn_create: {
148                // Generate a new key. Here we just use the current time.
149                long millis = System.currentTimeMillis();
150                while (mConfigPreferences.containsKey(Long.toHexString(millis))) {
151                    ++millis;
152                }
153                VpnProfile profile = new VpnProfile(Long.toHexString(millis));
154                ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */);
155                return true;
156            }
157            case R.id.vpn_lockdown: {
158                LockdownConfigFragment.show(this);
159                return true;
160            }
161        }
162        return super.onOptionsItemSelected(item);
163    }
164
165    @Override
166    public void onResume() {
167        super.onResume();
168
169        if (mUnavailable) {
170            TextView emptyView = (TextView) getView().findViewById(android.R.id.empty);
171            getListView().setEmptyView(emptyView);
172            if (emptyView != null) {
173                emptyView.setText(R.string.vpn_settings_not_available);
174            }
175            return;
176        }
177
178        final boolean pickLockdown = getActivity()
179                .getIntent().getBooleanExtra(EXTRA_PICK_LOCKDOWN, false);
180        if (pickLockdown) {
181            LockdownConfigFragment.show(this);
182        }
183
184        // Start monitoring
185        mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback);
186
187        // Trigger a refresh
188        if (mUpdater == null) {
189            mUpdater = new Handler(this);
190        }
191        mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
192    }
193
194    @Override
195    public void onPause() {
196        // Pause monitoring
197        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
198
199        if (mUpdater != null) {
200            mUpdater.removeCallbacksAndMessages(null);
201        }
202
203        super.onPause();
204    }
205
206    @Override
207    public boolean handleMessage(Message message) {
208        mUpdater.removeMessages(RESCAN_MESSAGE);
209
210        // Pref group within which to list VPNs
211        PreferenceGroup vpnGroup = getPreferenceScreen();
212        vpnGroup.removeAll();
213        mConfigPreferences.clear();
214        mAppPreferences.clear();
215
216        // Fetch configured VPN profiles from KeyStore
217        for (VpnProfile profile : loadVpnProfiles(mKeyStore)) {
218            final ConfigPreference pref = new ConfigPreference(getActivity(), mManageListener,
219                    profile);
220            pref.setOnPreferenceClickListener(this);
221            mConfigPreferences.put(profile.key, pref);
222            vpnGroup.addPreference(pref);
223        }
224
225        // 3rd-party VPN apps can change elsewhere. Reload them every time.
226        for (AppOpsManager.PackageOps pkg : getVpnApps()) {
227            String key = getVpnIdentifier(UserHandle.getUserId(pkg.getUid()), pkg.getPackageName());
228            final AppPreference pref = new AppPreference(getActivity(), mManageListener,
229                    pkg.getPackageName(), pkg.getUid());
230            pref.setOnPreferenceClickListener(this);
231            mAppPreferences.put(key, pref);
232            vpnGroup.addPreference(pref);
233        }
234
235        // Mark out connections with a subtitle
236        try {
237            // Legacy VPNs
238            mConnectedLegacyVpn = null;
239            LegacyVpnInfo info = mConnectivityService.getLegacyVpnInfo();
240            if (info != null) {
241                ConfigPreference preference = mConfigPreferences.get(info.key);
242                if (preference != null) {
243                    preference.setState(info.state);
244                    mConnectedLegacyVpn = info;
245                }
246            }
247
248            // Third-party VPNs
249            for (UserHandle profile : mUserManager.getUserProfiles()) {
250                VpnConfig cfg = mConnectivityService.getVpnConfig(profile.getIdentifier());
251                if (cfg != null) {
252                    final String key = getVpnIdentifier(profile.getIdentifier(), cfg.user);
253                    final AppPreference preference = mAppPreferences.get(key);
254                    if (preference != null) {
255                        preference.setState(AppPreference.STATE_CONNECTED);
256                    }
257                }
258            }
259        } catch (RemoteException e) {
260            // ignore
261        }
262
263        mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS);
264        return true;
265    }
266
267    @Override
268    public boolean onPreferenceClick(Preference preference) {
269        if (preference instanceof ConfigPreference) {
270            VpnProfile profile = ((ConfigPreference) preference).getProfile();
271            if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) &&
272                    mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) {
273                try {
274                    mConnectedLegacyVpn.intent.send();
275                    return true;
276                } catch (Exception e) {
277                    // ignore
278                }
279            }
280            ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */);
281            return true;
282        } else if (preference instanceof AppPreference) {
283            AppPreference pref = (AppPreference) preference;
284            boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
285
286            if (!connected) {
287                try {
288                    UserHandle user = new UserHandle(UserHandle.getUserId(pref.getUid()));
289                    Context userContext = getActivity().createPackageContextAsUser(
290                            getActivity().getPackageName(), 0 /* flags */, user);
291                    PackageManager pm = userContext.getPackageManager();
292                    Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName());
293                    if (appIntent != null) {
294                        userContext.startActivityAsUser(appIntent, user);
295                        return true;
296                    }
297                } catch (PackageManager.NameNotFoundException nnfe) {
298                    // Fall through
299                }
300            }
301
302            // Already onnected or no launch intent available - show an info dialog
303            PackageInfo pkgInfo = pref.getPackageInfo();
304            AppDialogFragment.show(this, pkgInfo, pref.getLabel(), false /* editing */, connected);
305            return true;
306        }
307        return false;
308    }
309
310    private View.OnClickListener mManageListener = new View.OnClickListener() {
311        @Override
312        public void onClick(View view) {
313            Object tag = view.getTag();
314
315            if (tag instanceof ConfigPreference) {
316                ConfigPreference pref = (ConfigPreference) tag;
317                ConfigDialogFragment.show(VpnSettings.this, pref.getProfile(), true /* editing */,
318                        true /* exists */);
319            } else if (tag instanceof AppPreference) {
320                AppPreference pref = (AppPreference) tag;
321                boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
322                AppDialogFragment.show(VpnSettings.this, pref.getPackageInfo(), pref.getLabel(),
323                        true /* editing */, connected);
324            }
325        }
326    };
327
328    private static String getVpnIdentifier(int userId, String packageName) {
329        return Integer.toString(userId)+ "_" + packageName;
330    }
331
332    private NetworkCallback mNetworkCallback = new NetworkCallback() {
333        @Override
334        public void onAvailable(Network network) {
335            if (mUpdater != null) {
336                mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
337            }
338        }
339
340        @Override
341        public void onLost(Network network) {
342            if (mUpdater != null) {
343                mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
344            }
345        }
346    };
347
348    @Override
349    protected int getHelpResource() {
350        return R.string.help_url_vpn;
351    }
352
353    private List<AppOpsManager.PackageOps> getVpnApps() {
354        List<AppOpsManager.PackageOps> result = Lists.newArrayList();
355
356        // Build a filter of currently active user profiles.
357        SparseArray<Boolean> currentProfileIds = new SparseArray<>();
358        for (UserHandle profile : mUserManager.getUserProfiles()) {
359            currentProfileIds.put(profile.getIdentifier(), Boolean.TRUE);
360        }
361
362        // Fetch VPN-enabled apps from AppOps.
363        AppOpsManager aom = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
364        List<AppOpsManager.PackageOps> apps = aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN});
365        if (apps != null) {
366            for (AppOpsManager.PackageOps pkg : apps) {
367                int userId = UserHandle.getUserId(pkg.getUid());
368                if (currentProfileIds.get(userId) == null) {
369                    // Skip packages for users outside of our profile group.
370                    continue;
371                }
372                // Look for a MODE_ALLOWED permission to activate VPN.
373                boolean allowed = false;
374                for (AppOpsManager.OpEntry op : pkg.getOps()) {
375                    if (op.getOp() == OP_ACTIVATE_VPN &&
376                            op.getMode() == AppOpsManager.MODE_ALLOWED) {
377                        allowed = true;
378                    }
379                }
380                if (allowed) {
381                    result.add(pkg);
382                }
383            }
384        }
385        return result;
386    }
387
388    protected static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
389        final ArrayList<VpnProfile> result = Lists.newArrayList();
390
391        // This might happen if the user does not yet have a keystore. Quietly short-circuit because
392        // no keystore means no VPN configs.
393        if (!keyStore.isUnlocked()) {
394            return result;
395        }
396
397        // We are the only user of profiles in KeyStore so no locks are needed.
398        for (String key : keyStore.saw(Credentials.VPN)) {
399            final VpnProfile profile = VpnProfile.decode(key, keyStore.get(Credentials.VPN + key));
400            if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {
401                result.add(profile);
402            }
403        }
404        return result;
405    }
406}
407