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      */
getMainThreadHandler()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 
arrayContains(Object[] a, Object o)90     public static boolean arrayContains(Object[] a, Object o) {
91         int index = arrayIndex(a, o);
92         return (index >= 0);
93     }
94 
arrayIndex(Object[] a, Object o)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      */
combine(Object[] parts, char separator)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 
isPortFieldValid(TextView view)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      */
isServerNameValid(TextView view)142     public static boolean isServerNameValid(TextView view) {
143         return isServerNameValid(view.getText().toString());
144     }
145 
isServerNameValid(String serverName)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      */
findExistingAccount(Context context, long allowAccountId, String hostName, String userLogin)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 
ThreadLocalDateFormat(String formatStr)217         public ThreadLocalDateFormat(String formatStr) {
218             mFormatStr = formatStr;
219         }
220 
221         @Override
initialValue()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 
parse(String date)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      */
parseDateTimeToMillis(String date)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
parseDateTimeToCalendar(String date)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
parseEmailDateTimeToMillis(String date)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 
encode(Charset charset, String s)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 
decode(Charset charset, byte[] b)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 */
toUtf8(String s)312     public static byte[] toUtf8(String s) {
313         return encode(UTF_8, s);
314     }
315 
316     /** Builds a String from UTF-8 bytes */
fromUtf8(byte[] b)317     public static String fromUtf8(byte[] b) {
318         return decode(UTF_8, b);
319     }
320 
321     /** Converts a String to ASCII bytes */
toAscii(String s)322     public static byte[] toAscii(String s) {
323         return encode(ASCII, s);
324     }
325 
326     /** Builds a String from ASCII bytes */
fromAscii(byte[] b)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      */
isFirstUtf8Byte(byte b)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 
byteToHex(int b)339     public static String byteToHex(int b) {
340         return byteToHex(new StringBuilder(), b).toString();
341     }
342 
byteToHex(StringBuilder sb, int b)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 
replaceBareLfWithCrlf(String str)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      */
cancelTaskInterrupt(AsyncTask<?, ?, ?> task)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      */
cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning)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 
getSmallHash(final String value)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      */
getSmallHashFromSha1(byte[] sha1)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      */
cleanUpMimeDate(String date)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 
streamFromAsciiString(String ascii)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      */
showToast(Context context, int resId)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      */
showToast(final Context context, final String message)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
runAsync(final Runnable r)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