diff options
author | Robin Lee <rgl@google.com> | 2015-04-09 17:13:08 +0100 |
---|---|---|
committer | Robin Lee <rgl@google.com> | 2015-04-21 19:42:53 +0100 |
commit | 2bd92d5d0685144aad566b9d29454fb519ff0371 (patch) | |
tree | abc7d9a8b87c83b7e6bfa2fde1f169d900f2d877 /src/com/android/settings/vpn2 | |
parent | 0ce64e26c03e5d826bbb4bc041b8ca59b6c19b8b (diff) | |
download | packages_apps_Settings-2bd92d5d0685144aad566b9d29454fb519ff0371.zip packages_apps_Settings-2bd92d5d0685144aad566b9d29454fb519ff0371.tar.gz packages_apps_Settings-2bd92d5d0685144aad566b9d29454fb519ff0371.tar.bz2 |
vpn2: show third-party VPN services
VPN apps are shown alongside configured VPNs now. The requirement that
a password is set is now only enforced when setting up a configured
VPN as this is not necessary for apps.
Some UI redesign.
Bug: 19573824
Bug: 17474682
Bug: 19575658
Change-Id: I02bd977136929647d65b9784fb4cc5df24b45428
Diffstat (limited to 'src/com/android/settings/vpn2')
-rw-r--r-- | src/com/android/settings/vpn2/AppDialog.java | 100 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/AppDialogFragment.java | 136 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/AppPreference.java | 132 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/ConfigDialog.java (renamed from src/com/android/settings/vpn2/VpnDialog.java) | 31 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/ConfigDialogFragment.java | 160 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/ConfigPreference.java | 93 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/LockdownConfigFragment.java | 137 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/ManageablePreference.java | 50 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/VpnSettings.java | 579 |
9 files changed, 1024 insertions, 394 deletions
diff --git a/src/com/android/settings/vpn2/AppDialog.java b/src/com/android/settings/vpn2/AppDialog.java new file mode 100644 index 0000000..2145297 --- /dev/null +++ b/src/com/android/settings/vpn2/AppDialog.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; + +import com.android.internal.net.VpnConfig; +import com.android.settings.R; + +/** + * UI for managing the connection controlled by an app. + * + * Among the actions available are (depending on context): + * <ul> + * <li><strong>Forget</strong>: revoke the managing app's VPN permission</li> + * <li><strong>Dismiss</strong>: continue to use the VPN</li> + * </ul> + * + * {@see ConfigDialog} + */ +class AppDialog extends AlertDialog implements DialogInterface.OnClickListener { + private final PackageInfo mPkgInfo; + private final Listener mListener; + private final boolean mConnected; + + AppDialog(Context context, Listener listener, PackageInfo pkgInfo, boolean connected) { + super(context); + + mListener = listener; + mPkgInfo = pkgInfo; + mConnected = connected; + } + + public final PackageInfo getPackageInfo() { + return mPkgInfo; + } + + @Override + protected void onCreate(Bundle savedState) { + CharSequence vpnName; + try { + vpnName = VpnConfig.getVpnLabel(getContext(), mPkgInfo.packageName); + } catch (PackageManager.NameNotFoundException ex) { + vpnName = mPkgInfo.packageName; + } + + setTitle(vpnName); + setMessage(getContext().getString(R.string.vpn_version, mPkgInfo.versionName)); + + createButtons(); + super.onCreate(savedState); + } + + protected void createButtons() { + Context context = getContext(); + + if (mConnected) { + // Forget the network + setButton(DialogInterface.BUTTON_NEGATIVE, + context.getString(R.string.vpn_forget), this); + } + + // Dismiss + setButton(DialogInterface.BUTTON_POSITIVE, + context.getString(R.string.vpn_done), this); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_NEGATIVE) { + mListener.onForget(dialog); + } + dismiss(); + } + + public interface Listener { + public void onForget(DialogInterface dialog); + } +} diff --git a/src/com/android/settings/vpn2/AppDialogFragment.java b/src/com/android/settings/vpn2/AppDialogFragment.java new file mode 100644 index 0000000..fc8d9e3 --- /dev/null +++ b/src/com/android/settings/vpn2/AppDialogFragment.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.IConnectivityManager; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import com.android.internal.net.VpnConfig; +import com.android.settings.R; + +/** + * Fragment wrapper around an {@link AppDialog}. + */ +public class AppDialogFragment extends DialogFragment implements AppDialog.Listener { + private static final String TAG_APP_DIALOG = "vpnappdialog"; + private static final String TAG = "AppDialogFragment"; + + private static final String ARG_MANAGING = "managing"; + private static final String ARG_PACKAGE = "package"; + private static final String ARG_CONNECTED = "connected"; + + private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + + public static void show(VpnSettings parent, PackageInfo pkgInfo, boolean managing, + boolean connected) { + if (!parent.isAdded()) return; + + Bundle args = new Bundle(); + args.putParcelable(ARG_PACKAGE, pkgInfo); + args.putBoolean(ARG_MANAGING, managing); + args.putBoolean(ARG_CONNECTED, connected); + + final AppDialogFragment frag = new AppDialogFragment(); + frag.setArguments(args); + frag.setTargetFragment(parent, 0); + frag.show(parent.getFragmentManager(), TAG_APP_DIALOG); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + PackageInfo pkgInfo = (PackageInfo) args.getParcelable(ARG_PACKAGE); + boolean managing = args.getBoolean(ARG_MANAGING); + boolean connected = args.getBoolean(ARG_CONNECTED); + + if (managing) { + return new AppDialog(getActivity(), this, pkgInfo, connected); + } else { + // Build an AlertDialog with an option to disconnect. + + CharSequence vpnName; + try { + vpnName = VpnConfig.getVpnLabel(getActivity(), pkgInfo.packageName); + } catch (PackageManager.NameNotFoundException ex) { + vpnName = pkgInfo.packageName; + } + + AlertDialog.Builder dlog = new AlertDialog.Builder(getActivity()) + .setTitle(vpnName) + .setMessage(getActivity().getString(R.string.vpn_disconnect_confirm)) + .setNegativeButton(getActivity().getString(R.string.vpn_cancel), null); + + if (connected) { + dlog.setPositiveButton(getActivity().getString(R.string.vpn_disconnect), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + onDisconnect(dialog); + } + }); + } + return dlog.create(); + } + } + + @Override + public void dismiss() { + ((VpnSettings) getTargetFragment()).update(); + super.dismiss(); + } + + @Override + public void onCancel(DialogInterface dialog) { + dismiss(); + super.onCancel(dialog); + } + + @Override + public void onForget(final DialogInterface dialog) { + PackageInfo pkgInfo = (PackageInfo) getArguments().getParcelable(ARG_PACKAGE); + final String pkg = pkgInfo.packageName; + try { + VpnConfig vpnConfig = mService.getVpnConfig(); + if (vpnConfig != null && pkg.equals(vpnConfig.user) && !vpnConfig.legacy) { + mService.setVpnPackageAuthorization(false); + onDisconnect(dialog); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to forget authorization for " + pkg, e); + } + } + + private void onDisconnect(final DialogInterface dialog) { + PackageInfo pkgInfo = (PackageInfo) getArguments().getParcelable(ARG_PACKAGE); + try { + mService.prepareVpn(pkgInfo.packageName, VpnConfig.LEGACY_VPN); + } catch (RemoteException e) { + Log.e(TAG, "Failed to disconnect package " + pkgInfo.packageName, e); + } + } +} diff --git a/src/com/android/settings/vpn2/AppPreference.java b/src/com/android/settings/vpn2/AppPreference.java new file mode 100644 index 0000000..1935dd8 --- /dev/null +++ b/src/com/android/settings/vpn2/AppPreference.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.app.AppGlobals; +import android.content.Context; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.RemoteException; +import android.os.UserHandle; +import android.preference.Preference; +import android.view.View.OnClickListener; + +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.settings.R; + +/** + * {@link android.preference.Preference} containing information about a VPN + * application. Tracks the package name and connection state. + */ +public class AppPreference extends ManageablePreference { + public static final int STATE_CONNECTED = LegacyVpnInfo.STATE_CONNECTED; + public static final int STATE_DISCONNECTED = LegacyVpnInfo.STATE_DISCONNECTED; + + private int mState = STATE_DISCONNECTED; + private String mPackageName; + private String mName; + private int mUid; + + public AppPreference(Context context, OnClickListener onManage, final String packageName, + int uid) { + super(context, null /* attrs */, onManage); + mPackageName = packageName; + mUid = uid; + update(); + } + + public PackageInfo getPackageInfo() { + UserHandle user = new UserHandle(UserHandle.getUserId(mUid)); + try { + IPackageManager ipm = AppGlobals.getPackageManager(); + return ipm.getPackageInfo(mPackageName, 0 /* flags */, user.getIdentifier()); + } catch (RemoteException rme) { + return null; + } + } + + public String getPackageName() { + return mPackageName; + } + + public int getUid() { + return mUid; + } + + public int getState() { + return mState; + } + + public void setState(int state) { + mState = state; + update(); + } + + private void update() { + final String[] states = getContext().getResources().getStringArray(R.array.vpn_states); + setSummary(mState != STATE_DISCONNECTED ? states[mState] : ""); + + mName = mPackageName; + Drawable icon = null; + try { + // Make all calls to the package manager as the appropriate user. + int userId = UserHandle.getUserId(mUid); + Context userContext = getContext().createPackageContextAsUser( + getContext().getPackageName(), 0 /* flags */, new UserHandle(userId)); + PackageManager pm = userContext.getPackageManager(); + + // Fetch icon and VPN label + PackageInfo pkgInfo = pm.getPackageInfo(mPackageName, 0 /* flags */); + if (pkgInfo != null) { + icon = pkgInfo.applicationInfo.loadIcon(pm); + mName = VpnConfig.getVpnLabel(userContext, mPackageName).toString(); + } + } catch (PackageManager.NameNotFoundException nnfe) { + // Failed - use default app label and icon as fallback + } + if (icon == null) { + icon = getContext().getPackageManager().getDefaultActivityIcon(); + } + setTitle(mName); + setIcon(icon); + + notifyHierarchyChanged(); + } + + public int compareTo(Preference preference) { + if (preference instanceof AppPreference) { + AppPreference another = (AppPreference) preference; + int result; + if ((result = another.mState - mState) == 0 && + (result = mName.compareToIgnoreCase(another.mName)) == 0 && + (result = mPackageName.compareTo(another.mPackageName)) == 0) { + result = mUid - another.mUid; + } + return result; + } else if (preference instanceof ConfigPreference) { + // Use comparator from ConfigPreference + ConfigPreference another = (ConfigPreference) preference; + return -another.compareTo(this); + } else { + return super.compareTo(preference); + } + } +} + diff --git a/src/com/android/settings/vpn2/VpnDialog.java b/src/com/android/settings/vpn2/ConfigDialog.java index 2f95bce..57f43f4 100644 --- a/src/com/android/settings/vpn2/VpnDialog.java +++ b/src/com/android/settings/vpn2/ConfigDialog.java @@ -16,9 +16,6 @@ package com.android.settings.vpn2; -import com.android.internal.net.VpnProfile; -import com.android.settings.R; - import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -35,15 +32,26 @@ import android.widget.CheckBox; import android.widget.Spinner; import android.widget.TextView; +import com.android.internal.net.VpnProfile; +import com.android.settings.R; + import java.net.InetAddress; -class VpnDialog extends AlertDialog implements TextWatcher, +/** + * Dialog showing information about a VPN configuration. The dialog + * can be launched to either edit or prompt for credentials to connect + * to a user-added VPN. + * + * {@see AppDialog} + */ +class ConfigDialog extends AlertDialog implements TextWatcher, View.OnClickListener, AdapterView.OnItemSelectedListener { private final KeyStore mKeyStore = KeyStore.getInstance(); private final DialogInterface.OnClickListener mListener; private final VpnProfile mProfile; private boolean mEditing; + private boolean mExists; private View mView; @@ -64,19 +72,20 @@ class VpnDialog extends AlertDialog implements TextWatcher, private Spinner mIpsecServerCert; private CheckBox mSaveLogin; - VpnDialog(Context context, DialogInterface.OnClickListener listener, - VpnProfile profile, boolean editing) { + ConfigDialog(Context context, DialogInterface.OnClickListener listener, + VpnProfile profile, boolean editing, boolean exists) { super(context); + mListener = listener; mProfile = profile; mEditing = editing; + mExists = exists; } @Override protected void onCreate(Bundle savedState) { mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null); setView(mView); - setInverseBackgroundForced(true); Context context = getContext(); @@ -154,6 +163,12 @@ class VpnDialog extends AlertDialog implements TextWatcher, onClick(showOptions); } + // Create a button to forget the profile if it has already been saved.. + if (mExists) { + setButton(DialogInterface.BUTTON_NEUTRAL, + context.getString(R.string.vpn_forget), mListener); + } + // Create a button to save the profile. setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.vpn_save), mListener); @@ -173,7 +188,7 @@ class VpnDialog extends AlertDialog implements TextWatcher, context.getString(R.string.vpn_cancel), mListener); // Let AlertDialog create everything. - super.onCreate(null); + super.onCreate(savedState); // Disable the action button if necessary. getButton(DialogInterface.BUTTON_POSITIVE) diff --git a/src/com/android/settings/vpn2/ConfigDialogFragment.java b/src/com/android/settings/vpn2/ConfigDialogFragment.java new file mode 100644 index 0000000..42e1614 --- /dev/null +++ b/src/com/android/settings/vpn2/ConfigDialogFragment.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.net.IConnectivityManager; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.security.Credentials; +import android.security.KeyStore; +import android.util.Log; +import android.widget.Toast; + +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; +import com.android.settings.R; + +/** + * Fragment wrapper around a {@link ConfigDialog}. + */ +public class ConfigDialogFragment extends DialogFragment implements + DialogInterface.OnClickListener { + private static final String TAG_CONFIG_DIALOG = "vpnconfigdialog"; + private static final String TAG = "ConfigDialogFragment"; + + private static final String ARG_PROFILE = "profile"; + private static final String ARG_EDITING = "editing"; + private static final String ARG_EXISTS = "exists"; + + private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + + private boolean mUnlocking = false; + + public static void show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists) { + if (!parent.isAdded()) return; + + Bundle args = new Bundle(); + args.putParcelable(ARG_PROFILE, profile); + args.putBoolean(ARG_EDITING, edit); + args.putBoolean(ARG_EXISTS, exists); + + final ConfigDialogFragment frag = new ConfigDialogFragment(); + frag.setArguments(args); + frag.setTargetFragment(parent, 0); + frag.show(parent.getFragmentManager(), TAG_CONFIG_DIALOG); + } + + @Override + public void onResume() { + super.onResume(); + + // Check KeyStore here, so others do not need to deal with it. + if (!KeyStore.getInstance().isUnlocked()) { + if (!mUnlocking) { + // Let us unlock KeyStore. See you later! + Credentials.getInstance().unlock(getActivity()); + } else { + // We already tried, but it is still not working! + dismiss(); + } + mUnlocking = !mUnlocking; + return; + } + + // Now KeyStore is always unlocked. Reset the flag. + mUnlocking = false; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + VpnProfile profile = (VpnProfile) args.getParcelable(ARG_PROFILE); + boolean editing = args.getBoolean(ARG_EDITING); + boolean exists = args.getBoolean(ARG_EXISTS); + + return new ConfigDialog(getActivity(), this, profile, editing, exists); + } + + @Override + public void onClick(DialogInterface dialogInterface, int button) { + ConfigDialog dialog = (ConfigDialog) getDialog(); + VpnProfile profile = dialog.getProfile(); + + if (button == DialogInterface.BUTTON_POSITIVE) { + // Update KeyStore entry + KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(), + KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED); + + // Flush out old version of profile + disconnect(profile); + + // If we are not editing, connect! + if (!dialog.isEditing()) { + try { + connect(profile); + } catch (RemoteException e) { + Log.e(TAG, "Failed to connect", e); + } + } + } else if (button == DialogInterface.BUTTON_NEUTRAL) { + // Disable profile if connected + disconnect(profile); + + // Delete from KeyStore + KeyStore.getInstance().delete(Credentials.VPN + profile.key, KeyStore.UID_SELF); + } + dismiss(); + } + + @Override + public void dismiss() { + ((VpnSettings) getTargetFragment()).update(); + super.dismiss(); + } + + @Override + public void onCancel(DialogInterface dialog) { + dismiss(); + super.onCancel(dialog); + } + + private void connect(VpnProfile profile) throws RemoteException { + try { + mService.startLegacyVpn(profile); + } catch (IllegalStateException e) { + Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); + } + } + + private void disconnect(VpnProfile profile) { + try { + LegacyVpnInfo connected = mService.getLegacyVpnInfo(); + if (connected != null && profile.key.equals(connected.key)) { + mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to disconnect", e); + } + } +} diff --git a/src/com/android/settings/vpn2/ConfigPreference.java b/src/com/android/settings/vpn2/ConfigPreference.java new file mode 100644 index 0000000..4e6e16f --- /dev/null +++ b/src/com/android/settings/vpn2/ConfigPreference.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.content.Context; +import android.preference.Preference; +import android.view.View.OnClickListener; + +import static com.android.internal.net.LegacyVpnInfo.STATE_CONNECTED; + +import com.android.internal.net.VpnProfile; +import com.android.settings.R; + +/** + * {@link android.preference.Preference} referencing a VPN + * configuration. Tracks the underlying profile and its connection + * state. + */ +public class ConfigPreference extends ManageablePreference { + private VpnProfile mProfile; + private int mState = -1; + + ConfigPreference(Context context, OnClickListener onManage, VpnProfile profile) { + super(context, null /* attrs */, onManage); + setProfile(profile); + } + + public VpnProfile getProfile() { + return mProfile; + } + + public void setProfile(VpnProfile profile) { + mProfile = profile; + update(); + } + + public void setState(int state) { + mState = state; + update(); + } + + private void update() { + if (mState < 0) { + setSummary(""); + } else { + String[] states = getContext().getResources() + .getStringArray(R.array.vpn_states); + setSummary(states[mState]); + } + setIcon(R.mipmap.ic_launcher_settings); + setTitle(mProfile.name); + notifyHierarchyChanged(); + } + + @Override + public int compareTo(Preference preference) { + if (preference instanceof ConfigPreference) { + ConfigPreference another = (ConfigPreference) preference; + int result; + if ((result = another.mState - mState) == 0 && + (result = mProfile.name.compareTo(another.mProfile.name)) == 0 && + (result = mProfile.type - another.mProfile.type) == 0) { + result = mProfile.key.compareTo(another.mProfile.key); + } + return result; + } else if (preference instanceof AppPreference) { + // Try to sort connected VPNs first + AppPreference another = (AppPreference) preference; + if (mState != STATE_CONNECTED && another.getState() == AppPreference.STATE_CONNECTED) { + return 1; + } + // Show configured VPNs before app VPNs + return -1; + } else { + return super.compareTo(preference); + } + } +} + diff --git a/src/com/android/settings/vpn2/LockdownConfigFragment.java b/src/com/android/settings/vpn2/LockdownConfigFragment.java new file mode 100644 index 0000000..f36cb46 --- /dev/null +++ b/src/com/android/settings/vpn2/LockdownConfigFragment.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.os.Bundle; +import android.security.Credentials; +import android.security.KeyStore; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; + +import com.android.internal.net.VpnProfile; +import com.android.settings.R; +import com.google.android.collect.Lists; + +import java.util.ArrayList; +import java.util.List; + +/** + * Dialog to configure always-on VPN. + */ +public class LockdownConfigFragment extends DialogFragment { + private List<VpnProfile> mProfiles; + private List<CharSequence> mTitles; + private int mCurrentIndex; + + private static final String TAG_LOCKDOWN = "lockdown"; + + private static class TitleAdapter extends ArrayAdapter<CharSequence> { + public TitleAdapter(Context context, List<CharSequence> objects) { + super(context, com.android.internal.R.layout.select_dialog_singlechoice_material, + android.R.id.text1, objects); + } + } + + public static void show(VpnSettings parent) { + if (!parent.isAdded()) return; + + final LockdownConfigFragment dialog = new LockdownConfigFragment(); + dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN); + } + + private static String getStringOrNull(KeyStore keyStore, String key) { + if (!keyStore.isUnlocked()) { + return null; + } + final byte[] value = keyStore.get(key); + return value == null ? null : new String(value); + } + + private void initProfiles(KeyStore keyStore, Resources res) { + final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN); + + mProfiles = VpnSettings.loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP); + mTitles = new ArrayList<>(1 + mProfiles.size()); + mTitles.add(res.getText(R.string.vpn_lockdown_none)); + + mCurrentIndex = 0; + for (VpnProfile profile : mProfiles) { + if (TextUtils.equals(profile.key, lockdownKey)) { + mCurrentIndex = mTitles.size(); + } + mTitles.add(profile.name); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + final KeyStore keyStore = KeyStore.getInstance(); + + initProfiles(keyStore, context.getResources()); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); + + builder.setTitle(R.string.vpn_menu_lockdown); + + final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false); + final ListView listView = (ListView) view.findViewById(android.R.id.list); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + listView.setAdapter(new TitleAdapter(context, mTitles)); + listView.setItemChecked(mCurrentIndex, true); + builder.setView(view); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final int newIndex = listView.getCheckedItemPosition(); + if (mCurrentIndex == newIndex) return; + + if (newIndex == 0) { + keyStore.delete(Credentials.LOCKDOWN_VPN); + } else { + final VpnProfile profile = mProfiles.get(newIndex - 1); + if (!profile.isValidLockdownProfile()) { + Toast.makeText(context, R.string.vpn_lockdown_config_error, + Toast.LENGTH_LONG).show(); + return; + } + keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes(), + KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED); + } + + // kick profiles since we changed them + ConnectivityManager.from(getActivity()).updateLockdownVpn(); + } + }); + + return builder.create(); + } +} + diff --git a/src/com/android/settings/vpn2/ManageablePreference.java b/src/com/android/settings/vpn2/ManageablePreference.java new file mode 100644 index 0000000..5e507c1 --- /dev/null +++ b/src/com/android/settings/vpn2/ManageablePreference.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; + +import com.android.settings.R; + +/** + * Preference with an additional gear icon. Touching the gear icon triggers an + * onChange event. + */ +public class ManageablePreference extends Preference { + OnClickListener mListener; + View mManageView; + + public ManageablePreference(Context context, AttributeSet attrs, OnClickListener onManage) { + super(context, attrs); + mListener = onManage; + setPersistent(false); + setOrder(0); + setWidgetLayoutResource(R.layout.preference_vpn); + } + + @Override + protected void onBindView(View view) { + mManageView = view.findViewById(R.id.manage); + mManageView.setOnClickListener(mListener); + mManageView.setTag(this); + super.onBindView(view); + } +} diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java index 04853f1..a333de9 100644 --- a/src/com/android/settings/vpn2/VpnSettings.java +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -16,39 +16,36 @@ package com.android.settings.vpn2; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; +import android.app.AppOpsManager; import android.content.Context; -import android.content.DialogInterface; -import android.content.res.Resources; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; import android.net.IConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; +import android.os.UserHandle; import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.security.Credentials; import android.security.KeyStore; -import android.text.TextUtils; -import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; +import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.ArrayAdapter; -import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; import com.android.internal.logging.MetricsLogger; import com.android.internal.net.LegacyVpnInfo; @@ -61,33 +58,39 @@ import com.google.android.collect.Lists; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; -public class VpnSettings extends SettingsPreferenceFragment implements - Handler.Callback, Preference.OnPreferenceClickListener, - DialogInterface.OnClickListener, DialogInterface.OnDismissListener { - private static final String TAG = "VpnSettings"; +import static android.app.AppOpsManager.OP_ACTIVATE_VPN; - private static final String TAG_LOCKDOWN = "lockdown"; +/** + * Settings screen listing VPNs. Configured VPNs and networks managed by apps + * are shown in the same list. + */ +public class VpnSettings extends SettingsPreferenceFragment implements + Handler.Callback, Preference.OnPreferenceClickListener { + private static final String LOG_TAG = "VpnSettings"; private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN"; + private static final NetworkRequest VPN_REQUEST = new NetworkRequest.Builder() + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + .build(); - // TODO: migrate to using DialogFragment when editing - - private final IConnectivityManager mService = IConnectivityManager.Stub + private final IConnectivityManager mConnectivityService = IConnectivityManager.Stub .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + private ConnectivityManager mConnectivityManager; + private UserManager mUserManager; + private final KeyStore mKeyStore = KeyStore.getInstance(); - private boolean mUnlocking = false; - private HashMap<String, VpnPreference> mPreferences = new HashMap<String, VpnPreference>(); - private VpnDialog mDialog; + private HashMap<String, ConfigPreference> mConfigPreferences = new HashMap<>(); + private HashMap<String, AppPreference> mAppPreferences = new HashMap<>(); private Handler mUpdater; - private LegacyVpnInfo mInfo; - private UserManager mUm; - - // The key of the profile for the current ContextMenu. - private String mSelectedKey; + private LegacyVpnInfo mConnectedLegacyVpn; + private HashSet<String> mConnectedVpns = new HashSet<>(); private boolean mUnavailable; @@ -100,25 +103,24 @@ public class VpnSettings extends SettingsPreferenceFragment implements public void onCreate(Bundle savedState) { super.onCreate(savedState); - mUm = (UserManager) getSystemService(Context.USER_SERVICE); - - if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) { + mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); + if (mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) { mUnavailable = true; setPreferenceScreen(new PreferenceScreen(getActivity(), null)); return; } + mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback); + setHasOptionsMenu(true); addPreferencesFromResource(R.xml.vpn_settings2); + } - if (savedState != null) { - VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"), - savedState.getByteArray("VpnProfile")); - if (profile != null) { - mDialog = new VpnDialog(getActivity(), this, profile, - savedState.getBoolean("VpnEditing")); - } - } + @Override + public void onDestroy() { + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + super.onDestroy(); } @Override @@ -143,13 +145,11 @@ public class VpnSettings extends SettingsPreferenceFragment implements case R.id.vpn_create: { // Generate a new key. Here we just use the current time. long millis = System.currentTimeMillis(); - while (mPreferences.containsKey(Long.toHexString(millis))) { + while (mConfigPreferences.containsKey(Long.toHexString(millis))) { ++millis; } - mDialog = new VpnDialog( - getActivity(), this, new VpnProfile(Long.toHexString(millis)), true); - mDialog.setOnDismissListener(this); - mDialog.show(); + VpnProfile profile = new VpnProfile(Long.toHexString(millis)); + ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */); return true; } case R.id.vpn_lockdown: { @@ -161,18 +161,6 @@ public class VpnSettings extends SettingsPreferenceFragment implements } @Override - public void onSaveInstanceState(Bundle savedState) { - // We do not save view hierarchy, as they are just profiles. - if (mDialog != null) { - VpnProfile profile = mDialog.getProfile(); - savedState.putString("VpnKey", profile.key); - savedState.putByteArray("VpnProfile", profile.encode()); - savedState.putBoolean("VpnEditing", mDialog.isEditing()); - } - // else? - } - - @Override public void onResume() { super.onResume(); @@ -191,42 +179,32 @@ public class VpnSettings extends SettingsPreferenceFragment implements LockdownConfigFragment.show(this); } - // Check KeyStore here, so others do not need to deal with it. - if (!mKeyStore.isUnlocked()) { - if (!mUnlocking) { - // Let us unlock KeyStore. See you later! - Credentials.getInstance().unlock(getActivity()); - } else { - // We already tried, but it is still not working! - finishFragment(); - } - mUnlocking = !mUnlocking; - return; - } + update(); + } - // Now KeyStore is always unlocked. Reset the flag. - mUnlocking = false; - - // Currently we are the only user of profiles in KeyStore. - // Assuming KeyStore and KeyGuard do the right thing, we can - // safely cache profiles in the memory. - if (mPreferences.size() == 0) { - PreferenceGroup group = getPreferenceScreen(); - - final Context context = getActivity(); - final List<VpnProfile> profiles = loadVpnProfiles(mKeyStore); - for (VpnProfile profile : profiles) { - final VpnPreference pref = new VpnPreference(context, profile); - pref.setOnPreferenceClickListener(this); - mPreferences.put(profile.key, pref); - group.addPreference(pref); - } + public void update() { + // Pref group within which to list VPNs + PreferenceGroup vpnGroup = getPreferenceScreen(); + vpnGroup.removeAll(); + mConfigPreferences.clear(); + mAppPreferences.clear(); + + // Fetch configured VPN profiles from KeyStore + for (VpnProfile profile : loadVpnProfiles(mKeyStore)) { + final ConfigPreference pref = new ConfigPreference(getActivity(), mManageListener, + profile); + pref.setOnPreferenceClickListener(this); + mConfigPreferences.put(profile.key, pref); + vpnGroup.addPreference(pref); } - // Show the dialog if there is one. - if (mDialog != null) { - mDialog.setOnDismissListener(this); - mDialog.show(); + // 3rd-party VPN apps can change elsewhere. Reload them every time. + for (AppOpsManager.PackageOps pkg : getVpnApps()) { + final AppPreference pref = new AppPreference(getActivity(), mManageListener, + pkg.getPackageName(), pkg.getUid()); + pref.setOnPreferenceClickListener(this); + mAppPreferences.put(pkg.getPackageName(), pref); + vpnGroup.addPreference(pref); } // Start monitoring. @@ -234,149 +212,67 @@ public class VpnSettings extends SettingsPreferenceFragment implements mUpdater = new Handler(this); } mUpdater.sendEmptyMessage(0); - - // Register for context menu. Hmmm, getListView() is hidden? - registerForContextMenu(getListView()); - } - - @Override - public void onPause() { - super.onPause(); - - if (mUnavailable) { - return; - } - - // Hide the dialog if there is one. - if (mDialog != null) { - mDialog.setOnDismissListener(null); - mDialog.dismiss(); - } - - // Unregister for context menu. - if (getView() != null) { - unregisterForContextMenu(getListView()); - } } @Override - public void onDismiss(DialogInterface dialog) { - // Here is the exit of a dialog. - mDialog = null; - } - - @Override - public void onClick(DialogInterface dialog, int button) { - if (button == DialogInterface.BUTTON_POSITIVE) { - // Always save the profile. - VpnProfile profile = mDialog.getProfile(); - mKeyStore.put(Credentials.VPN + profile.key, profile.encode(), KeyStore.UID_SELF, - KeyStore.FLAG_ENCRYPTED); - - // Update the preference. - VpnPreference preference = mPreferences.get(profile.key); - if (preference != null) { - disconnect(profile.key); - preference.update(profile); - } else { - preference = new VpnPreference(getActivity(), profile); - preference.setOnPreferenceClickListener(this); - mPreferences.put(profile.key, preference); - getPreferenceScreen().addPreference(preference); - } - - // If we are not editing, connect! - if (!mDialog.isEditing()) { + public boolean onPreferenceClick(Preference preference) { + if (preference instanceof ConfigPreference) { + VpnProfile profile = ((ConfigPreference) preference).getProfile(); + if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) && + mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) { try { - connect(profile); + mConnectedLegacyVpn.intent.send(); + return true; } catch (Exception e) { - Log.e(TAG, "connect", e); + // ignore } } - } - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) { - if (mDialog != null) { - Log.v(TAG, "onCreateContextMenu() is called when mDialog != null"); - return; - } + ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */); + return true; + } else if (preference instanceof AppPreference) { + AppPreference pref = (AppPreference) preference; + boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED); - if (info instanceof AdapterContextMenuInfo) { - Preference preference = (Preference) getListView().getItemAtPosition( - ((AdapterContextMenuInfo) info).position); - if (preference instanceof VpnPreference) { - VpnProfile profile = ((VpnPreference) preference).getProfile(); - mSelectedKey = profile.key; - menu.setHeaderTitle(profile.name); - menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit); - menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete); + if (!connected) { + try { + UserHandle user = new UserHandle(UserHandle.getUserId(pref.getUid())); + Context userContext = getActivity().createPackageContextAsUser( + getActivity().getPackageName(), 0 /* flags */, user); + PackageManager pm = userContext.getPackageManager(); + Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName()); + if (appIntent != null) { + userContext.startActivityAsUser(appIntent, user); + return true; + } + } catch (PackageManager.NameNotFoundException nnfe) { + // Fall through + } } - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - if (mDialog != null) { - Log.v(TAG, "onContextItemSelected() is called when mDialog != null"); - return false; - } - - VpnPreference preference = mPreferences.get(mSelectedKey); - if (preference == null) { - Log.v(TAG, "onContextItemSelected() is called but no preference is found"); - return false; - } - switch (item.getItemId()) { - case R.string.vpn_menu_edit: - mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true); - mDialog.setOnDismissListener(this); - mDialog.show(); - return true; - case R.string.vpn_menu_delete: - disconnect(mSelectedKey); - getPreferenceScreen().removePreference(preference); - mPreferences.remove(mSelectedKey); - mKeyStore.delete(Credentials.VPN + mSelectedKey); - return true; + // Already onnected or no launch intent available - show an info dialog + PackageInfo pkgInfo = pref.getPackageInfo(); + AppDialogFragment.show(this, pkgInfo, false /* editing */, connected); + return true; } return false; } - @Override - public boolean onPreferenceClick(Preference preference) { - if (mDialog != null) { - Log.v(TAG, "onPreferenceClick() is called when mDialog != null"); - return true; - } - - if (preference instanceof VpnPreference) { - VpnProfile profile = ((VpnPreference) preference).getProfile(); - if (mInfo != null && profile.key.equals(mInfo.key) && - mInfo.state == LegacyVpnInfo.STATE_CONNECTED) { - try { - mInfo.intent.send(); - return true; - } catch (Exception e) { - // ignore - } - } - mDialog = new VpnDialog(getActivity(), this, profile, false); - } else { - // Generate a new key. Here we just use the current time. - long millis = System.currentTimeMillis(); - while (mPreferences.containsKey(Long.toHexString(millis))) { - ++millis; + private View.OnClickListener mManageListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + Object tag = view.getTag(); + + if (tag instanceof ConfigPreference) { + ConfigPreference pref = (ConfigPreference) tag; + ConfigDialogFragment.show(VpnSettings.this, pref.getProfile(), true /* editing */, + true /* exists */); + } else if (tag instanceof AppPreference) { + AppPreference pref = (AppPreference) tag; + AppDialogFragment.show(VpnSettings.this, pref.getPackageInfo(), true /* editing */, + (pref.getState() == AppPreference.STATE_CONNECTED) /* connected */); } - mDialog = new VpnDialog(getActivity(), this, - new VpnProfile(Long.toHexString(millis)), true); } - mDialog.setOnDismissListener(this); - mDialog.show(); - return true; - } + }; @Override public boolean handleMessage(Message message) { @@ -384,22 +280,43 @@ public class VpnSettings extends SettingsPreferenceFragment implements if (isResumed()) { try { - LegacyVpnInfo info = mService.getLegacyVpnInfo(); - if (mInfo != null) { - VpnPreference preference = mPreferences.get(mInfo.key); + // Legacy VPNs + LegacyVpnInfo info = mConnectivityService.getLegacyVpnInfo(); + if (mConnectedLegacyVpn != null) { + ConfigPreference preference = mConfigPreferences.get(mConnectedLegacyVpn.key); if (preference != null) { - preference.update(-1); + preference.setState(-1); } - mInfo = null; + mConnectedLegacyVpn = null; } if (info != null) { - VpnPreference preference = mPreferences.get(info.key); + ConfigPreference preference = mConfigPreferences.get(info.key); + if (preference != null) { + preference.setState(info.state); + mConnectedLegacyVpn = info; + } + } + + // VPN apps + for (String key : mConnectedVpns) { + AppPreference preference = mAppPreferences.get(key); if (preference != null) { - preference.update(info.state); - mInfo = info; + preference.setState(AppPreference.STATE_DISCONNECTED); } } - } catch (Exception e) { + mConnectedVpns.clear(); + // TODO: also query VPN services in user profiles STOPSHIP + VpnConfig cfg = mConnectivityService.getVpnConfig(); + if (cfg != null) { + mConnectedVpns.add(cfg.user); + } + for (String key : mConnectedVpns) { + AppPreference preference = mAppPreferences.get(key); + if (preference != null) { + preference.setState(AppPreference.STATE_CONNECTED); + } + } + } catch (RemoteException e) { // ignore } mUpdater.sendEmptyMessageDelayed(0, 1000); @@ -407,186 +324,76 @@ public class VpnSettings extends SettingsPreferenceFragment implements return true; } - private void connect(VpnProfile profile) throws Exception { - try { - mService.startLegacyVpn(profile); - } catch (IllegalStateException e) { - Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); + private NetworkCallback mNetworkCallback = new NetworkCallback() { + @Override + public void onAvailable(Network network) { + if (mUpdater != null) { + mUpdater.sendEmptyMessage(0); + } } - } - private void disconnect(String key) { - if (mInfo != null && key.equals(mInfo.key)) { - try { - mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN); - } catch (Exception e) { - // ignore + @Override + public void onLost(Network network) { + if (mUpdater != null) { + mUpdater.sendEmptyMessage(0); } } - } + }; @Override protected int getHelpResource() { return R.string.help_url_vpn; } - private static class VpnPreference extends Preference { - private VpnProfile mProfile; - private int mState = -1; - - VpnPreference(Context context, VpnProfile profile) { - super(context); - setPersistent(false); - setOrder(0); - - mProfile = profile; - update(); - } - - VpnProfile getProfile() { - return mProfile; - } - - void update(VpnProfile profile) { - mProfile = profile; - update(); - } + private List<AppOpsManager.PackageOps> getVpnApps() { + List<AppOpsManager.PackageOps> result = Lists.newArrayList(); - void update(int state) { - mState = state; - update(); + // Build a filter of currently active user profiles. + SparseArray<Boolean> currentProfileIds = new SparseArray<>(); + for (UserHandle profile : mUserManager.getUserProfiles()) { + currentProfileIds.put(profile.getIdentifier(), Boolean.TRUE); } - void update() { - if (mState < 0) { - String[] types = getContext().getResources() - .getStringArray(R.array.vpn_types_long); - setSummary(types[mProfile.type]); - } else { - String[] states = getContext().getResources() - .getStringArray(R.array.vpn_states); - setSummary(states[mState]); - } - setTitle(mProfile.name); - notifyHierarchyChanged(); - } - - @Override - public int compareTo(Preference preference) { - int result = -1; - if (preference instanceof VpnPreference) { - VpnPreference another = (VpnPreference) preference; - if ((result = another.mState - mState) == 0 && - (result = mProfile.name.compareTo(another.mProfile.name)) == 0 && - (result = mProfile.type - another.mProfile.type) == 0) { - result = mProfile.key.compareTo(another.mProfile.key); + // Fetch VPN-enabled apps from AppOps. + AppOpsManager aom = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); + List<AppOpsManager.PackageOps> apps = aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN}); + if (apps != null) { + for (AppOpsManager.PackageOps pkg : apps) { + int userId = UserHandle.getUserId(pkg.getUid()); + if (currentProfileIds.get(userId) == null) { + // Skip packages for users outside of our profile group. + continue; } - } - return result; - } - } - - /** - * Dialog to configure always-on VPN. - */ - public static class LockdownConfigFragment extends DialogFragment { - private List<VpnProfile> mProfiles; - private List<CharSequence> mTitles; - private int mCurrentIndex; - - private static class TitleAdapter extends ArrayAdapter<CharSequence> { - public TitleAdapter(Context context, List<CharSequence> objects) { - super(context, com.android.internal.R.layout.select_dialog_singlechoice_material, - android.R.id.text1, objects); - } - } - - public static void show(VpnSettings parent) { - if (!parent.isAdded()) return; - - final LockdownConfigFragment dialog = new LockdownConfigFragment(); - dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN); - } - - private static String getStringOrNull(KeyStore keyStore, String key) { - final byte[] value = keyStore.get(Credentials.LOCKDOWN_VPN); - return value == null ? null : new String(value); - } - - private void initProfiles(KeyStore keyStore, Resources res) { - final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN); - - mProfiles = loadVpnProfiles(keyStore, VpnProfile.TYPE_PPTP); - mTitles = Lists.newArrayList(); - mTitles.add(res.getText(R.string.vpn_lockdown_none)); - mCurrentIndex = 0; - - for (VpnProfile profile : mProfiles) { - if (TextUtils.equals(profile.key, lockdownKey)) { - mCurrentIndex = mTitles.size(); + // Look for a MODE_ALLOWED permission to activate VPN. + boolean allowed = false; + for (AppOpsManager.OpEntry op : pkg.getOps()) { + if (op.getOp() == OP_ACTIVATE_VPN && + op.getMode() == AppOpsManager.MODE_ALLOWED) { + allowed = true; + } + } + if (allowed) { + result.add(pkg); } - mTitles.add(profile.name); } } + return result; + } - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Context context = getActivity(); - final KeyStore keyStore = KeyStore.getInstance(); - - initProfiles(keyStore, context.getResources()); - - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); - - builder.setTitle(R.string.vpn_menu_lockdown); - - final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false); - final ListView listView = (ListView) view.findViewById(android.R.id.list); - listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); - listView.setAdapter(new TitleAdapter(context, mTitles)); - listView.setItemChecked(mCurrentIndex, true); - builder.setView(view); - - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - final int newIndex = listView.getCheckedItemPosition(); - if (mCurrentIndex == newIndex) return; - - if (newIndex == 0) { - keyStore.delete(Credentials.LOCKDOWN_VPN); - - } else { - final VpnProfile profile = mProfiles.get(newIndex - 1); - if (!profile.isValidLockdownProfile()) { - Toast.makeText(context, R.string.vpn_lockdown_config_error, - Toast.LENGTH_LONG).show(); - return; - } - keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes(), - KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED); - } - - // kick profiles since we changed them - ConnectivityManager.from(getActivity()).updateLockdownVpn(); - } - }); + protected static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) { + final ArrayList<VpnProfile> result = Lists.newArrayList(); - return builder.create(); + // This might happen if the user does not yet have a keystore. Quietly short-circuit because + // no keystore means no VPN configs. + if (!keyStore.isUnlocked()) { + return result; } - } - private static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) { - final ArrayList<VpnProfile> result = Lists.newArrayList(); - final String[] keys = keyStore.saw(Credentials.VPN); - if (keys != null) { - for (String key : keys) { - final VpnProfile profile = VpnProfile.decode( - key, keyStore.get(Credentials.VPN + key)); - if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) { - result.add(profile); - } + // We are the only user of profiles in KeyStore so no locks are needed. + for (String key : keyStore.saw(Credentials.VPN)) { + final VpnProfile profile = VpnProfile.decode(key, keyStore.get(Credentials.VPN + key)); + if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) { + result.add(profile); } } return result; |