Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b6f787c4 authored by Robin Lee's avatar Robin Lee
Browse files

Show a disclaimer about enabling vpn lockdown

Lockdown is now the default option, not best-effort mode. It's easier
to shoot oneself in the foot now so we'll show a warning to explain that
before switching it on.

Bug: 29052115
Bug: 29076208
Test: com.android.settings.vpn2.AppSettingsTest
Change-Id: Ia6845e6a7d57baa5476b8a021fb1255fd74aabea
parent 0cff709a
Loading
Loading
Loading
Loading
+12 −2
Original line number Diff line number Diff line
@@ -5347,9 +5347,19 @@
    <!-- Button label to forget a VPN profile [CHAR LIMIT=40] -->
    <string name="vpn_forget_long">Forget VPN</string>
    <!-- Dialog message title to set another VPN app to be always-on [CHAR LIMIT=40] -->
    <string name="vpn_replace_always_on_vpn_title">Replace existing VPN?</string>
    <string name="vpn_replace_vpn_title">Replace existing VPN?</string>
    <!-- Dialog message title to set always-on VPN when another app was not already set. -->
    <string name="vpn_set_vpn_title">Set always-on VPN?</string>
    <!-- Dialog message body to explain that always-on VPN will disable network traffic while the VPN is connecting. -->
    <string name="vpn_first_always_on_vpn_message">By turning on this setting, you won\'t have an Internet connection until the VPN successfully connects</string>
    <!-- Dialog message body to explain that always-on VPN will disable network traffic while the VPN is connecting, and that this will replace the current VPN. -->
    <string name="vpn_replace_always_on_vpn_enable_message">Your existing VPN will be replaced, and you won\'t have an Internet connection until the VPN successfully connects</string>
    <!-- Dialog message body to connect a VPN app, replacing another VPN app that is already always-on [CHAR LIMIT=NONE] -->
    <string name="vpn_replace_always_on_vpn_disable_message">You\'re already connected to an always-on VPN. If you connect to a different one, your existing VPN will be replaced, and always-on mode will turn off.</string>
    <!-- Dialog message body to set another VPN app to be always-on [CHAR LIMIT=NONE] -->
    <string name="vpn_replace_always_on_vpn_message">You\'re already connected to a VPN. If you connect to a different one, your existing VPN will be replaced.</string>
    <string name="vpn_replace_vpn_message">You\'re already connected to a VPN. If you connect to a different one, your existing VPN will be replaced.</string>
    <!-- Dialog action button to turn on a VPN. -->
    <string name="vpn_turn_on">Turn on</string>
    <!-- Dialog mesage title when the user can't connect an always-on vpn [CHAR LIMIT=NONE] -->
    <string name="vpn_cant_connect_title"><xliff:g id="vpn_name" example="OpenVPN">%1$s</xliff:g> can\'t connect</string>
    <!-- Dialog message subtitle when the user can't connect an always-on vpn [CHAR LIMIT=NONE] -->
