Home | History | Annotate | Download | only in utility
      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