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     /**
    369      * Generate a time in milliseconds from an email date string that represents a date/time in GMT
    370      * @param date string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339)
    371      * @return the time in milliseconds (since Jan 1, 1970)
    372      */
    373     public static long parseEmailDateTimeToMillis(String date) {
    374         GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
    375                 Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
    376                 Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)),
    377                 Integer.parseInt(date.substring(17, 19)));
    378         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
    379         return cal.getTimeInMillis();
    380     }
    381 
    382     private static byte[] encode(Charset charset, String s) {
    383         if (s == null) {
    384             return null;
    385         }
    386         final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
    387         final byte[] bytes = new byte[buffer.limit()];
    388         buffer.get(bytes);
    389         return bytes;
    390     }
    391 
    392     private static String decode(Charset charset, byte[] b) {
    393         if (b == null) {
    394             return null;
    395         }
    396         final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
    397         return new String(cb.array(), 0, cb.length());
    398     }
    399 
    400     /** Converts a String to UTF-8 */
    401     public static byte[] toUtf8(String s) {
    402         return encode(UTF_8, s);
    403     }
    404 
    405     /** Builds a String from UTF-8 bytes */
    406     public static String fromUtf8(byte[] b) {
    407         return decode(UTF_8, b);
    408     }
    409 
    410     /** Converts a String to ASCII bytes */
    411     public static byte[] toAscii(String s) {
    412         return encode(ASCII, s);
    413     }
    414 
    415     /** Builds a String from ASCII bytes */
    416     public static String fromAscii(byte[] b) {
    417         return decode(ASCII, b);
    418     }
    419 
    420     /**
    421      * @return true if the input is the first (or only) byte in a UTF-8 character
    422      */
    423     public static boolean isFirstUtf8Byte(byte b) {
    424         // If the top 2 bits is '10', it's not a first byte.
    425         return (b & 0xc0) != 0x80;
    426     }
    427 
    428     public static String byteToHex(int b) {
    429         return byteToHex(new StringBuilder(), b).toString();
    430     }
    431 
    432     public static StringBuilder byteToHex(StringBuilder sb, int b) {
    433         b &= 0xFF;
    434         sb.append("0123456789ABCDEF".charAt(b >> 4));
    435         sb.append("0123456789ABCDEF".charAt(b & 0xF));
    436         return sb;
    437     }
    438 
    439     public static String replaceBareLfWithCrlf(String str) {
    440         return str.replace("\r", "").replace("\n", "\r\n");
    441     }
    442 
    443     /**
    444      * Cancel an {@link AsyncTask}.  If it's already running, it'll be interrupted.
    445      */
    446     public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
    447         cancelTask(task, true);
    448     }
    449 
    450     /**
    451      * Cancel an {@link EmailAsyncTask}.  If it's already running, it'll be interrupted.
    452      */
    453     public static void cancelTaskInterrupt(EmailAsyncTask<?, ?, ?> task) {
    454         if (task != null) {
    455             task.cancel(true);
    456         }
    457     }
    458 
    459     /**
    460      * Cancel an {@link AsyncTask}.
    461      *
    462      * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
    463      *        task should be interrupted; otherwise, in-progress tasks are allowed
    464      *        to complete.
    465      */
    466     public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
    467         if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
    468             task.cancel(mayInterruptIfRunning);
    469         }
    470     }
    471 
    472     public static String getSmallHash(final String value) {
    473         final MessageDigest sha;
    474         try {
    475             sha = MessageDigest.getInstance("SHA-1");
    476         } catch (NoSuchAlgorithmException impossible) {
    477             return null;
    478         }
    479         sha.update(Utility.toUtf8(value));
    480         final int hash = getSmallHashFromSha1(sha.digest());
    481         return Integer.toString(hash);
    482     }
    483 
    484     /**
    485      * @return a non-negative integer generated from 20 byte SHA-1 hash.
    486      */
    487     /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) {
    488         final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes.
    489         return ((sha1[offset]  & 0x7f) << 24)
    490                 | ((sha1[offset + 1] & 0xff) << 16)
    491                 | ((sha1[offset + 2] & 0xff) << 8)
    492                 | ((sha1[offset + 3] & 0xff));
    493     }
    494 
    495     /**
    496      * Try to make a date MIME(RFC 2822/5322)-compliant.
    497      *
    498      * It fixes:
    499      * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
    500      *   (4 digit zone value can't be preceded by "GMT")
    501      *   We got a report saying eBay sends a date in this format
    502      */
    503     public static String cleanUpMimeDate(String date) {
    504         if (TextUtils.isEmpty(date)) {
    505             return date;
    506         }
    507         date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
    508         return date;
    509     }
    510 
    511     public static ByteArrayInputStream streamFromAsciiString(String ascii) {
    512         return new ByteArrayInputStream(toAscii(ascii));
    513     }
    514 
    515     /**
    516      * A thread safe way to show a Toast.  Can be called from any thread.
    517      *
    518      * @param context context
    519      * @param resId Resource ID of the message string.
    520      */
    521     public static void showToast(Context context, int resId) {
    522         showToast(context, context.getResources().getString(resId));
    523     }
    524 
    525     /**
    526      * A thread safe way to show a Toast.  Can be called from any thread.
    527      *
    528      * @param context context
    529      * @param message Message to show.
    530      */
    531     public static void showToast(final Context context, final String message) {
    532         getMainThreadHandler().post(new Runnable() {
    533             @Override
    534             public void run() {
    535                 Toast.makeText(context, message, Toast.LENGTH_LONG).show();
    536             }
    537         });
    538     }
    539 
    540     /**
    541      * Run {@code r} on a worker thread, returning the AsyncTask
    542      * @return the AsyncTask; this is primarily for use by unit tests, which require the
    543      * result of the task
    544      *
    545      * @deprecated use {@link EmailAsyncTask#runAsyncParallel} or
    546      *     {@link EmailAsyncTask#runAsyncSerial}
    547      */
    548     @Deprecated
    549     public static AsyncTask<Void, Void, Void> runAsync(final Runnable r) {
    550         return new AsyncTask<Void, Void, Void>() {
    551             @Override protected Void doInBackground(Void... params) {
    552                 r.run();
    553                 return null;
    554             }
    555         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    556     }
    557 
    558     /**
    559      * Interface used in {@link #createUniqueFile} instead of {@link File#createNewFile()} to make
    560      * it testable.
    561      */
    562     /* package */ interface NewFileCreator {
    563         public static final NewFileCreator DEFAULT = new NewFileCreator() {
    564                     @Override public boolean createNewFile(File f) throws IOException {
    565                         return f.createNewFile();
    566                     }
    567         };
    568         public boolean createNewFile(File f) throws IOException ;
    569     }
    570 
    571     /**
    572      * Creates a new empty file with a unique name in the given directory by appending a hyphen and
    573      * a number to the given filename.
    574      *
    575      * @return a new File object, or null if one could not be created
    576      */
    577     public static File createUniqueFile(File directory, String filename) throws IOException {
    578         return createUniqueFileInternal(NewFileCreator.DEFAULT, directory, filename);
    579     }
    580 
    581     /* package */ static File createUniqueFileInternal(NewFileCreator nfc,
    582             File directory, String filename) throws IOException {
    583         File file = new File(directory, filename);
    584         if (nfc.createNewFile(file)) {
    585             return file;
    586         }
    587         // Get the extension of the file, if any.
    588         int index = filename.lastIndexOf('.');
    589         String format;
    590         if (index != -1) {
    591             String name = filename.substring(0, index);
    592             String extension = filename.substring(index);
    593             format = name + "-%d" + extension;
    594         } else {
    595             format = filename + "-%d";
    596         }
    597 
    598         for (int i = 2; i < Integer.MAX_VALUE; i++) {
    599             file = new File(directory, String.format(format, i));
    600             if (nfc.createNewFile(file)) {
    601                 return file;
    602             }
    603         }
    604         return null;
    605     }
    606 
    607     public interface CursorGetter<T> {
    608         T get(Cursor cursor, int column);
    609     }
    610 
    611     private static final CursorGetter<Long> LONG_GETTER = new CursorGetter<Long>() {
    612         @Override
    613         public Long get(Cursor cursor, int column) {
    614             return cursor.getLong(column);
    615         }
    616     };
    617 
    618     private static final CursorGetter<Integer> INT_GETTER = new CursorGetter<Integer>() {
    619         @Override
    620         public Integer get(Cursor cursor, int column) {
    621             return cursor.getInt(column);
    622         }
    623     };
    624 
    625     private static final CursorGetter<String> STRING_GETTER = new CursorGetter<String>() {
    626         @Override
    627         public String get(Cursor cursor, int column) {
    628             return cursor.getString(column);
    629         }
    630     };
    631 
    632     private static final CursorGetter<byte[]> BLOB_GETTER = new CursorGetter<byte[]>() {
    633         @Override
    634         public byte[] get(Cursor cursor, int column) {
    635             return cursor.getBlob(column);
    636         }
    637     };
    638 
    639     /**
    640      * @return if {@code original} is to the EmailProvider, add "?limit=1".  Otherwise just returns
    641      * {@code original}.
    642      *
    643      * Other providers don't support the limit param.  Also, changing URI passed from other apps
    644      * can cause permission errors.
    645      */
    646     /* package */ static Uri buildLimitOneUri(Uri original) {
    647         if ("content".equals(original.getScheme()) &&
    648                 EmailContent.AUTHORITY.equals(original.getAuthority())) {
    649             return EmailContent.uriWithLimit(original, 1);
    650         }
    651         return original;
    652     }
    653 
    654     /**
    655      * @return a generic in column {@code column} of the first result row, if the query returns at
    656      * least 1 row.  Otherwise returns {@code defaultValue}.
    657      */
    658     public static <T extends Object> T getFirstRowColumn(Context context, Uri uri,
    659             String[] projection, String selection, String[] selectionArgs, String sortOrder,
    660             int column, T defaultValue, CursorGetter<T> getter) {
    661         // Use PARAMETER_LIMIT to restrict the query to the single row we need
    662         uri = buildLimitOneUri(uri);
    663         Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs,
    664                 sortOrder);
    665         if (c != null) {
    666             try {
    667                 if (c.moveToFirst()) {
    668                     return getter.get(c, column);
    669                 }
    670             } finally {
    671                 c.close();
    672             }
    673         }
    674         return defaultValue;
    675     }
    676 
    677     /**
    678      * {@link #getFirstRowColumn} for a Long with null as a default value.
    679      */
    680     public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
    681             String selection, String[] selectionArgs, String sortOrder, int column) {
    682         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    683                 sortOrder, column, null, LONG_GETTER);
    684     }
    685 
    686     /**
    687      * {@link #getFirstRowColumn} for a Long with a provided default value.
    688      */
    689     public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
    690             String selection, String[] selectionArgs, String sortOrder, int column,
    691             Long defaultValue) {
    692         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    693                 sortOrder, column, defaultValue, LONG_GETTER);
    694     }
    695 
    696     /**
    697      * {@link #getFirstRowColumn} for an Integer with null as a default value.
    698      */
    699     public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
    700             String selection, String[] selectionArgs, String sortOrder, int column) {
    701         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    702                 sortOrder, column, null, INT_GETTER);
    703     }
    704 
    705     /**
    706      * {@link #getFirstRowColumn} for an Integer with a provided default value.
    707      */
    708     public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
    709             String selection, String[] selectionArgs, String sortOrder, int column,
    710             Integer defaultValue) {
    711         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    712                 sortOrder, column, defaultValue, INT_GETTER);
    713     }
    714 
    715     /**
    716      * {@link #getFirstRowColumn} for a String with null as a default value.
    717      */
    718     public static String getFirstRowString(Context context, Uri uri, String[] projection,
    719             String selection, String[] selectionArgs, String sortOrder, int column) {
    720         return getFirstRowString(context, uri, projection, selection, selectionArgs, sortOrder,
    721                 column, null);
    722     }
    723 
    724     /**
    725      * {@link #getFirstRowColumn} for a String with a provided default value.
    726      */
    727     public static String getFirstRowString(Context context, Uri uri, String[] projection,
    728             String selection, String[] selectionArgs, String sortOrder, int column,
    729             String defaultValue) {
    730         return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
    731                 sortOrder, column, defaultValue, STRING_GETTER);
    732     }
    733 
    734     /**
    735      * {@link #getFirstRowColumn} for a byte array with a provided default value.
    736      */
    737     public static byte[] getFirstRowBlob(Context context, Uri uri, String[] projection,
    738             String selection, String[] selectionArgs, String sortOrder, int column,
    739             byte[] defaultValue) {
    740         return getFirstRowColumn(context, uri, projection, selection, selectionArgs, sortOrder,
    741                 column, defaultValue, BLOB_GETTER);
    742     }
    743 
    744     public static boolean attachmentExists(Context context, Attachment attachment) {
    745         if (attachment == null) {
    746             return false;
    747         } else if (attachment.mContentBytes != null) {
    748             return true;
    749         } else if (TextUtils.isEmpty(attachment.mContentUri)) {
    750             return false;
    751         }
    752         try {
    753             Uri fileUri = Uri.parse(attachment.mContentUri);
    754             try {
    755                 InputStream inStream = context.getContentResolver().openInputStream(fileUri);
    756                 try {
    757                     inStream.close();
    758                 } catch (IOException e) {
    759                     // Nothing to be done if can't close the stream
    760                 }
    761                 return true;
    762             } catch (FileNotFoundException e) {
    763                 return false;
    764             }
    765         } catch (RuntimeException re) {
    766             Log.w(Logging.LOG_TAG, "attachmentExists RuntimeException=" + re);
    767             return false;
    768         }
    769     }
    770 
    771     /**
    772      * Check whether the message with a given id has unloaded attachments.  If the message is
    773      * a forwarded message, we look instead at the messages's source for the attachments.  If the
    774      * message or forward source can't be found, we return false
    775      * @param context the caller's context
    776      * @param messageId the id of the message
    777      * @return whether or not the message has unloaded attachments
    778      */
    779     public static boolean hasUnloadedAttachments(Context context, long messageId) {
    780         Message msg = Message.restoreMessageWithId(context, messageId);
    781         if (msg == null) return false;
    782         Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
    783         for (Attachment att: atts) {
    784             if (!attachmentExists(context, att)) {
    785                 // If the attachment doesn't exist and isn't marked for download, we're in trouble
    786                 // since the outbound message will be stuck indefinitely in the Outbox.  Instead,
    787                 // we'll just delete the attachment and continue; this is far better than the
    788                 // alternative.  In theory, this situation shouldn't be possible.
    789                 if ((att.mFlags & (Attachment.FLAG_DOWNLOAD_FORWARD |
    790                         Attachment.FLAG_DOWNLOAD_USER_REQUEST)) == 0) {
    791                     Log.d(Logging.LOG_TAG, "Unloaded attachment isn't marked for download: " +
    792                             att.mFileName + ", #" + att.mId);
    793                     Attachment.delete(context, Attachment.CONTENT_URI, att.mId);
    794                 } else if (att.mContentUri != null) {
    795                     // In this case, the attachment file is gone from the cache; let's clear the
    796                     // contentUri; this should be a very unusual case
    797                     ContentValues cv = new ContentValues();
    798                     cv.putNull(AttachmentColumns.CONTENT_URI);
    799                     Attachment.update(context, Attachment.CONTENT_URI, att.mId, cv);
    800                 }
    801                 return true;
    802             }
    803         }
    804         return false;
    805     }
    806 
    807     /**
    808      * Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
    809      * The arguments are exactly the same as to contentResolver.query().  Results are returned in
    810      * an array of Strings corresponding to the columns in the projection.  If the cursor has no
    811      * rows, null is returned.
    812      */
    813     public static String[] getRowColumns(Context context, Uri contentUri, String[] projection,
    814             String selection, String[] selectionArgs) {
    815         String[] values = new String[projection.length];
    816         ContentResolver cr = context.getContentResolver();
    817         Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null);
    818         try {
    819             if (c.moveToFirst()) {
    820                 for (int i = 0; i < projection.length; i++) {
    821                     values[i] = c.getString(i);
    822                 }
    823             } else {
    824                 return null;
    825             }
    826         } finally {
    827             c.close();
    828         }
    829         return values;
    830     }
    831 
    832     /**
    833      * Convenience method for retrieving columns from a particular row in EmailProvider.
    834      * Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and
    835      * a projection.  This method calls the previous one with the appropriate URI.
    836      */
    837     public static String[] getRowColumns(Context context, Uri baseUri, long id,
    838             String ... projection) {
    839         return getRowColumns(context, ContentUris.withAppendedId(baseUri, id), projection, null,
    840                 null);
    841     }
    842 
    843     public static boolean isExternalStorageMounted() {
    844         return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    845     }
    846 
    847     /**
    848      * Class that supports running any operation for each account.
    849      */
    850     public abstract static class ForEachAccount extends AsyncTask<Void, Void, Long[]> {
    851         private final Context mContext;
    852 
    853         public ForEachAccount(Context context) {
    854             mContext = context;
    855         }
    856 
    857         @Override
    858         protected final Long[] doInBackground(Void... params) {
    859             ArrayList<Long> ids = new ArrayList<Long>();
    860             Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
    861                     Account.ID_PROJECTION, null, null, null);
    862             try {
    863                 while (c.moveToNext()) {
    864                     ids.add(c.getLong(Account.ID_PROJECTION_COLUMN));
    865                 }
    866             } finally {
    867                 c.close();
    868             }
    869             return ids.toArray(EMPTY_LONGS);
    870         }
    871 
    872         @Override
    873         protected final void onPostExecute(Long[] ids) {
    874             if (ids != null && !isCancelled()) {
    875                 for (long id : ids) {
    876                     performAction(id);
    877                 }
    878             }
    879             onFinished();
    880         }
    881 
    882         /**
    883          * This method will be called for each account.
    884          */
    885         protected abstract void performAction(long accountId);
    886 
    887         /**
    888          * Called when the iteration is finished.
    889          */
    890         protected void onFinished() {
    891         }
    892     }
    893 
    894     /**
    895      * Updates the last seen message key in the mailbox data base for the INBOX of the currently
    896      * shown account. If the account is {@link Account#ACCOUNT_ID_COMBINED_VIEW}, the INBOX for
    897      * all accounts are updated.
    898      * @return an {@link EmailAsyncTask} for test only.
    899      */
    900     public static EmailAsyncTask<Void, Void, Void> updateLastSeenMessageKey(final Context context,
    901             final long accountId) {
    902         return EmailAsyncTask.runAsyncParallel(new Runnable() {
    903             private void updateLastSeenMessageKeyForAccount(long accountId) {
    904                 ContentResolver resolver = context.getContentResolver();
    905                 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
    906                     Cursor c = resolver.query(
    907                             Account.CONTENT_URI, EmailContent.ID_PROJECTION, null, null, 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                             updateLastSeenMessageKeyForAccount(id);
    913                         }
    914                     } finally {
    915                         c.close();
    916                     }
    917                 } else if (accountId > 0L) {
    918                     Mailbox mailbox =
    919                         Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_INBOX);
    920 
    921                     // mailbox has been removed
    922                     if (mailbox == null) {
    923                         return;
    924                     }
    925                     // We use the highest _id for the account the mailbox table as the "last seen
    926                     // message key". We don't care if the message has been read or not. We only
    927                     // need a point at which we can compare against in the future. By setting this
    928                     // value, we are claiming that every message before this has potentially been
    929                     // seen by the user.
    930                     long messageId = Utility.getFirstRowLong(
    931                             context,
    932                             Message.CONTENT_URI,
    933                             EmailContent.ID_PROJECTION,
    934                             MessageColumns.MAILBOX_KEY + "=?",
    935                             new String[] { Long.toString(mailbox.mId) },
    936                             MessageColumns.ID + " DESC",
    937                             EmailContent.ID_PROJECTION_COLUMN, 0L);
    938                     long oldLastSeenMessageId = Utility.getFirstRowLong(
    939                             context, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailbox.mId),
    940                             new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY },
    941                             null, null, null, 0, 0L);
    942                     // Only update the db if the value has changed
    943                     if (messageId != oldLastSeenMessageId) {
    944                         ContentValues values = mailbox.toContentValues();
    945                         values.put(MailboxColumns.LAST_SEEN_MESSAGE_KEY, messageId);
    946                         resolver.update(
    947                                 Mailbox.CONTENT_URI,
    948                                 values,
    949                                 EmailContent.ID_SELECTION,
    950                                 new String[] { Long.toString(mailbox.mId) });
    951                     }
    952                 }
    953             }
    954 
    955             @Override
    956             public void run() {
    957                 updateLastSeenMessageKeyForAccount(accountId);
    958             }
    959         });
    960     }
    961 
    962     public static long[] toPrimitiveLongArray(Collection<Long> collection) {
    963         // Need to do this manually because we're converting to a primitive long array, not
    964         // a Long array.
    965         final int size = collection.size();
    966         final long[] ret = new long[size];
    967         // Collection doesn't have get(i).  (Iterable doesn't have size())
    968         int i = 0;
    969         for (Long value : collection) {
    970             ret[i++] = value;
    971         }
    972         return ret;
    973     }
    974 
    975     public static Set<Long> toLongSet(long[] array) {
    976         // Need to do this manually because we're converting from a primitive long array, not
    977         // a Long array.
    978         final int size = array.length;
    979         HashSet<Long> ret = new HashSet<Long>(size);
    980         for (int i = 0; i < size; i++) {
    981             ret.add(array[i]);
    982         }
    983         return ret;
    984     }
    985 
    986     /**
    987      * Workaround for the {@link ListView#smoothScrollToPosition} randomly scroll the view bug
    988      * if it's called right after {@link ListView#setAdapter}.
    989      */
    990     public static void listViewSmoothScrollToPosition(final Activity activity,
    991             final ListView listView, final int position) {
    992         // Workarond: delay-call smoothScrollToPosition()
    993         new Handler().post(new Runnable() {
    994             @Override
    995             public void run() {
    996                 if (activity.isFinishing()) {
    997                     return; // Activity being destroyed
    998                 }
    999                 listView.smoothScrollToPosition(position);
   1000             }
   1001         });
   1002     }
   1003 
   1004     private static final String[] ATTACHMENT_META_NAME_PROJECTION = {
   1005         OpenableColumns.DISPLAY_NAME
   1006     };
   1007     private static final int ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME = 0;
   1008 
   1009     /**
   1010      * @return Filename of a content of {@code contentUri}.  If the provider doesn't provide the
   1011      * filename, returns the last path segment of the URI.
   1012      */
   1013     public static String getContentFileName(Context context, Uri contentUri) {
   1014         String name = getFirstRowString(context, contentUri, ATTACHMENT_META_NAME_PROJECTION, null,
   1015                 null, null, ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME);
   1016         if (name == null) {
   1017             name = contentUri.getLastPathSegment();
   1018         }
   1019         return name;
   1020     }
   1021 
   1022     /**
   1023      * Append a bold span to a {@link SpannableStringBuilder}.
   1024      */
   1025     public static SpannableStringBuilder appendBold(SpannableStringBuilder ssb, String text) {
   1026         if (!TextUtils.isEmpty(text)) {
   1027             SpannableString ss = new SpannableString(text);
   1028             ss.setSpan(new StyleSpan(Typeface.BOLD), 0, ss.length(),
   1029                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1030             ssb.append(ss);
   1031         }
   1032 
   1033         return ssb;
   1034     }
   1035 
   1036     /**
   1037      * Stringify a cursor for logging purpose.
   1038      */
   1039     public static String dumpCursor(Cursor c) {
   1040         StringBuilder sb = new StringBuilder();
   1041         sb.append("[");
   1042         while (c != null) {
   1043             sb.append(c.getClass()); // Class name may not be available if toString() is overridden
   1044             sb.append("/");
   1045             sb.append(c.toString());
   1046             if (c.isClosed()) {
   1047                 sb.append(" (closed)");
   1048             }
   1049             if (c instanceof CursorWrapper) {
   1050                 c = ((CursorWrapper) c).getWrappedCursor();
   1051                 sb.append(", ");
   1052             } else {
   1053                 break;
   1054             }
   1055         }
   1056         sb.append("]");
   1057         return sb.toString();
   1058     }
   1059 
   1060     /**
   1061      * Cursor wrapper that remembers where it was closed.
   1062      *
   1063      * Use {@link #get} to create a wrapped cursor.
   1064      * USe {@link #getTraceIfAvailable} to get the stack trace.
   1065      * Use {@link #log} to log if/where it was closed.
   1066      */
   1067     public static class CloseTraceCursorWrapper extends CursorWrapper {
   1068         private static final boolean TRACE_ENABLED = false;
   1069 
   1070         private Exception mTrace;
   1071 
   1072         private CloseTraceCursorWrapper(Cursor cursor) {
   1073             super(cursor);
   1074         }
   1075 
   1076         @Override
   1077         public void close() {
   1078             mTrace = new Exception("STACK TRACE");
   1079             super.close();
   1080         }
   1081 
   1082         public static Exception getTraceIfAvailable(Cursor c) {
   1083             if (c instanceof CloseTraceCursorWrapper) {
   1084                 return ((CloseTraceCursorWrapper) c).mTrace;
   1085             } else {
   1086                 return null;
   1087             }
   1088         }
   1089 
   1090         public static void log(Cursor c) {
   1091             if (c == null) {
   1092                 return;
   1093             }
   1094             if (c.isClosed()) {
   1095                 Log.w(Logging.LOG_TAG, "Cursor was closed here: Cursor=" + c,
   1096                         getTraceIfAvailable(c));
   1097             } else {
   1098                 Log.w(Logging.LOG_TAG, "Cursor not closed.  Cursor=" + c);
   1099             }
   1100         }
   1101 
   1102         public static Cursor get(Cursor original) {
   1103             return TRACE_ENABLED ? new CloseTraceCursorWrapper(original) : original;
   1104         }
   1105 
   1106         /* package */ static CloseTraceCursorWrapper alwaysCreateForTest(Cursor original) {
   1107             return new CloseTraceCursorWrapper(original);
   1108         }
   1109     }
   1110 
   1111     /**
   1112      * Test that the given strings are equal in a null-pointer safe fashion.
   1113      */
   1114     public static boolean areStringsEqual(String s1, String s2) {
   1115         return (s1 != null && s1.equals(s2)) || (s1 == null && s2 == null);
   1116     }
   1117 
   1118     public static void enableStrictMode(boolean enabled) {
   1119         StrictMode.setThreadPolicy(enabled
   1120                 ? new StrictMode.ThreadPolicy.Builder().detectAll().build()
   1121                 : StrictMode.ThreadPolicy.LAX);
   1122         StrictMode.setVmPolicy(enabled
   1123                 ? new StrictMode.VmPolicy.Builder().detectAll().build()
   1124                 : StrictMode.VmPolicy.LAX);
   1125     }
   1126 
   1127     public static String dumpFragment(Fragment f) {
   1128         StringWriter sw = new StringWriter();
   1129         PrintWriter w = new PrintWriter(sw);
   1130         f.dump("", new FileDescriptor(), w, new String[0]);
   1131         return sw.toString();
   1132     }
   1133 
   1134     /**
   1135      * Builds an "in" expression for SQLite.
   1136      *
   1137      * e.g. "ID" + 1,2,3 -> "ID in (1,2,3)".  If {@code values} is empty or null, it returns an
   1138      * empty string.
   1139      */
   1140     public static String buildInSelection(String columnName, Collection<? extends Number> values) {
   1141         if ((values == null) || (values.size() == 0)) {
   1142             return "";
   1143         }
   1144         StringBuilder sb = new StringBuilder();
   1145         sb.append(columnName);
   1146         sb.append(" in (");
   1147         String sep = "";
   1148         for (Number n : values) {
   1149             sb.append(sep);
   1150             sb.append(n.toString());
   1151             sep = ",";
   1152         }
   1153         sb.append(')');
   1154         return sb.toString();
   1155     }
   1156 }
   1157