+1 −7
Original line number Diff line number Diff line
@@ -158,7 +158,7 @@ public class AppDialogFragment extends InstrumentedDialogFragment implements App
        }
        final int userId = getUserId();
        try {
            if (mPackageInfo.packageName.equals(getConnectedPackage(mService, userId))) {
            if (mPackageInfo.packageName.equals(VpnUtils.getConnectedPackage(mService, userId))) {
                mService.setAlwaysOnVpnPackage(userId, null, /* lockdownEnabled */ false);
                mService.prepareVpn(mPackageInfo.packageName, VpnConfig.LEGACY_VPN, userId);
            }
@@ -176,10 +176,4 @@ public class AppDialogFragment extends InstrumentedDialogFragment implements App
    private int getUserId() {
        return UserHandle.getUserId(mPackageInfo.applicationInfo.uid);
    }

    private static String getConnectedPackage(IConnectivityManager service, final int userId)
            throws RemoteException {
        final VpnConfig config = service.getVpnConfig(userId);
        return config != null ? config.user : null;
    }
}
+36 −55
Original line number Diff line number Diff line
@@ -21,18 +21,21 @@ import android.app.AppOpsManager;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.IConnectivityManager;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.support.v7.preference.Preference;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
@@ -51,7 +54,8 @@ import java.util.List;
import static android.app.AppOpsManager.OP_ACTIVATE_VPN;

public class AppManagementFragment extends SettingsPreferenceFragment
        implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener {
        implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
        ConfirmLockdownFragment.ConfirmLockdownListener {

    private static final String TAG = "AppManagementFragment";

@@ -63,6 +67,7 @@ public class AppManagementFragment extends SettingsPreferenceFragment

    private PackageManager mPackageManager;
    private ConnectivityManager mConnectivityManager;
    private IConnectivityManager mConnectivityService;

    // VPN app info
    private final int mUserId = UserHandle.myUserId();
@@ -108,6 +113,8 @@ public class AppManagementFragment extends SettingsPreferenceFragment

        mPackageManager = getContext().getPackageManager();
        mConnectivityManager = getContext().getSystemService(ConnectivityManager.class);
        mConnectivityService = IConnectivityManager.Stub
                .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));

        mPreferenceVersion = findPreference(KEY_VERSION);
        mPreferenceAlwaysOn = (RestrictedSwitchPreference) findPreference(KEY_ALWAYS_ON_VPN);
@@ -169,13 +176,23 @@ public class AppManagementFragment extends SettingsPreferenceFragment
        return true;
    }

    private boolean onAlwaysOnVpnClick(final boolean isChecked) {
        if (isChecked && isLegacyVpnLockDownOrAnotherPackageAlwaysOn()) {
            // Show dialog if user replace always-on-vpn package and show not checked first
            ReplaceExistingVpnFragment.show(this);
    private boolean onAlwaysOnVpnClick(final boolean alwaysOnSetting) {
        final boolean replacing = isAnotherVpnActive();
        final boolean wasAlwaysOn = VpnUtils.isAlwaysOnOrLegacyLockdownActive(getActivity());
        if (ConfirmLockdownFragment.shouldShow(replacing, wasAlwaysOn, alwaysOnSetting)) {
            // Place a dialog to confirm that traffic should be locked down.
            final Bundle options = null;
            ConfirmLockdownFragment.show(this, replacing, wasAlwaysOn, alwaysOnSetting, options);
            return false;
        } else {
            return setAlwaysOnVpnByUI(isChecked);
        }
        // No need to show the dialog. Change the setting straight away.
        return setAlwaysOnVpnByUI(alwaysOnSetting);
    }

    @Override
    public void onConfirmLockdown(Bundle options, boolean isEnabled) {
        if (setAlwaysOnVpnByUI(isEnabled)) {
            updateUI();
        }
    }

@@ -197,7 +214,7 @@ public class AppManagementFragment extends SettingsPreferenceFragment

    private boolean setAlwaysOnVpn(boolean isEnabled) {
         return mConnectivityManager.setAlwaysOnVpnPackageForUser(mUserId,
                isEnabled ? mPackageName : null, /* lockdownEnabled */ false);
                isEnabled ? mPackageName : null, /* lockdownEnabled */ true);
    }

    @VisibleForTesting
@@ -293,15 +310,17 @@ public class AppManagementFragment extends SettingsPreferenceFragment
        return !ArrayUtils.isEmpty(ops);
    }

    private boolean isLegacyVpnLockDownOrAnotherPackageAlwaysOn() {
        if (mUserId == UserHandle.USER_SYSTEM) {
            String lockdownKey = VpnUtils.getLockdownVpn();
            if (lockdownKey != null) {
                return true;
            }
    /**
     * @return {@code true} if another VPN (VpnService or legacy) is connected or set as always-on.
     */
    private boolean isAnotherVpnActive() {
        try {
            final VpnConfig config = mConnectivityService.getVpnConfig(mUserId);
            return config != null && !TextUtils.equals(config.user, mPackageName);
        } catch (RemoteException e) {
            Log.w(TAG, "Failure to look up active VPN", e);
            return false;
        }

        return getAlwaysOnVpnPackage() != null && !isVpnAlwaysOn();
    }

    public static class CannotConnectFragment extends InstrumentedDialogFragment {
@@ -334,42 +353,4 @@ public class AppManagementFragment extends SettingsPreferenceFragment
                    .create();
        }
    }

    public static class ReplaceExistingVpnFragment extends InstrumentedDialogFragment
            implements DialogInterface.OnClickListener {
        private static final String TAG = "ReplaceExistingVpn";

        @Override
        public int getMetricsCategory() {
            return MetricsEvent.DIALOG_VPN_REPLACE_EXISTING;
        }

        public static void show(AppManagementFragment parent) {
            if (parent.getFragmentManager().findFragmentByTag(TAG) == null) {
                final ReplaceExistingVpnFragment frag = new ReplaceExistingVpnFragment();
                frag.setTargetFragment(parent, 0);
                frag.show(parent.getFragmentManager(), TAG);
            }
        }

        @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_replace), this)
                    .create();
        }

        @Override
        public void onClick(DialogInterface dialog, int which) {
            if (getTargetFragment() instanceof AppManagementFragment) {
                final AppManagementFragment target = (AppManagementFragment) getTargetFragment();
                if (target.setAlwaysOnVpnByUI(true)) {
                    target.updateUI();
                }
            }
        }
    }
}
+72 −25
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.settings.vpn2;

