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 readInputStream(InputStream in, String encoding)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 arrayContains(Object[] a, Object o)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 */ combine(Object[] parts, char seperator)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 } base64Decode(String encoded)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 base64Encode(String s)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 requiredFieldValid(TextView view)124 public static boolean requiredFieldValid(TextView view) { 125 return view.getText() != null && view.getText().length() > 0; 126 } 127 requiredFieldValid(Editable s)128 public static boolean requiredFieldValid(Editable s) { 129 return s != null && s.length() > 0; 130 } 131 isPortFieldValid(TextView view)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 */ quoteString(String s)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 */ imapQuoted(String s)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 */ fastUrlDecode(String s)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 */ isDateToday(Date date)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 */ setCompoundDrawablesAlpha(TextView view, int alpha)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 buildMailboxIdSelection(ContentResolver resolver, long mailboxId)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 FolderProperties(Context context)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 getInstance(Context context)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 */ getDisplayName(int type)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 */ getIconIds(int type)375 public Drawable getIconIds(int type) { 376 if (type < mSpecialMailboxDrawable.length()) { 377 return mSpecialMailboxDrawable.getDrawable(type); 378 } 379 return mDefaultMailboxDrawable; 380 } 381 getSummaryMailboxIconIds(long mailboxKey)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 */ findDuplicateAccount(Context context, long allowAccountId, String hostName, String userLogin)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 */ generateMessageId()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 */ parseDateTimeToMillis(String date)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 */ parseDateTimeToCalendar(String date)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 */ parseEmailDateTimeToMillis(String date)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 encode(Charset charset, String s)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 decode(Charset charset, byte[] b)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 */ toUtf8(String s)514 public static byte[] toUtf8(String s) { 515 return encode(UTF_8, s); 516 } 517 518 /** Builds a String from UTF-8 bytes */ fromUtf8(byte[] b)519 public static String fromUtf8(byte[] b) { 520 return decode(UTF_8, b); 521 } 522 523 /** Converts a String to ASCII bytes */ toAscii(String s)524 public static byte[] toAscii(String s) { 525 return encode(ASCII, s); 526 } 527 528 /** Builds a String from ASCII bytes */ fromAscii(byte[] b)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 */ isFirstUtf8Byte(byte b)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 byteToHex(int b)541 public static String byteToHex(int b) { 542 return byteToHex(new StringBuilder(), b).toString(); 543 } 544 byteToHex(StringBuilder sb, int b)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 replaceBareLfWithCrlf(String str)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 */ cancelTaskInterrupt(AsyncTask<?, ?, ?> task)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 */ cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning)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 */ getConsistentDeviceId(Context context)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 */ getSmallHashFromSha1(byte[] sha1)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 */ cleanUpMimeDate(String date)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 streamFromAsciiString(String ascii)633 public static ByteArrayInputStream streamFromAsciiString(String ascii) { 634 return new ByteArrayInputStream(toAscii(ascii)); 635 } 636 } 637