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