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.app.Activity;
     20 import android.app.Fragment;
     21 import android.content.ContentResolver;
     22 import android.content.ContentUris;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.database.Cursor;
     26 import android.database.CursorWrapper;
     27 import android.graphics.Typeface;
     28 import android.net.Uri;
     29 import android.os.AsyncTask;
     30 import android.os.Environment;
     31 import android.os.Handler;
     32 import android.os.Looper;
     33 import android.os.StrictMode;
     34 import android.provider.OpenableColumns;
     35 import android.text.Spannable;
     36 import android.text.SpannableString;
     37 import android.text.SpannableStringBuilder;
     38 import android.text.TextUtils;
     39 import android.text.style.StyleSpan;
     40 import android.util.Base64;
     41 import android.widget.ListView;
     42 import android.widget.TextView;
     43 import android.widget.Toast;
     44 
     45 import com.android.emailcommon.Logging;
     46 import com.android.emailcommon.provider.Account;
     47 import com.android.emailcommon.provider.EmailContent;
     48 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     49 import com.android.emailcommon.provider.EmailContent.Attachment;
     50 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     51 import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
     52 import com.android.emailcommon.provider.EmailContent.Message;
     53 import com.android.emailcommon.provider.HostAuth;
     54 import com.android.emailcommon.provider.ProviderUnavailableException;
     55 import com.android.mail.utils.LogUtils;
     56 
     57 import java.io.ByteArrayInputStream;
     58 import java.io.File;
     59 import java.io.FileDescriptor;
     60 import java.io.FileNotFoundException;
     61 import java.io.IOException;
     62 import java.io.InputStream;
     63 import java.io.InputStreamReader;
     64 import java.io.PrintWriter;
     65 import java.io.StringWriter;
     66 import java.io.UnsupportedEncodingException;
     67 import java.net.URI;
     68 import java.net.URISyntaxException;
     69 import java.nio.ByteBuffer;
     70 import java.nio.CharBuffer;
     71 import java.nio.charset.Charset;
     72 import java.security.MessageDigest;
     73 import java.security.NoSuchAlgorithmException;
     74 import java.util.ArrayList;
     75 import java.util.Collection;
     76 import java.util.GregorianCalendar;
     77 import java.util.HashSet;
     78 import java.util.Set;
     79 import java.util.TimeZone;
     80 import java.util.regex.Pattern;
     81 
     82 public class Utility {
     83     public static final Charset UTF_8 = Charset.forName("UTF-8");
     84     public static final Charset ASCII = Charset.forName("US-ASCII");
     85 
     86     public static final String[] EMPTY_STRINGS = new String[0];
     87     public static final Long[] EMPTY_LONGS = new Long[0];
     88 
     89     // "GMT" + "+" or "-" + 4 digits
     90     private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
     91             Pattern.compile("GMT([-+]\\d{4})$");
     92 
     93     private static Handler sMainThreadHandler;
     94 
     95     /**
     96      * @return a {@link Handler} tied to the main thread.
     97      */
     98     public static Handler getMainThreadHandler() {
     99         if (sMainThreadHandler == null) {
    100             // No need to synchronize -- it's okay to create an extra Handler, which will be used
    101             // only once and then thrown away.
    102             sMainThreadHandler = new Handler(Looper.getMainLooper());
    103         }
    104         return sMainThreadHandler;
    105     }
    106 
    107     public final static String readInputStream(InputStream in, String encoding) throws IOException {
    108         InputStreamReader reader = new InputStreamReader(in, encoding);
    109         StringBuffer sb = new StringBuffer();
    110         int count;
    111         char[] buf = new char[512];
    112         while ((count = reader.read(buf)) != -1) {
    113             sb.append(buf, 0, count);
    114         }
    115         return sb.toString();
    116     }
    117 
    118     public final static boolean arrayContains(Object[] a, Object o) {
    119         int index = arrayIndex(a, o);
    120         return (index >= 0);
    121     }
    122 
    123     public final static int arrayIndex(Object[] a, Object o) {
    124         for (int i = 0, count = a.length; i < count; i++) {
    125             if (a[i].equals(o)) {
    126                 return i;
    127             }
    128         }
    129         return -1;
    130     }
    131 
    132     /**
    133      * Returns a concatenated string containing the output of every Object's
    134      * toString() method, each separated by the given separator character.
    135      */
    136     public static String combine(Object[] parts, char separator) {
    137         if (parts == null) {
    138             return null;
    139         }
    140         StringBuffer sb = new StringBuffer();
    141         for (int i = 0; i < parts.length; i++) {
    142             sb.append(parts[i].toString());
    143             if (i < parts.length - 1) {
    144                 sb.append(separator);
    145             }
    146         }
    147         return sb.toString();
    148     }
    149 
    150     public static boolean isPortFieldValid(TextView view) {
    151         CharSequence chars = view.getText();
    152         if (TextUtils.isEmpty(chars)) return false;
    153         Integer port;
    154         // In theory, we can't get an illegal value here, since the field is monitored for valid
    155         // numeric input. But this might be used elsewhere without such a check.
    156         try {
    157             port = Integer.parseInt(chars.toString());
    158         } catch (NumberFormatException e) {
    159             return false;
    160         }
    161         return port > 0 && port < 65536;
    162     }
    163 
    164     /**
    165      * Validate a hostname name field.
    166      *
    167      * Because we just use the {@link URI} class for validation, it'll accept some invalid
    168      * host names, but it works well enough...
    169      */
    170     public static boolean isServerNameValid(TextView view) {
    171         return isServerNameValid(view.getText().toString());
    172     }
    173 
    174     public static boolean isServerNameValid(String serverName) {
    175         serverName = serverName.trim();
    176         if (TextUtils.isEmpty(serverName)) {
    177             return false;
    178         }
    179         try {
    180             URI uri = new URI(
    181                     "http",
    182                     null,
    183                     serverName,
    184                     -1,
    185                     null, // path
    186                     null, // query
    187                     null);
    188             return true;
    189         } catch (URISyntaxException e) {
    190             return false;
    191         }
    192     }
    193 
    194     /**
    195      * A fast version of  URLDecoder.decode() that works only with UTF-8 and does only two
    196      * allocations. This version is around 3x as fast as the standard one and I'm using it
    197      * hundreds of times in places that slow down the UI, so it helps.
    198      */
    199     public static String fastUrlDecode(String s) {
    200         try {
    201             byte[] bytes = s.getBytes("UTF-8");
    202             byte ch;
    203             int length = 0;
    204             for (int i = 0, count = bytes.length; i < count; i++) {
    205                 ch = bytes[i];
    206                 if (ch == '%') {
    207                     int h = (bytes[i + 1] - '0');
    208                     int l = (bytes[i + 2] - '0');
    209                     if (h > 9) {
    210                         h -= 7;
    211                     }
    212                     if (l > 9) {
    213                         l -= 7;
    214                     }
    215                     bytes[length] = (byte) ((h << 4) | l);
    216                     i += 2;
    217                 }
    218                 else if (ch == '+') {
    219                     bytes[length] = ' ';
    220                 }
    221                 else {
    222                     bytes[length] = bytes[i];
    223                 }
    224                 length++;
    225             }
    226             return new String(bytes, 0, length, "UTF-8");
    227         }
    228         catch (UnsupportedEncodingException uee) {
    229             return null;
    230         }
    231     }
    232     private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?"
    233             + " and " + HostAuthColumns.LOGIN + " like ?  ESCAPE '\\'"
    234             + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\"";
    235     private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?";
    236 
    237     /**
    238      * Look for an existing account with the same username & server
    239      *
    240      * @param context a system context
    241      * @param allowAccountId this account Id will not trigger (when editing an existing account)
    242      * @param hostName the server's address
    243      * @param userLogin the user's login string
    244      * @result null = no matching account found.  Account = matching account
    245      */
    246     public static Account findExistingAccount(Context context, long allowAccountId,
    247             String hostName, String userLogin) {
    248         ContentResolver resolver = context.getContentResolver();
    249         String userName = userLogin.replace("_", "\\_");
    250         Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION,
    251                 HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userName }, null);
    252         if (c == null) throw new ProviderUnavailableException();
    253         try {
    254             while (c.moveToNext()) {
    255                 long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN);
    256                 // Find account with matching hostauthrecv key, and return it
    257                 Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
    258                         ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null);
    259                 try {
    260                     while (c2.moveToNext()) {
    261                         long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN);
    262                         if (accountId != allowAccountId) {
    263                             Account account = Account.restoreAccountWithId(context, accountId);
    264                             if (account != null) {
    265                                 return account;
    266                             }
    267                         }
    268                     }
    269                 } finally {
    270                     c2.close();
    271                 }
    272             }
    273         } finally {
    274             c.close();
    275         }
    276 
    277         return null;
    278     }
    279 
    280     /**
    281      * This only actually matches against the email address. It's technically kosher to allow the
    282      * same address across different account types, but that's a pretty rare use case and isn't well
    283      * handled in the UI.
    284      *
    285      * @param context context
    286      * @param syncAuthority the account manager type to check against or null for all types
    287      * @param address email address to match against
    288      * @return account name for match found or null
    289      */
    290    public static String findExistingAccount(final Context context, final String syncAuthority,
    291            final String address) {
    292        final ContentResolver resolver = context.getContentResolver();
    293        final Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
    294                AccountColumns.EMAIL_ADDRESS + "=?", new String[] {address}, null);
    295        try {
    296            if (!c.moveToFirst()) {
    297                return null;
    298            }
    299            return c.getString(c.getColumnIndex(Account.DISPLAY_NAME));
    300            /*
    301            do {
    302                if (syncAuthority != null) {
    303                    // TODO: actually compare the sync authority to allow creating the same account
    304                    // on different protocols. Sadly this code can't directly access the service info
    305                } else {
    306                    final Account account = new Account();
    307                    account.restore(c);
    308                    return account.mDisplayName;
    309                }
    310            } while (c.moveToNext());
    311            */
    312        } finally {
    313            c.close();
    314        }
    315        /*
    316        return null;
    317        */
    318    }
    319 
    320     /**
    321      * Generate a random message-id header for locally-generated messages.
    322      */
    323     public static String generateMessageId() {
    324         StringBuffer sb = new StringBuffer();
    325         sb.append("<");
    326         for (int i = 0; i < 24; i++) {
    327             sb.append(Integer.toString((int)(Math.random() * 35), 36));
    328         }
    329         sb.append(".");
    330         sb.append(Long.toString(System.currentTimeMillis()));
    331         sb.append("@email.android.com>");
    332         return sb.toString();
    333     }
    334 
    335     /**
    336      * Generate a time in milliseconds from a date string that represents a date/time in GMT
    337      * @param date string in format 20090211T180303Z (rfc2445, iCalendar).
    338      * @return the time in milliseconds (since Jan 1, 1970)
    339      */
    340     public static long parseDateTimeToMillis(String date) {
    341         GregorianCalendar cal = parseDateTimeToCalendar(date);
    342         return cal.getTimeInMillis();
    343     }
    344 
    345     /**
    346      * Generate a GregorianCalendar from a date string that represents a date/time in GMT
    347      * @param date string in format 20090211T180303Z (rfc2445, iCalendar).
    348      * @return the GregorianCalendar
    349      */
    350     public static GregorianCalendar parseDateTimeToCalendar(String date) {
    351         GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
    352                 Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
    353                 Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
    354                 Integer.parseInt(date.substring(13, 15)));
    355         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
    356         return cal;
    357     }
    358 
    359     /**
    360      * Generate a time in milliseconds from an email date string that represents a date/time in GMT
    361      * @param date string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339)
    362      * @return the time in milliseconds (since Jan 1, 1970)
    363      */
    364     public static long parseEmailDateTimeToMillis(String date) {
    365         GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
    366                 Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
    367                 Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)),
    368                 Integer.parseInt(date.substring(17, 19)));
    369         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
    370         return cal.getTimeInMillis();
    371     }
    372 
    373     private static byte[] encode(Charset charset, String s) {
    374         if (s == null) {
    375             return null;
    376         }
    377         final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
    378         final byte[] bytes = new byte[buffer.limit()];
    379         buffer.get(bytes);
    380         return bytes;
    381     }
    382 
    383     private static String decode(Charset charset, byte[] b) {
    384         if (b == null) {
    385             return null;
    386         }
    387         final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
    388         return new String(cb.array(), 0, cb.length());
    389     }
    390 
    391     /** Converts a String to UTF-8 */
    392     public static byte[] toUtf8(String s) {
    393         return encode(UTF_8, s);
    394     }
    395 
    396     /** Builds a String from UTF-8 bytes */
    397     public static String fromUtf8(byte[] b) {
    398         return decode(UTF_8, b);
    399     }
    400 
    401     /** Converts a String to ASCII bytes */
    402     public static byte[] toAscii(String s) {
    403         return encode(ASCII, s);
    404     }
    405 
    406     /** Builds a String from ASCII bytes */
    407     public static String fromAscii(byte[] b) {
    408         return decode(ASCII, b);
    409     }
    410 
    411     /**
    412      * @return true if the input is the first (or only) byte in a UTF-8 character
    413      */
    414     public static boolean isFirstUtf8Byte(byte b) {
    415         // If the top 2 bits is '10', it's not a first byte.
    416         return (b & 0xc0) != 0x80;
    417     }
    418 
    419     public static String byteToHex(int b) {
    420         return byteToHex(new StringBuilder(), b).toString();
    421     }
    422 
    423     public static StringBuilder byteToHex(StringBuilder sb, int b) {
    424         b &= 0xFF;
    425         sb.append("0123456789ABCDEF".charAt(b >> 4));
    426         sb.append("0123456789ABCDEF".charAt(b & 0xF));
    427         return sb;
    428     }
    429 
    430     public static String replaceBareLfWithCrlf(String str) {
    431         return str.replace("\r", "").replace("\n", "\r\n");
    432     }
    433 
    434     /**
    435      * Cancel an {@link AsyncTask}.  If it's already running, it'll be interrupted.
    436      */
    437     public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
    438         cancelTask(task, true);
    439     }
    440 
    441     /**
    442      * Cancel an {@link EmailAsyncTask}.  If it's already running, it'll be interrupted.
    443      */
    444     public static void cancelTaskInterrupt(EmailAsyncTask<?, ?, ?> task) {
    445         if (task != null) {
    446             task.cancel(true);
    447         }
    448     }
    449 
    450     /**
    451      * Cancel an {@link AsyncTask}.
    452      *
    453      * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
    454      *        task should be interrupted; otherwise, in-progress tasks are allowed
    455      *        to complete.
    456      */
    457     public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
    458         if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
    459             task.cancel(mayInterruptIfRunning);
    460         }
    461     }
    462 
    463     public static String getSmallHash(final String value) {
    464         final MessageDigest sha;
    465         try {
    466             sha = MessageDigest.getInstance("SHA-1");
    467         } catch (NoSuchAlgorithmException impossible) {
    468             return null;
    469         }
    470         sha.update(Utility.toUtf8(value));
    471         final int hash = getSmallHashFromSha1(sha.digest());
    472         return Integer.toString(hash);
    473     }
    474 
    475     /**
    476      * @return a non-negative integer generated from 20 byte SHA-1 hash.
    477      */
    478     /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) {
    479         final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes.
    480         return ((sha1[offset]  & 0x7f) << 24)
    481                 | ((sha1[offset + 1] & 0xff) << 16)
    482                 | ((sha1[offset + 2] & 0xff) << 8)
    483                 | ((sha1[offset + 3] & 0xff));
    484     }
    485 
    486     /**
    487      * Try to make a date MIME(RFC 2822/5322)-compliant.
    488      *
    489      * It fixes:
    490      * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
    491      *   (4 digit zone value can't be preceded by "GMT")
    492      *   We got a report saying eBay sends a date in this format
    493      */
    494     public static String cleanUpMimeDate(String date) {
    495         if (TextUtils.isEmpty(date)) {
    496             return date;
    497         }
    498         date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
    499         return date;
    500     }
    501 
    502     public static ByteArrayInputStream streamFromAsciiString(String ascii) {
    503         return new ByteArrayInputStream(toAscii(ascii));
    504     }
    505 
    506     /**
    507      * A thread safe way to show a Toast.  Can be called from any thread.
    508      *
    509      * @param context context
    510      * @param resId Resource ID of the message string.
    511      */
    512     public static void showToast(Context context, int resId) {
    513         showToast(context, context.getResources().getString(resId));
    514     }
    515 
    516     /**
    517      * A thread safe way to show a Toast.  Can be called from any thread.
    518      *
    519      * @param context context
    520      * @param message Message to show.
    521      */
    522     public static void showToast(final Context context, final String message) {
    523         getMainThreadHandler().post(new Runnable() {
    524             @Override
    525             public void run() {
    526                 Toast.makeText(context, message, Toast.LENGTH_LONG).show();
    527             }
    528         });
    529     }
    530 
    531     /**
    532      * Run {@code r} on a worker thread, returning the AsyncTask
    533      * @return the AsyncTask; this is primarily for use by unit tests, which require the
    534      * result of the task
    535      *
    536      * @deprecated use {@link EmailAsyncTask#runAsyncParallel} or
    537      *     {@link EmailAsyncTask#runAsyncSerial}
    538      */
    539     @Deprecated
    540     public static AsyncTask<Void, Void, Void> runAsync(final Runnable r) {
    541         return new AsyncTask<Void, Void, Void>() {
    542             @Override protected Void doInBackground(Void... params) {
    543                 r.run();
    544                 return null;
    545             }
    546         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    547     }
    548 
    549     /**
    550      * Interface used in {@link #createUniqueFile} instead of {@link File#createNewFile()} to make
    551      * it testable.
    552      */
    553     /* package */ interface NewFileCreator {
    554         public static final NewFileCreator DEFAULT = new NewFileCreator() {
    555                     @Override public boolean createNewFile(File f) throws IOException {
    556                         return f.createNewFile();
    557                     }
    558         };
    559         public boolean createNewFile(File f) throws IOException ;
    560     }
    561 
    562     /**
    563      * Creates a new empty file with a unique name in the given directory by appending a hyphen and
    564      * a number to the given filename.
    565      *
    566      * @return a new File object, or null if one could not be created
    567      */
    568     public static File createUniqueFile(File directory, String filename) throws IOException {
    569         return createUniqueFileInternal(NewFileCreator.DEFAULT, directory, filename);
    570     }
    571 
    572     /* package */ static File createUniqueFileInternal(NewFileCreator nfc,
    573             File directory, String filename) throws IOException {
    574         File file = new File(directory, filename);
    575         if (nfc.createNewFile(file)) {
    576             return file;
    577         }
    578         // Get the extension of the file, if any.
    579         int index = filename.lastIndexOf('.');
    580         String format;
    581         if (index != -1) {
    582             String name = filename.substring(0, index);
    583             String extension = filename.substring(index);
    584             format = name + "-%d" + extension;
    585         } else {
    586             format = filename + "-%d";
    587         }
    588 
    589         for (int i = 2; i < Integer.MAX_VALUE; i++) {
    590             file = new File(directory, String.format(format, i));
    591             if (nfc.createNewFile(file)) {
    592                 return file;
    593             }
    594         }
    595         return null;
    596     }
    597 
    598     public interface CursorGetter<T> {
    599         T get(Cursor cursor, int column);
    600     }
    601 
    602     private static final CursorGetter<Long> LONG_GETTER = new CursorGetter<Long>() {
    603         @Override
    604         public Long get(Cursor cursor, int column) {
    605             return cursor.getLong(column);
    606         }
    607     };
    608 
    609     private static final CursorGetter<Integer> INT_GETTER = new CursorGetter<Integer>() {
    610         @Override
    611         public Integer get(Cursor cursor, int column) {
    612             return cursor.getInt(column);
    613         }
    614     };
    615 
    616     private static final CursorGetter<String> STRING_GETTER = new CursorGetter<String>() {
    617         @Override
    618         public String get(Cursor cursor, int column) {
    619             return cursor.getString(column);
    620         }
    621     };
    622 
    623     private static final CursorGetter<byte[]> BLOB_GETTER = new CursorGetter<byte[]>() {
    624         @Override
    625         public byte[] get(Cursor cursor, int column) {
    626             return cursor.getBlob(column);
    627         }
    628     };
    629 
    630     /**
    631      * @return if {@code original} is to the EmailProvider, add "?limit=1".  Otherwise just returns
    632      * {@code original}.
    633      *
    634      * Other providers don't support the limit param.  Also, changing URI passed from other apps
    635      * can cause permission errors.
    636      */
    637     /* package */ static Uri buildLimitOneUri(Uri original) {
    638         if ("content".equals(original.getScheme()) &&
    639                 EmailContent.AUTHORITY.equals(original.getAuthority())) {
    640             return EmailContent.uriWithLimit(original, 1);
    641         }
    642         return original;
    643     }
    644 
    645     /**
    646      * @return a generic in column {@code column} of the first result row, if the query returns at
    647      * least 1 row.  Otherwise returns {@code defaultValue}.
    648      */
    649     public static <T extends Object> T getFirstRowColumn(Context context, Uri uri,
    650             String[] projection, String selection, String[] selectionArgs, String sortOrder,
    651             int column, T defaultValue, CursorGetter<T> getter) {
    652         // Use PARAMETER_LIMIT to restrict the query to the single row we need
    653         uri = buildLimitOneUri(uri);
    654         Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs,
    655                 sortOrder);
    656         if (c != null) {
    657             try {
    658                 if (c.moveToFirst()) {
    659                     return getter.get(c, column);
    660                 }
    661             } finally {
    662                 c.close();
    663             }
    664         }
    665         return defaultValue;
    666     }
    667 
    668     /**
    669      * {@link #getFirstRowColumn} for a Long with null as a default value.
    670      */
    671     public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
    672             String selection, String[] selectionArgs, String sortOrder, int column) {
    673         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    674                 sortOrder, column, null, LONG_GETTER);
    675     }
    676 
    677     /**
    678      * {@link #getFirstRowColumn} for a Long with a provided default value.
    679      */
    680     public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
    681             String selection, String[] selectionArgs, String sortOrder, int column,
    682             Long defaultValue) {
    683         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    684                 sortOrder, column, defaultValue, LONG_GETTER);
    685     }
    686 
    687     /**
    688      * {@link #getFirstRowColumn} for an Integer with null as a default value.
    689      */
    690     public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
    691             String selection, String[] selectionArgs, String sortOrder, int column) {
    692         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    693                 sortOrder, column, null, INT_GETTER);
    694     }
    695 
    696     /**
    697      * {@link #getFirstRowColumn} for an Integer with a provided default value.
    698      */
    699     public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
    700             String selection, String[] selectionArgs, String sortOrder, int column,
    701             Integer defaultValue) {
    702         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    703                 sortOrder, column, defaultValue, INT_GETTER);
    704     }
    705 
    706     /**
    707      * {@link #getFirstRowColumn} for a String with null as a default value.
    708      */
    709     public static String getFirstRowString(Context context, Uri uri, String[] projection,
    710             String selection, String[] selectionArgs, String sortOrder, int column) {
    711         return getFirstRowString(context, uri, projection, selection, selectionArgs, sortOrder,
    712                 column, null);
    713     }
    714 
    715     /**
    716      * {@link #getFirstRowColumn} for a String with a provided default value.
    717      */
    718     public static String getFirstRowString(Context context, Uri uri, String[] projection,
    719             String selection, String[] selectionArgs, String sortOrder, int column,
    720             String defaultValue) {
    721         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    722                 sortOrder, column, defaultValue, STRING_GETTER);
    723     }
    724 
    725     /**
    726      * {@link #getFirstRowColumn} for a byte array with a provided default value.
    727      */
    728     public static byte[] getFirstRowBlob(Context context, Uri uri, String[] projection,
    729             String selection, String[] selectionArgs, String sortOrder, int column,
    730             byte[] defaultValue) {
    731         return getFirstRowColumn(context, uri, projection, selection, selectionArgs, sortOrder,
    732                 column, defaultValue, BLOB_GETTER);
    733     }
    734 
    735     public static boolean attachmentExists(Context context, Attachment attachment) {
    736         if (attachment == null) {
    737             return false;
    738         } else if (attachment.mContentBytes != null) {
    739             return true;
    740         } else {
    741             final String cachedFile = attachment.getCachedFileUri();
    742             // Try the cached file first
    743             if (!TextUtils.isEmpty(cachedFile)) {
    744                 final Uri cachedFileUri = Uri.parse(cachedFile);
    745                 try {
    746                     final InputStream inStream =
    747                             context.getContentResolver().openInputStream(cachedFileUri);
    748                     try {
    749                         inStream.close();
    750                     } catch (IOException e) {
    751                         // Nothing to be done if can't close the stream
    752                     }
    753                     return true;
    754                 } catch (FileNotFoundException e) {
    755                     // We weren't able to open the file, try the content uri below
    756                     LogUtils.e(Logging.LOG_TAG, e, "not able to open cached file");
    757                 }
    758             }
    759             final String contentUri = attachment.getContentUri();
    760             if (TextUtils.isEmpty(contentUri)) {
    761                 return false;
    762             }
    763             try {
    764                 final Uri fileUri = Uri.parse(contentUri);
    765                 try {
    766                     final InputStream inStream =
    767                             context.getContentResolver().openInputStream(fileUri);
    768                     try {
    769                         inStream.close();
    770                     } catch (IOException e) {
    771                         // Nothing to be done if can't close the stream
    772                     }
    773                     return true;
    774                 } catch (FileNotFoundException e) {
    775                     return false;
    776                 }
    777             } catch (RuntimeException re) {
    778                 LogUtils.w(Logging.LOG_TAG, "attachmentExists RuntimeException=" + re);
    779                 return false;
    780             }
    781         }
    782     }
    783 
    784     /**
    785      * Check whether the message with a given id has unloaded attachments.  If the message is
    786      * a forwarded message, we look instead at the messages's source for the attachments.  If the
    787      * message or forward source can't be found, we return false
    788      * @param context the caller's context
    789      * @param messageId the id of the message
    790      * @return whether or not the message has unloaded attachments
    791      */
    792     public static boolean hasUnloadedAttachments(Context context, long messageId) {
    793         Message msg = Message.restoreMessageWithId(context, messageId);
    794         if (msg == null) return false;
    795         Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
    796         for (Attachment att: atts) {
    797             if (!attachmentExists(context, att)) {
    798                 // If the attachment doesn't exist and isn't marked for download, we're in trouble
    799                 // since the outbound message will be stuck indefinitely in the Outbox.  Instead,
    800                 // we'll just delete the attachment and continue; this is far better than the
    801                 // alternative.  In theory, this situation shouldn't be possible.
    802                 if ((att.mFlags & (Attachment.FLAG_DOWNLOAD_FORWARD |
    803                         Attachment.FLAG_DOWNLOAD_USER_REQUEST)) == 0) {
    804                     LogUtils.d(Logging.LOG_TAG, "Unloaded attachment isn't marked for download: " +
    805                             att.mFileName + ", #" + att.mId);
    806                     Account acct = Account.restoreAccountWithId(context, msg.mAccountKey);
    807                     if (acct == null) return true;
    808                     // If smart forward is set and the message is a forward, we'll act as though
    809                     // the attachment has been loaded
    810                     // In Email1 this test wasn't necessary, as the UI handled it...
    811                     if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) {
    812                         if ((acct.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) != 0) {
    813                             continue;
    814                         }
    815                     }
    816                     Attachment.delete(context, Attachment.CONTENT_URI, att.mId);
    817                 } else if (att.getContentUri() != null) {
    818                     // In this case, the attachment file is gone from the cache; let's clear the
    819                     // contentUri; this should be a very unusual case
    820                     ContentValues cv = new ContentValues();
    821                     cv.putNull(AttachmentColumns.CONTENT_URI);
    822                     Attachment.update(context, Attachment.CONTENT_URI, att.mId, cv);
    823                 }
    824                 return true;
    825             }
    826         }
    827         return false;
    828     }
    829 
    830     /**
    831      * Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
    832      * The arguments are exactly the same as to contentResolver.query().  Results are returned in
    833      * an array of Strings corresponding to the columns in the projection.  If the cursor has no
    834      * rows, null is returned.
    835      */
    836     public static String[] getRowColumns(Context context, Uri contentUri, String[] projection,
    837             String selection, String[] selectionArgs) {
    838         String[] values = new String[projection.length];
    839         ContentResolver cr = context.getContentResolver();
    840         Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null);
    841         try {
    842             if (c.moveToFirst()) {
    843                 for (int i = 0; i < projection.length; i++) {
    844                     values[i] = c.getString(i);
    845                 }
    846             } else {
    847                 return null;
    848             }
    849         } finally {
    850             c.close();
    851         }
    852         return values;
    853     }
    854 
    855     /**
    856      * Convenience method for retrieving columns from a particular row in EmailProvider.
    857      * Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and
    858      * a projection.  This method calls the previous one with the appropriate URI.
    859      */
    860     public static String[] getRowColumns(Context context, Uri baseUri, long id,
    861             String ... projection) {
    862         return getRowColumns(context, ContentUris.withAppendedId(baseUri, id), projection, null,
    863                 null);
    864     }
    865 
    866     public static boolean isExternalStorageMounted() {
    867         return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    868     }
    869 
    870     /**
    871      * Class that supports running any operation for each account.
    872      */
    873     public abstract static class ForEachAccount extends AsyncTask<Void, Void, Long[]> {
    874         private final Context mContext;
    875 
    876         public ForEachAccount(Context context) {
    877             mContext = context;
    878         }
    879 
    880         @Override
    881         protected final Long[] doInBackground(Void... params) {
    882             ArrayList<Long> ids = new ArrayList<Long>();
    883             Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
    884                     Account.ID_PROJECTION, null, null, null);
    885             try {
    886                 while (c.moveToNext()) {
    887                     ids.add(c.getLong(Account.ID_PROJECTION_COLUMN));
    888                 }
    889             } finally {
    890                 c.close();
    891             }
    892             return ids.toArray(EMPTY_LONGS);
    893         }
    894 
    895         @Override
    896         protected final void onPostExecute(Long[] ids) {
    897             if (ids != null && !isCancelled()) {
    898                 for (long id : ids) {
    899                     performAction(id);
    900                 }
    901             }
    902             onFinished();
    903         }
    904 
    905         /**
    906          * This method will be called for each account.
    907          */
    908         protected abstract void performAction(long accountId);
    909 
    910         /**
    911          * Called when the iteration is finished.
    912          */
    913         protected void onFinished() {
    914         }
    915     }
    916 
    917     public static long[] toPrimitiveLongArray(Collection<Long> collection) {
    918         // Need to do this manually because we're converting to a primitive long array, not
    919         // a Long array.
    920         final int size = collection.size();
    921         final long[] ret = new long[size];
    922         // Collection doesn't have get(i).  (Iterable doesn't have size())
    923         int i = 0;
    924         for (Long value : collection) {
    925             ret[i++] = value;
    926         }
    927         return ret;
    928     }
    929 
    930     public static Set<Long> toLongSet(long[] array) {
    931         // Need to do this manually because we're converting from a primitive long array, not
    932         // a Long array.
    933         final int size = array.length;
    934         HashSet<Long> ret = new HashSet<Long>(size);
    935         for (int i = 0; i < size; i++) {
    936             ret.add(array[i]);
    937         }
    938         return ret;
    939     }
    940 
    941     /**
    942      * Workaround for the {@link ListView#smoothScrollToPosition} randomly scroll the view bug
    943      * if it's called right after {@link ListView#setAdapter}.
    944      */
    945     public static void listViewSmoothScrollToPosition(final Activity activity,
    946             final ListView listView, final int position) {
    947         // Workarond: delay-call smoothScrollToPosition()
    948         new Handler().post(new Runnable() {
    949             @Override
    950             public void run() {
    951                 if (activity.isFinishing()) {
    952                     return; // Activity being destroyed
    953                 }
    954                 listView.smoothScrollToPosition(position);
    955             }
    956         });
    957     }
    958 
    959     private static final String[] ATTACHMENT_META_NAME_PROJECTION = {
    960         OpenableColumns.DISPLAY_NAME
    961     };
    962     private static final int ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME = 0;
    963 
    964     /**
    965      * @return Filename of a content of {@code contentUri}.  If the provider doesn't provide the
    966      * filename, returns the last path segment of the URI.
    967      */
    968     public static String getContentFileName(Context context, Uri contentUri) {
    969         String name = getFirstRowString(context, contentUri, ATTACHMENT_META_NAME_PROJECTION, null,
    970                 null, null, ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME);
    971         if (name == null) {
    972             name = contentUri.getLastPathSegment();
    973         }
    974         return name;
    975     }
    976 
    977     /**
    978      * Append a bold span to a {@link SpannableStringBuilder}.
    979      */
    980     public static SpannableStringBuilder appendBold(SpannableStringBuilder ssb, String text) {
    981         if (!TextUtils.isEmpty(text)) {
    982             SpannableString ss = new SpannableString(text);
    983             ss.setSpan(new StyleSpan(Typeface.BOLD), 0, ss.length(),
    984                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    985             ssb.append(ss);
    986         }
    987 
    988         return ssb;
    989     }
    990 
    991     /**
    992      * Stringify a cursor for logging purpose.
    993      */
    994     public static String dumpCursor(Cursor c) {
    995         StringBuilder sb = new StringBuilder();
    996         sb.append("[");
    997         while (c != null) {
    998             sb.append(c.getClass()); // Class name may not be available if toString() is overridden
    999             sb.append("/");
   1000             sb.append(c.toString());
   1001             if (c.isClosed()) {
   1002                 sb.append(" (closed)");
   1003             }
   1004             if (c instanceof CursorWrapper) {
   1005                 c = ((CursorWrapper) c).getWrappedCursor();
   1006                 sb.append(", ");
   1007             } else {
   1008                 break;
   1009             }
   1010         }
   1011         sb.append("]");
   1012         return sb.toString();
   1013     }
   1014 
   1015     /**
   1016      * Cursor wrapper that remembers where it was closed.
   1017      *
   1018      * Use {@link #get} to create a wrapped cursor.
   1019      * USe {@link #getTraceIfAvailable} to get the stack trace.
   1020      * Use {@link #log} to log if/where it was closed.
   1021      */
   1022     public static class CloseTraceCursorWrapper extends CursorWrapper {
   1023         private static final boolean TRACE_ENABLED = false;
   1024 
   1025         private Exception mTrace;
   1026 
   1027         private CloseTraceCursorWrapper(Cursor cursor) {
   1028             super(cursor);
   1029         }
   1030 
   1031         @Override
   1032         public void close() {
   1033             mTrace = new Exception("STACK TRACE");
   1034             super.close();
   1035         }
   1036 
   1037         public static Exception getTraceIfAvailable(Cursor c) {
   1038             if (c instanceof CloseTraceCursorWrapper) {
   1039                 return ((CloseTraceCursorWrapper) c).mTrace;
   1040             } else {
   1041                 return null;
   1042             }
   1043         }
   1044 
   1045         public static void log(Cursor c) {
   1046             if (c == null) {
   1047                 return;
   1048             }
   1049             if (c.isClosed()) {
   1050                 LogUtils.w(Logging.LOG_TAG, "Cursor was closed here: Cursor=" + c,
   1051                         getTraceIfAvailable(c));
   1052             } else {
   1053                 LogUtils.w(Logging.LOG_TAG, "Cursor not closed.  Cursor=" + c);
   1054             }
   1055         }
   1056 
   1057         public static Cursor get(Cursor original) {
   1058             return TRACE_ENABLED ? new CloseTraceCursorWrapper(original) : original;
   1059         }
   1060 
   1061         /* package */ static CloseTraceCursorWrapper alwaysCreateForTest(Cursor original) {
   1062             return new CloseTraceCursorWrapper(original);
   1063         }
   1064     }
   1065 
   1066     /**
   1067      * Test that the given strings are equal in a null-pointer safe fashion.
   1068      */
   1069     public static boolean areStringsEqual(String s1, String s2) {
   1070         return (s1 != null && s1.equals(s2)) || (s1 == null && s2 == null);
   1071     }
   1072 
   1073     public static void enableStrictMode(boolean enabled) {
   1074         StrictMode.setThreadPolicy(enabled
   1075                 ? new StrictMode.ThreadPolicy.Builder().detectAll().build()
   1076                 : StrictMode.ThreadPolicy.LAX);
   1077         StrictMode.setVmPolicy(enabled
   1078                 ? new StrictMode.VmPolicy.Builder().detectAll().build()
   1079                 : StrictMode.VmPolicy.LAX);
   1080     }
   1081 
   1082     public static String dumpFragment(Fragment f) {
   1083         StringWriter sw = new StringWriter();
   1084         PrintWriter w = new PrintWriter(sw);
   1085         f.dump("", new FileDescriptor(), w, new String[0]);
   1086         return sw.toString();
   1087     }
   1088 
   1089     /**
   1090      * Builds an "in" expression for SQLite.
   1091      *
   1092      * e.g. "ID" + 1,2,3 -> "ID in (1,2,3)".  If {@code values} is empty or null, it returns an
   1093      * empty string.
   1094      */
   1095     public static String buildInSelection(String columnName, Collection<? extends Number> values) {
   1096         if ((values == null) || (values.size() == 0)) {
   1097             return "";
   1098         }
   1099         StringBuilder sb = new StringBuilder();
   1100         sb.append(columnName);
   1101         sb.append(" in (");
   1102         String sep = "";
   1103         for (Number n : values) {
   1104             sb.append(sep);
   1105             sb.append(n.toString());
   1106             sep = ",";
   1107         }
   1108         sb.append(')');
   1109         return sb.toString();
   1110     }
   1111 }
   1112