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