• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
getMainThreadHandler()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 
readInputStream(InputStream in, String encoding)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 
arrayContains(Object[] a, Object o)121     public final static boolean arrayContains(Object[] a, Object o) {
122         int index = arrayIndex(a, o);
123         return (index >= 0);
124     }
125 
arrayIndex(Object[] a, Object o)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      */
combine(Object[] parts, char separator)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     }
base64Decode(String encoded)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 
base64Encode(String s)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 
isTextViewNotEmpty(TextView view)167     public static boolean isTextViewNotEmpty(TextView view) {
168         return !TextUtils.isEmpty(view.getText());
169     }
170 
isPortFieldValid(TextView view)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      */
isServerNameValid(TextView view)191     public static boolean isServerNameValid(TextView view) {
192         return isServerNameValid(view.getText().toString());
193     }
194 
isServerNameValid(String serverName)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      */
quoteString(String s)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      */
fastUrlDecode(String s)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      */
findExistingAccount(Context context, long allowAccountId, String hostName, String userLogin)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      */
generateMessageId()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      */
parseDateTimeToMillis(String date)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      */
parseDateTimeToCalendar(String date)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      */
parseEmailDateTimeToMillis(String date)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 
encode(Charset charset, String s)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 
decode(Charset charset, byte[] b)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 */
toUtf8(String s)400     public static byte[] toUtf8(String s) {
401         return encode(UTF_8, s);
402     }
403 
404     /** Builds a String from UTF-8 bytes */
fromUtf8(byte[] b)405     public static String fromUtf8(byte[] b) {
406         return decode(UTF_8, b);
407     }
408 
409     /** Converts a String to ASCII bytes */
toAscii(String s)410     public static byte[] toAscii(String s) {
411         return encode(ASCII, s);
412     }
413 
414     /** Builds a String from ASCII bytes */
fromAscii(byte[] b)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      */
isFirstUtf8Byte(byte b)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 
byteToHex(int b)427     public static String byteToHex(int b) {
428         return byteToHex(new StringBuilder(), b).toString();
429     }
430 
byteToHex(StringBuilder sb, int b)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 
replaceBareLfWithCrlf(String str)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      */
cancelTaskInterrupt(AsyncTask<?, ?, ?> task)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      */
cancelTaskInterrupt(EmailAsyncTask<?, ?, ?> task)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      */
cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning)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 
getSmallHash(final String value)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      */
getSmallHashFromSha1(byte[] sha1)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      */
cleanUpMimeDate(String date)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 
streamFromAsciiString(String ascii)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      */
showToast(Context context, int resId)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      */
showToast(final Context context, final String message)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
runAsync(final Runnable r)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