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:
39
res/layout/preference_vpn.xml
Normal file
39
res/layout/preference_vpn.xml
Normal 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>
|
@@ -20,7 +20,7 @@
|
|||||||
<LinearLayout android:layout_width="match_parent"
|
<LinearLayout android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="3mm">
|
android:padding="8dp">
|
||||||
|
|
||||||
<LinearLayout android:id="@+id/editor"
|
<LinearLayout android:id="@+id/editor"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@@ -5239,14 +5239,24 @@
|
|||||||
|
|
||||||
<!-- Button label to cancel changing a VPN profile. [CHAR LIMIT=40] -->
|
<!-- Button label to cancel changing a VPN profile. [CHAR LIMIT=40] -->
|
||||||
<string name="vpn_cancel">Cancel</string>
|
<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] -->
|
<!-- Button label to save a VPN profile. [CHAR LIMIT=40] -->
|
||||||
<string name="vpn_save">Save</string>
|
<string name="vpn_save">Save</string>
|
||||||
<!-- Button label to connect to a VPN profile. [CHAR LIMIT=40] -->
|
<!-- Button label to connect to a VPN profile. [CHAR LIMIT=40] -->
|
||||||
<string name="vpn_connect">Connect</string>
|
<string name="vpn_connect">Connect</string>
|
||||||
<!-- Dialog title to edit a VPN profile. [CHAR LIMIT=40] -->
|
<!-- Dialog title to edit a VPN profile. [CHAR LIMIT=40] -->
|
||||||
<string name="vpn_edit">Edit VPN profile</string>
|
<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] -->
|
<!-- 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>
|
<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] -->
|
<!-- Preference title for VPN settings. [CHAR LIMIT=40] -->
|
||||||
<string name="vpn_title">VPN</string>
|
<string name="vpn_title">VPN</string>
|
||||||
|
100
src/com/android/settings/vpn2/AppDialog.java
Normal file
100
src/com/android/settings/vpn2/AppDialog.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
136
src/com/android/settings/vpn2/AppDialogFragment.java
Normal file
136
src/com/android/settings/vpn2/AppDialogFragment.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
src/com/android/settings/vpn2/AppPreference.java
Normal file
132
src/com/android/settings/vpn2/AppPreference.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -16,9 +16,6 @@
|
|||||||
|
|
||||||
package com.android.settings.vpn2;
|
package com.android.settings.vpn2;
|
||||||
|
|
||||||
import com.android.internal.net.VpnProfile;
|
|
||||||
import com.android.settings.R;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
@@ -35,15 +32,26 @@ import android.widget.CheckBox;
|
|||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.android.internal.net.VpnProfile;
|
||||||
|
import com.android.settings.R;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
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 {
|
View.OnClickListener, AdapterView.OnItemSelectedListener {
|
||||||
private final KeyStore mKeyStore = KeyStore.getInstance();
|
private final KeyStore mKeyStore = KeyStore.getInstance();
|
||||||
private final DialogInterface.OnClickListener mListener;
|
private final DialogInterface.OnClickListener mListener;
|
||||||
private final VpnProfile mProfile;
|
private final VpnProfile mProfile;
|
||||||
|
|
||||||
private boolean mEditing;
|
private boolean mEditing;
|
||||||
|
private boolean mExists;
|
||||||
|
|
||||||
private View mView;
|
private View mView;
|
||||||
|
|
||||||
@@ -64,19 +72,20 @@ class VpnDialog extends AlertDialog implements TextWatcher,
|
|||||||
private Spinner mIpsecServerCert;
|
private Spinner mIpsecServerCert;
|
||||||
private CheckBox mSaveLogin;
|
private CheckBox mSaveLogin;
|
||||||
|
|
||||||
VpnDialog(Context context, DialogInterface.OnClickListener listener,
|
ConfigDialog(Context context, DialogInterface.OnClickListener listener,
|
||||||
VpnProfile profile, boolean editing) {
|
VpnProfile profile, boolean editing, boolean exists) {
|
||||||
super(context);
|
super(context);
|
||||||
|
|
||||||
mListener = listener;
|
mListener = listener;
|
||||||
mProfile = profile;
|
mProfile = profile;
|
||||||
mEditing = editing;
|
mEditing = editing;
|
||||||
|
mExists = exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedState) {
|
protected void onCreate(Bundle savedState) {
|
||||||
mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
|
mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
|
||||||
setView(mView);
|
setView(mView);
|
||||||
setInverseBackgroundForced(true);
|
|
||||||
|
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
|
|
||||||
@@ -154,6 +163,12 @@ class VpnDialog extends AlertDialog implements TextWatcher,
|
|||||||
onClick(showOptions);
|
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.
|
// Create a button to save the profile.
|
||||||
setButton(DialogInterface.BUTTON_POSITIVE,
|
setButton(DialogInterface.BUTTON_POSITIVE,
|
||||||
context.getString(R.string.vpn_save), mListener);
|
context.getString(R.string.vpn_save), mListener);
|
||||||
@@ -173,7 +188,7 @@ class VpnDialog extends AlertDialog implements TextWatcher,
|
|||||||
context.getString(R.string.vpn_cancel), mListener);
|
context.getString(R.string.vpn_cancel), mListener);
|
||||||
|
|
||||||
// Let AlertDialog create everything.
|
// Let AlertDialog create everything.
|
||||||
super.onCreate(null);
|
super.onCreate(savedState);
|
||||||
|
|
||||||
// Disable the action button if necessary.
|
// Disable the action button if necessary.
|
||||||
getButton(DialogInterface.BUTTON_POSITIVE)
|
getButton(DialogInterface.BUTTON_POSITIVE)
|
160
src/com/android/settings/vpn2/ConfigDialogFragment.java
Normal file
160
src/com/android/settings/vpn2/ConfigDialogFragment.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
src/com/android/settings/vpn2/ConfigPreference.java
Normal file
93
src/com/android/settings/vpn2/ConfigPreference.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
137
src/com/android/settings/vpn2/LockdownConfigFragment.java
Normal file
137
src/com/android/settings/vpn2/LockdownConfigFragment.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
50
src/com/android/settings/vpn2/ManageablePreference.java
Normal file
50
src/com/android/settings/vpn2/ManageablePreference.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@@ -16,39 +16,36 @@
|
|||||||
|
|
||||||
package com.android.settings.vpn2;
|
package com.android.settings.vpn2;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AppOpsManager;
|
||||||
import android.app.Dialog;
|
|
||||||
import android.app.DialogFragment;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.ConnectivityManager.NetworkCallback;
|
||||||
import android.net.IConnectivityManager;
|
import android.net.IConnectivityManager;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.net.NetworkCapabilities;
|
||||||
|
import android.net.NetworkRequest;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
import android.os.RemoteException;
|
||||||
import android.os.ServiceManager;
|
import android.os.ServiceManager;
|
||||||
import android.os.SystemProperties;
|
import android.os.SystemProperties;
|
||||||
|
import android.os.UserHandle;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.security.Credentials;
|
import android.security.Credentials;
|
||||||
import android.security.KeyStore;
|
import android.security.KeyStore;
|
||||||
import android.text.TextUtils;
|
import android.util.SparseArray;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.ContextMenu;
|
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.android.internal.logging.MetricsLogger;
|
import com.android.internal.logging.MetricsLogger;
|
||||||
import com.android.internal.net.LegacyVpnInfo;
|
import com.android.internal.net.LegacyVpnInfo;
|
||||||
@@ -61,33 +58,39 @@ import com.google.android.collect.Lists;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class VpnSettings extends SettingsPreferenceFragment implements
|
import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
|
||||||
Handler.Callback, Preference.OnPreferenceClickListener,
|
|
||||||
DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
|
|
||||||
private static final String TAG = "VpnSettings";
|
|
||||||
|
|
||||||
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 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 mConnectivityService = IConnectivityManager.Stub
|
||||||
|
|
||||||
private final IConnectivityManager mService = IConnectivityManager.Stub
|
|
||||||
.asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
|
.asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
|
||||||
private final KeyStore mKeyStore = KeyStore.getInstance();
|
private ConnectivityManager mConnectivityManager;
|
||||||
private boolean mUnlocking = false;
|
private UserManager mUserManager;
|
||||||
|
|
||||||
private HashMap<String, VpnPreference> mPreferences = new HashMap<String, VpnPreference>();
|
private final KeyStore mKeyStore = KeyStore.getInstance();
|
||||||
private VpnDialog mDialog;
|
|
||||||
|
private HashMap<String, ConfigPreference> mConfigPreferences = new HashMap<>();
|
||||||
|
private HashMap<String, AppPreference> mAppPreferences = new HashMap<>();
|
||||||
|
|
||||||
private Handler mUpdater;
|
private Handler mUpdater;
|
||||||
private LegacyVpnInfo mInfo;
|
private LegacyVpnInfo mConnectedLegacyVpn;
|
||||||
private UserManager mUm;
|
private HashSet<String> mConnectedVpns = new HashSet<>();
|
||||||
|
|
||||||
// The key of the profile for the current ContextMenu.
|
|
||||||
private String mSelectedKey;
|
|
||||||
|
|
||||||
private boolean mUnavailable;
|
private boolean mUnavailable;
|
||||||
|
|
||||||
@@ -100,25 +103,24 @@ public class VpnSettings extends SettingsPreferenceFragment implements
|
|||||||
public void onCreate(Bundle savedState) {
|
public void onCreate(Bundle savedState) {
|
||||||
super.onCreate(savedState);
|
super.onCreate(savedState);
|
||||||
|
|
||||||
mUm = (UserManager) getSystemService(Context.USER_SERVICE);
|
mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
|
||||||
|
if (mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) {
|
||||||
if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) {
|
|
||||||
mUnavailable = true;
|
mUnavailable = true;
|
||||||
setPreferenceScreen(new PreferenceScreen(getActivity(), null));
|
setPreferenceScreen(new PreferenceScreen(getActivity(), null));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback);
|
||||||
|
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
addPreferencesFromResource(R.xml.vpn_settings2);
|
addPreferencesFromResource(R.xml.vpn_settings2);
|
||||||
|
}
|
||||||
|
|
||||||
if (savedState != null) {
|
@Override
|
||||||
VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"),
|
public void onDestroy() {
|
||||||
savedState.getByteArray("VpnProfile"));
|
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
|
||||||
if (profile != null) {
|
super.onDestroy();
|
||||||
mDialog = new VpnDialog(getActivity(), this, profile,
|
|
||||||
savedState.getBoolean("VpnEditing"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -143,13 +145,11 @@ public class VpnSettings extends SettingsPreferenceFragment implements
|
|||||||
case R.id.vpn_create: {
|
case R.id.vpn_create: {
|
||||||
// Generate a new key. Here we just use the current time.
|
// Generate a new key. Here we just use the current time.
|
||||||
long millis = System.currentTimeMillis();
|
long millis = System.currentTimeMillis();
|
||||||
while (mPreferences.containsKey(Long.toHexString(millis))) {
|
while (mConfigPreferences.containsKey(Long.toHexString(millis))) {
|
||||||
++millis;
|
++millis;
|
||||||
}
|
}
|
||||||
mDialog = new VpnDialog(
|
VpnProfile profile = new VpnProfile(Long.toHexString(millis));
|
||||||
getActivity(), this, new VpnProfile(Long.toHexString(millis)), true);
|
ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */);
|
||||||
mDialog.setOnDismissListener(this);
|
|
||||||
mDialog.show();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case R.id.vpn_lockdown: {
|
case R.id.vpn_lockdown: {
|
||||||
@@ -160,18 +160,6 @@ public class VpnSettings extends SettingsPreferenceFragment implements
|
|||||||
return super.onOptionsItemSelected(item);
|
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
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@@ -191,42 +179,32 @@ public class VpnSettings extends SettingsPreferenceFragment implements
|
|||||||
LockdownConfigFragment.show(this);
|
LockdownConfigFragment.show(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check KeyStore here, so others do not need to deal with it.
|
update();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now KeyStore is always unlocked. Reset the flag.
|
public void update() {
|
||||||
mUnlocking = false;
|
// Pref group within which to list VPNs
|
||||||
|
PreferenceGroup vpnGroup = getPreferenceScreen();
|
||||||
|
vpnGroup.removeAll();
|
||||||
|
mConfigPreferences.clear();
|
||||||
|
mAppPreferences.clear();
|
||||||
|
|
||||||
// Currently we are the only user of profiles in KeyStore.
|
// Fetch configured VPN profiles from KeyStore
|
||||||
// Assuming KeyStore and KeyGuard do the right thing, we can
|
for (VpnProfile profile : loadVpnProfiles(mKeyStore)) {
|
||||||
// safely cache profiles in the memory.
|
final ConfigPreference pref = new ConfigPreference(getActivity(), mManageListener,
|
||||||
if (mPreferences.size() == 0) {
|
profile);
|
||||||
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);
|
pref.setOnPreferenceClickListener(this);
|
||||||
mPreferences.put(profile.key, pref);
|
mConfigPreferences.put(profile.key, pref);
|
||||||
group.addPreference(pref);
|
vpnGroup.addPreference(pref);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the dialog if there is one.
|
// 3rd-party VPN apps can change elsewhere. Reload them every time.
|
||||||
if (mDialog != null) {
|
for (AppOpsManager.PackageOps pkg : getVpnApps()) {
|
||||||
mDialog.setOnDismissListener(this);
|
final AppPreference pref = new AppPreference(getActivity(), mManageListener,
|
||||||
mDialog.show();
|
pkg.getPackageName(), pkg.getUid());
|
||||||
|
pref.setOnPreferenceClickListener(this);
|
||||||
|
mAppPreferences.put(pkg.getPackageName(), pref);
|
||||||
|
vpnGroup.addPreference(pref);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start monitoring.
|
// Start monitoring.
|
||||||
@@ -234,149 +212,67 @@ public class VpnSettings extends SettingsPreferenceFragment implements
|
|||||||
mUpdater = new Handler(this);
|
mUpdater = new Handler(this);
|
||||||
}
|
}
|
||||||
mUpdater.sendEmptyMessage(0);
|
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
|
@Override
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
if (mDialog != null) {
|
if (preference instanceof ConfigPreference) {
|
||||||
Log.v(TAG, "onPreferenceClick() is called when mDialog != null");
|
VpnProfile profile = ((ConfigPreference) preference).getProfile();
|
||||||
return true;
|
if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) &&
|
||||||
}
|
mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) {
|
||||||
|
|
||||||
if (preference instanceof VpnPreference) {
|
|
||||||
VpnProfile profile = ((VpnPreference) preference).getProfile();
|
|
||||||
if (mInfo != null && profile.key.equals(mInfo.key) &&
|
|
||||||
mInfo.state == LegacyVpnInfo.STATE_CONNECTED) {
|
|
||||||
try {
|
try {
|
||||||
mInfo.intent.send();
|
mConnectedLegacyVpn.intent.send();
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mDialog = new VpnDialog(getActivity(), this, profile, false);
|
ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */);
|
||||||
} else {
|
return true;
|
||||||
// Generate a new key. Here we just use the current time.
|
} else if (preference instanceof AppPreference) {
|
||||||
long millis = System.currentTimeMillis();
|
AppPreference pref = (AppPreference) preference;
|
||||||
while (mPreferences.containsKey(Long.toHexString(millis))) {
|
boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
|
||||||
++millis;
|
|
||||||
}
|
if (!connected) {
|
||||||
mDialog = new VpnDialog(getActivity(), this,
|
try {
|
||||||
new VpnProfile(Long.toHexString(millis)), true);
|
UserHandle user = new UserHandle(UserHandle.getUserId(pref.getUid()));
|
||||||
}
|
Context userContext = getActivity().createPackageContextAsUser(
|
||||||
mDialog.setOnDismissListener(this);
|
getActivity().getPackageName(), 0 /* flags */, user);
|
||||||
mDialog.show();
|
PackageManager pm = userContext.getPackageManager();
|
||||||
|
Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName());
|
||||||
|
if (appIntent != null) {
|
||||||
|
userContext.startActivityAsUser(appIntent, user);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
} catch (PackageManager.NameNotFoundException nnfe) {
|
||||||
|
// Fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@Override
|
||||||
public boolean handleMessage(Message message) {
|
public boolean handleMessage(Message message) {
|
||||||
@@ -384,22 +280,43 @@ public class VpnSettings extends SettingsPreferenceFragment implements
|
|||||||
|
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
try {
|
try {
|
||||||
LegacyVpnInfo info = mService.getLegacyVpnInfo();
|
// Legacy VPNs
|
||||||
if (mInfo != null) {
|
LegacyVpnInfo info = mConnectivityService.getLegacyVpnInfo();
|
||||||
VpnPreference preference = mPreferences.get(mInfo.key);
|
if (mConnectedLegacyVpn != null) {
|
||||||
|
ConfigPreference preference = mConfigPreferences.get(mConnectedLegacyVpn.key);
|
||||||
if (preference != null) {
|
if (preference != null) {
|
||||||
preference.update(-1);
|
preference.setState(-1);
|
||||||
}
|
}
|
||||||
mInfo = null;
|
mConnectedLegacyVpn = null;
|
||||||
}
|
}
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
VpnPreference preference = mPreferences.get(info.key);
|
ConfigPreference preference = mConfigPreferences.get(info.key);
|
||||||
if (preference != null) {
|
if (preference != null) {
|
||||||
preference.update(info.state);
|
preference.setState(info.state);
|
||||||
mInfo = info;
|
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
|
// ignore
|
||||||
}
|
}
|
||||||
mUpdater.sendEmptyMessageDelayed(0, 1000);
|
mUpdater.sendEmptyMessageDelayed(0, 1000);
|
||||||
@@ -407,188 +324,78 @@ public class VpnSettings extends SettingsPreferenceFragment implements
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void connect(VpnProfile profile) throws Exception {
|
private NetworkCallback mNetworkCallback = new NetworkCallback() {
|
||||||
try {
|
@Override
|
||||||
mService.startLegacyVpn(profile);
|
public void onAvailable(Network network) {
|
||||||
} catch (IllegalStateException e) {
|
if (mUpdater != null) {
|
||||||
Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show();
|
mUpdater.sendEmptyMessage(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void disconnect(String key) {
|
@Override
|
||||||
if (mInfo != null && key.equals(mInfo.key)) {
|
public void onLost(Network network) {
|
||||||
try {
|
if (mUpdater != null) {
|
||||||
mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
|
mUpdater.sendEmptyMessage(0);
|
||||||
} catch (Exception e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getHelpResource() {
|
protected int getHelpResource() {
|
||||||
return R.string.help_url_vpn;
|
return R.string.help_url_vpn;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class VpnPreference extends Preference {
|
private List<AppOpsManager.PackageOps> getVpnApps() {
|
||||||
private VpnProfile mProfile;
|
List<AppOpsManager.PackageOps> result = Lists.newArrayList();
|
||||||
private int mState = -1;
|
|
||||||
|
|
||||||
VpnPreference(Context context, VpnProfile profile) {
|
// Build a filter of currently active user profiles.
|
||||||
super(context);
|
SparseArray<Boolean> currentProfileIds = new SparseArray<>();
|
||||||
setPersistent(false);
|
for (UserHandle profile : mUserManager.getUserProfiles()) {
|
||||||
setOrder(0);
|
currentProfileIds.put(profile.getIdentifier(), Boolean.TRUE);
|
||||||
|
|
||||||
mProfile = profile;
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VpnProfile getProfile() {
|
// Fetch VPN-enabled apps from AppOps.
|
||||||
return mProfile;
|
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;
|
||||||
}
|
}
|
||||||
|
// Look for a MODE_ALLOWED permission to activate VPN.
|
||||||
void update(VpnProfile profile) {
|
boolean allowed = false;
|
||||||
mProfile = profile;
|
for (AppOpsManager.OpEntry op : pkg.getOps()) {
|
||||||
update();
|
if (op.getOp() == OP_ACTIVATE_VPN &&
|
||||||
|
op.getMode() == AppOpsManager.MODE_ALLOWED) {
|
||||||
|
allowed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(int state) {
|
|
||||||
mState = state;
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
if (allowed) {
|
||||||
void update() {
|
result.add(pkg);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
protected static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 ArrayList<VpnProfile> result = Lists.newArrayList();
|
||||||
final String[] keys = keyStore.saw(Credentials.VPN);
|
|
||||||
if (keys != null) {
|
// This might happen if the user does not yet have a keystore. Quietly short-circuit because
|
||||||
for (String key : keys) {
|
// no keystore means no VPN configs.
|
||||||
final VpnProfile profile = VpnProfile.decode(
|
if (!keyStore.isUnlocked()) {
|
||||||
key, keyStore.get(Credentials.VPN + key));
|
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)) {
|
if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {
|
||||||
result.add(profile);
|
result.add(profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user