import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
@@ -29,6 +30,7 @@ import android.os.UserHandle;
import android.security.Credentials;
import android.security.KeyStore;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import com.android.internal.logging.MetricsProto;
@@ -41,8 +43,9 @@ import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
/**
 * Fragment wrapper around a {@link ConfigDialog}.
 */
public class ConfigDialogFragment extends InstrumentedDialogFragment
        implements DialogInterface.OnClickListener {
public class ConfigDialogFragment extends InstrumentedDialogFragment implements
        DialogInterface.OnClickListener, DialogInterface.OnShowListener, View.OnClickListener,
        ConfirmLockdownFragment.ConfirmLockdownListener {
    private static final String TAG_CONFIG_DIALOG = "vpnconfigdialog";
    private static final String TAG = "ConfigDialogFragment";

@@ -103,33 +106,57 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment
        boolean editing = args.getBoolean(ARG_EDITING);
        boolean exists = args.getBoolean(ARG_EXISTS);

        return new ConfigDialog(getActivity(), this, profile, editing, exists);
        final Dialog dialog = new ConfigDialog(getActivity(), this, profile, editing, exists);
        dialog.setOnShowListener(this);
        return dialog;
    }

    /**
     * Override for the default onClick handler which also calls dismiss().
     *
     * @see DialogInterface.OnClickListener#onClick(DialogInterface, int)
     */
    @Override
    public void onClick(DialogInterface dialogInterface, int button) {
        ConfigDialog dialog = (ConfigDialog) getDialog();
        VpnProfile profile = dialog.getProfile();
    public void onShow(DialogInterface dialogInterface) {
        ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
    }

        if (button == DialogInterface.BUTTON_POSITIVE) {
            // Update KeyStore entry
            KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(),
                    KeyStore.UID_SELF, /* flags */ 0);
    @Override
    public void onClick(View positiveButton) {
        onClick(getDialog(), AlertDialog.BUTTON_POSITIVE);
    }

            // Flush out previous connection, which may be an old version of the profile
            if (!disconnect(profile)) {
                Log.w(TAG, "Unable to remove previous connection. Continuing anyway.");
    @Override
    public void onConfirmLockdown(Bundle options, boolean isEnabled) {
        VpnProfile profile = (VpnProfile) options.getParcelable(ARG_PROFILE);
        connect(profile, isEnabled);
        dismiss();
    }

            updateLockdownVpn(dialog.isVpnAlwaysOn(), profile);
    @Override
    public void onClick(DialogInterface dialogInterface, int button) {
        ConfigDialog dialog = (ConfigDialog) getDialog();
        VpnProfile profile = dialog.getProfile();

            // If we are not editing, connect!
            if (!dialog.isEditing() && !VpnUtils.isVpnLockdown(profile.key)) {
        if (button == DialogInterface.BUTTON_POSITIVE) {
            // Possibly throw up a dialog to explain lockdown VPN.
            final boolean shouldLockdown = dialog.isVpnAlwaysOn();
            final boolean shouldConnect = shouldLockdown || !dialog.isEditing();
            final boolean wasAlwaysOn = VpnUtils.isAlwaysOnOrLegacyLockdownActive(getContext());
            try {
                    connect(profile);
                } catch (RemoteException e) {
                    Log.e(TAG, "Failed to connect", e);
                final boolean replace = VpnUtils.isVpnActive(getContext());
                if (shouldConnect && !isConnected(profile) &&
                        ConfirmLockdownFragment.shouldShow(replace, wasAlwaysOn, shouldLockdown)) {
                    final Bundle opts = new Bundle();
                    opts.putParcelable(ARG_PROFILE, profile);
                    ConfirmLockdownFragment.show(this, replace, wasAlwaysOn, shouldLockdown, opts);
                } else if (shouldConnect) {
                    connect(profile, shouldLockdown);
                } else {
                    save(profile, false);
                }
            } catch (RemoteException e) {
                Log.w(TAG, "Failed to check active VPN state. Skipping.", e);
            }
        } else if (button == DialogInterface.BUTTON_NEUTRAL) {
            // Disable profile if connected
@@ -175,11 +202,31 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment
        }
    }

    private void connect(VpnProfile profile) throws RemoteException {
    private void save(VpnProfile profile, boolean lockdown) {
        KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(),
                KeyStore.UID_SELF, /* flags */ 0);

        // Flush out old version of profile
        disconnect(profile);

        // Notify lockdown VPN that the profile has changed.
        updateLockdownVpn(lockdown, profile);
    }

    private void connect(VpnProfile profile, boolean lockdown) {
        save(profile, lockdown);

        // Now try to start the VPN - this is not necessary if the profile is set as lockdown,
        // because just saving the profile in this mode will start a connection.
        if (!VpnUtils.isVpnLockdown(profile.key)) {
            VpnUtils.clearLockdownVpn(getContext());
            try {
                mService.startLegacyVpn(profile);
            } catch (IllegalStateException e) {
                Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show();
            } catch (RemoteException e) {
                Log.e(TAG, "Failed to connect", e);
            }
        }
    }

+110 −0
Original line number Diff line number Diff line
/*
 * 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.Fragment;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;

import com.android.internal.logging.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;

public class ConfirmLockdownFragment extends InstrumentedDialogFragment
        implements DialogInterface.OnClickListener {
    public interface ConfirmLockdownListener {
        public void onConfirmLockdown(Bundle options, boolean isEnabled);
    }

    private static final String TAG = "ConfirmLockdown";

    @Override
    public int getMetricsCategory() {
        return MetricsEvent.DIALOG_VPN_REPLACE_EXISTING;
    }

    private static final String ARG_REPLACING = "replacing";
    private static final String ARG_LOCKDOWN_SRC = "lockdown_old";
    private static final String ARG_LOCKDOWN_DST = "lockdown_new";
    private static final String ARG_OPTIONS = "options";

    public static boolean shouldShow(boolean replacing, boolean fromLockdown, boolean toLockdown) {
        // We only need to show this if we are:
        //  - replacing an existing connection
        //  - switching on always-on mode where it was not enabled before.
        return replacing || (toLockdown && !fromLockdown);
    }

    public static void show(Fragment parent, boolean replacing,
            boolean fromLockdown, boolean toLockdown, Bundle options) {
        if (parent.getFragmentManager().findFragmentByTag(TAG) != null) {
            // Already exists. Don't show it twice.
            return;
        }
        final Bundle args = new Bundle();
        args.putBoolean(ARG_REPLACING, replacing);
        args.putBoolean(ARG_LOCKDOWN_SRC, fromLockdown);
        args.putBoolean(ARG_LOCKDOWN_DST, toLockdown);
        args.putParcelable(ARG_OPTIONS, options);

        final ConfirmLockdownFragment frag = new ConfirmLockdownFragment();
        frag.setArguments(args);
        frag.setTargetFragment(parent, 0);
        frag.show(parent.getFragmentManager(), TAG);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final boolean replacing = getArguments().getBoolean(ARG_REPLACING);
        final boolean wasAlwaysOn = getArguments().getBoolean(ARG_LOCKDOWN_SRC);
        final boolean nowAlwaysOn = getArguments().getBoolean(ARG_LOCKDOWN_DST);

        final int titleId = replacing ? R.string.vpn_replace_vpn_title : R.string.vpn_set_vpn_title;
        final int actionId =
                (replacing ? R.string.vpn_replace :
                (nowAlwaysOn ? R.string.vpn_turn_on : R.string.okay));
        final int messageId;
        if (nowAlwaysOn) {
            messageId = replacing
                    ? R.string.vpn_replace_always_on_vpn_enable_message
                    : R.string.vpn_first_always_on_vpn_message;
        } else {
            messageId = wasAlwaysOn
                    ? R.string.vpn_replace_always_on_vpn_disable_message
                    : R.string.vpn_replace_vpn_message;
        }

        return new AlertDialog.Builder(getActivity())
                .setTitle(titleId)
                .setMessage(messageId)
                .setNegativeButton(R.string.cancel, null)
                .setPositiveButton(actionId, this)
                .create();
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        if (getTargetFragment() instanceof ConfirmLockdownListener) {
            ((ConfirmLockdownListener) getTargetFragment()).onConfirmLockdown(
                    getArguments().getParcelable(ARG_OPTIONS),
                    getArguments().getBoolean(ARG_LOCKDOWN_DST));
        }
    }
}
Loading