diff --git a/res/values/strings.xml b/res/values/strings.xml index b788d09f5ef..f06383fe053 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5174,6 +5174,8 @@ Save Connect + + Continue Edit VPN profile @@ -5186,6 +5188,12 @@ Disconnect Version %s + + Forget VPN + + Replace existing VPN? + + You already have a VPN connected to this profile. If you connected to one, your existing VPN will be replaced. VPN diff --git a/res/xml/vpn_app_management.xml b/res/xml/vpn_app_management.xml new file mode 100644 index 00000000000..9e9b6cecb4e --- /dev/null +++ b/res/xml/vpn_app_management.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 5a76bf3fdf8..60973e143c0 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -521,21 +521,32 @@ public final class Utils extends com.android.settingslib.Utils { public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args, int titleResId, CharSequence title, boolean isShortcut, UserHandle userHandle) { - Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, - null /* titleResPackageName */, titleResId, title, isShortcut); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - context.startActivityAsUser(intent, userHandle); + // workaround to avoid crash in b/17523189 + if (userHandle.getIdentifier() == UserHandle.myUserId()) { + startWithFragment(context, fragmentName, args, null, 0, titleResId, title, isShortcut); + } else { + Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, + null /* titleResPackageName */, titleResId, title, isShortcut); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivityAsUser(intent, userHandle); + } } public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args, String titleResPackageName, int titleResId, CharSequence title, boolean isShortcut, UserHandle userHandle) { - Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, titleResPackageName, - titleResId, title, isShortcut); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - context.startActivityAsUser(intent, userHandle); + // workaround to avoid crash in b/17523189 + if (userHandle.getIdentifier() == UserHandle.myUserId()) { + startWithFragment(context, fragmentName, args, null, 0, titleResPackageName, titleResId, + title, isShortcut); + } else { + Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, + titleResPackageName, titleResId, title, isShortcut); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivityAsUser(intent, userHandle); + } } /** diff --git a/src/com/android/settings/vpn2/AppDialogFragment.java b/src/com/android/settings/vpn2/AppDialogFragment.java index 0e411175b94..e70b41289e6 100644 --- a/src/com/android/settings/vpn2/AppDialogFragment.java +++ b/src/com/android/settings/vpn2/AppDialogFragment.java @@ -19,6 +19,7 @@ package com.android.settings.vpn2; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; +import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageInfo; @@ -45,13 +46,25 @@ public class AppDialogFragment extends DialogFragment implements AppDialog.Liste private static final String ARG_PACKAGE = "package"; private PackageInfo mPackageInfo; + private Listener mListener; private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface( ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); - public static void show(VpnSettings parent, PackageInfo packageInfo, String label, + public interface Listener { + public void onForget(); + public void onCancel(); + } + + public static void show(Fragment parent, PackageInfo packageInfo, String label, boolean managing, boolean connected) { - if (!parent.isAdded()) return; + show(parent, null, packageInfo, label, managing, connected); + } + + public static void show(Fragment parent, Listener listener, PackageInfo packageInfo, + String label, boolean managing, boolean connected) { + if (!parent.isAdded()) + return; Bundle args = new Bundle(); args.putParcelable(ARG_PACKAGE, packageInfo); @@ -60,6 +73,7 @@ public class AppDialogFragment extends DialogFragment implements AppDialog.Liste args.putBoolean(ARG_CONNECTED, connected); final AppDialogFragment frag = new AppDialogFragment(); + frag.mListener = listener; frag.setArguments(args); frag.setTargetFragment(parent, 0); frag.show(parent.getFragmentManager(), TAG_APP_DIALOG); @@ -98,6 +112,9 @@ public class AppDialogFragment extends DialogFragment implements AppDialog.Liste @Override public void onCancel(DialogInterface dialog) { dismiss(); + if (mListener != null) { + mListener.onCancel(); + } super.onCancel(dialog); } @@ -111,6 +128,10 @@ public class AppDialogFragment extends DialogFragment implements AppDialog.Liste Log.e(TAG, "Failed to forget authorization of " + mPackageInfo.packageName + " for user " + userId, e); } + + if (mListener != null) { + mListener.onForget(); + } } private void onDisconnect(final DialogInterface dialog) { diff --git a/src/com/android/settings/vpn2/AppManagementFragment.java b/src/com/android/settings/vpn2/AppManagementFragment.java new file mode 100644 index 00000000000..0707d12ad4e --- /dev/null +++ b/src/com/android/settings/vpn2/AppManagementFragment.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2016 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.AppOpsManager; +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.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.support.v7.preference.Preference; +import android.util.Log; + +import com.android.internal.logging.MetricsProto.MetricsEvent; +import com.android.internal.net.VpnConfig; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settingslib.RestrictedSwitchPreference; +import com.android.settingslib.RestrictedPreference; +import com.android.settings.Utils; + +import java.util.List; + +import static android.app.AppOpsManager.OP_ACTIVATE_VPN; + +public class AppManagementFragment extends SettingsPreferenceFragment + implements Preference.OnPreferenceClickListener { + + private static final String TAG = "AppManagementFragment"; + + private static final String ARG_PACKAGE_NAME = "package"; + + private static final String KEY_VERSION = "version"; + private static final String KEY_ALWAYS_ON_VPN = "always_on_vpn"; + private static final String KEY_FORGET_VPN = "forget_vpn"; + + private AppOpsManager mAppOpsManager; + private PackageManager mPackageManager; + private ConnectivityManager mConnectivityManager; + + // VPN app info + private final int mUserId = UserHandle.myUserId(); + private int mPackageUid; + private String mPackageName; + private PackageInfo mPackageInfo; + private String mVpnLabel; + + // UI preference + private Preference mPreferenceVersion; + private RestrictedSwitchPreference mPreferenceAlwaysOn; + private RestrictedPreference mPreferenceForget; + + // Listener + private final AppDialogFragment.Listener mForgetVpnDialogFragmentListener = + new AppDialogFragment.Listener() { + @Override + public void onForget() { + // Unset always-on-vpn when forgetting the VPN + if (isVpnAlwaysOn()) { + setAlwaysOnVpn(false); + } + // Also dismiss and go back to VPN list + finish(); + } + + @Override + public void onCancel() { + // do nothing + } + }; + + public static void show(Context context, AppPreference pref) { + Bundle args = new Bundle(); + args.putString(ARG_PACKAGE_NAME, pref.getPackageName()); + Utils.startWithFragmentAsUser(context, AppManagementFragment.class.getName(), args, -1, + pref.getLabel(), false, new UserHandle(pref.getUserId())); + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + addPreferencesFromResource(R.xml.vpn_app_management); + + mPackageManager = getContext().getPackageManager(); + mAppOpsManager = getContext().getSystemService(AppOpsManager.class); + mConnectivityManager = getContext().getSystemService(ConnectivityManager.class); + + mPreferenceVersion = findPreference(KEY_VERSION); + mPreferenceAlwaysOn = (RestrictedSwitchPreference) findPreference(KEY_ALWAYS_ON_VPN); + mPreferenceForget = (RestrictedPreference) findPreference(KEY_FORGET_VPN); + + mPreferenceAlwaysOn.setOnPreferenceClickListener(this); + mPreferenceForget.setOnPreferenceClickListener(this); + } + + @Override + public void onResume() { + super.onResume(); + + boolean isInfoLoaded = loadInfo(); + if (isInfoLoaded) { + mPreferenceVersion.setTitle( + getPrefContext().getString(R.string.vpn_version, mPackageInfo.versionName)); + updateUI(); + } else { + finish(); + } + } + + @Override + public boolean onPreferenceClick(Preference preference) { + String key = preference.getKey(); + switch (key) { + case KEY_FORGET_VPN: + return onForgetVpnClick(); + case KEY_ALWAYS_ON_VPN: + return onAlwaysOnVpnClick(); + default: + Log.w(TAG, "unknown key is clicked: " + key); + return false; + } + } + + @Override + protected int getMetricsCategory() { + return MetricsEvent.VPN; + } + + private boolean onForgetVpnClick() { + AppDialogFragment.show(this, mForgetVpnDialogFragmentListener, mPackageInfo, mVpnLabel, + true /* editing */, true); + return true; + } + + private boolean onAlwaysOnVpnClick() { + final boolean isChecked = mPreferenceAlwaysOn.isChecked(); + if (isChecked && isLegacyVpnLockDownOrAnotherPackageAlwaysOn()) { + // Show dialog if user replace always-on-vpn package and show not checked first + mPreferenceAlwaysOn.setChecked(false); + ReplaceExistingVpnFragment.show(this); + } else { + setAlwaysOnVpn(isChecked); + } + return true; + } + + private void setAlwaysOnVpn(boolean isEnabled) { + // Only clear legacy lockdown vpn in system user. + if (mUserId == UserHandle.USER_SYSTEM) { + VpnUtils.clearLockdownVpn(getContext()); + } + mConnectivityManager.setAlwaysOnVpnPackageForUser(mUserId, isEnabled ? mPackageName : null); + updateUI(); + } + + private void updateUI() { + if (isAdded()) { + mPreferenceAlwaysOn.setChecked(isVpnAlwaysOn()); + } + } + + private String getAlwaysOnVpnPackage() { + return mConnectivityManager.getAlwaysOnVpnPackageForUser(mUserId); + } + + private boolean isVpnAlwaysOn() { + return mPackageName.equals(getAlwaysOnVpnPackage()); + } + + /** + * @return false if the intent doesn't contain an existing package or can't retrieve activated + * vpn info. + */ + private boolean loadInfo() { + final Bundle args = getArguments(); + if (args == null) { + Log.e(TAG, "empty bundle"); + return false; + } + + mPackageName = args.getString(ARG_PACKAGE_NAME); + if (mPackageName == null) { + Log.e(TAG, "empty package name"); + return false; + } + + try { + mPackageUid = mPackageManager.getPackageUid(mPackageName, /* PackageInfoFlags */ 0); + mPackageInfo = mPackageManager.getPackageInfo(mPackageName, /* PackageInfoFlags */ 0); + mVpnLabel = VpnConfig.getVpnLabel(getPrefContext(), mPackageName).toString(); + } catch (NameNotFoundException nnfe) { + Log.e(TAG, "package not found", nnfe); + return false; + } + + if (!isVpnActivated()) { + Log.e(TAG, "package didn't register VPN profile"); + return false; + } + + return true; + } + + private boolean isVpnActivated() { + final List apps = mAppOpsManager.getOpsForPackage(mPackageUid, + mPackageName, new int[]{OP_ACTIVATE_VPN}); + return apps != null && apps.size() > 0 && apps.get(0) != null; + } + + private boolean isLegacyVpnLockDownOrAnotherPackageAlwaysOn() { + if (mUserId == UserHandle.USER_SYSTEM) { + String lockdownKey = VpnUtils.getLockdownVpn(); + if (lockdownKey != null) { + return true; + } + } + + return getAlwaysOnVpnPackage() != null && !isVpnAlwaysOn(); + } + + public static class ReplaceExistingVpnFragment extends DialogFragment + implements DialogInterface.OnClickListener { + + public static void show(AppManagementFragment parent) { + final ReplaceExistingVpnFragment frag = new ReplaceExistingVpnFragment(); + frag.setTargetFragment(parent, 0); + frag.show(parent.getFragmentManager(), null); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.vpn_replace_always_on_vpn_title) + .setMessage(getActivity().getString(R.string.vpn_replace_always_on_vpn_message)) + .setNegativeButton(getActivity().getString(R.string.vpn_cancel), null) + .setPositiveButton(getActivity().getString(R.string.vpn_continue), this) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (getTargetFragment() instanceof AppManagementFragment) { + ((AppManagementFragment) getTargetFragment()).setAlwaysOnVpn(true); + } + } + } +} diff --git a/src/com/android/settings/vpn2/LockdownConfigFragment.java b/src/com/android/settings/vpn2/LockdownConfigFragment.java index 1a06565f279..8f19fa6c2bb 100644 --- a/src/com/android/settings/vpn2/LockdownConfigFragment.java +++ b/src/com/android/settings/vpn2/LockdownConfigFragment.java @@ -71,14 +71,9 @@ public class LockdownConfigFragment extends DialogFragment { dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN); } - private static String getStringOrNull(KeyStore keyStore, String key) { - final byte[] value = keyStore.get(key); - return value == null ? null : new String(value); - } - private void initProfiles(KeyStore keyStore, Resources res) { final ConnectivityManager cm = ConnectivityManager.from(getActivity()); - final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN); + final String lockdownKey = VpnUtils.getLockdownVpn(); final String alwaysOnPackage = cm.getAlwaysOnVpnPackageForUser(UserHandle.myUserId()); // Legacy VPN has a separate always-on mechanism which takes over the whole device, so diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java index e5a78b68f7e..c4576151c14 100644 --- a/src/com/android/settings/vpn2/VpnSettings.java +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -355,8 +355,7 @@ public class VpnSettings extends RestrictedSettingsFragment implements } else if (tag instanceof AppPreference) { AppPreference pref = (AppPreference) tag; boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED); - AppDialogFragment.show(VpnSettings.this, pref.getPackageInfo(), pref.getLabel(), - true /* editing */, connected); + AppManagementFragment.show(getPrefContext(), pref); } } }; diff --git a/src/com/android/settings/vpn2/VpnUtils.java b/src/com/android/settings/vpn2/VpnUtils.java new file mode 100644 index 00000000000..122816ad487 --- /dev/null +++ b/src/com/android/settings/vpn2/VpnUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016 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.net.ConnectivityManager; +import android.security.Credentials; +import android.security.KeyStore; + +/** + * Utility functions for vpn + */ +public class VpnUtils { + + public static String getLockdownVpn() { + final byte[] value = KeyStore.getInstance().get(Credentials.LOCKDOWN_VPN); + return value == null ? null : new String(value); + } + + public static void clearLockdownVpn(Context context) { + KeyStore.getInstance().delete(Credentials.LOCKDOWN_VPN); + // Always notify ConnectivityManager after keystore update + context.getSystemService(ConnectivityManager.class).updateLockdownVpn(); + } +}