1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.vpn2; 18 19 import android.app.Dialog; 20 import android.app.settings.SettingsEnums; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.net.ConnectivityManager; 24 import android.net.IConnectivityManager; 25 import android.os.Bundle; 26 import android.os.RemoteException; 27 import android.os.ServiceManager; 28 import android.os.UserHandle; 29 import android.security.Credentials; 30 import android.security.KeyStore; 31 import android.util.Log; 32 import android.view.View; 33 import android.widget.Toast; 34 35 import androidx.appcompat.app.AlertDialog; 36 37 import com.android.internal.net.LegacyVpnInfo; 38 import com.android.internal.net.VpnProfile; 39 import com.android.settings.R; 40 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 41 42 /** 43 * Fragment wrapper around a {@link ConfigDialog}. 44 */ 45 public class ConfigDialogFragment extends InstrumentedDialogFragment implements 46 DialogInterface.OnClickListener, DialogInterface.OnShowListener, View.OnClickListener, 47 ConfirmLockdownFragment.ConfirmLockdownListener { 48 private static final String TAG_CONFIG_DIALOG = "vpnconfigdialog"; 49 private static final String TAG = "ConfigDialogFragment"; 50 51 private static final String ARG_PROFILE = "profile"; 52 private static final String ARG_EDITING = "editing"; 53 private static final String ARG_EXISTS = "exists"; 54 55 private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface( 56 ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); 57 private Context mContext; 58 59 60 @Override getMetricsCategory()61 public int getMetricsCategory() { 62 return SettingsEnums.DIALOG_LEGACY_VPN_CONFIG; 63 } 64 show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists)65 public static void show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists) { 66 if (!parent.isAdded()) return; 67 68 Bundle args = new Bundle(); 69 args.putParcelable(ARG_PROFILE, profile); 70 args.putBoolean(ARG_EDITING, edit); 71 args.putBoolean(ARG_EXISTS, exists); 72 73 final ConfigDialogFragment frag = new ConfigDialogFragment(); 74 frag.setArguments(args); 75 frag.setTargetFragment(parent, 0); 76 frag.show(parent.getFragmentManager(), TAG_CONFIG_DIALOG); 77 } 78 79 @Override onAttach(final Context context)80 public void onAttach(final Context context) { 81 super.onAttach(context); 82 mContext = context; 83 } 84 85 @Override onCreateDialog(Bundle savedInstanceState)86 public Dialog onCreateDialog(Bundle savedInstanceState) { 87 Bundle args = getArguments(); 88 VpnProfile profile = (VpnProfile) args.getParcelable(ARG_PROFILE); 89 boolean editing = args.getBoolean(ARG_EDITING); 90 boolean exists = args.getBoolean(ARG_EXISTS); 91 92 final Dialog dialog = new ConfigDialog(getActivity(), this, profile, editing, exists); 93 dialog.setOnShowListener(this); 94 return dialog; 95 } 96 97 /** 98 * Override for the default onClick handler which also calls dismiss(). 99 * 100 * @see DialogInterface.OnClickListener#onClick(DialogInterface, int) 101 */ 102 @Override onShow(DialogInterface dialogInterface)103 public void onShow(DialogInterface dialogInterface) { 104 ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this); 105 } 106 107 @Override onClick(View positiveButton)108 public void onClick(View positiveButton) { 109 onClick(getDialog(), AlertDialog.BUTTON_POSITIVE); 110 } 111 112 @Override onConfirmLockdown(Bundle options, boolean isAlwaysOn, boolean isLockdown)113 public void onConfirmLockdown(Bundle options, boolean isAlwaysOn, boolean isLockdown) { 114 VpnProfile profile = (VpnProfile) options.getParcelable(ARG_PROFILE); 115 connect(profile, isAlwaysOn); 116 dismiss(); 117 } 118 119 @Override onClick(DialogInterface dialogInterface, int button)120 public void onClick(DialogInterface dialogInterface, int button) { 121 ConfigDialog dialog = (ConfigDialog) getDialog(); 122 VpnProfile profile = dialog.getProfile(); 123 124 if (button == DialogInterface.BUTTON_POSITIVE) { 125 // Possibly throw up a dialog to explain lockdown VPN. 126 final boolean shouldLockdown = dialog.isVpnAlwaysOn(); 127 final boolean shouldConnect = shouldLockdown || !dialog.isEditing(); 128 final boolean wasLockdown = VpnUtils.isAnyLockdownActive(mContext); 129 try { 130 final boolean replace = VpnUtils.isVpnActive(mContext); 131 if (shouldConnect && !isConnected(profile) && 132 ConfirmLockdownFragment.shouldShow(replace, wasLockdown, shouldLockdown)) { 133 final Bundle opts = new Bundle(); 134 opts.putParcelable(ARG_PROFILE, profile); 135 ConfirmLockdownFragment.show(this, replace, /* alwaysOn */ shouldLockdown, 136 /* from */ wasLockdown, /* to */ shouldLockdown, opts); 137 } else if (shouldConnect) { 138 connect(profile, shouldLockdown); 139 } else { 140 save(profile, false); 141 } 142 } catch (RemoteException e) { 143 Log.w(TAG, "Failed to check active VPN state. Skipping.", e); 144 } 145 } else if (button == DialogInterface.BUTTON_NEUTRAL) { 146 // Disable profile if connected 147 if (!disconnect(profile)) { 148 Log.e(TAG, "Failed to disconnect VPN. Leaving profile in keystore."); 149 return; 150 } 151 152 // Delete from KeyStore 153 KeyStore keyStore = KeyStore.getInstance(); 154 keyStore.delete(Credentials.VPN + profile.key, KeyStore.UID_SELF); 155 156 updateLockdownVpn(false, profile); 157 } 158 dismiss(); 159 } 160 161 @Override onCancel(DialogInterface dialog)162 public void onCancel(DialogInterface dialog) { 163 dismiss(); 164 super.onCancel(dialog); 165 } 166 updateLockdownVpn(boolean isVpnAlwaysOn, VpnProfile profile)167 private void updateLockdownVpn(boolean isVpnAlwaysOn, VpnProfile profile) { 168 // Save lockdown vpn 169 if (isVpnAlwaysOn) { 170 // Show toast if vpn profile is not valid 171 if (!profile.isValidLockdownProfile()) { 172 Toast.makeText(mContext, R.string.vpn_lockdown_config_error, 173 Toast.LENGTH_LONG).show(); 174 return; 175 } 176 177 final ConnectivityManager conn = ConnectivityManager.from(mContext); 178 conn.setAlwaysOnVpnPackageForUser(UserHandle.myUserId(), null, 179 /* lockdownEnabled */ false, /* lockdownWhitelist */ null); 180 VpnUtils.setLockdownVpn(mContext, profile.key); 181 } else { 182 // update only if lockdown vpn has been changed 183 if (VpnUtils.isVpnLockdown(profile.key)) { 184 VpnUtils.clearLockdownVpn(mContext); 185 } 186 } 187 } 188 save(VpnProfile profile, boolean lockdown)189 private void save(VpnProfile profile, boolean lockdown) { 190 KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(), 191 KeyStore.UID_SELF, /* flags */ 0); 192 193 // Flush out old version of profile 194 disconnect(profile); 195 196 // Notify lockdown VPN that the profile has changed. 197 updateLockdownVpn(lockdown, profile); 198 } 199 connect(VpnProfile profile, boolean lockdown)200 private void connect(VpnProfile profile, boolean lockdown) { 201 save(profile, lockdown); 202 203 // Now try to start the VPN - this is not necessary if the profile is set as lockdown, 204 // because just saving the profile in this mode will start a connection. 205 if (!VpnUtils.isVpnLockdown(profile.key)) { 206 VpnUtils.clearLockdownVpn(mContext); 207 try { 208 mService.startLegacyVpn(profile); 209 } catch (IllegalStateException e) { 210 Toast.makeText(mContext, R.string.vpn_no_network, Toast.LENGTH_LONG).show(); 211 } catch (RemoteException e) { 212 Log.e(TAG, "Failed to connect", e); 213 } 214 } 215 } 216 217 /** 218 * Ensure that the VPN profile pointed at by {@param profile} is disconnected. 219 * 220 * @return {@code true} iff this VPN profile is no longer connected. Note that another profile 221 * may still be active - this function will then do nothing but still return success. 222 */ disconnect(VpnProfile profile)223 private boolean disconnect(VpnProfile profile) { 224 try { 225 if (!isConnected(profile)) { 226 return true; 227 } 228 return VpnUtils.disconnectLegacyVpn(getContext()); 229 } catch (RemoteException e) { 230 Log.e(TAG, "Failed to disconnect", e); 231 return false; 232 } 233 } 234 isConnected(VpnProfile profile)235 private boolean isConnected(VpnProfile profile) throws RemoteException { 236 LegacyVpnInfo connected = mService.getLegacyVpnInfo(UserHandle.myUserId()); 237 return connected != null && profile.key.equals(connected.key); 238 } 239 } 240