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