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