summaryrefslogtreecommitdiffstats
path: root/src/com/android/settings/vpn2
diff options
context:
space:
mode:
authorRobin Lee <rgl@google.com>2015-04-09 17:13:08 +0100
committerRobin Lee <rgl@google.com>2015-04-21 19:42:53 +0100
commit2bd92d5d0685144aad566b9d29454fb519ff0371 (patch)
treeabc7d9a8b87c83b7e6bfa2fde1f169d900f2d877 /src/com/android/settings/vpn2
parent0ce64e26c03e5d826bbb4bc041b8ca59b6c19b8b (diff)
downloadpackages_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.java100
-rw-r--r--src/com/android/settings/vpn2/AppDialogFragment.java136
-rw-r--r--src/com/android/settings/vpn2/AppPreference.java132
-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.java160
-rw-r--r--src/com/android/settings/vpn2/ConfigPreference.java93
-rw-r--r--src/com/android/settings/vpn2/LockdownConfigFragment.java137
-rw-r--r--src/com/android/settings/vpn2/ManageablePreference.java50
-rw-r--r--src/com/android/settings/vpn2/VpnSettings.java579
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;