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.AlertDialog; 20 import android.app.Dialog; 21 import android.app.DialogFragment; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.net.ConnectivityManager; 25 import android.net.IConnectivityManager; 26 import android.os.Bundle; 27 import android.os.RemoteException; 28 import android.os.ServiceManager; 29 import android.os.UserHandle; 30 import android.security.Credentials; 31 import android.security.KeyStore; 32 import android.util.Log; 33 import android.view.View; 34 import android.widget.Toast; 35 36 import com.android.internal.logging.nano.MetricsProto; 37 import com.android.internal.net.LegacyVpnInfo; 38 import com.android.internal.net.VpnConfig; 39 import com.android.internal.net.VpnProfile; 40 import com.android.settings.R; 41 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 42 43 /** 44 * Fragment wrapper around a {@link ConfigDialog}. 45 */ 46 public class ConfigDialogFragment extends InstrumentedDialogFragment implements 47 DialogInterface.OnClickListener, DialogInterface.OnShowListener, View.OnClickListener, 48 ConfirmLockdownFragment.ConfirmLockdownListener { 49 private static final String TAG_CONFIG_DIALOG = "vpnconfigdialog"; 50 private static final String TAG = "ConfigDialogFragment"; 51 52 private static final String ARG_PROFILE = "profile"; 53 private static final String ARG_EDITING = "editing"; 54 private static final String ARG_EXISTS = "exists"; 55 56 private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface( 57 ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); 58 private Context mContext; 59 60 private boolean mUnlocking = false; 61 62 63 @Override getMetricsCategory()64 public int getMetricsCategory() { 65 return MetricsProto.MetricsEvent.DIALOG_LEGACY_VPN_CONFIG; 66 } 67 show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists)68 public static void show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists) { 69 if (!parent.isAdded()) return; 70 71 Bundle args = new Bundle(); 72 args.putParcelable(ARG_PROFILE, profile); 73 args.putBoolean(ARG_EDITING, edit); 74 args.putBoolean(ARG_EXISTS, exists); 75 76 final ConfigDialogFragment frag = new ConfigDialogFragment(); 77 frag.setArguments(args); 78 frag.setTargetFragment(parent, 0); 79 frag.show(parent.getFragmentManager(), TAG_CONFIG_DIALOG); 80 } 81 82 @Override onAttach(final Context context)83 public void onAttach(final Context context) { 84 super.onAttach(context); 85 mContext = context; 86 } 87 88 @Override onResume()89 public void onResume() { 90 super.onResume(); 91 92 // Check KeyStore here, so others do not need to deal with it. 93 if (!KeyStore.getInstance().isUnlocked()) { 94 if (!mUnlocking) { 95 // Let us unlock KeyStore. See you later! 96 Credentials.getInstance().unlock(mContext); 97 } else { 98 // We already tried, but it is still not working! 99 dismiss(); 100 } 101 mUnlocking = !mUnlocking; 102 return; 103 } 104 105 // Now KeyStore is always unlocked. Reset the flag. 106 mUnlocking = false; 107 } 108 109 @Override onCreateDialog(Bundle savedInstanceState)110 public Dialog onCreateDialog(Bundle savedInstanceState) { 111 Bundle args = getArguments(); 112 VpnProfile profile = (VpnProfile) args.getParcelable(ARG_PROFILE); 113 boolean editing = args.getBoolean(ARG_EDITING); 114 boolean exists = args.getBoolean(ARG_EXISTS); 115 116 final Dialog dialog = new ConfigDialog(getActivity(), this, profile, editing, exists); 117 dialog.setOnShowListener(this); 118 return dialog; 119 } 120 121 /** 122 * Override for the default onClick handler which also calls dismiss(). 123 * 124 * @see DialogInterface.OnClickListener#onClick(DialogInterface, int) 125 */ 126 @Override onShow(DialogInterface dialogInterface)127 public void onShow(DialogInterface dialogInterface) { 128 ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this); 129 } 130 131 @Override onClick(View positiveButton)132 public void onClick(View positiveButton) { 133 onClick(getDialog(), AlertDialog.BUTTON_POSITIVE); 134 } 135 136 @Override onConfirmLockdown(Bundle options, boolean isAlwaysOn, boolean isLockdown)137 public void onConfirmLockdown(Bundle options, boolean isAlwaysOn, boolean isLockdown) { 138 VpnProfile profile = (VpnProfile) options.getParcelable(ARG_PROFILE); 139 connect(profile, isAlwaysOn); 140 dismiss(); 141 } 142 143 @Override onClick(DialogInterface dialogInterface, int button)144 public void onClick(DialogInterface dialogInterface, int button) { 145 ConfigDialog dialog = (ConfigDialog) getDialog(); 146 VpnProfile profile = dialog.getProfile(); 147 148 if (button == DialogInterface.BUTTON_POSITIVE) { 149 // Possibly throw up a dialog to explain lockdown VPN. 150 final boolean shouldLockdown = dialog.isVpnAlwaysOn(); 151 final boolean shouldConnect = shouldLockdown || !dialog.isEditing(); 152 final boolean wasLockdown = VpnUtils.isAnyLockdownActive(mContext); 153 try { 154 final boolean replace = VpnUtils.isVpnActive(mContext); 155 if (shouldConnect && !isConnected(profile) && 156 ConfirmLockdownFragment.shouldShow(replace, wasLockdown, shouldLockdown)) { 157 final Bundle opts = new Bundle(); 158 opts.putParcelable(ARG_PROFILE, profile); 159 ConfirmLockdownFragment.show(this, replace, /* alwaysOn */ shouldLockdown, 160 /* from */ wasLockdown, /* to */ shouldLockdown, opts); 161 } else if (shouldConnect) { 162 connect(profile, shouldLockdown); 163 } else { 164 save(profile, false); 165 } 166 } catch (RemoteException e) { 167 Log.w(TAG, "Failed to check active VPN state. Skipping.", e); 168 } 169 } else if (button == DialogInterface.BUTTON_NEUTRAL) { 170 // Disable profile if connected 171 if (!disconnect(profile)) { 172 Log.e(TAG, "Failed to disconnect VPN. Leaving profile in keystore."); 173 return; 174 } 175 176 // Delete from KeyStore 177 KeyStore keyStore = KeyStore.getInstance(); 178 keyStore.delete(Credentials.VPN + profile.key, KeyStore.UID_SELF); 179 180 updateLockdownVpn(false, profile); 181 } 182 dismiss(); 183 } 184 185 @Override onCancel(DialogInterface dialog)186 public void onCancel(DialogInterface dialog) { 187 dismiss(); 188 super.onCancel(dialog); 189 } 190 updateLockdownVpn(boolean isVpnAlwaysOn, VpnProfile profile)191 private void updateLockdownVpn(boolean isVpnAlwaysOn, VpnProfile profile) { 192 // Save lockdown vpn 193 if (isVpnAlwaysOn) { 194 // Show toast if vpn profile is not valid 195 if (!profile.isValidLockdownProfile()) { 196 Toast.makeText(mContext, R.string.vpn_lockdown_config_error, 197 Toast.LENGTH_LONG).show(); 198 return; 199 } 200 201 final ConnectivityManager conn = ConnectivityManager.from(mContext); 202 conn.setAlwaysOnVpnPackageForUser(UserHandle.myUserId(), null, 203 /* lockdownEnabled */ false); 204 VpnUtils.setLockdownVpn(mContext, profile.key); 205 } else { 206 // update only if lockdown vpn has been changed 207 if (VpnUtils.isVpnLockdown(profile.key)) { 208 VpnUtils.clearLockdownVpn(mContext); 209 } 210 } 211 } 212 save(VpnProfile profile, boolean lockdown)213 private void save(VpnProfile profile, boolean lockdown) { 214 KeyStore.getInstance().put(Credentials.VPN + profile.key, profile.encode(), 215 KeyStore.UID_SELF, /* flags */ 0); 216 217 // Flush out old version of profile 218 disconnect(profile); 219 220 // Notify lockdown VPN that the profile has changed. 221 updateLockdownVpn(lockdown, profile); 222 } 223 connect(VpnProfile profile, boolean lockdown)224 private void connect(VpnProfile profile, boolean lockdown) { 225 save(profile, lockdown); 226 227 // Now try to start the VPN - this is not necessary if the profile is set as lockdown, 228 // because just saving the profile in this mode will start a connection. 229 if (!VpnUtils.isVpnLockdown(profile.key)) { 230 VpnUtils.clearLockdownVpn(mContext); 231 try { 232 mService.startLegacyVpn(profile); 233 } catch (IllegalStateException e) { 234 Toast.makeText(mContext, R.string.vpn_no_network, Toast.LENGTH_LONG).show(); 235 } catch (RemoteException e) { 236 Log.e(TAG, "Failed to connect", e); 237 } 238 } 239 } 240 241 /** 242 * Ensure that the VPN profile pointed at by {@param profile} is disconnected. 243 * 244 * @return {@code true} iff this VPN profile is no longer connected. Note that another profile 245 * may still be active - this function will then do nothing but still return success. 246 */ disconnect(VpnProfile profile)247 private boolean disconnect(VpnProfile profile) { 248 try { 249 if (!isConnected(profile)) { 250 return true; 251 } 252 return VpnUtils.disconnectLegacyVpn(getContext()); 253 } catch (RemoteException e) { 254 Log.e(TAG, "Failed to disconnect", e); 255 return false; 256 } 257 } 258 isConnected(VpnProfile profile)259 private boolean isConnected(VpnProfile profile) throws RemoteException { 260 LegacyVpnInfo connected = mService.getLegacyVpnInfo(UserHandle.myUserId()); 261 return connected != null && profile.key.equals(connected.key); 262 } 263 } 264