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
17package com.android.emailcommon.utility;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.net.Uri;
25import android.os.AsyncTask;
26import android.os.Environment;
27import android.os.Handler;
28import android.os.Looper;
29import android.os.StrictMode;
30import android.text.TextUtils;
31import android.widget.TextView;
32import android.widget.Toast;
33
34import com.android.emailcommon.provider.Account;
35import com.android.emailcommon.provider.EmailContent;
36import com.android.emailcommon.provider.EmailContent.AccountColumns;
37import com.android.emailcommon.provider.EmailContent.Attachment;
38import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
39import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
40import com.android.emailcommon.provider.EmailContent.Message;
41import com.android.emailcommon.provider.HostAuth;
42import com.android.emailcommon.provider.ProviderUnavailableException;
43import com.android.mail.utils.LogUtils;
44import com.google.common.annotations.VisibleForTesting;
45
46import java.io.ByteArrayInputStream;
47import java.io.File;
48import java.io.FileNotFoundException;
49import java.io.IOException;
50import java.io.InputStream;
51import java.lang.ThreadLocal;
52import java.net.URI;
53import java.net.URISyntaxException;
54import java.nio.ByteBuffer;
55import java.nio.CharBuffer;
56import java.nio.charset.Charset;
57import java.security.MessageDigest;
58import java.security.NoSuchAlgorithmException;
59import java.text.ParseException;
60import java.text.SimpleDateFormat;
61import java.util.Date;
62import java.util.GregorianCalendar;
63import java.util.TimeZone;
64import java.util.regex.Pattern;
65
66public class Utility {
67    public static final Charset UTF_8 = Charset.forName("UTF-8");
68    public static final Charset ASCII = Charset.forName("US-ASCII");
69
70    public static final String[] EMPTY_STRINGS = new String[0];
71
72    // "GMT" + "+" or "-" + 4 digits
73    private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
74            Pattern.compile("GMT([-+]\\d{4})$");
75
76    private static Handler sMainThreadHandler;
77
78    /**
79     * @return a {@link Handler} tied to the main thread.
80     */
81    public static Handler getMainThreadHandler() {
82        if (sMainThreadHandler == null) {
83            // No need to synchronize -- it's okay to create an extra Handler, which will be used
84            // only once and then thrown away.
85            sMainThreadHandler = new Handler(Looper.getMainLooper());
86        }
87        return sMainThreadHandler;
88    }
89
90    public static boolean arrayContains(Object[] a, Object o) {
91        int index = arrayIndex(a, o);
92        return (index >= 0);
93    }
94
95    public static int arrayIndex(Object[] a, Object o) {
96        for (int i = 0, count = a.length; i < count; i++) {
97            if (a[i].equals(o)) {
98                return i;
99            }
100        }
101        return -1;
102    }
103
104    /**
105     * Returns a concatenated string containing the output of every Object's
106     * toString() method, each separated by the given separator character.
107     */
108    public static String combine(Object[] parts, char separator) {
109        if (parts == null) {
110            return null;
111        }
112        StringBuilder sb = new StringBuilder();
113        for (int i = 0; i < parts.length; i++) {
114            sb.append(parts[i].toString());
115            if (i < parts.length - 1) {
116                sb.append(separator);
117            }
118        }
119        return sb.toString();
120    }
121
122    public static boolean isPortFieldValid(TextView view) {
123        CharSequence chars = view.getText();
124        if (TextUtils.isEmpty(chars)) return false;
125        Integer port;
126        // In theory, we can't get an illegal value here, since the field is monitored for valid
127        // numeric input. But this might be used elsewhere without such a check.
128        try {
129            port = Integer.parseInt(chars.toString());
130        } catch (NumberFormatException e) {
131            return false;
132        }
133        return port > 0 && port < 65536;
134    }
135
136    /**
137     * Validate a hostname name field.
138     *
139     * Because we just use the {@link URI} class for validation, it'll accept some invalid
140     * host names, but it works well enough...
141     */
142    public static boolean isServerNameValid(TextView view) {
143        return isServerNameValid(view.getText().toString());
144    }
145
146    public static boolean isServerNameValid(String serverName) {
147        serverName = serverName.trim();
148        if (TextUtils.isEmpty(serverName)) {
149            return false;
150        }
151        try {
152            new URI(
153                    "http",
154                    null,
155                    serverName,
156                    -1,
157                    null, // path
158                    null, // query
159                    null);
160            return true;
161        } catch (URISyntaxException e) {
162            return false;
163        }
164    }
165
166    private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?"
167            + " and " + HostAuthColumns.LOGIN + " like ?  ESCAPE '\\'"
168            + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\"";
169    private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?";
170
171    /**
172     * Look for an existing account with the same username & server
173     *
174     * @param context a system context
175     * @param allowAccountId this account Id will not trigger (when editing an existing account)
176     * @param hostName the server's address
177     * @param userLogin the user's login string
178     * @return null = no matching account found.  Account = matching account
179     */
180    public static Account findExistingAccount(Context context, long allowAccountId,
181            String hostName, String userLogin) {
182        ContentResolver resolver = context.getContentResolver();
183        String userName = userLogin.replace("_", "\\_");
184        Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION,
185                HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userName }, null);
186        if (c == null) throw new ProviderUnavailableException();
187        try {
188            while (c.moveToNext()) {
189                long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN);
190                // Find account with matching hostauthrecv key, and return it
191                Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
192                        ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null);
193                try {
194                    while (c2.moveToNext()) {
195                        long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN);
196                        if (accountId != allowAccountId) {
197                            Account account = Account.restoreAccountWithId(context, accountId);
198                            if (account != null) {
199                                return account;
200                            }
201                        }
202                    }
203                } finally {
204                    c2.close();
205                }
206            }
207        } finally {
208            c.close();
209        }
210
211        return null;
212    }
213
214    private static class ThreadLocalDateFormat extends ThreadLocal<SimpleDateFormat> {
215        private final String mFormatStr;
216
217        public ThreadLocalDateFormat(String formatStr) {
218            mFormatStr = formatStr;
219        }
220
221        @Override
222        protected SimpleDateFormat initialValue() {
223            final SimpleDateFormat format = new SimpleDateFormat(mFormatStr);
224            final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
225            format.setCalendar(cal);
226            return format;
227        }
228
229        public Date parse(String date) throws ParseException {
230            return super.get().parse(date);
231        }
232    }
233
234    /**
235     * Generate a time in milliseconds from a date string that represents a date/time in GMT
236     * @param date string in format 20090211T180303Z (rfc2445, iCalendar).
237     * @return the time in milliseconds (since Jan 1, 1970)
238     */
239    public static long parseDateTimeToMillis(String date) throws ParseException {
240        return parseDateTimeToCalendar(date).getTimeInMillis();
241    }
242
243    private static final ThreadLocalDateFormat mFullDateTimeFormat =
244        new ThreadLocalDateFormat("yyyyMMdd'T'HHmmss'Z'");
245
246    private static final ThreadLocalDateFormat mAbbrevDateTimeFormat =
247        new ThreadLocalDateFormat("yyyyMMdd");
248
249    /**
250     * Generate a GregorianCalendar from a date string that represents a date/time in GMT
251     * @param date string in format 20090211T180303Z (rfc2445, iCalendar), or
252     *             in abbreviated format 20090211.
253     * @return the GregorianCalendar
254     */
255    @VisibleForTesting
256    public static GregorianCalendar parseDateTimeToCalendar(String date) throws ParseException {
257        final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
258        if (date.length() <= 8) {
259            cal.setTime(mAbbrevDateTimeFormat.parse(date));
260        } else {
261            cal.setTime(mFullDateTimeFormat.parse(date));
262        }
263        return cal;
264    }
265
266    private static final ThreadLocalDateFormat mAbbrevEmailDateTimeFormat =
267        new ThreadLocalDateFormat("yyyy-MM-dd");
268
269    private static final ThreadLocalDateFormat mEmailDateTimeFormat =
270        new ThreadLocalDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
271
272    private static final ThreadLocalDateFormat mEmailDateTimeFormatWithMillis =
273        new ThreadLocalDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
274
275    /**
276     * Generate a time in milliseconds from an email date string that represents a date/time in GMT
277     * @param date string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339)
278     * @return the time in milliseconds (since Jan 1, 1970)
279     */
280    @VisibleForTesting
281    public static long parseEmailDateTimeToMillis(String date) throws ParseException {
282        final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
283        if (date.length() <= 10) {
284            cal.setTime(mAbbrevEmailDateTimeFormat.parse(date));
285        } else if (date.length() <= 20) {
286            cal.setTime(mEmailDateTimeFormat.parse(date));
287        } else {
288            cal.setTime(mEmailDateTimeFormatWithMillis.parse(date));
289        }
290        return cal.getTimeInMillis();
291    }
292
293    private static byte[] encode(Charset charset, String s) {
294        if (s == null) {
295            return null;
296        }
297        final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
298        final byte[] bytes = new byte[buffer.limit()];
299        buffer.get(bytes);
300        return bytes;
301    }
302
303    private static String decode(Charset charset, byte[] b) {
304        if (b == null) {
305            return null;
306        }
307        final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
308        return new String(cb.array(), 0, cb.length());
309    }
310
311    /** Converts a String to UTF-8 */
312    public static byte[] toUtf8(String s) {
313        return encode(UTF_8, s);
314    }
315
316    /** Builds a String from UTF-8 bytes */
317    public static String fromUtf8(byte[] b) {
318        return decode(UTF_8, b);
319    }
320
321    /** Converts a String to ASCII bytes */
322    public static byte[] toAscii(String s) {
323        return encode(ASCII, s);
324    }
325
326    /** Builds a String from ASCII bytes */
327    public static String fromAscii(byte[] b) {
328        return decode(ASCII, b);
329    }
330
331    /**
332     * @return true if the input is the first (or only) byte in a UTF-8 character
333     */
334    public static boolean isFirstUtf8Byte(byte b) {
335        // If the top 2 bits is '10', it's not a first byte.
336        return (b & 0xc0) != 0x80;
337    }
338
339    public static String byteToHex(int b) {
340        return byteToHex(new StringBuilder(), b).toString();
341    }
342
343    public static StringBuilder byteToHex(StringBuilder sb, int b) {
344        b &= 0xFF;
345        sb.append("0123456789ABCDEF".charAt(b >> 4));
346        sb.append("0123456789ABCDEF".charAt(b & 0xF));
347        return sb;
348    }
349
350    public static String replaceBareLfWithCrlf(String str) {
351        return str.replace("\r", "").replace("\n", "\r\n");
352    }
353
354    /**
355     * Cancel an {@link AsyncTask}.  If it's already running, it'll be interrupted.
356     */
357    public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
358        cancelTask(task, true);
359    }
360
361    /**
362     * Cancel an {@link AsyncTask}.
363     *
364     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
365     *        task should be interrupted; otherwise, in-progress tasks are allowed
366     *        to complete.
367     */
368    public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
369        if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
370            task.cancel(mayInterruptIfRunning);
371        }
372    }
373
374    public static String getSmallHash(final String value) {
375        final MessageDigest sha;
376        try {
377            sha = MessageDigest.getInstance("SHA-1");
378        } catch (NoSuchAlgorithmException impossible) {
379            return null;
380        }
381        sha.update(Utility.toUtf8(value));
382        final int hash = getSmallHashFromSha1(sha.digest());
383        return Integer.toString(hash);
384    }
385
386    /**
387     * @return a non-negative integer generated from 20 byte SHA-1 hash.
388     */
389    /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) {
390        final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes.
391        return ((sha1[offset]  & 0x7f) << 24)
392                | ((sha1[offset + 1] & 0xff) << 16)
393                | ((sha1[offset + 2] & 0xff) << 8)
394                | ((sha1[offset + 3] & 0xff));
395    }
396
397    /**
398     * Try to make a date MIME(RFC 2822/5322)-compliant.
399     *
400     * It fixes:
401     * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
402     *   (4 digit zone value can't be preceded by "GMT")
403     *   We got a report saying eBay sends a date in this format
404     */
405    public static String cleanUpMimeDate(String date) {
406        if (TextUtils.isEmpty(date)) {
407            return date;
408        }
409        date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
410        return date;
411    }
412
413    public static ByteArrayInputStream streamFromAsciiString(String ascii) {
414        return new ByteArrayInputStream(toAscii(ascii));
415    }
416
417    /**
418     * A thread safe way to show a Toast.  Can be called from any thread.
419     *
420     * @param context context
421     * @param resId Resource ID of the message string.
422     */
423    public static void showToast(Context context, int resId) {
424        showToast(context, context.getResources().getString(resId));
425    }
426
427    /**
428     * A thread safe way to show a Toast.  Can be called from any thread.
429     *
430     * @param context context
431     * @param message Message to show.
432     */
433    public static void showToast(final Context context, final String message) {
434        getMainThreadHandler().post(new Runnable() {
435            @Override
436            public void run() {
437                Toast.makeText(context, message, Toast.LENGTH_LONG).show();
438            }
439        });
440    }
441
442    /**
443     * Run {@code r} on a worker thread, returning the AsyncTask
444     * @return the AsyncTask; this is primarily for use by unit tests, which require the
445     * result of the task
446     *
447     * @deprecated use {@link EmailAsyncTask#runAsyncParallel} or
448     *     {@link EmailAsyncTask#runAsyncSerial}
449     */
450    @Deprecated
451    public static AsyncTask<Void, Void, Void> runAsync(final Runnable r) {
452        return new AsyncTask<Void, Void, Void>() {
453            @Override protected Void doInBackground(Void... params) {
454                r.run();
455                return null;
456            }
457        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
458    }
459
460    /**
461     * Interface used in {@link #createUniqueFile} instead of {@link File#createNewFile()} to make
462     * it testable.
463     */
464    /* package */ interface NewFileCreator {
465        public static final NewFileCreator DEFAULT = new NewFileCreator() {
466                    @Override public boolean createNewFile(File f) throws IOException {
467                        return f.createNewFile();
468                    }
469        };
470        public boolean createNewFile(File f) throws IOException ;
471    }
472
473    /**
474     * Creates a new empty file with a unique name in the given directory by appending a hyphen and
475     * a number to the given filename.
476     *
477     * @return a new File object, or null if one could not be created
478     */
479    public static File createUniqueFile(File directory, String filename) throws IOException {
480        return createUniqueFileInternal(NewFileCreator.DEFAULT, directory, filename);
481    }
482
483    /* package */ static File createUniqueFileInternal(final NewFileCreator nfc,
484            final File directory, final String filename) throws IOException {
485        final File file = new File(directory, filename);
486        if (nfc.createNewFile(file)) {
487            return file;
488        }
489        // Get the extension of the file, if any.
490        final int index = filename.lastIndexOf('.');
491        final String name;
492        final String extension;
493        if (index != -1) {
494            name = filename.substring(0, index);
495            extension = filename.substring(index);
496        } else {
497            name = filename;
498            extension = "";
499        }
500
501        for (int i = 2; i < Integer.MAX_VALUE; i++) {
502            final File numberedFile =
503                    new File(directory, name + "-" + Integer.toString(i) + extension);
504            if (nfc.createNewFile(numberedFile)) {
505                return numberedFile;
506            }
507        }
508        return null;
509    }
510
511    public interface CursorGetter<T> {
512        T get(Cursor cursor, int column);
513    }
514
515    private static final CursorGetter<Long> LONG_GETTER = new CursorGetter<Long>() {
516        @Override
517        public Long get(Cursor cursor, int column) {
518            return cursor.getLong(column);
519        }
520    };
521
522    private static final CursorGetter<Integer> INT_GETTER = new CursorGetter<Integer>() {
523        @Override
524        public Integer get(Cursor cursor, int column) {
525            return cursor.getInt(column);
526        }
527    };
528
529    private static final CursorGetter<String> STRING_GETTER = new CursorGetter<String>() {
530        @Override
531        public String get(Cursor cursor, int column) {
532            return cursor.getString(column);
533        }
534    };
535
536    private static final CursorGetter<byte[]> BLOB_GETTER = new CursorGetter<byte[]>() {
537        @Override
538        public byte[] get(Cursor cursor, int column) {
539            return cursor.getBlob(column);
540        }
541    };
542
543    /**
544     * @return if {@code original} is to the EmailProvider, add "?limit=1".  Otherwise just returns
545     * {@code original}.
546     *
547     * Other providers don't support the limit param.  Also, changing URI passed from other apps
548     * can cause permission errors.
549     */
550    /* package */ static Uri buildLimitOneUri(Uri original) {
551        if ("content".equals(original.getScheme()) &&
552                EmailContent.AUTHORITY.equals(original.getAuthority())) {
553            return EmailContent.uriWithLimit(original, 1);
554        }
555        return original;
556    }
557
558    /**
559     * @return a generic in column {@code column} of the first result row, if the query returns at
560     * least 1 row.  Otherwise returns {@code defaultValue}.
561     */
562    public static <T> T getFirstRowColumn(Context context, Uri uri,
563            String[] projection, String selection, String[] selectionArgs, String sortOrder,
564            int column, T defaultValue, CursorGetter<T> getter) {
565        // Use PARAMETER_LIMIT to restrict the query to the single row we need
566        uri = buildLimitOneUri(uri);
567        Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs,
568                sortOrder);
569        if (c != null) {
570            try {
571                if (c.moveToFirst()) {
572                    return getter.get(c, column);
573                }
574            } finally {
575                c.close();
576            }
577        }
578        return defaultValue;
579    }
580
581    /**
582     * {@link #getFirstRowColumn} for a Long with null as a default value.
583     */
584    public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
585            String selection, String[] selectionArgs, String sortOrder, int column) {
586        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
587                sortOrder, column, null, LONG_GETTER);
588    }
589
590    /**
591     * {@link #getFirstRowColumn} for a Long with a provided default value.
592     */
593    public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
594            String selection, String[] selectionArgs, String sortOrder, int column,
595            Long defaultValue) {
596        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
597                sortOrder, column, defaultValue, LONG_GETTER);
598    }
599
600    /**
601     * {@link #getFirstRowColumn} for an Integer with null as a default value.
602     */
603    public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
604            String selection, String[] selectionArgs, String sortOrder, int column) {
605        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
606                sortOrder, column, null, INT_GETTER);
607    }
608
609    /**
610     * {@link #getFirstRowColumn} for an Integer with a provided default value.
611     */
612    public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
613            String selection, String[] selectionArgs, String sortOrder, int column,
614            Integer defaultValue) {
615        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
616                sortOrder, column, defaultValue, INT_GETTER);
617    }
618
619    /**
620     * {@link #getFirstRowColumn} for a String with null as a default value.
621     */
622    public static String getFirstRowString(Context context, Uri uri, String[] projection,
623            String selection, String[] selectionArgs, String sortOrder, int column) {
624        return getFirstRowString(context, uri, projection, selection, selectionArgs, sortOrder,
625                column, null);
626    }
627
628    /**
629     * {@link #getFirstRowColumn} for a String with a provided default value.
630     */
631    public static String getFirstRowString(Context context, Uri uri, String[] projection,
632            String selection, String[] selectionArgs, String sortOrder, int column,
633            String defaultValue) {
634        return getFirstRowColumn(context, uri, projection, selection, selectionArgs,
635                sortOrder, column, defaultValue, STRING_GETTER);
636    }
637
638    /**
639     * {@link #getFirstRowColumn} for a byte array with a provided default value.
640     */
641    public static byte[] getFirstRowBlob(Context context, Uri uri, String[] projection,
642            String selection, String[] selectionArgs, String sortOrder, int column,
643            byte[] defaultValue) {
644        return getFirstRowColumn(context, uri, projection, selection, selectionArgs, sortOrder,
645                column, defaultValue, BLOB_GETTER);
646    }
647
648    public static boolean attachmentExists(Context context, Attachment attachment) {
649        if (attachment == null) {
650            return false;
651        } else if (attachment.mContentBytes != null) {
652            return true;
653        } else {
654            final String cachedFile = attachment.getCachedFileUri();
655            // Try the cached file first
656            if (!TextUtils.isEmpty(cachedFile)) {
657                final Uri cachedFileUri = Uri.parse(cachedFile);
658                try {
659                    final InputStream inStream =
660                            context.getContentResolver().openInputStream(cachedFileUri);
661                    try {
662                        inStream.close();
663                    } catch (IOException e) {
664                        // Nothing to be done if can't close the stream
665                    }
666                    return true;
667                } catch (FileNotFoundException e) {
668                    // We weren't able to open the file, try the content uri below
669                    LogUtils.e(LogUtils.TAG, e, "not able to open cached file");
670                }
671            }
672            final String contentUri = attachment.getContentUri();
673            if (TextUtils.isEmpty(contentUri)) {
674                return false;
675            }
676            try {
677                final Uri fileUri = Uri.parse(contentUri);
678                try {
679                    final InputStream inStream =
680                            context.getContentResolver().openInputStream(fileUri);
681                    try {
682                        inStream.close();
683                    } catch (IOException e) {
684                        // Nothing to be done if can't close the stream
685                    }
686                    return true;
687                } catch (FileNotFoundException e) {
688                    return false;
689                }
690            } catch (RuntimeException re) {
691                LogUtils.w(LogUtils.TAG, re, "attachmentExists RuntimeException");
692                return false;
693            }
694        }
695    }
696
697    /**
698     * Check whether the message with a given id has unloaded attachments.  If the message is
699     * a forwarded message, we look instead at the messages's source for the attachments.  If the
700     * message or forward source can't be found, we return false
701     * @param context the caller's context
702     * @param messageId the id of the message
703     * @return whether or not the message has unloaded attachments
704     */
705    public static boolean hasUnloadedAttachments(Context context, long messageId) {
706        Message msg = Message.restoreMessageWithId(context, messageId);
707        if (msg == null) return false;
708        Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
709        for (Attachment att: atts) {
710            if (!attachmentExists(context, att)) {
711                // If the attachment doesn't exist and isn't marked for download, we're in trouble
712                // since the outbound message will be stuck indefinitely in the Outbox.  Instead,
713                // we'll just delete the attachment and continue; this is far better than the
714                // alternative.  In theory, this situation shouldn't be possible.
715                if ((att.mFlags & (Attachment.FLAG_DOWNLOAD_FORWARD |
716                        Attachment.FLAG_DOWNLOAD_USER_REQUEST)) == 0) {
717                    LogUtils.d(LogUtils.TAG, "Unloaded attachment isn't marked for download: %s" +
718                            ", #%d", att.mFileName, att.mId);
719                    Account acct = Account.restoreAccountWithId(context, msg.mAccountKey);
720                    if (acct == null) return true;
721                    // If smart forward is set and the message is a forward, we'll act as though
722                    // the attachment has been loaded
723                    // In Email1 this test wasn't necessary, as the UI handled it...
724                    if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) {
725                        if ((acct.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) != 0) {
726                            continue;
727                        }
728                    }
729                    Attachment.delete(context, Attachment.CONTENT_URI, att.mId);
730                } else if (att.getContentUri() != null) {
731                    // In this case, the attachment file is gone from the cache; let's clear the
732                    // contentUri; this should be a very unusual case
733                    ContentValues cv = new ContentValues();
734                    cv.putNull(AttachmentColumns.CONTENT_URI);
735                    Attachment.update(context, Attachment.CONTENT_URI, att.mId, cv);
736                }
737                return true;
738            }
739        }
740        return false;
741    }
742
743    /**
744     * Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
745     * The arguments are exactly the same as to contentResolver.query().  Results are returned in
746     * an array of Strings corresponding to the columns in the projection.  If the cursor has no
747     * rows, null is returned.
748     */
749    public static String[] getRowColumns(Context context, Uri contentUri, String[] projection,
750            String selection, String[] selectionArgs) {
751        String[] values = new String[projection.length];
752        ContentResolver cr = context.getContentResolver();
753        Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null);
754        try {
755            if (c.moveToFirst()) {
756                for (int i = 0; i < projection.length; i++) {
757                    values[i] = c.getString(i);
758                }
759            } else {
760                return null;
761            }
762        } finally {
763            c.close();
764        }
765        return values;
766    }
767
768    /**
769     * Convenience method for retrieving columns from a particular row in EmailProvider.
770     * Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and
771     * a projection.  This method calls the previous one with the appropriate URI.
772     */
773    public static String[] getRowColumns(Context context, Uri baseUri, long id,
774            String ... projection) {
775        return getRowColumns(context, ContentUris.withAppendedId(baseUri, id), projection, null,
776                null);
777    }
778
779    public static boolean isExternalStorageMounted() {
780        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
781    }
782
783    public static void enableStrictMode(boolean enabled) {
784        StrictMode.setThreadPolicy(enabled
785                ? new StrictMode.ThreadPolicy.Builder().detectAll().build()
786                : StrictMode.ThreadPolicy.LAX);
787        StrictMode.setVmPolicy(enabled
788                ? new StrictMode.VmPolicy.Builder().detectAll().build()
789                : StrictMode.VmPolicy.LAX);
790    }
791}
792