Home | History | Annotate | Download | only in email
      1 /*
      2  * Copyright (C) 2008 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;
     18 
     19 import com.android.email.provider.EmailContent;
     20 import com.android.email.provider.EmailContent.Account;
     21 import com.android.email.provider.EmailContent.AccountColumns;
     22 import com.android.email.provider.EmailContent.HostAuth;
     23 import com.android.email.provider.EmailContent.HostAuthColumns;
     24 import com.android.email.provider.EmailContent.Mailbox;
     25 import com.android.email.provider.EmailContent.MailboxColumns;
     26 import com.android.email.provider.EmailContent.Message;
     27 import com.android.email.provider.EmailContent.MessageColumns;
     28 
     29 import android.content.ContentResolver;
     30 import android.content.Context;
     31 import android.content.res.TypedArray;
     32 import android.database.Cursor;
     33 import android.graphics.drawable.Drawable;
     34 import android.os.AsyncTask;
     35 import android.security.MessageDigest;
     36 import android.telephony.TelephonyManager;
     37 import android.text.Editable;
     38 import android.util.Base64;
     39 import android.util.Log;
     40 import android.widget.TextView;
     41 
     42 import java.io.IOException;
     43 import java.io.InputStream;
     44 import java.io.InputStreamReader;
     45 import java.io.UnsupportedEncodingException;
     46 import java.nio.ByteBuffer;
     47 import java.nio.CharBuffer;
     48 import java.nio.charset.Charset;
     49 import java.security.NoSuchAlgorithmException;
     50 import java.util.Date;
     51 import java.util.GregorianCalendar;
     52 import java.util.TimeZone;
     53 
     54 public class Utility {
     55     public static final Charset UTF_8 = Charset.forName("UTF-8");
     56 
     57     public final static String readInputStream(InputStream in, String encoding) throws IOException {
     58         InputStreamReader reader = new InputStreamReader(in, encoding);
     59         StringBuffer sb = new StringBuffer();
     60         int count;
     61         char[] buf = new char[512];
     62         while ((count = reader.read(buf)) != -1) {
     63             sb.append(buf, 0, count);
     64         }
     65         return sb.toString();
     66     }
     67 
     68     public final static boolean arrayContains(Object[] a, Object o) {
     69         for (int i = 0, count = a.length; i < count; i++) {
     70             if (a[i].equals(o)) {
     71                 return true;
     72             }
     73         }
     74         return false;
     75     }
     76 
     77     /**
     78      * Combines the given array of Objects into a single string using the
     79      * seperator character and each Object's toString() method. between each
     80      * part.
     81      *
     82      * @param parts
     83      * @param seperator
     84      * @return
     85      */
     86     public static String combine(Object[] parts, char seperator) {
     87         if (parts == null) {
     88             return null;
     89         }
     90         StringBuffer sb = new StringBuffer();
     91         for (int i = 0; i < parts.length; i++) {
     92             sb.append(parts[i].toString());
     93             if (i < parts.length - 1) {
     94                 sb.append(seperator);
     95             }
     96         }
     97         return sb.toString();
     98     }
     99 
    100     public static String base64Decode(String encoded) {
    101         if (encoded == null) {
    102             return null;
    103         }
    104         byte[] decoded = Base64.decode(encoded, Base64.DEFAULT);
    105         return new String(decoded);
    106     }
    107 
    108     public static String base64Encode(String s) {
    109         if (s == null) {
    110             return s;
    111         }
    112         return Base64.encodeToString(s.getBytes(), Base64.NO_WRAP);
    113     }
    114 
    115     public static boolean requiredFieldValid(TextView view) {
    116         return view.getText() != null && view.getText().length() > 0;
    117     }
    118 
    119     public static boolean requiredFieldValid(Editable s) {
    120         return s != null && s.length() > 0;
    121     }
    122 
    123     /**
    124      * Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the
    125      * double quote character to start and end if it's not already there.
    126      *
    127      * TODO: Rename this, because "quoteString()" can mean so many different things.
    128      *
    129      * sample -> "sample"
    130      * "sample" -> "sample"
    131      * ""sample"" -> "sample"
    132      * "sample"" -> "sample"
    133      * sa"mp"le -> "sa"mp"le"
    134      * "sa"mp"le" -> "sa"mp"le"
    135      * (empty string) -> ""
    136      * " -> ""
    137      * @param s
    138      * @return
    139      */
    140     public static String quoteString(String s) {
    141         if (s == null) {
    142             return null;
    143         }
    144         if (!s.matches("^\".*\"$")) {
    145             return "\"" + s + "\"";
    146         }
    147         else {
    148             return s;
    149         }
    150     }
    151 
    152     /**
    153      * Apply quoting rules per IMAP RFC,
    154      * quoted          = DQUOTE *QUOTED-CHAR DQUOTE
    155      * QUOTED-CHAR     = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
    156      * quoted-specials = DQUOTE / "\"
    157      *
    158      * This is used primarily for IMAP login, but might be useful elsewhere.
    159      *
    160      * NOTE:  Not very efficient - you may wish to preflight this, or perhaps it should check
    161      * for trouble chars before calling the replace functions.
    162      *
    163      * @param s The string to be quoted.
    164      * @return A copy of the string, having undergone quoting as described above
    165      */
    166     public static String imapQuoted(String s) {
    167 
    168         // First, quote any backslashes by replacing \ with \\
    169         // regex Pattern:  \\    (Java string const = \\\\)
    170         // Substitute:     \\\\  (Java string const = \\\\\\\\)
    171         String result = s.replaceAll("\\\\", "\\\\\\\\");
    172 
    173         // Then, quote any double-quotes by replacing " with \"
    174         // regex Pattern:  "    (Java string const = \")
    175         // Substitute:     \\"  (Java string const = \\\\\")
    176         result = result.replaceAll("\"", "\\\\\"");
    177 
    178         // return string with quotes around it
    179         return "\"" + result + "\"";
    180     }
    181 
    182     /**
    183      * A fast version of  URLDecoder.decode() that works only with UTF-8 and does only two
    184      * allocations. This version is around 3x as fast as the standard one and I'm using it
    185      * hundreds of times in places that slow down the UI, so it helps.
    186      */
    187     public static String fastUrlDecode(String s) {
    188         try {
    189             byte[] bytes = s.getBytes("UTF-8");
    190             byte ch;
    191             int length = 0;
    192             for (int i = 0, count = bytes.length; i < count; i++) {
    193                 ch = bytes[i];
    194                 if (ch == '%') {
    195                     int h = (bytes[i + 1] - '0');
    196                     int l = (bytes[i + 2] - '0');
    197                     if (h > 9) {
    198                         h -= 7;
    199                     }
    200                     if (l > 9) {
    201                         l -= 7;
    202                     }
    203                     bytes[length] = (byte) ((h << 4) | l);
    204                     i += 2;
    205                 }
    206                 else if (ch == '+') {
    207                     bytes[length] = ' ';
    208                 }
    209                 else {
    210                     bytes[length] = bytes[i];
    211                 }
    212                 length++;
    213             }
    214             return new String(bytes, 0, length, "UTF-8");
    215         }
    216         catch (UnsupportedEncodingException uee) {
    217             return null;
    218         }
    219     }
    220 
    221     /**
    222      * Returns true if the specified date is within today. Returns false otherwise.
    223      * @param date
    224      * @return
    225      */
    226     public static boolean isDateToday(Date date) {
    227         // TODO But Calendar is so slowwwwwww....
    228         Date today = new Date();
    229         if (date.getYear() == today.getYear() &&
    230                 date.getMonth() == today.getMonth() &&
    231                 date.getDate() == today.getDate()) {
    232             return true;
    233         }
    234         return false;
    235     }
    236 
    237     /*
    238      * TODO disabled this method globally. It is used in all the settings screens but I just
    239      * noticed that an unrelated icon was dimmed. Android must share drawables internally.
    240      */
    241     public static void setCompoundDrawablesAlpha(TextView view, int alpha) {
    242 //        Drawable[] drawables = view.getCompoundDrawables();
    243 //        for (Drawable drawable : drawables) {
    244 //            if (drawable != null) {
    245 //                drawable.setAlpha(alpha);
    246 //            }
    247 //        }
    248     }
    249 
    250     // TODO: unit test this
    251     public static String buildMailboxIdSelection(ContentResolver resolver, long mailboxId) {
    252         // Setup default selection & args, then add to it as necessary
    253         StringBuilder selection = new StringBuilder(
    254                 MessageColumns.FLAG_LOADED + " IN ("
    255                 + Message.FLAG_LOADED_PARTIAL + "," + Message.FLAG_LOADED_COMPLETE
    256                 + ") AND ");
    257         if (mailboxId == Mailbox.QUERY_ALL_INBOXES
    258             || mailboxId == Mailbox.QUERY_ALL_DRAFTS
    259             || mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
    260             // query for all mailboxes of type INBOX, DRAFTS, or OUTBOX
    261             int type;
    262             if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
    263                 type = Mailbox.TYPE_INBOX;
    264             } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
    265                 type = Mailbox.TYPE_DRAFTS;
    266             } else {
    267                 type = Mailbox.TYPE_OUTBOX;
    268             }
    269             StringBuilder inboxes = new StringBuilder();
    270             Cursor c = resolver.query(Mailbox.CONTENT_URI,
    271                         EmailContent.ID_PROJECTION,
    272                         MailboxColumns.TYPE + "=? AND " + MailboxColumns.FLAG_VISIBLE + "=1",
    273                         new String[] { Integer.toString(type) }, null);
    274             // build an IN (mailboxId, ...) list
    275             // TODO do this directly in the provider
    276             while (c.moveToNext()) {
    277                 if (inboxes.length() != 0) {
    278                     inboxes.append(",");
    279                 }
    280                 inboxes.append(c.getLong(EmailContent.ID_PROJECTION_COLUMN));
    281             }
    282             c.close();
    283             selection.append(MessageColumns.MAILBOX_KEY + " IN ");
    284             selection.append("(").append(inboxes).append(")");
    285         } else  if (mailboxId == Mailbox.QUERY_ALL_UNREAD) {
    286             selection.append(Message.FLAG_READ + "=0");
    287         } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
    288             selection.append(Message.FLAG_FAVORITE + "=1");
    289         } else {
    290             selection.append(MessageColumns.MAILBOX_KEY + "=" + mailboxId);
    291         }
    292         return selection.toString();
    293     }
    294 
    295     public static class FolderProperties {
    296 
    297         private static FolderProperties sInstance;
    298 
    299         // Caches for frequently accessed resources.
    300         private String[] mSpecialMailbox = new String[] {};
    301         private TypedArray mSpecialMailboxDrawable;
    302         private Drawable mDefaultMailboxDrawable;
    303         private Drawable mSummaryStarredMailboxDrawable;
    304         private Drawable mSummaryCombinedInboxDrawable;
    305 
    306         private FolderProperties(Context context) {
    307             mSpecialMailbox = context.getResources().getStringArray(R.array.mailbox_display_names);
    308             for (int i = 0; i < mSpecialMailbox.length; ++i) {
    309                 if ("".equals(mSpecialMailbox[i])) {
    310                     // there is no localized name, so use the display name from the server
    311                     mSpecialMailbox[i] = null;
    312                 }
    313             }
    314             mSpecialMailboxDrawable =
    315                 context.getResources().obtainTypedArray(R.array.mailbox_display_icons);
    316             mDefaultMailboxDrawable =
    317                 context.getResources().getDrawable(R.drawable.ic_list_folder);
    318             mSummaryStarredMailboxDrawable =
    319                 context.getResources().getDrawable(R.drawable.ic_list_starred);
    320             mSummaryCombinedInboxDrawable =
    321                 context.getResources().getDrawable(R.drawable.ic_list_combined_inbox);
    322         }
    323 
    324         public static FolderProperties getInstance(Context context) {
    325             if (sInstance == null) {
    326                 synchronized (FolderProperties.class) {
    327                     if (sInstance == null) {
    328                         sInstance = new FolderProperties(context);
    329                     }
    330                 }
    331             }
    332             return sInstance;
    333         }
    334 
    335         /**
    336          * Lookup names of localized special mailboxes
    337          * @param type
    338          * @return Localized strings
    339          */
    340         public String getDisplayName(int type) {
    341             if (type < mSpecialMailbox.length) {
    342                 return mSpecialMailbox[type];
    343             }
    344             return null;
    345         }
    346 
    347         /**
    348          * Lookup icons of special mailboxes
    349          * @param type
    350          * @return icon's drawable
    351          */
    352         public Drawable getIconIds(int type) {
    353             if (type < mSpecialMailboxDrawable.length()) {
    354                 return mSpecialMailboxDrawable.getDrawable(type);
    355             }
    356             return mDefaultMailboxDrawable;
    357         }
    358 
    359         public Drawable getSummaryMailboxIconIds(long mailboxKey) {
    360             if (mailboxKey == Mailbox.QUERY_ALL_INBOXES) {
    361                 return mSummaryCombinedInboxDrawable;
    362             } else if (mailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
    363                 return mSummaryStarredMailboxDrawable;
    364             } else if (mailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
    365                 return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_DRAFTS);
    366             } else if (mailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
    367                 return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_OUTBOX);
    368             }
    369             return mDefaultMailboxDrawable;
    370         }
    371     }
    372 
    373     private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?"
    374             + " and " + HostAuthColumns.LOGIN + " like ?"
    375             + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\"";
    376     private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?";
    377 
    378     /**
    379      * Look for an existing account with the same username & server
    380      *
    381      * @param context a system context
    382      * @param allowAccountId this account Id will not trigger (when editing an existing account)
    383      * @param hostName the server
    384      * @param userLogin the user login string
    385      * @result null = no dupes found.  non-null = dupe account's display name
    386      */
    387     public static String findDuplicateAccount(Context context, long allowAccountId, String hostName,
    388             String userLogin) {
    389         ContentResolver resolver = context.getContentResolver();
    390         Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION,
    391                 HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userLogin }, null);
    392         try {
    393             while (c.moveToNext()) {
    394                 long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN);
    395                 // Find account with matching hostauthrecv key, and return its display name
    396                 Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
    397                         ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null);
    398                 try {
    399                     while (c2.moveToNext()) {
    400                         long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN);
    401                         if (accountId != allowAccountId) {
    402                             Account account = Account.restoreAccountWithId(context, accountId);
    403                             if (account != null) {
    404                                 return account.mDisplayName;
    405                             }
    406                         }
    407                     }
    408                 } finally {
    409                     c2.close();
    410                 }
    411             }
    412         } finally {
    413             c.close();
    414         }
    415 
    416         return null;
    417     }
    418 
    419     /**
    420      * Generate a random message-id header for locally-generated messages.
    421      */
    422     public static String generateMessageId() {
    423         StringBuffer sb = new StringBuffer();
    424         sb.append("<");
    425         for (int i = 0; i < 24; i++) {
    426             sb.append(Integer.toString((int)(Math.random() * 35), 36));
    427         }
    428         sb.append(".");
    429         sb.append(Long.toString(System.currentTimeMillis()));
    430         sb.append("@email.android.com>");
    431         return sb.toString();
    432     }
    433 
    434     /**
    435      * Generate a time in milliseconds from a date string that represents a date/time in GMT
    436      * @param DateTime date string in format 20090211T180303Z (rfc2445, iCalendar).
    437      * @return the time in milliseconds (since Jan 1, 1970)
    438      */
    439     public static long parseDateTimeToMillis(String date) {
    440         GregorianCalendar cal = parseDateTimeToCalendar(date);
    441         return cal.getTimeInMillis();
    442     }
    443 
    444     /**
    445      * Generate a GregorianCalendar from a date string that represents a date/time in GMT
    446      * @param DateTime date string in format 20090211T180303Z (rfc2445, iCalendar).
    447      * @return the GregorianCalendar
    448      */
    449     public static GregorianCalendar parseDateTimeToCalendar(String date) {
    450         GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
    451                 Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
    452                 Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
    453                 Integer.parseInt(date.substring(13, 15)));
    454         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
    455         return cal;
    456     }
    457 
    458     /**
    459      * Generate a time in milliseconds from an email date string that represents a date/time in GMT
    460      * @param Email style DateTime string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339)
    461      * @return the time in milliseconds (since Jan 1, 1970)
    462      */
    463     public static long parseEmailDateTimeToMillis(String date) {
    464         GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
    465                 Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
    466                 Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)),
    467                 Integer.parseInt(date.substring(17, 19)));
    468         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
    469         return cal.getTimeInMillis();
    470     }
    471 
    472     /** Converts a String to UTF-8 */
    473     public static byte[] toUtf8(String s) {
    474         if (s == null) {
    475             return null;
    476         }
    477         final ByteBuffer buffer = UTF_8.encode(CharBuffer.wrap(s));
    478         final byte[] bytes = new byte[buffer.limit()];
    479         buffer.get(bytes);
    480         return bytes;
    481     }
    482 
    483     /** Build a String from UTF-8 bytes */
    484     public static String fromUtf8(byte[] b) {
    485         if (b == null) {
    486             return null;
    487         }
    488         final CharBuffer cb = Utility.UTF_8.decode(ByteBuffer.wrap(b));
    489         return new String(cb.array(), 0, cb.length());
    490     }
    491 
    492     /**
    493      * @return true if the input is the first (or only) byte in a UTF-8 character
    494      */
    495     public static boolean isFirstUtf8Byte(byte b) {
    496         // If the top 2 bits is '10', it's not a first byte.
    497         return (b & 0xc0) != 0x80;
    498     }
    499 
    500     public static String byteToHex(int b) {
    501         return byteToHex(new StringBuilder(), b).toString();
    502     }
    503 
    504     public static StringBuilder byteToHex(StringBuilder sb, int b) {
    505         b &= 0xFF;
    506         sb.append("0123456789ABCDEF".charAt(b >> 4));
    507         sb.append("0123456789ABCDEF".charAt(b & 0xF));
    508         return sb;
    509     }
    510 
    511     public static String replaceBareLfWithCrlf(String str) {
    512         return str.replace("\r", "").replace("\n", "\r\n");
    513     }
    514 
    515     /**
    516      * Cancel an {@link AsyncTask}.  If it's already running, it'll be interrupted.
    517      */
    518     public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
    519         cancelTask(task, true);
    520     }
    521 
    522     /**
    523      * Cancel an {@link AsyncTask}.
    524      *
    525      * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
    526      *        task should be interrupted; otherwise, in-progress tasks are allowed
    527      *        to complete.
    528      */
    529     public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
    530         if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
    531             task.cancel(mayInterruptIfRunning);
    532         }
    533     }
    534 
    535     /**
    536      * @return Device's unique ID if available.  null if the device has no unique ID.
    537      */
    538     public static String getConsistentDeviceId(Context context) {
    539         final String deviceId;
    540         try {
    541             TelephonyManager tm =
    542                     (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    543             if (tm == null) {
    544                 return null;
    545             }
    546             deviceId = tm.getDeviceId();
    547             if (deviceId == null) {
    548                 return null;
    549             }
    550         } catch (Exception e) {
    551             Log.d(Email.LOG_TAG, "Error in TelephonyManager.getDeviceId(): " + e.getMessage());
    552             return null;
    553         }
    554         final MessageDigest sha;
    555         try {
    556             sha = MessageDigest.getInstance("SHA-1");
    557         } catch (NoSuchAlgorithmException impossible) {
    558             return null;
    559         }
    560         sha.update(Utility.toUtf8(deviceId));
    561         final int hash = getSmallHashFromSha1(sha.digest());
    562         return Integer.toString(hash);
    563     }
    564 
    565     /**
    566      * @return a non-negative integer generated from 20 byte SHA-1 hash.
    567      */
    568     /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) {
    569         final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes.
    570         return ((sha1[offset]  & 0x7f) << 24)
    571                 | ((sha1[offset + 1] & 0xff) << 16)
    572                 | ((sha1[offset + 2] & 0xff) << 8)
    573                 | ((sha1[offset + 3] & 0xff));
    574     }
    575 }
    576