• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
commitSettings(Context context, Account account)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      */
getAccountContentValues(Account account)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     */
createOAuthRegistrationRequest(final Context context, final OAuthProvider provider, final String emailAddress)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     */
findOAuthProvider(final Context context, final String id)147    public static OAuthProvider findOAuthProvider(final Context context, final String id) {
148        return findOAuthProvider(context, id, R.xml.oauth);
149    }
150 
getAllOAuthProviders(final Context context)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     */
findOAuthProvider(final Context context, final String id, final int resourceId)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      */
findProviderForDomain(Context context, String domain)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      */
findProviderForDomain( Context context, String domain, int resourceId)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
matchProvider(String testDomain, String providerDomain)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 
matchWithWildcards(String testPart, String providerPart)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      */
getXmlAttribute(Context context, XmlResourceParser xml, String name)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      */
inferServerName(Context context, String server, String incoming, String outgoing)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      */
checkPasswordSpaces(Context context, EditText passwordField)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