Utility.java revision f52afae9424fe41071cc34a8d6cbcb82b992a411
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 17package com.android.email; 18 19import com.android.email.provider.EmailContent; 20import com.android.email.provider.EmailContent.Account; 21import com.android.email.provider.EmailContent.AccountColumns; 22import com.android.email.provider.EmailContent.HostAuth; 23import com.android.email.provider.EmailContent.HostAuthColumns; 24import com.android.email.provider.EmailContent.Mailbox; 25import com.android.email.provider.EmailContent.MailboxColumns; 26import com.android.email.provider.EmailContent.Message; 27import com.android.email.provider.EmailContent.MessageColumns; 28 29import android.app.Activity; 30import android.content.ContentResolver; 31import android.content.Context; 32import android.content.pm.ActivityInfo; 33import android.content.res.Resources; 34import android.content.res.TypedArray; 35import android.database.Cursor; 36import android.graphics.drawable.Drawable; 37import android.net.Uri; 38import android.os.AsyncTask; 39import android.os.Parcelable; 40import android.security.MessageDigest; 41import android.telephony.TelephonyManager; 42import android.text.TextUtils; 43import android.util.Base64; 44import android.util.Log; 45import android.widget.AbsListView; 46import android.widget.TextView; 47import android.widget.Toast; 48 49import java.io.ByteArrayInputStream; 50import java.io.File; 51import java.io.IOException; 52import java.io.InputStream; 53import java.io.InputStreamReader; 54import java.io.UnsupportedEncodingException; 55import java.nio.ByteBuffer; 56import java.nio.CharBuffer; 57import java.nio.charset.Charset; 58import java.security.NoSuchAlgorithmException; 59import java.util.ArrayList; 60import java.util.Date; 61import java.util.GregorianCalendar; 62import java.util.TimeZone; 63import java.util.regex.Pattern; 64 65public class Utility { 66 public static final Charset UTF_8 = Charset.forName("UTF-8"); 67 public static final Charset ASCII = Charset.forName("US-ASCII"); 68 69 public static final String[] EMPTY_STRINGS = new String[0]; 70 public static final Long[] EMPTY_LONGS = new Long[0]; 71 72 // "GMT" + "+" or "-" + 4 digits 73 private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE = 74 Pattern.compile("GMT([-+]\\d{4})$"); 75 76 public final static String readInputStream(InputStream in, String encoding) throws IOException { 77 InputStreamReader reader = new InputStreamReader(in, encoding); 78 StringBuffer sb = new StringBuffer(); 79 int count; 80 char[] buf = new char[512]; 81 while ((count = reader.read(buf)) != -1) { 82 sb.append(buf, 0, count); 83 } 84 return sb.toString(); 85 } 86 87 public final static boolean arrayContains(Object[] a, Object o) { 88 for (int i = 0, count = a.length; i < count; i++) { 89 if (a[i].equals(o)) { 90 return true; 91 } 92 } 93 return false; 94 } 95 96 /** 97 * Combines the given array of Objects into a single string using the 98 * seperator character and each Object's toString() method. between each 99 * part. 100 * 101 * @param parts 102 * @param seperator 103 * @return 104 */ 105 public static String combine(Object[] parts, char seperator) { 106 if (parts == null) { 107 return null; 108 } 109 StringBuffer sb = new StringBuffer(); 110 for (int i = 0; i < parts.length; i++) { 111 sb.append(parts[i].toString()); 112 if (i < parts.length - 1) { 113 sb.append(seperator); 114 } 115 } 116 return sb.toString(); 117 } 118 public static String base64Decode(String encoded) { 119 if (encoded == null) { 120 return null; 121 } 122 byte[] decoded = Base64.decode(encoded, Base64.DEFAULT); 123 return new String(decoded); 124 } 125 126 public static String base64Encode(String s) { 127 if (s == null) { 128 return s; 129 } 130 return Base64.encodeToString(s.getBytes(), Base64.NO_WRAP); 131 } 132 133 public static boolean isTextViewNotEmpty(TextView view) { 134 return !TextUtils.isEmpty(view.getText()); 135 } 136 137 public static boolean isPortFieldValid(TextView view) { 138 CharSequence chars = view.getText(); 139 if (TextUtils.isEmpty(chars)) return false; 140 Integer port; 141 // In theory, we can't get an illegal value here, since the field is monitored for valid 142 // numeric input. But this might be used elsewhere without such a check. 143 try { 144 port = Integer.parseInt(chars.toString()); 145 } catch (NumberFormatException e) { 146 return false; 147 } 148 return port > 0 && port < 65536; 149 } 150 151 /** 152 * Ensures that the given string starts and ends with the double quote character. The string is 153 * not modified in any way except to add the double quote character to start and end if it's not 154 * already there. 155 * 156 * TODO: Rename this, because "quoteString()" can mean so many different things. 157 * 158 * sample -> "sample" 159 * "sample" -> "sample" 160 * ""sample"" -> "sample" 161 * "sample"" -> "sample" 162 * sa"mp"le -> "sa"mp"le" 163 * "sa"mp"le" -> "sa"mp"le" 164 * (empty string) -> "" 165 * " -> "" 166 * @param s 167 * @return 168 */ 169 public static String quoteString(String s) { 170 if (s == null) { 171 return null; 172 } 173 if (!s.matches("^\".*\"$")) { 174 return "\"" + s + "\""; 175 } 176 else { 177 return s; 178 } 179 } 180 181 /** 182 * Apply quoting rules per IMAP RFC, 183 * quoted = DQUOTE *QUOTED-CHAR DQUOTE 184 * QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials 185 * quoted-specials = DQUOTE / "\" 186 * 187 * This is used primarily for IMAP login, but might be useful elsewhere. 188 * 189 * NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check 190 * for trouble chars before calling the replace functions. 191 * 192 * @param s The string to be quoted. 193 * @return A copy of the string, having undergone quoting as described above 194 */ 195 public static String imapQuoted(String s) { 196 197 // First, quote any backslashes by replacing \ with \\ 198 // regex Pattern: \\ (Java string const = \\\\) 199 // Substitute: \\\\ (Java string const = \\\\\\\\) 200 String result = s.replaceAll("\\\\", "\\\\\\\\"); 201 202 // Then, quote any double-quotes by replacing " with \" 203 // regex Pattern: " (Java string const = \") 204 // Substitute: \\" (Java string const = \\\\\") 205 result = result.replaceAll("\"", "\\\\\""); 206 207 // return string with quotes around it 208 return "\"" + result + "\""; 209 } 210 211 /** 212 * A fast version of URLDecoder.decode() that works only with UTF-8 and does only two 213 * allocations. This version is around 3x as fast as the standard one and I'm using it 214 * hundreds of times in places that slow down the UI, so it helps. 215 */ 216 public static String fastUrlDecode(String s) { 217 try { 218 byte[] bytes = s.getBytes("UTF-8"); 219 byte ch; 220 int length = 0; 221 for (int i = 0, count = bytes.length; i < count; i++) { 222 ch = bytes[i]; 223 if (ch == '%') { 224 int h = (bytes[i + 1] - '0'); 225 int l = (bytes[i + 2] - '0'); 226 if (h > 9) { 227 h -= 7; 228 } 229 if (l > 9) { 230 l -= 7; 231 } 232 bytes[length] = (byte) ((h << 4) | l); 233 i += 2; 234 } 235 else if (ch == '+') { 236 bytes[length] = ' '; 237 } 238 else { 239 bytes[length] = bytes[i]; 240 } 241 length++; 242 } 243 return new String(bytes, 0, length, "UTF-8"); 244 } 245 catch (UnsupportedEncodingException uee) { 246 return null; 247 } 248 } 249 250 /** 251 * Returns true if the specified date is within today. Returns false otherwise. 252 * @param date 253 * @return 254 */ 255 public static boolean isDateToday(Date date) { 256 // TODO But Calendar is so slowwwwwww.... 257 Date today = new Date(); 258 if (date.getYear() == today.getYear() && 259 date.getMonth() == today.getMonth() && 260 date.getDate() == today.getDate()) { 261 return true; 262 } 263 return false; 264 } 265 266 /* 267 * TODO disabled this method globally. It is used in all the settings screens but I just 268 * noticed that an unrelated icon was dimmed. Android must share drawables internally. 269 */ 270 public static void setCompoundDrawablesAlpha(TextView view, int alpha) { 271// Drawable[] drawables = view.getCompoundDrawables(); 272// for (Drawable drawable : drawables) { 273// if (drawable != null) { 274// drawable.setAlpha(alpha); 275// } 276// } 277 } 278 279 // TODO: unit test this 280 public static String buildMailboxIdSelection(ContentResolver resolver, long mailboxId) { 281 // Setup default selection & args, then add to it as necessary 282 StringBuilder selection = new StringBuilder( 283 MessageColumns.FLAG_LOADED + " IN (" 284 + Message.FLAG_LOADED_PARTIAL + "," + Message.FLAG_LOADED_COMPLETE 285 + ") AND "); 286 if (mailboxId == Mailbox.QUERY_ALL_INBOXES 287 || mailboxId == Mailbox.QUERY_ALL_DRAFTS 288 || mailboxId == Mailbox.QUERY_ALL_OUTBOX) { 289 // query for all mailboxes of type INBOX, DRAFTS, or OUTBOX 290 int type; 291 if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { 292 type = Mailbox.TYPE_INBOX; 293 } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) { 294 type = Mailbox.TYPE_DRAFTS; 295 } else { 296 type = Mailbox.TYPE_OUTBOX; 297 } 298 StringBuilder inboxes = new StringBuilder(); 299 Cursor c = resolver.query(Mailbox.CONTENT_URI, 300 EmailContent.ID_PROJECTION, 301 MailboxColumns.TYPE + "=? AND " + MailboxColumns.FLAG_VISIBLE + "=1", 302 new String[] { Integer.toString(type) }, null); 303 // build an IN (mailboxId, ...) list 304 // TODO do this directly in the provider 305 while (c.moveToNext()) { 306 if (inboxes.length() != 0) { 307 inboxes.append(","); 308 } 309 inboxes.append(c.getLong(EmailContent.ID_PROJECTION_COLUMN)); 310 } 311 c.close(); 312 selection.append(MessageColumns.MAILBOX_KEY + " IN "); 313 selection.append("(").append(inboxes).append(")"); 314 } else if (mailboxId == Mailbox.QUERY_ALL_UNREAD) { 315 selection.append(Message.FLAG_READ + "=0"); 316 } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) { 317 selection.append(Message.FLAG_FAVORITE + "=1"); 318 } else { 319 selection.append(MessageColumns.MAILBOX_KEY + "=" + mailboxId); 320 } 321 return selection.toString(); 322 } 323 324 // TODO When the UI is settled, cache all strings/drawables 325 // TODO When the UI is settled, write up tests 326 // TODO When the UI is settled, remove backward-compatibility methods 327 public static class FolderProperties { 328 329 private static FolderProperties sInstance; 330 331 private final Context mContext; 332 333 // Caches for frequently accessed resources. 334 private final String[] mSpecialMailbox; 335 private final TypedArray mSpecialMailboxDrawable; 336 private final Drawable mDefaultMailboxDrawable; 337 private final Drawable mSummaryStarredMailboxDrawable; 338 private final Drawable mSummaryCombinedInboxDrawable; 339 340 private FolderProperties(Context context) { 341 mContext = context.getApplicationContext(); 342 mSpecialMailbox = context.getResources().getStringArray(R.array.mailbox_display_names); 343 for (int i = 0; i < mSpecialMailbox.length; ++i) { 344 if ("".equals(mSpecialMailbox[i])) { 345 // there is no localized name, so use the display name from the server 346 mSpecialMailbox[i] = null; 347 } 348 } 349 mSpecialMailboxDrawable = 350 context.getResources().obtainTypedArray(R.array.mailbox_display_icons); 351 mDefaultMailboxDrawable = 352 context.getResources().getDrawable(R.drawable.ic_list_folder); 353 mSummaryStarredMailboxDrawable = 354 context.getResources().getDrawable(R.drawable.ic_list_starred); 355 mSummaryCombinedInboxDrawable = 356 context.getResources().getDrawable(R.drawable.ic_list_combined_inbox); 357 } 358 359 public static synchronized FolderProperties getInstance(Context context) { 360 if (sInstance == null) { 361 sInstance = new FolderProperties(context); 362 } 363 return sInstance; 364 } 365 366 // For backward compatibility. 367 public String getDisplayName(int type) { 368 return getDisplayName(type, -1); 369 } 370 371 // For backward compatibility. 372 public Drawable getSummaryMailboxIconIds(long id) { 373 return getIcon(-1, id); 374 } 375 376 public Drawable getIconIds(int type) { 377 return getIcon(type, -1); 378 } 379 380 /** 381 * Lookup names of localized special mailboxes 382 */ 383 public String getDisplayName(int type, long mailboxId) { 384 // Special combined mailboxes 385 int resId = 0; 386 387 // Can't use long for switch!? 388 if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { 389 resId = R.string.account_folder_list_summary_inbox; 390 } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) { 391 resId = R.string.account_folder_list_summary_starred; 392 } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) { 393 resId = R.string.account_folder_list_summary_drafts; 394 } else if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) { 395 resId = R.string.account_folder_list_summary_outbox; 396 } 397 if (resId != 0) { 398 return mContext.getString(resId); 399 } 400 401 if (type < mSpecialMailbox.length) { 402 return mSpecialMailbox[type]; 403 } 404 return null; 405 } 406 407 /** 408 * Lookup icons of special mailboxes 409 */ 410 public Drawable getIcon(int type, long mailboxId) { 411 if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { 412 return mSummaryCombinedInboxDrawable; 413 } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) { 414 return mSummaryStarredMailboxDrawable; 415 } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) { 416 return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_DRAFTS); 417 } else if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) { 418 return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_OUTBOX); 419 } 420 if (0 <= type && type < mSpecialMailboxDrawable.length()) { 421 return mSpecialMailboxDrawable.getDrawable(type); 422 } 423 return mDefaultMailboxDrawable; 424 } 425 } 426 427 private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?" 428 + " and " + HostAuthColumns.LOGIN + " like ?" 429 + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\""; 430 private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?"; 431 432 /** 433 * Look for an existing account with the same username & server 434 * 435 * @param context a system context 436 * @param allowAccountId this account Id will not trigger (when editing an existing account) 437 * @param hostName the server's address 438 * @param userLogin the user's login string 439 * @result null = no matching account found. Account = matching account 440 */ 441 public static Account findExistingAccount(Context context, long allowAccountId, 442 String hostName, String userLogin) { 443 ContentResolver resolver = context.getContentResolver(); 444 Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION, 445 HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userLogin }, null); 446 try { 447 while (c.moveToNext()) { 448 long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN); 449 // Find account with matching hostauthrecv key, and return it 450 Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, 451 ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null); 452 try { 453 while (c2.moveToNext()) { 454 long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN); 455 if (accountId != allowAccountId) { 456 Account account = Account.restoreAccountWithId(context, accountId); 457 if (account != null) { 458 return account; 459 } 460 } 461 } 462 } finally { 463 c2.close(); 464 } 465 } 466 } finally { 467 c.close(); 468 } 469 470 return null; 471 } 472 473 /** 474 * Generate a random message-id header for locally-generated messages. 475 */ 476 public static String generateMessageId() { 477 StringBuffer sb = new StringBuffer(); 478 sb.append("<"); 479 for (int i = 0; i < 24; i++) { 480 sb.append(Integer.toString((int)(Math.random() * 35), 36)); 481 } 482 sb.append("."); 483 sb.append(Long.toString(System.currentTimeMillis())); 484 sb.append("@email.android.com>"); 485 return sb.toString(); 486 } 487 488 /** 489 * Generate a time in milliseconds from a date string that represents a date/time in GMT 490 * @param DateTime date string in format 20090211T180303Z (rfc2445, iCalendar). 491 * @return the time in milliseconds (since Jan 1, 1970) 492 */ 493 public static long parseDateTimeToMillis(String date) { 494 GregorianCalendar cal = parseDateTimeToCalendar(date); 495 return cal.getTimeInMillis(); 496 } 497 498 /** 499 * Generate a GregorianCalendar from a date string that represents a date/time in GMT 500 * @param DateTime date string in format 20090211T180303Z (rfc2445, iCalendar). 501 * @return the GregorianCalendar 502 */ 503 public static GregorianCalendar parseDateTimeToCalendar(String date) { 504 GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)), 505 Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)), 506 Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)), 507 Integer.parseInt(date.substring(13, 15))); 508 cal.setTimeZone(TimeZone.getTimeZone("GMT")); 509 return cal; 510 } 511 512 /** 513 * Generate a time in milliseconds from an email date string that represents a date/time in GMT 514 * @param Email style DateTime string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339) 515 * @return the time in milliseconds (since Jan 1, 1970) 516 */ 517 public static long parseEmailDateTimeToMillis(String date) { 518 GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)), 519 Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)), 520 Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)), 521 Integer.parseInt(date.substring(17, 19))); 522 cal.setTimeZone(TimeZone.getTimeZone("GMT")); 523 return cal.getTimeInMillis(); 524 } 525 526 private static byte[] encode(Charset charset, String s) { 527 if (s == null) { 528 return null; 529 } 530 final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s)); 531 final byte[] bytes = new byte[buffer.limit()]; 532 buffer.get(bytes); 533 return bytes; 534 } 535 536 private static String decode(Charset charset, byte[] b) { 537 if (b == null) { 538 return null; 539 } 540 final CharBuffer cb = charset.decode(ByteBuffer.wrap(b)); 541 return new String(cb.array(), 0, cb.length()); 542 } 543 544 /** Converts a String to UTF-8 */ 545 public static byte[] toUtf8(String s) { 546 return encode(UTF_8, s); 547 } 548 549 /** Builds a String from UTF-8 bytes */ 550 public static String fromUtf8(byte[] b) { 551 return decode(UTF_8, b); 552 } 553 554 /** Converts a String to ASCII bytes */ 555 public static byte[] toAscii(String s) { 556 return encode(ASCII, s); 557 } 558 559 /** Builds a String from ASCII bytes */ 560 public static String fromAscii(byte[] b) { 561 return decode(ASCII, b); 562 } 563 564 /** 565 * @return true if the input is the first (or only) byte in a UTF-8 character 566 */ 567 public static boolean isFirstUtf8Byte(byte b) { 568 // If the top 2 bits is '10', it's not a first byte. 569 return (b & 0xc0) != 0x80; 570 } 571 572 public static String byteToHex(int b) { 573 return byteToHex(new StringBuilder(), b).toString(); 574 } 575 576 public static StringBuilder byteToHex(StringBuilder sb, int b) { 577 b &= 0xFF; 578 sb.append("0123456789ABCDEF".charAt(b >> 4)); 579 sb.append("0123456789ABCDEF".charAt(b & 0xF)); 580 return sb; 581 } 582 583 public static String replaceBareLfWithCrlf(String str) { 584 return str.replace("\r", "").replace("\n", "\r\n"); 585 } 586 587 /** 588 * Cancel an {@link AsyncTask}. If it's already running, it'll be interrupted. 589 */ 590 public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) { 591 cancelTask(task, true); 592 } 593 594 /** 595 * Cancel an {@link AsyncTask}. 596 * 597 * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this 598 * task should be interrupted; otherwise, in-progress tasks are allowed 599 * to complete. 600 */ 601 public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) { 602 if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) { 603 task.cancel(mayInterruptIfRunning); 604 } 605 } 606 607 /** 608 * @return Device's unique ID if available. null if the device has no unique ID. 609 */ 610 public static String getConsistentDeviceId(Context context) { 611 final String deviceId; 612 try { 613 TelephonyManager tm = 614 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 615 if (tm == null) { 616 return null; 617 } 618 deviceId = tm.getDeviceId(); 619 if (deviceId == null) { 620 return null; 621 } 622 } catch (Exception e) { 623 Log.d(Email.LOG_TAG, "Error in TelephonyManager.getDeviceId(): " + e.getMessage()); 624 return null; 625 } 626 final MessageDigest sha; 627 try { 628 sha = MessageDigest.getInstance("SHA-1"); 629 } catch (NoSuchAlgorithmException impossible) { 630 return null; 631 } 632 sha.update(Utility.toUtf8(deviceId)); 633 final int hash = getSmallHashFromSha1(sha.digest()); 634 return Integer.toString(hash); 635 } 636 637 /** 638 * @return a non-negative integer generated from 20 byte SHA-1 hash. 639 */ 640 /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) { 641 final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes. 642 return ((sha1[offset] & 0x7f) << 24) 643 | ((sha1[offset + 1] & 0xff) << 16) 644 | ((sha1[offset + 2] & 0xff) << 8) 645 | ((sha1[offset + 3] & 0xff)); 646 } 647 648 /** 649 * Try to make a date MIME(RFC 2822/5322)-compliant. 650 * 651 * It fixes: 652 * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700" 653 * (4 digit zone value can't be preceded by "GMT") 654 * We got a report saying eBay sends a date in this format 655 */ 656 public static String cleanUpMimeDate(String date) { 657 if (TextUtils.isEmpty(date)) { 658 return date; 659 } 660 date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1"); 661 return date; 662 } 663 664 public static ByteArrayInputStream streamFromAsciiString(String ascii) { 665 return new ByteArrayInputStream(toAscii(ascii)); 666 } 667 668 /** 669 * A thread safe way to show a Toast. This method uses {@link Activity#runOnUiThread}, so it 670 * can be called on any thread. 671 * 672 * @param activity Parent activity. 673 * @param resId Resource ID of the message string. 674 */ 675 public static void showToast(Activity activity, int resId) { 676 showToast(activity, activity.getResources().getString(resId)); 677 } 678 679 /** 680 * A thread safe way to show a Toast. This method uses {@link Activity#runOnUiThread}, so it 681 * can be called on any thread. 682 * 683 * @param activity Parent activity. 684 * @param message Message to show. 685 */ 686 public static void showToast(final Activity activity, final String message) { 687 activity.runOnUiThread(new Runnable() { 688 public void run() { 689 Toast.makeText(activity, message, Toast.LENGTH_LONG).show(); 690 } 691 }); 692 } 693 694 /** 695 * Run {@code r} on a worker thread. 696 */ 697 public static void runAsync(final Runnable r) { 698 new AsyncTask<Void, Void, Void>() { 699 @Override protected Void doInBackground(Void... params) { 700 r.run(); 701 return null; 702 } 703 }.execute(); 704 } 705 706 /** 707 * Formats the given size as a String in bytes, kB, MB or GB. Ex: 12,315,000 = 11 MB 708 */ 709 public static String formatSize(Context context, long size) { 710 final Resources res = context.getResources(); 711 final long KB = 1024; 712 final long MB = (KB * 1024); 713 final long GB = (MB * 1024); 714 715 int resId; 716 int value; 717 718 if (size < KB) { 719 resId = R.plurals.message_view_attachment_bytes; 720 value = (int) size; 721 } else if (size < MB) { 722 resId = R.plurals.message_view_attachment_kilobytes; 723 value = (int) (size / KB); 724 } else if (size < GB) { 725 resId = R.plurals.message_view_attachment_megabytes; 726 value = (int) (size / MB); 727 } else { 728 resId = R.plurals.message_view_attachment_gigabytes; 729 value = (int) (size / GB); 730 } 731 return res.getQuantityString(resId, value, value); 732 } 733 734 /** 735 * Interface used in {@link #createUniqueFile} instead of {@link File#createNewFile()} to make 736 * it testable. 737 */ 738 /* package */ interface NewFileCreator { 739 public static final NewFileCreator DEFAULT = new NewFileCreator() { 740 @Override public boolean createNewFile(File f) throws IOException { 741 return f.createNewFile(); 742 } 743 }; 744 public boolean createNewFile(File f) throws IOException ; 745 } 746 747 /** 748 * Creates a new empty file with a unique name in the given directory by appending a hyphen and 749 * a number to the given filename. 750 * 751 * @return a new File object, or null if one could not be created 752 */ 753 public static File createUniqueFile(File directory, String filename) throws IOException { 754 return createUniqueFileInternal(NewFileCreator.DEFAULT, directory, filename); 755 } 756 757 /* package */ static File createUniqueFileInternal(NewFileCreator nfc, 758 File directory, String filename) throws IOException { 759 File file = new File(directory, filename); 760 if (nfc.createNewFile(file)) { 761 return file; 762 } 763 // Get the extension of the file, if any. 764 int index = filename.lastIndexOf('.'); 765 String format; 766 if (index != -1) { 767 String name = filename.substring(0, index); 768 String extension = filename.substring(index); 769 format = name + "-%d" + extension; 770 } else { 771 format = filename + "-%d"; 772 } 773 774 for (int i = 2; i < Integer.MAX_VALUE; i++) { 775 file = new File(directory, String.format(format, i)); 776 if (nfc.createNewFile(file)) { 777 return file; 778 } 779 } 780 return null; 781 } 782 783 /** 784 * @return a long in column {@code column} of the first result row, if the query returns at 785 * least 1 row. Otherwise returns {@code defaultValue}. 786 */ 787 public static Long getFirstRowLong(Context context, Uri uri, String[] projection, 788 String selection, String[] selectionArgs, String sortOrder, int column, 789 Long defaultValue) { 790 Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs, 791 sortOrder); 792 try { 793 if (c.moveToFirst()) { 794 return c.getLong(column); 795 } 796 } finally { 797 c.close(); 798 } 799 return defaultValue; 800 } 801 802 /** 803 * {@link #getFirstRowLong} with null as a default value. 804 */ 805 public static Long getFirstRowLong(Context context, Uri uri, String[] projection, 806 String selection, String[] selectionArgs, String sortOrder, int column) { 807 return getFirstRowLong(context, uri, projection, selection, selectionArgs, 808 sortOrder, column, null); 809 } 810 811 /** 812 * @return an integer in column {@code column} of the first result row, if the query returns at 813 * least 1 row. Otherwise returns {@code defaultValue}. 814 */ 815 public static Integer getFirstRowInt(Context context, Uri uri, String[] projection, 816 String selection, String[] selectionArgs, String sortOrder, int column, 817 Integer defaultValue) { 818 Long longDefault = (defaultValue == null) ? null : defaultValue.longValue(); 819 Long result = getFirstRowLong(context, uri, projection, selection, selectionArgs, sortOrder, 820 column, longDefault); 821 return (result == null) ? null : result.intValue(); 822 } 823 824 /** 825 * {@link #getFirstRowInt} with null as a default value. 826 */ 827 public static Integer getFirstRowInt(Context context, Uri uri, String[] projection, 828 String selection, String[] selectionArgs, String sortOrder, int column) { 829 return getFirstRowInt(context, uri, projection, selection, selectionArgs, 830 sortOrder, column, null); 831 } 832 833 /** 834 * A class used to restore ListView state (e.g. scroll position) when changing adapter. 835 * 836 * TODO For some reason it doesn't always work. Investigate and fix it. 837 */ 838 public static class ListStateSaver { 839 private final Parcelable mState; 840 841 public ListStateSaver(AbsListView lv) { 842 mState = lv.onSaveInstanceState(); 843 } 844 845 public void restore(AbsListView lv) { 846 lv.onRestoreInstanceState(mState); 847 } 848 } 849 850 /** 851 * STOPSHIP Remove this method 852 * Toggle between portrait and landscape. Developement use only. 853 */ 854 public static void changeOrientation(Activity activity) { 855 activity.setRequestedOrientation( 856 (activity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) 857 ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 858 : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 859 } 860 861 /** 862 * Class that supports running any operation for each account. 863 */ 864 public abstract static class ForEachAccount extends AsyncTask<Void, Void, Long[]> { 865 private final Context mContext; 866 867 public ForEachAccount(Context context) { 868 mContext = context; 869 } 870 871 @Override 872 protected final Long[] doInBackground(Void... params) { 873 ArrayList<Long> ids = new ArrayList<Long>(); 874 Cursor c = mContext.getContentResolver().query(EmailContent.Account.CONTENT_URI, 875 EmailContent.Account.ID_PROJECTION, null, null, null); 876 try { 877 while (c.moveToNext()) { 878 ids.add(c.getLong(EmailContent.Account.ID_PROJECTION_COLUMN)); 879 } 880 } finally { 881 c.close(); 882 } 883 return ids.toArray(EMPTY_LONGS); 884 } 885 886 @Override 887 protected final void onPostExecute(Long[] ids) { 888 if (ids != null && !isCancelled()) { 889 for (long id : ids) { 890 performAction(id); 891 } 892 } 893 onFinished(); 894 } 895 896 /** 897 * This method will be called for each account. 898 */ 899 protected abstract void performAction(long accountId); 900 901 /** 902 * Called when the iteration is finished. 903 */ 904 protected void onFinished() { 905 } 906 } 907} 908