AccountSettingsUtils.java revision aeee10e57ef4d931e7708fde218d590453a82aea
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
17package com.android.email.activity.setup;
18
19import com.android.email.AccountBackupRestore;
20import com.android.email.R;
21import com.android.email.VendorPolicyLoader;
22import com.android.emailcommon.Logging;
23import com.android.emailcommon.provider.EmailContent;
24import com.android.emailcommon.provider.EmailContent.AccountColumns;
25
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.res.XmlResourceParser;
29import android.text.Editable;
30import android.util.Log;
31import android.widget.EditText;
32
33import java.io.Serializable;
34import java.util.regex.Pattern;
35
36public class AccountSettingsUtils {
37
38    /** Pattern to match globals in the domain */
39    private final static Pattern DOMAIN_GLOB_PATTERN = Pattern.compile("\\*");
40    /** Will match any, single character */
41    private final static char WILD_CHARACTER = '?';
42
43    /**
44     * Commits the UI-related settings of an account to the provider.  This is static so that it
45     * can be used by the various account activities.  If the account has never been saved, this
46     * method saves it; otherwise, it just saves the settings.
47     * @param context the context of the caller
48     * @param account the account whose settings will be committed
49     */
50    public static void commitSettings(Context context, EmailContent.Account account) {
51        if (!account.isSaved()) {
52            account.save(context);
53        } else {
54            ContentValues cv = getAccountContentValues(account);
55            account.update(context, cv);
56        }
57        // Update the backup (side copy) of the accounts
58        AccountBackupRestore.backupAccounts(context);
59    }
60
61    /**
62     * Returns a set of content values to commit account changes (not including the foreign keys
63     * for the two host auth's and policy) to the database.  Does not actually commit anything.
64     */
65    public static ContentValues getAccountContentValues(EmailContent.Account account) {
66        ContentValues cv = new ContentValues();
67        cv.put(AccountColumns.IS_DEFAULT, account.mIsDefault);
68        cv.put(AccountColumns.DISPLAY_NAME, account.getDisplayName());
69        cv.put(AccountColumns.SENDER_NAME, account.getSenderName());
70        cv.put(AccountColumns.SIGNATURE, account.getSignature());
71        cv.put(AccountColumns.SYNC_INTERVAL, account.mSyncInterval);
72        cv.put(AccountColumns.RINGTONE_URI, account.mRingtoneUri);
73        cv.put(AccountColumns.FLAGS, account.mFlags);
74        cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback);
75        cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey);
76        return cv;
77    }
78
79    /**
80     * Search the list of known Email providers looking for one that matches the user's email
81     * domain.  We check for vendor supplied values first, then we look in providers_product.xml,
82     * and finally by the entries in platform providers.xml.  This provides a nominal override
83     * capability.
84     *
85     * A match is defined as any provider entry for which the "domain" attribute matches.
86     *
87     * @param domain The domain portion of the user's email address
88     * @return suitable Provider definition, or null if no match found
89     */
90    public static Provider findProviderForDomain(Context context, String domain) {
91        Provider p = VendorPolicyLoader.getInstance(context).findProviderForDomain(domain);
92        if (p == null) {
93            p = findProviderForDomain(context, domain, R.xml.providers_product);
94        }
95        if (p == null) {
96            p = findProviderForDomain(context, domain, R.xml.providers);
97        }
98        return p;
99    }
100
101    /**
102     * Search a single resource containing known Email provider definitions.
103     *
104     * @param domain The domain portion of the user's email address
105     * @param resourceId Id of the provider resource to scan
106     * @return suitable Provider definition, or null if no match found
107     */
108    /*package*/ static Provider findProviderForDomain(
109            Context context, String domain, int resourceId) {
110        try {
111            XmlResourceParser xml = context.getResources().getXml(resourceId);
112            int xmlEventType;
113            Provider provider = null;
114            while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
115                if (xmlEventType == XmlResourceParser.START_TAG
116                        && "provider".equals(xml.getName())) {
117                    String providerDomain = getXmlAttribute(context, xml, "domain");
118                    try {
119                        if (globMatchIgnoreCase(domain, providerDomain)) {
120                            provider = new Provider();
121                            provider.id = getXmlAttribute(context, xml, "id");
122                            provider.label = getXmlAttribute(context, xml, "label");
123                            provider.domain = domain.toLowerCase();
124                            provider.note = getXmlAttribute(context, xml, "note");
125                        }
126                    } catch (IllegalArgumentException e) {
127                        Log.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() +
128                                "; Domain contains multiple globals");
129                    }
130                }
131                else if (xmlEventType == XmlResourceParser.START_TAG
132                        && "incoming".equals(xml.getName())
133                        && provider != null) {
134                    provider.incomingUriTemplate = getXmlAttribute(context, xml, "uri");
135                    provider.incomingUsernameTemplate = getXmlAttribute(context, xml, "username");
136                }
137                else if (xmlEventType == XmlResourceParser.START_TAG
138                        && "outgoing".equals(xml.getName())
139                        && provider != null) {
140                    provider.outgoingUriTemplate = getXmlAttribute(context, xml, "uri");
141                    provider.outgoingUsernameTemplate = getXmlAttribute(context, xml, "username");
142                }
143                else if (xmlEventType == XmlResourceParser.END_TAG
144                        && "provider".equals(xml.getName())
145                        && provider != null) {
146                    return provider;
147                }
148            }
149        }
150        catch (Exception e) {
151            Log.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e);
152        }
153        return null;
154    }
155
156    /**
157     * Returns if the string <code>s1</code> matches the string <code>s2</code>. The string
158     * <code>s2</code> may contain any number of wildcards -- a '?' character -- and/or a
159     * single global character -- a '*' character. Wildcards match any, single character
160     * while a global character matches zero or more characters.
161     * @throws IllegalArgumentException if either string is null or <code>s2</code> has
162     * multiple globals.
163     */
164    /*package*/ static boolean globMatchIgnoreCase(String s1, String s2)
165            throws IllegalArgumentException {
166        if (s1 == null || s2 == null) {
167            throw new IllegalArgumentException("one or both strings are null");
168        }
169
170        // Handle the possible global in the domain name
171        String[] globParts = DOMAIN_GLOB_PATTERN.split(s2);
172        switch (globParts.length) {
173            case 1:
174                // No globals; test for simple equality
175                if (!wildEqualsIgnoreCase(s1, globParts[0])) {
176                    return false;
177                }
178                break;
179            case 2:
180                // Global; test the front & end parts of the domain
181                String d1 = globParts[0];
182                String d2 = globParts[1];
183                if (!wildStartsWithIgnoreCase(s1, d1) ||
184                        !wildEndsWithIgnoreCase(s1.substring(d1.length()), d2)) {
185                    return false;
186                }
187                break;
188            default:
189                throw new IllegalArgumentException("Multiple globals");
190        }
191        return true;
192    }
193
194    /**
195     * Returns if the string <code>s1</code> equals the string <code>s2</code>. The string
196     * <code>s2</code> may contain zero or more wildcards -- a '?' character.
197     * @throws IllegalArgumentException if the strings are null.
198     */
199    /*package*/ static boolean wildEqualsIgnoreCase(String s1, String s2)
200            throws IllegalArgumentException {
201        if (s1 == null || s2 == null) {
202            throw new IllegalArgumentException("one or both strings are null");
203        }
204        if (s1.length() != s2.length()) {
205            return false;
206        }
207        char[] charArray1 = s1.toLowerCase().toCharArray();
208        char[] charArray2 = s2.toLowerCase().toCharArray();
209        for (int i = 0; i < charArray2.length; i++) {
210            if (charArray2[i] == WILD_CHARACTER || charArray1[i] == charArray2[i]) continue;
211            return false;
212        }
213        return true;
214    }
215
216    /**
217     * Returns if the string <code>s1</code> starts with the string <code>s2</code>. The string
218     * <code>s2</code> may contain zero or more wildcards -- a '?' character.
219     * @throws IllegalArgumentException if the strings are null.
220     */
221    /*package*/ static boolean wildStartsWithIgnoreCase(String s1, String s2)
222            throws IllegalArgumentException {
223        if (s1 == null || s2 == null) {
224            throw new IllegalArgumentException("one or both strings are null");
225        }
226        if (s1.length() < s2.length()) {
227            return false;
228        }
229        s1 = s1.substring(0, s2.length());
230        return wildEqualsIgnoreCase(s1, s2);
231    }
232
233    /**
234     * Returns if the string <code>s1</code> ends with the string <code>s2</code>. The string
235     * <code>s2</code> may contain zero or more wildcards -- a '?' character.
236     * @throws IllegalArgumentException if the strings are null.
237     */
238    /*package*/ static boolean wildEndsWithIgnoreCase(String s1, String s2)
239            throws IllegalArgumentException {
240        if (s1 == null || s2 == null) {
241            throw new IllegalArgumentException("one or both strings are null");
242        }
243        if (s1.length() < s2.length()) {
244            return false;
245        }
246        s1 = s1.substring(s1.length() - s2.length(), s1.length());
247        return wildEqualsIgnoreCase(s1, s2);
248    }
249
250    /**
251     * Attempts to get the given attribute as a String resource first, and if it fails
252     * returns the attribute as a simple String value.
253     * @param xml
254     * @param name
255     * @return the requested resource
256     */
257    private static String getXmlAttribute(Context context, XmlResourceParser xml, String name) {
258        int resId = xml.getAttributeResourceValue(null, name, 0);
259        if (resId == 0) {
260            return xml.getAttributeValue(null, name);
261        }
262        else {
263            return context.getString(resId);
264        }
265    }
266
267    public static class Provider implements Serializable {
268        private static final long serialVersionUID = 8511656164616538989L;
269
270        public String id;
271        public String label;
272        public String domain;
273        public String incomingUriTemplate;
274        public String incomingUsernameTemplate;
275        public String outgoingUriTemplate;
276        public String outgoingUsernameTemplate;
277        public String incomingUri;
278        public String incomingUsername;
279        public String outgoingUri;
280        public String outgoingUsername;
281        public String note;
282
283        /**
284         * Expands templates in all of the  provider fields that support them. Currently,
285         * templates are used in 4 fields -- incoming and outgoing URI and user name.
286         * @param email user-specified data used to replace template values
287         */
288        public void expandTemplates(String email) {
289            String[] emailParts = email.split("@");
290            String user = emailParts[0];
291
292            incomingUri = expandTemplate(incomingUriTemplate, email, user);
293            incomingUsername = expandTemplate(incomingUsernameTemplate, email, user);
294            outgoingUri = expandTemplate(outgoingUriTemplate, email, user);
295            outgoingUsername = expandTemplate(outgoingUsernameTemplate, email, user);
296        }
297
298        /**
299         * Replaces all parameterized values in the given template. The values replaced are
300         * $domain, $user and $email.
301         */
302        private String expandTemplate(String template, String email, String user) {
303            String returnString = template;
304            returnString = returnString.replaceAll("\\$email", email);
305            returnString = returnString.replaceAll("\\$user", user);
306            returnString = returnString.replaceAll("\\$domain", domain);
307            return returnString;
308        }
309    }
310
311    /**
312     * Infer potential email server addresses from domain names
313     *
314     * Incoming: Prepend "imap" or "pop3" to domain, unless "pop", "pop3",
315     *          "imap", or "mail" are found.
316     * Outgoing: Prepend "smtp" if "pop", "pop3", "imap" are found.
317     *          Leave "mail" as-is.
318     * TBD: Are there any useful defaults for exchange?
319     *
320     * @param server name as we know it so far
321     * @param incoming "pop3" or "imap" (or null)
322     * @param outgoing "smtp" or null
323     * @return the post-processed name for use in the UI
324     */
325    public static String inferServerName(String server, String incoming, String outgoing) {
326        // Default values cause entire string to be kept, with prepended server string
327        int keepFirstChar = 0;
328        int firstDotIndex = server.indexOf('.');
329        if (firstDotIndex != -1) {
330            // look at first word and decide what to do
331            String firstWord = server.substring(0, firstDotIndex).toLowerCase();
332            boolean isImapOrPop = "imap".equals(firstWord)
333                    || "pop3".equals(firstWord) || "pop".equals(firstWord);
334            boolean isMail = "mail".equals(firstWord);
335            // Now decide what to do
336            if (incoming != null) {
337                // For incoming, we leave imap/pop/pop3/mail alone, or prepend incoming
338                if (isImapOrPop || isMail) {
339                    return server;
340                }
341            } else {
342                // For outgoing, replace imap/pop/pop3 with outgoing, leave mail alone, or
343                // prepend outgoing
344                if (isImapOrPop) {
345                    keepFirstChar = firstDotIndex + 1;
346                } else if (isMail) {
347                    return server;
348                } else {
349                    // prepend
350                }
351            }
352        }
353        return ((incoming != null) ? incoming : outgoing) + '.' + server.substring(keepFirstChar);
354    }
355
356    /**
357     * Helper to set error status on password fields that have leading or trailing spaces
358     */
359    public static void checkPasswordSpaces(Context context, EditText passwordField) {
360        Editable password = passwordField.getText();
361        int length = password.length();
362        if (length > 0) {
363            if (password.charAt(0) == ' ' || password.charAt(length-1) == ' ') {
364                passwordField.setError(context.getString(R.string.account_password_spaces_error));
365            }
366        }
367    }
368
369}
370