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
This commit is contained in:
Robin Lee
2015-04-09 17:13:08 +01:00
parent 0ce64e26c0
commit 2bd92d5d06
12 changed files with 1085 additions and 406 deletions

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<View
android:id="@+id/divider_manage"
android:layout_width="2dip"
android:layout_height="match_parent"
android:layout_marginTop="5dip"
android:layout_marginBottom="5dip"
android:background="@android:drawable/divider_horizontal_dark" />
<ImageView
android:id="@+id/manage"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:paddingStart="16dip"
android:paddingEnd="16dip"
android:src="@drawable/ic_sysbar_quicksettings"
android:contentDescription="@string/settings_label"
android:layout_gravity="center"
android:background="?android:attr/selectableItemBackground" />
</LinearLayout>

View File

@@ -20,7 +20,7 @@
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="3mm">
android:padding="8dp">
<LinearLayout android:id="@+id/editor"
android:layout_width="match_parent"

View File

@@ -5239,14 +5239,24 @@
<!-- Button label to cancel changing a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_cancel">Cancel</string>
<!-- Button label to finish editing a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_done">Dismiss</string>
<!-- Button label to save a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_save">Save</string>
<!-- Button label to connect to a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_connect">Connect</string>
<!-- Dialog title to edit a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_edit">Edit VPN profile</string>
<!-- Button label to forget a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_forget">Forget</string>
<!-- Dialog title to connect to a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_connect_to">Connect to <xliff:g id="profile" example="School">%s</xliff:g></string>
<!-- Dialog message body to disconnect from a VPN profile. -->
<string name="vpn_disconnect_confirm">Disconnect this VPN.</string>
<!-- Button label to disconnect from a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_disconnect">Disconnect</string>
<!-- Field label to show the version number for a VPN app. [CHAR LIMIT=40] -->
<string name="vpn_version">Version <xliff:g id="version" example="3.3.0">%s</xliff:g></string>
<!-- Preference title for VPN settings. [CHAR LIMIT=40] -->
<string name="vpn_title">VPN</string>

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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 final KeyStore mKeyStore = KeyStore.getInstance();
private boolean mUnlocking = false;
private ConnectivityManager mConnectivityManager;
private UserManager mUserManager;
private HashMap<String, VpnPreference> mPreferences = new HashMap<String, VpnPreference>();
private VpnDialog mDialog;
private final KeyStore mKeyStore = KeyStore.getInstance();
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: {
@@ -160,18 +160,6 @@ public class VpnSettings extends SettingsPreferenceFragment implements
return super.onOptionsItemSelected(item);
}
@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();
}
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);
}
// 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);
}
}
// 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,172 +212,111 @@ 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()) {
try {
connect(profile);
} catch (Exception e) {
Log.e(TAG, "connect", e);
}
}
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
if (mDialog != null) {
Log.v(TAG, "onCreateContextMenu() is called when mDialog != null");
return;
}
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);
}
}
}
@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;
}
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) {
if (preference instanceof ConfigPreference) {
VpnProfile profile = ((ConfigPreference) preference).getProfile();
if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) &&
mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) {
try {
mInfo.intent.send();
mConnectedLegacyVpn.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;
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 (!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
}
}
mDialog = new VpnDialog(getActivity(), this,
new VpnProfile(Long.toHexString(millis)), 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;
}
mDialog.setOnDismissListener(this);
mDialog.show();
return true;
return false;
}
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 */);
}
}
};
@Override
public boolean handleMessage(Message message) {
mUpdater.removeMessages(0);
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.update(info.state);
mInfo = info;
preference.setState(info.state);
mConnectedLegacyVpn = info;
}
}
} catch (Exception e) {
// VPN apps
for (String key : mConnectedVpns) {
AppPreference preference = mAppPreferences.get(key);
if (preference != null) {
preference.setState(AppPreference.STATE_DISCONNECTED);
}
}
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,188 +324,78 @@ 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 void disconnect(String key) {
if (mInfo != null && key.equals(mInfo.key)) {
try {
mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
} catch (Exception e) {
// ignore
private NetworkCallback mNetworkCallback = new NetworkCallback() {
@Override
public void onAvailable(Network network) {
if (mUpdater != null) {
mUpdater.sendEmptyMessage(0);
}
}
}
@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;
private List<AppOpsManager.PackageOps> getVpnApps() {
List<AppOpsManager.PackageOps> result = Lists.newArrayList();
VpnPreference(Context context, VpnProfile profile) {
super(context);
setPersistent(false);
setOrder(0);
mProfile = profile;
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);
}
VpnProfile getProfile() {
return mProfile;
}
void update(VpnProfile profile) {
mProfile = profile;
update();
}
void update(int state) {
mState = state;
update();
}
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();
}
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);
// 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;
}
// kick profiles since we changed them
ConnectivityManager.from(getActivity()).updateLockdownVpn();
}
});
return builder.create();
}
}
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);
if (allowed) {
result.add(pkg);
}
}
}
return result;
}
protected static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
final ArrayList<VpnProfile> result = Lists.newArrayList();
// 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;
}
// 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;
}
}