1 /* 2 * Copyright (C) 2009 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.email.activity.setup; 18 19 import android.content.ContentResolver; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.res.XmlResourceParser; 23 import android.net.Uri; 24 import android.text.Editable; 25 import android.text.TextUtils; 26 import android.widget.EditText; 27 28 import com.android.email.R; 29 import com.android.email.SecurityPolicy; 30 import com.android.email.provider.AccountBackupRestore; 31 import com.android.emailcommon.Logging; 32 import com.android.emailcommon.VendorPolicyLoader; 33 import com.android.emailcommon.VendorPolicyLoader.OAuthProvider; 34 import com.android.emailcommon.VendorPolicyLoader.Provider; 35 import com.android.emailcommon.provider.Account; 36 import com.android.emailcommon.provider.EmailContent.AccountColumns; 37 import com.android.emailcommon.provider.QuickResponse; 38 import com.android.emailcommon.service.PolicyServiceProxy; 39 import com.android.emailcommon.utility.Utility; 40 import com.android.mail.utils.LogUtils; 41 import com.google.common.annotations.VisibleForTesting; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 46 public class AccountSettingsUtils { 47 48 /** Pattern to match any part of a domain */ 49 private final static String WILD_STRING = "*"; 50 /** Will match any, single character */ 51 private final static char WILD_CHARACTER = '?'; 52 private final static String DOMAIN_SEPARATOR = "\\."; 53 54 /** 55 * Commits the UI-related settings of an account to the provider. This is static so that it 56 * can be used by the various account activities. If the account has never been saved, this 57 * method saves it; otherwise, it just saves the settings. 58 * @param context the context of the caller 59 * @param account the account whose settings will be committed 60 */ 61 public static void commitSettings(Context context, Account account) { 62 if (!account.isSaved()) { 63 account.save(context); 64 65 if (account.mPolicy != null) { 66 // TODO: we need better handling for unsupported policies 67 // For now, just clear the unsupported policies, as the server will (hopefully) 68 // just reject our sync attempts if it's not happy with half-measures 69 if (account.mPolicy.mProtocolPoliciesUnsupported != null) { 70 LogUtils.d(LogUtils.TAG, "Clearing unsupported policies " 71 + account.mPolicy.mProtocolPoliciesUnsupported); 72 account.mPolicy.mProtocolPoliciesUnsupported = null; 73 } 74 PolicyServiceProxy.setAccountPolicy2(context, 75 account.getId(), 76 account.mPolicy, 77 account.mSecuritySyncKey == null ? "" : account.mSecuritySyncKey, 78 false /* notify */); 79 } 80 81 // Set up default quick responses here... 82 String[] defaultQuickResponses = 83 context.getResources().getStringArray(R.array.default_quick_responses); 84 ContentValues cv = new ContentValues(); 85 cv.put(QuickResponse.ACCOUNT_KEY, account.mId); 86 ContentResolver resolver = context.getContentResolver(); 87 for (String quickResponse: defaultQuickResponses) { 88 // Allow empty entries (some localizations may not want to have the maximum 89 // number) 90 if (!TextUtils.isEmpty(quickResponse)) { 91 cv.put(QuickResponse.TEXT, quickResponse); 92 resolver.insert(QuickResponse.CONTENT_URI, cv); 93 } 94 } 95 } else { 96 ContentValues cv = getAccountContentValues(account); 97 account.update(context, cv); 98 } 99 100 // Update the backup (side copy) of the accounts 101 AccountBackupRestore.backup(context); 102 } 103 104 /** 105 * Returns a set of content values to commit account changes (not including the foreign keys 106 * for the two host auth's and policy) to the database. Does not actually commit anything. 107 */ 108 public static ContentValues getAccountContentValues(Account account) { 109 ContentValues cv = new ContentValues(); 110 cv.put(AccountColumns.DISPLAY_NAME, account.getDisplayName()); 111 cv.put(AccountColumns.SENDER_NAME, account.getSenderName()); 112 cv.put(AccountColumns.SIGNATURE, account.getSignature()); 113 cv.put(AccountColumns.SYNC_INTERVAL, account.mSyncInterval); 114 cv.put(AccountColumns.FLAGS, account.mFlags); 115 cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback); 116 cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey); 117 return cv; 118 } 119 120 /** 121 * Create the request to get the authorization code. 122 * 123 * @param context 124 * @param provider The OAuth provider to register with 125 * @param emailAddress Email address to send as a hint to the oauth service. 126 * @return 127 */ 128 public static Uri createOAuthRegistrationRequest(final Context context, 129 final OAuthProvider provider, final String emailAddress) { 130 final Uri.Builder b = Uri.parse(provider.authEndpoint).buildUpon(); 131 b.appendQueryParameter("response_type", provider.responseType); 132 b.appendQueryParameter("client_id", provider.clientId); 133 b.appendQueryParameter("redirect_uri", provider.redirectUri); 134 b.appendQueryParameter("scope", provider.scope); 135 b.appendQueryParameter("state", provider.state); 136 b.appendQueryParameter("login_hint", emailAddress); 137 return b.build(); 138 } 139 140 /** 141 * Search for a single resource containing known oauth provider definitions. 142 * 143 * @param context 144 * @param id String Id of the oauth provider. 145 * @return The OAuthProvider if found, null if not. 146 */ 147 public static OAuthProvider findOAuthProvider(final Context context, final String id) { 148 return findOAuthProvider(context, id, R.xml.oauth); 149 } 150 151 public static List<OAuthProvider> getAllOAuthProviders(final Context context) { 152 try { 153 List<OAuthProvider> providers = new ArrayList<OAuthProvider>(); 154 final XmlResourceParser xml = context.getResources().getXml(R.xml.oauth); 155 int xmlEventType; 156 OAuthProvider provider = null; 157 while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { 158 if (xmlEventType == XmlResourceParser.START_TAG 159 && "provider".equals(xml.getName())) { 160 try { 161 provider = new OAuthProvider(); 162 provider.id = getXmlAttribute(context, xml, "id"); 163 provider.label = getXmlAttribute(context, xml, "label"); 164 provider.authEndpoint = getXmlAttribute(context, xml, "auth_endpoint"); 165 provider.tokenEndpoint = getXmlAttribute(context, xml, "token_endpoint"); 166 provider.refreshEndpoint = getXmlAttribute(context, xml, 167 "refresh_endpoint"); 168 provider.responseType = getXmlAttribute(context, xml, "response_type"); 169 provider.redirectUri = getXmlAttribute(context, xml, "redirect_uri"); 170 provider.scope = getXmlAttribute(context, xml, "scope"); 171 provider.state = getXmlAttribute(context, xml, "state"); 172 provider.clientId = getXmlAttribute(context, xml, "client_id"); 173 provider.clientSecret = getXmlAttribute(context, xml, "client_secret"); 174 providers.add(provider); 175 } catch (IllegalArgumentException e) { 176 LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() + 177 "; Domain contains multiple globals"); 178 } 179 } 180 } 181 return providers; 182 } catch (Exception e) { 183 LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e); 184 } 185 return null; 186 } 187 188 /** 189 * Search for a single resource containing known oauth provider definitions. 190 * 191 * @param context 192 * @param id String Id of the oauth provider. 193 * @param resourceId ResourceId of the xml file to search. 194 * @return The OAuthProvider if found, null if not. 195 */ 196 public static OAuthProvider findOAuthProvider(final Context context, final String id, 197 final int resourceId) { 198 // TODO: Consider adding a way to cache this file during new account setup, so that we 199 // don't need to keep loading the file over and over. 200 // TODO: need a mechanism to get a list of all supported OAuth providers so that we can 201 // offer the user a choice of who to authenticate with. 202 try { 203 final XmlResourceParser xml = context.getResources().getXml(resourceId); 204 int xmlEventType; 205 OAuthProvider provider = null; 206 while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { 207 if (xmlEventType == XmlResourceParser.START_TAG 208 && "provider".equals(xml.getName())) { 209 String providerId = getXmlAttribute(context, xml, "id"); 210 try { 211 if (TextUtils.equals(id, providerId)) { 212 provider = new OAuthProvider(); 213 provider.id = id; 214 provider.label = getXmlAttribute(context, xml, "label"); 215 provider.authEndpoint = getXmlAttribute(context, xml, "auth_endpoint"); 216 provider.tokenEndpoint = getXmlAttribute(context, xml, "token_endpoint"); 217 provider.refreshEndpoint = getXmlAttribute(context, xml, 218 "refresh_endpoint"); 219 provider.responseType = getXmlAttribute(context, xml, "response_type"); 220 provider.redirectUri = getXmlAttribute(context, xml, "redirect_uri"); 221 provider.scope = getXmlAttribute(context, xml, "scope"); 222 provider.state = getXmlAttribute(context, xml, "state"); 223 provider.clientId = getXmlAttribute(context, xml, "client_id"); 224 provider.clientSecret = getXmlAttribute(context, xml, "client_secret"); 225 return provider; 226 } 227 } catch (IllegalArgumentException e) { 228 LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() + 229 "; Domain contains multiple globals"); 230 } 231 } 232 } 233 } catch (Exception e) { 234 LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e); 235 } 236 return null; 237 } 238 239 /** 240 * Search the list of known Email providers looking for one that matches the user's email 241 * domain. We check for vendor supplied values first, then we look in providers_product.xml, 242 * and finally by the entries in platform providers.xml. This provides a nominal override 243 * capability. 244 * 245 * A match is defined as any provider entry for which the "domain" attribute matches. 246 * 247 * @param domain The domain portion of the user's email address 248 * @return suitable Provider definition, or null if no match found 249 */ 250 public static Provider findProviderForDomain(Context context, String domain) { 251 Provider p = VendorPolicyLoader.getInstance(context).findProviderForDomain(domain); 252 if (p == null) { 253 p = findProviderForDomain(context, domain, R.xml.providers_product); 254 } 255 if (p == null) { 256 p = findProviderForDomain(context, domain, R.xml.providers); 257 } 258 return p; 259 } 260 261 /** 262 * Search a single resource containing known Email provider definitions. 263 * 264 * @param domain The domain portion of the user's email address 265 * @param resourceId Id of the provider resource to scan 266 * @return suitable Provider definition, or null if no match found 267 */ 268 /*package*/ static Provider findProviderForDomain( 269 Context context, String domain, int resourceId) { 270 try { 271 XmlResourceParser xml = context.getResources().getXml(resourceId); 272 int xmlEventType; 273 Provider provider = null; 274 while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { 275 if (xmlEventType == XmlResourceParser.START_TAG 276 && "provider".equals(xml.getName())) { 277 String providerDomain = getXmlAttribute(context, xml, "domain"); 278 try { 279 if (matchProvider(domain, providerDomain)) { 280 provider = new Provider(); 281 provider.id = getXmlAttribute(context, xml, "id"); 282 provider.label = getXmlAttribute(context, xml, "label"); 283 provider.domain = domain.toLowerCase(); 284 provider.note = getXmlAttribute(context, xml, "note"); 285 // TODO: Maybe this should actually do a lookup of the OAuth provider 286 // here, and keep a pointer to it rather than a textual key. 287 // To do this probably requires caching oauth.xml, otherwise the lookup 288 // is expensive and likely to happen repeatedly. 289 provider.oauth = getXmlAttribute(context, xml, "oauth"); 290 } 291 } catch (IllegalArgumentException e) { 292 LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() + 293 "; Domain contains multiple globals"); 294 } 295 } 296 else if (xmlEventType == XmlResourceParser.START_TAG 297 && "incoming".equals(xml.getName()) 298 && provider != null) { 299 provider.incomingUriTemplate = getXmlAttribute(context, xml, "uri"); 300 provider.incomingUsernameTemplate = getXmlAttribute(context, xml, "username"); 301 } 302 else if (xmlEventType == XmlResourceParser.START_TAG 303 && "outgoing".equals(xml.getName()) 304 && provider != null) { 305 provider.outgoingUriTemplate = getXmlAttribute(context, xml, "uri"); 306 provider.outgoingUsernameTemplate = getXmlAttribute(context, xml, "username"); 307 } 308 else if (xmlEventType == XmlResourceParser.START_TAG 309 && "incoming-fallback".equals(xml.getName()) 310 && provider != null) { 311 provider.altIncomingUriTemplate = getXmlAttribute(context, xml, "uri"); 312 provider.altIncomingUsernameTemplate = 313 getXmlAttribute(context, xml, "username"); 314 } 315 else if (xmlEventType == XmlResourceParser.START_TAG 316 && "outgoing-fallback".equals(xml.getName()) 317 && provider != null) { 318 provider.altOutgoingUriTemplate = getXmlAttribute(context, xml, "uri"); 319 provider.altOutgoingUsernameTemplate = 320 getXmlAttribute(context, xml, "username"); 321 } 322 else if (xmlEventType == XmlResourceParser.END_TAG 323 && "provider".equals(xml.getName()) 324 && provider != null) { 325 return provider; 326 } 327 } 328 } 329 catch (Exception e) { 330 LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e); 331 } 332 return null; 333 } 334 335 /** 336 * Returns true if the string <code>s1</code> matches the string <code>s2</code>. The string 337 * <code>s2</code> may contain any number of wildcards -- a '?' character -- and/or asterisk 338 * characters -- '*'. Wildcards match any single character, while the asterisk matches a domain 339 * part (i.e. substring demarcated by a period, '.') 340 */ 341 @VisibleForTesting 342 public static boolean matchProvider(String testDomain, String providerDomain) { 343 String[] testParts = testDomain.split(DOMAIN_SEPARATOR); 344 String[] providerParts = providerDomain.split(DOMAIN_SEPARATOR); 345 if (testParts.length != providerParts.length) { 346 return false; 347 } 348 for (int i = 0; i < testParts.length; i++) { 349 String testPart = testParts[i].toLowerCase(); 350 String providerPart = providerParts[i].toLowerCase(); 351 if (!providerPart.equals(WILD_STRING) && 352 !matchWithWildcards(testPart, providerPart)) { 353 return false; 354 } 355 } 356 return true; 357 } 358 359 private static boolean matchWithWildcards(String testPart, String providerPart) { 360 int providerLength = providerPart.length(); 361 if (testPart.length() != providerLength){ 362 return false; 363 } 364 for (int i = 0; i < providerLength; i++) { 365 char testChar = testPart.charAt(i); 366 char providerChar = providerPart.charAt(i); 367 if (testChar != providerChar && providerChar != WILD_CHARACTER) { 368 return false; 369 } 370 } 371 return true; 372 } 373 374 /** 375 * Attempts to get the given attribute as a String resource first, and if it fails 376 * returns the attribute as a simple String value. 377 * @param xml 378 * @param name 379 * @return the requested resource 380 */ 381 private static String getXmlAttribute(Context context, XmlResourceParser xml, String name) { 382 int resId = xml.getAttributeResourceValue(null, name, 0); 383 if (resId == 0) { 384 return xml.getAttributeValue(null, name); 385 } 386 else { 387 return context.getString(resId); 388 } 389 } 390 391 /** 392 * Infer potential email server addresses from domain names 393 * 394 * Incoming: Prepend "imap" or "pop3" to domain, unless "pop", "pop3", 395 * "imap", or "mail" are found. 396 * Outgoing: Prepend "smtp" if domain starts with any in the host prefix array 397 * 398 * @param server name as we know it so far 399 * @param incoming "pop3" or "imap" (or null) 400 * @param outgoing "smtp" or null 401 * @return the post-processed name for use in the UI 402 */ 403 public static String inferServerName(Context context, String server, String incoming, 404 String outgoing) { 405 // Default values cause entire string to be kept, with prepended server string 406 int keepFirstChar = 0; 407 int firstDotIndex = server.indexOf('.'); 408 if (firstDotIndex != -1) { 409 // look at first word and decide what to do 410 String firstWord = server.substring(0, firstDotIndex).toLowerCase(); 411 String[] hostPrefixes = 412 context.getResources().getStringArray(R.array.smtp_host_prefixes); 413 boolean canSubstituteSmtp = Utility.arrayContains(hostPrefixes, firstWord); 414 boolean isMail = "mail".equals(firstWord); 415 // Now decide what to do 416 if (incoming != null) { 417 // For incoming, we leave imap/pop/pop3/mail alone, or prepend incoming 418 if (canSubstituteSmtp || isMail) { 419 return server; 420 } 421 } else { 422 // For outgoing, replace imap/pop/pop3 with outgoing, leave mail alone, or 423 // prepend outgoing 424 if (canSubstituteSmtp) { 425 keepFirstChar = firstDotIndex + 1; 426 } else if (isMail) { 427 return server; 428 } else { 429 // prepend 430 } 431 } 432 } 433 return ((incoming != null) ? incoming : outgoing) + '.' + server.substring(keepFirstChar); 434 } 435 436 /** 437 * Helper to set error status on password fields that have leading or trailing spaces 438 */ 439 public static void checkPasswordSpaces(Context context, EditText passwordField) { 440 Editable password = passwordField.getText(); 441 int length = password.length(); 442 if (length > 0) { 443 if (password.charAt(0) == ' ' || password.charAt(length-1) == ' ') { 444 passwordField.setError(context.getString(R.string.account_password_spaces_error)); 445 } 446 } 447 } 448 449 } 450