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