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.emailcommon.utility; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.os.AsyncTask; 26 import android.os.Environment; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.StrictMode; 30 import android.text.TextUtils; 31 import android.widget.TextView; 32 import android.widget.Toast; 33 34 import com.android.emailcommon.provider.Account; 35 import com.android.emailcommon.provider.EmailContent; 36 import com.android.emailcommon.provider.EmailContent.AccountColumns; 37 import com.android.emailcommon.provider.EmailContent.Attachment; 38 import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 39 import com.android.emailcommon.provider.EmailContent.HostAuthColumns; 40 import com.android.emailcommon.provider.EmailContent.Message; 41 import com.android.emailcommon.provider.HostAuth; 42 import com.android.emailcommon.provider.ProviderUnavailableException; 43 import com.android.mail.utils.LogUtils; 44 import com.google.common.annotations.VisibleForTesting; 45 46 import java.io.ByteArrayInputStream; 47 import java.io.File; 48 import java.io.FileNotFoundException; 49 import java.io.IOException; 50 import java.io.InputStream; 51 import java.lang.ThreadLocal; 52 import java.net.URI; 53 import java.net.URISyntaxException; 54 import java.nio.ByteBuffer; 55 import java.nio.CharBuffer; 56 import java.nio.charset.Charset; 57 import java.security.MessageDigest; 58 import java.security.NoSuchAlgorithmException; 59 import java.text.ParseException; 60 import java.text.SimpleDateFormat; 61 import java.util.Date; 62 import java.util.GregorianCalendar; 63 import java.util.TimeZone; 64 import java.util.regex.Pattern; 65 66 public class Utility { 67 public static final Charset UTF_8 = Charset.forName("UTF-8"); 68 public static final Charset ASCII = Charset.forName("US-ASCII"); 69 70 public static final String[] EMPTY_STRINGS = new String[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 private static Handler sMainThreadHandler; 77 78 /** 79 * @return a {@link Handler} tied to the main thread. 80 */ 81 public static Handler getMainThreadHandler() { 82 if (sMainThreadHandler == null) { 83 // No need to synchronize -- it's okay to create an extra Handler, which will be used 84 // only once and then thrown away. 85 sMainThreadHandler = new Handler(Looper.getMainLooper()); 86 } 87 return sMainThreadHandler; 88 } 89 90 public static boolean arrayContains(Object[] a, Object o) { 91 int index = arrayIndex(a, o); 92 return (index >= 0); 93 } 94 95 public static int arrayIndex(Object[] a, Object o) { 96 for (int i = 0, count = a.length; i < count; i++) { 97 if (a[i].equals(o)) { 98 return i; 99 } 100 } 101 return -1; 102 } 103 104 /** 105 * Returns a concatenated string containing the output of every Object's 106 * toString() method, each separated by the given separator character. 107 */ 108 public static String combine(Object[] parts, char separator) { 109 if (parts == null) { 110 return null; 111 } 112 StringBuilder sb = new StringBuilder(); 113 for (int i = 0; i < parts.length; i++) { 114 sb.append(parts[i].toString()); 115 if (i < parts.length - 1) { 116 sb.append(separator); 117 } 118 } 119 return sb.toString(); 120 } 121 122 public static boolean isPortFieldValid(TextView view) { 123 CharSequence chars = view.getText(); 124 if (TextUtils.isEmpty(chars)) return false; 125 Integer port; 126 // In theory, we can't get an illegal value here, since the field is monitored for valid 127 // numeric input. But this might be used elsewhere without such a check. 128 try { 129 port = Integer.parseInt(chars.toString()); 130 } catch (NumberFormatException e) { 131 return false; 132 } 133 return port > 0 && port < 65536; 134 } 135 136 /** 137 * Validate a hostname name field. 138 * 139 * Because we just use the {@link URI} class for validation, it'll accept some invalid 140 * host names, but it works well enough... 141 */ 142 public static boolean isServerNameValid(TextView view) { 143 return isServerNameValid(view.getText().toString()); 144 } 145 146 public static boolean isServerNameValid(String serverName) { 147 serverName = serverName.trim(); 148 if (TextUtils.isEmpty(serverName)) { 149 return false; 150 } 151 try { 152 new URI( 153 "http", 154 null, 155 serverName, 156 -1, 157 null, // path 158 null, // query 159 null); 160 return true; 161 } catch (URISyntaxException e) { 162 return false; 163 } 164 } 165 166 private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?" 167 + " and " + HostAuthColumns.LOGIN + " like ? ESCAPE '\\'" 168 + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\""; 169 private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?"; 170 171 /** 172 * Look for an existing account with the same username & server 173 * 174 * @param context a system context 175 * @param allowAccountId this account Id will not trigger (when editing an existing account) 176 * @param hostName the server's address 177 * @param userLogin the user's login string 178 * @return null = no matching account found. Account = matching account 179 */ 180 public static Account findExistingAccount(Context context, long allowAccountId, 181 String hostName, String userLogin) { 182 ContentResolver resolver = context.getContentResolver(); 183 String userName = userLogin.replace("_", "\\_"); 184 Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION, 185 HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userName }, null); 186 if (c == null) throw new ProviderUnavailableException(); 187 try { 188 while (c.moveToNext()) { 189 long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN); 190 // Find account with matching hostauthrecv key, and return it 191 Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, 192 ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null); 193 try { 194 while (c2.moveToNext()) { 195 long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN); 196 if (accountId != allowAccountId) { 197 Account account = Account.restoreAccountWithId(context, accountId); 198 if (account != null) { 199 return account; 200 } 201 } 202 } 203 } finally { 204 c2.close(); 205 } 206 } 207 } finally { 208 c.close(); 209 } 210 211 return null; 212 } 213 214 private static class ThreadLocalDateFormat extends ThreadLocal<SimpleDateFormat> { 215 private final String mFormatStr; 216 217 public ThreadLocalDateFormat(String formatStr) { 218 mFormatStr = formatStr; 219 } 220 221 @Override 222 protected SimpleDateFormat initialValue() { 223 final SimpleDateFormat format = new SimpleDateFormat(mFormatStr); 224 final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); 225 format.setCalendar(cal); 226 return format; 227 } 228 229 public Date parse(String date) throws ParseException { 230 return super.get().parse(date); 231 } 232 } 233 234 /** 235 * Generate a time in milliseconds from a date string that represents a date/time in GMT 236 * @param date string in format 20090211T180303Z (rfc2445, iCalendar). 237 * @return the time in milliseconds (since Jan 1, 1970) 238 */ 239 public static long parseDateTimeToMillis(String date) throws ParseException { 240 return parseDateTimeToCalendar(date).getTimeInMillis(); 241 } 242 243 private static final ThreadLocalDateFormat mFullDateTimeFormat = 244 new ThreadLocalDateFormat("yyyyMMdd'T'HHmmss'Z'"); 245 246 private static final ThreadLocalDateFormat mAbbrevDateTimeFormat = 247 new ThreadLocalDateFormat("yyyyMMdd"); 248 249 /** 250 * Generate a GregorianCalendar from a date string that represents a date/time in GMT 251 * @param date string in format 20090211T180303Z (rfc2445, iCalendar), or 252 * in abbreviated format 20090211. 253 * @return the GregorianCalendar 254 */ 255 @VisibleForTesting 256 public static GregorianCalendar parseDateTimeToCalendar(String date) throws ParseException { 257 final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); 258 if (date.length() <= 8) { 259 cal.setTime(mAbbrevDateTimeFormat.parse(date)); 260 } else { 261 cal.setTime(mFullDateTimeFormat.parse(date)); 262 } 263 return cal; 264 } 265 266 private static final ThreadLocalDateFormat mAbbrevEmailDateTimeFormat = 267 new ThreadLocalDateFormat("yyyy-MM-dd"); 268 269 private static final ThreadLocalDateFormat mEmailDateTimeFormat = 270 new ThreadLocalDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 271 272 private static final ThreadLocalDateFormat mEmailDateTimeFormatWithMillis = 273 new ThreadLocalDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); 274 275 /** 276 * Generate a time in milliseconds from an email date string that represents a date/time in GMT 277 * @param date string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339) 278 * @return the time in milliseconds (since Jan 1, 1970) 279 */ 280 @VisibleForTesting 281 public static long parseEmailDateTimeToMillis(String date) throws ParseException { 282 final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); 283 if (date.length() <= 10) { 284 cal.setTime(mAbbrevEmailDateTimeFormat.parse(date)); 285 } else if (date.length() <= 20) { 286 cal.setTime(mEmailDateTimeFormat.parse(date)); 287 } else { 288 cal.setTime(mEmailDateTimeFormatWithMillis.parse(date)); 289 } 290 return cal.getTimeInMillis(); 291 } 292 293 private static byte[] encode(Charset charset, String s) { 294 if (s == null) { 295 return null; 296 } 297 final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s)); 298 final byte[] bytes = new byte[buffer.limit()]; 299 buffer.get(bytes); 300 return bytes; 301 } 302 303 private static String decode(Charset charset, byte[] b) { 304 if (b == null) { 305 return null; 306 } 307 final CharBuffer cb = charset.decode(ByteBuffer.wrap(b)); 308 return new String(cb.array(), 0, cb.length()); 309 } 310 311 /** Converts a String to UTF-8 */ 312 public static byte[] toUtf8(String s) { 313 return encode(UTF_8, s); 314 } 315 316 /** Builds a String from UTF-8 bytes */ 317 public static String fromUtf8(byte[] b) { 318 return decode(UTF_8, b); 319 } 320 321 /** Converts a String to ASCII bytes */ 322 public static byte[] toAscii(String s) { 323 return encode(ASCII, s); 324 } 325 326 /** Builds a String from ASCII bytes */ 327 public static String fromAscii(byte[] b) { 328 return decode(ASCII, b); 329 } 330 331 /** 332 * @return true if the input is the first (or only) byte in a UTF-8 character 333 */ 334 public static boolean isFirstUtf8Byte(byte b) { 335 // If the top 2 bits is '10', it's not a first byte. 336 return (b & 0xc0) != 0x80; 337 } 338 339 public static String byteToHex(int b) { 340 return byteToHex(new StringBuilder(), b).toString(); 341 } 342 343 public static StringBuilder byteToHex(StringBuilder sb, int b) { 344 b &= 0xFF; 345 sb.append("0123456789ABCDEF".charAt(b >> 4)); 346 sb.append("0123456789ABCDEF".charAt(b & 0xF)); 347 return sb; 348 } 349 350 public static String replaceBareLfWithCrlf(String str) { 351 return str.replace("\r", "").replace("\n", "\r\n"); 352 } 353 354 /** 355 * Cancel an {@link AsyncTask}. If it's already running, it'll be interrupted. 356 */ 357 public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) { 358 cancelTask(task, true); 359 } 360 361 /** 362 * Cancel an {@link AsyncTask}. 363 * 364 * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this 365 * task should be interrupted; otherwise, in-progress tasks are allowed 366 * to complete. 367 */ 368 public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) { 369 if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) { 370 task.cancel(mayInterruptIfRunning); 371 } 372 } 373 374 public static String getSmallHash(final String value) { 375 final MessageDigest sha; 376 try { 377 sha = MessageDigest.getInstance("SHA-1"); 378 } catch (NoSuchAlgorithmException impossible) { 379 return null; 380 } 381 sha.update(Utility.toUtf8(value)); 382 final int hash = getSmallHashFromSha1(sha.digest()); 383 return Integer.toString(hash); 384 } 385 386 /** 387 * @return a non-negative integer generated from 20 byte SHA-1 hash. 388 */ 389 /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) { 390 final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes. 391 return ((sha1[offset] & 0x7f) << 24) 392 | ((sha1[offset + 1] & 0xff) << 16) 393 | ((sha1[offset + 2] & 0xff) << 8) 394 | ((sha1[offset + 3] & 0xff)); 395 } 396 397 /** 398 * Try to make a date MIME(RFC 2822/5322)-compliant. 399 * 400 * It fixes: 401 * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700" 402 * (4 digit zone value can't be preceded by "GMT") 403 * We got a report saying eBay sends a date in this format 404 */ 405 public static String cleanUpMimeDate(String date) { 406 if (TextUtils.isEmpty(date)) { 407 return date; 408 } 409 date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1"); 410 return date; 411 } 412 413 public static ByteArrayInputStream streamFromAsciiString(String ascii) { 414 return new ByteArrayInputStream(toAscii(ascii)); 415 } 416 417 /** 418 * A thread safe way to show a Toast. Can be called from any thread. 419 * 420 * @param context context 421 * @param resId Resource ID of the message string. 422 */ 423 public static void showToast(Context context, int resId) { 424 showToast(context, context.getResources().getString(resId)); 425 } 426 427 /** 428 * A thread safe way to show a Toast. Can be called from any thread. 429 * 430 * @param context context 431 * @param message Message to show. 432 */ 433 public static void showToast(final Context context, final String message) { 434 getMainThreadHandler().post(new Runnable() { 435 @Override 436 public void run() { 437 Toast.makeText(context, message, Toast.LENGTH_LONG).show(); 438 } 439 }); 440 } 441 442 /** 443 * Run {@code r} on a worker thread, returning the AsyncTask 444 * @return the AsyncTask; this is primarily for use by unit tests, which require the 445 * result of the task 446 * 447 * @deprecated use {@link EmailAsyncTask#runAsyncParallel} or 448 * {@link EmailAsyncTask#runAsyncSerial} 449 */ 450 @Deprecated 451 public static AsyncTask<Void, Void, Void> runAsync(final Runnable r) { 452 return new AsyncTask<Void, Void, Void>() { 453 @Override protected Void doInBackground(Void... params) { 454 r.run(); 455 return null; 456 } 457 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 458 } 459 460 /** 461 * Interface used in {@link #createUniqueFile} instead of {@link File#createNewFile()} to make 462 * it testable. 463 */ 464 /* package */ interface NewFileCreator { 465 public static final NewFileCreator DEFAULT = new NewFileCreator() { 466 @Override public boolean createNewFile(File f) throws IOException { 467 return f.createNewFile(); 468 } 469 }; 470 public boolean createNewFile(File f) throws IOException ; 471 } 472 473 /** 474 * Creates a new empty file with a unique name in the given directory by appending a hyphen and 475 * a number to the given filename. 476 * 477 * @return a new File object, or null if one could not be created 478 */ 479 public static File createUniqueFile(File directory, String filename) throws IOException { 480 return createUniqueFileInternal(NewFileCreator.DEFAULT, directory, filename); 481 } 482 483 /* package */ static File createUniqueFileInternal(final NewFileCreator nfc, 484 final File directory, final String filename) throws IOException { 485 final File file = new File(directory, filename); 486 if (nfc.createNewFile(file)) { 487 return file; 488 } 489 // Get the extension of the file, if any. 490 final int index = filename.lastIndexOf('.'); 491 final String name; 492 final String extension; 493 if (index != -1) { 494 name = filename.substring(0, index); 495 extension = filename.substring(index); 496 } else { 497 name = filename; 498 extension = ""; 499 } 500 501 for (int i = 2; i < Integer.MAX_VALUE; i++) { 502 final File numberedFile = 503 new File(directory, name + "-" + Integer.toString(i) + extension); 504 if (nfc.createNewFile(numberedFile)) { 505 return numberedFile; 506 } 507 } 508 return null; 509 } 510 511 public interface CursorGetter<T> { 512 T get(Cursor cursor, int column); 513 } 514 515 private static final CursorGetter<Long> LONG_GETTER = new CursorGetter<Long>() { 516 @Override 517 public Long get(Cursor cursor, int column) { 518 return cursor.getLong(column); 519 } 520 }; 521 522 private static final CursorGetter<Integer> INT_GETTER = new CursorGetter<Integer>() { 523 @Override 524 public Integer get(Cursor cursor, int column) { 525 return cursor.getInt(column); 526 } 527 }; 528 529 private static final CursorGetter<String> STRING_GETTER = new CursorGetter<String>() { 530 @Override 531 public String get(Cursor cursor, int column) { 532 return cursor.getString(column); 533 } 534 }; 535 536 private static final CursorGetter<byte[]> BLOB_GETTER = new CursorGetter<byte[]>() { 537 @Override 538 public byte[] get(Cursor cursor, int column) { 539 return cursor.getBlob(column); 540 } 541 }; 542 543 /** 544 * @return if {@code original} is to the EmailProvider, add "?limit=1". Otherwise just returns 545 * {@code original}. 546 * 547 * Other providers don't support the limit param. Also, changing URI passed from other apps 548 * can cause permission errors. 549 */ 550 /* package */ static Uri buildLimitOneUri(Uri original) { 551 if ("content".equals(original.getScheme()) && 552 EmailContent.AUTHORITY.equals(original.getAuthority())) { 553 return EmailContent.uriWithLimit(original, 1); 554 } 555 return original; 556 } 557 558 /** 559 * @return a generic in column {@code column} of the first result row, if the query returns at 560 * least 1 row. Otherwise returns {@code defaultValue}. 561 */ 562 public static <T> T getFirstRowColumn(Context context, Uri uri, 563 String[] projection, String selection, String[] selectionArgs, String sortOrder, 564 int column, T defaultValue, CursorGetter<T> getter) { 565 // Use PARAMETER_LIMIT to restrict the query to the single row we need 566 uri = buildLimitOneUri(uri); 567 Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs, 568 sortOrder); 569 if (c != null) { 570 try { 571 if (c.moveToFirst()) { 572 return getter.get(c, column); 573 } 574 } finally { 575 c.close(); 576 } 577 } 578 return defaultValue; 579 } 580 581 /** 582 * {@link #getFirstRowColumn} for a Long with null as a default value. 583 */ 584 public static Long getFirstRowLong(Context context, Uri uri, String[] projection, 585 String selection, String[] selectionArgs, String sortOrder, int column) { 586 return getFirstRowColumn(context, uri, projection, selection, selectionArgs, 587 sortOrder, column, null, LONG_GETTER); 588 } 589 590 /** 591 * {@link #getFirstRowColumn} for a Long with a provided default value. 592 */ 593 public static Long getFirstRowLong(Context context, Uri uri, String[] projection, 594 String selection, String[] selectionArgs, String sortOrder, int column, 595 Long defaultValue) { 596 return getFirstRowColumn(context, uri, projection, selection, selectionArgs, 597 sortOrder, column, defaultValue, LONG_GETTER); 598 } 599 600 /** 601 * {@link #getFirstRowColumn} for an Integer with null as a default value. 602 */ 603 public static Integer getFirstRowInt(Context context, Uri uri, String[] projection, 604 String selection, String[] selectionArgs, String sortOrder, int column) { 605 return getFirstRowColumn(context, uri, projection, selection, selectionArgs, 606 sortOrder, column, null, INT_GETTER); 607 } 608 609 /** 610 * {@link #getFirstRowColumn} for an Integer with a provided default value. 611 */ 612 public static Integer getFirstRowInt(Context context, Uri uri, String[] projection, 613 String selection, String[] selectionArgs, String sortOrder, int column, 614 Integer defaultValue) { 615 return getFirstRowColumn(context, uri, projection, selection, selectionArgs, 616 sortOrder, column, defaultValue, INT_GETTER); 617 } 618 619 /** 620 * {@link #getFirstRowColumn} for a String with null as a default value. 621 */ 622 public static String getFirstRowString(Context context, Uri uri, String[] projection, 623 String selection, String[] selectionArgs, String sortOrder, int column) { 624 return getFirstRowString(context, uri, projection, selection, selectionArgs, sortOrder, 625 column, null); 626 } 627 628 /** 629 * {@link #getFirstRowColumn} for a String with a provided default value. 630 */ 631 public static String getFirstRowString(Context context, Uri uri, String[] projection, 632 String selection, String[] selectionArgs, String sortOrder, int column, 633 String defaultValue) { 634 return getFirstRowColumn(context, uri, projection, selection, selectionArgs, 635 sortOrder, column, defaultValue, STRING_GETTER); 636 } 637 638 /** 639 * {@link #getFirstRowColumn} for a byte array with a provided default value. 640 */ 641 public static byte[] getFirstRowBlob(Context context, Uri uri, String[] projection, 642 String selection, String[] selectionArgs, String sortOrder, int column, 643 byte[] defaultValue) { 644 return getFirstRowColumn(context, uri, projection, selection, selectionArgs, sortOrder, 645 column, defaultValue, BLOB_GETTER); 646 } 647 648 public static boolean attachmentExists(Context context, Attachment attachment) { 649 if (attachment == null) { 650 return false; 651 } else if (attachment.mContentBytes != null) { 652 return true; 653 } else { 654 final String cachedFile = attachment.getCachedFileUri(); 655 // Try the cached file first 656 if (!TextUtils.isEmpty(cachedFile)) { 657 final Uri cachedFileUri = Uri.parse(cachedFile); 658 try { 659 final InputStream inStream = 660 context.getContentResolver().openInputStream(cachedFileUri); 661 try { 662 inStream.close(); 663 } catch (IOException e) { 664 // Nothing to be done if can't close the stream 665 } 666 return true; 667 } catch (FileNotFoundException e) { 668 // We weren't able to open the file, try the content uri below 669 LogUtils.e(LogUtils.TAG, e, "not able to open cached file"); 670 } 671 } 672 final String contentUri = attachment.getContentUri(); 673 if (TextUtils.isEmpty(contentUri)) { 674 return false; 675 } 676 try { 677 final Uri fileUri = Uri.parse(contentUri); 678 try { 679 final InputStream inStream = 680 context.getContentResolver().openInputStream(fileUri); 681 try { 682 inStream.close(); 683 } catch (IOException e) { 684 // Nothing to be done if can't close the stream 685 } 686 return true; 687 } catch (FileNotFoundException e) { 688 return false; 689 } 690 } catch (RuntimeException re) { 691 LogUtils.w(LogUtils.TAG, re, "attachmentExists RuntimeException"); 692 return false; 693 } 694 } 695 } 696 697 /** 698 * Check whether the message with a given id has unloaded attachments. If the message is 699 * a forwarded message, we look instead at the messages's source for the attachments. If the 700 * message or forward source can't be found, we return false 701 * @param context the caller's context 702 * @param messageId the id of the message 703 * @return whether or not the message has unloaded attachments 704 */ 705 public static boolean hasUnloadedAttachments(Context context, long messageId) { 706 Message msg = Message.restoreMessageWithId(context, messageId); 707 if (msg == null) return false; 708 Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); 709 for (Attachment att: atts) { 710 if (!attachmentExists(context, att)) { 711 // If the attachment doesn't exist and isn't marked for download, we're in trouble 712 // since the outbound message will be stuck indefinitely in the Outbox. Instead, 713 // we'll just delete the attachment and continue; this is far better than the 714 // alternative. In theory, this situation shouldn't be possible. 715 if ((att.mFlags & (Attachment.FLAG_DOWNLOAD_FORWARD | 716 Attachment.FLAG_DOWNLOAD_USER_REQUEST)) == 0) { 717 LogUtils.d(LogUtils.TAG, "Unloaded attachment isn't marked for download: %s" + 718 ", #%d", att.mFileName, att.mId); 719 Account acct = Account.restoreAccountWithId(context, msg.mAccountKey); 720 if (acct == null) return true; 721 // If smart forward is set and the message is a forward, we'll act as though 722 // the attachment has been loaded 723 // In Email1 this test wasn't necessary, as the UI handled it... 724 if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) { 725 if ((acct.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) != 0) { 726 continue; 727 } 728 } 729 Attachment.delete(context, Attachment.CONTENT_URI, att.mId); 730 } else if (att.getContentUri() != null) { 731 // In this case, the attachment file is gone from the cache; let's clear the 732 // contentUri; this should be a very unusual case 733 ContentValues cv = new ContentValues(); 734 cv.putNull(AttachmentColumns.CONTENT_URI); 735 Attachment.update(context, Attachment.CONTENT_URI, att.mId, cv); 736 } 737 return true; 738 } 739 } 740 return false; 741 } 742 743 /** 744 * Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider. 745 * The arguments are exactly the same as to contentResolver.query(). Results are returned in 746 * an array of Strings corresponding to the columns in the projection. If the cursor has no 747 * rows, null is returned. 748 */ 749 public static String[] getRowColumns(Context context, Uri contentUri, String[] projection, 750 String selection, String[] selectionArgs) { 751 String[] values = new String[projection.length]; 752 ContentResolver cr = context.getContentResolver(); 753 Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null); 754 try { 755 if (c.moveToFirst()) { 756 for (int i = 0; i < projection.length; i++) { 757 values[i] = c.getString(i); 758 } 759 } else { 760 return null; 761 } 762 } finally { 763 c.close(); 764 } 765 return values; 766 } 767 768 /** 769 * Convenience method for retrieving columns from a particular row in EmailProvider. 770 * Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and 771 * a projection. This method calls the previous one with the appropriate URI. 772 */ 773 public static String[] getRowColumns(Context context, Uri baseUri, long id, 774 String ... projection) { 775 return getRowColumns(context, ContentUris.withAppendedId(baseUri, id), projection, null, 776 null); 777 } 778 779 public static boolean isExternalStorageMounted() { 780 return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); 781 } 782 783 public static void enableStrictMode(boolean enabled) { 784 StrictMode.setThreadPolicy(enabled 785 ? new StrictMode.ThreadPolicy.Builder().detectAll().build() 786 : StrictMode.ThreadPolicy.LAX); 787 StrictMode.setVmPolicy(enabled 788 ? new StrictMode.VmPolicy.Builder().detectAll().build() 789 : StrictMode.VmPolicy.LAX); 790 } 791 } 792