Home | History | Annotate | Download | only in setup
      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