Utility.java revision f52afae9424fe41071cc34a8d6cbcb82b992a411
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.email;
18
19import com.android.email.provider.EmailContent;
20import com.android.email.provider.EmailContent.Account;
21import com.android.email.provider.EmailContent.AccountColumns;
22import com.android.email.provider.EmailContent.HostAuth;
23import com.android.email.provider.EmailContent.HostAuthColumns;
24import com.android.email.provider.EmailContent.Mailbox;
25import com.android.email.provider.EmailContent.MailboxColumns;
26import com.android.email.provider.EmailContent.Message;
27import com.android.email.provider.EmailContent.MessageColumns;
28
29import android.app.Activity;
30import android.content.ContentResolver;
31import android.content.Context;
32import android.content.pm.ActivityInfo;
33import android.content.res.Resources;
34import android.content.res.TypedArray;
35import android.database.Cursor;
36import android.graphics.drawable.Drawable;
37import android.net.Uri;
38import android.os.AsyncTask;
39import android.os.Parcelable;
40import android.security.MessageDigest;
41import android.telephony.TelephonyManager;
42import android.text.TextUtils;
43import android.util.Base64;
44import android.util.Log;
45import android.widget.AbsListView;
46import android.widget.TextView;
47import android.widget.Toast;
48
49import java.io.ByteArrayInputStream;
50import java.io.File;
51import java.io.IOException;
52import java.io.InputStream;
53import java.io.InputStreamReader;
54import java.io.UnsupportedEncodingException;
55import java.nio.ByteBuffer;
56import java.nio.CharBuffer;
57import java.nio.charset.Charset;
58import java.security.NoSuchAlgorithmException;
59import java.util.ArrayList;
60import java.util.Date;
61import java.util.GregorianCalendar;
62import java.util.TimeZone;
63import java.util.regex.Pattern;
64
65public class Utility {
66    public static final Charset UTF_8 = Charset.forName("UTF-8");
67    public static final Charset ASCII = Charset.forName("US-ASCII");
68
69    public static final String[] EMPTY_STRINGS = new String[0];
70    public static final Long[] EMPTY_LONGS = new Long[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    public final static String readInputStream(InputStream in, String encoding) throws IOException {
77        InputStreamReader reader = new InputStreamReader(in, encoding);
78        StringBuffer sb = new StringBuffer();
79        int count;
80        char[] buf = new char[512];
81        while ((count = reader.read(buf)) != -1) {
82            sb.append(buf, 0, count);
83        }
84        return sb.toString();
85    }
86
87    public final static boolean arrayContains(Object[] a, Object o) {
88        for (int i = 0, count = a.length; i < count; i++) {
89            if (a[i].equals(o)) {
90                return true;
91            }
92        }
93        return false;
94    }
95
96    /**
97     * Combines the given array of Objects into a single string using the
98     * seperator character and each Object's toString() method. between each
99     * part.
100     *
101     * @param parts
102     * @param seperator
103     * @return
104     */
105    public static String combine(Object[] parts, char seperator) {
106        if (parts == null) {
107            return null;
108        }
109        StringBuffer sb = new StringBuffer();
110        for (int i = 0; i < parts.length; i++) {
111            sb.append(parts[i].toString());
112            if (i < parts.length - 1) {
113                sb.append(seperator);
114            }
115        }
116        return sb.toString();
117    }
118    public static String base64Decode(String encoded) {
119        if (encoded == null) {
120            return null;
121        }
122        byte[] decoded = Base64.decode(encoded, Base64.DEFAULT);
123        return new String(decoded);
124    }
125
126    public static String base64Encode(String s) {
127        if (s == null) {
128            return s;
129        }
130        return Base64.encodeToString(s.getBytes(), Base64.NO_WRAP);
131    }
132
133    public static boolean isTextViewNotEmpty(TextView view) {
134        return !TextUtils.isEmpty(view.getText());
135    }
136
137    public static boolean isPortFieldValid(TextView view) {
138        CharSequence chars = view.getText();
139        if (TextUtils.isEmpty(chars)) return false;
140        Integer port;
141        // In theory, we can't get an illegal value here, since the field is monitored for valid
142        // numeric input. But this might be used elsewhere without such a check.
143        try {
144            port = Integer.parseInt(chars.toString());
145        } catch (NumberFormatException e) {
146            return false;
147        }
148        return port > 0 && port < 65536;
149     }
150
151    /**
152     * Ensures that the given string starts and ends with the double quote character. The string is
153     * not modified in any way except to add the double quote character to start and end if it's not
154     * already there.
155     *
156     * TODO: Rename this, because "quoteString()" can mean so many different things.
157     *
158     * sample -> "sample"
159     * "sample" -> "sample"
160     * ""sample"" -> "sample"
161     * "sample"" -> "sample"
162     * sa"mp"le -> "sa"mp"le"
163     * "sa"mp"le" -> "sa"mp"le"
164     * (empty string) -> ""
165     * " -> ""
166     * @param s
167     * @return
168     */
169    public static String quoteString(String s) {
170        if (s == null) {
171            return null;
172        }
173        if (!s.matches("^\".*\"$")) {
174            return "\"" + s + "\"";
175        }
176        else {
177            return s;
178        }
179    }
180
181    /**
182     * Apply quoting rules per IMAP RFC,
183     * quoted          = DQUOTE *QUOTED-CHAR DQUOTE
184     * QUOTED-CHAR     = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
185     * quoted-specials = DQUOTE / "\"
186     *
187     * This is used primarily for IMAP login, but might be useful elsewhere.
188     *
189     * NOTE:  Not very efficient - you may wish to preflight this, or perhaps it should check
190     * for trouble chars before calling the replace functions.
191     *
192     * @param s The string to be quoted.
193     * @return A copy of the string, having undergone quoting as described above
194     */
195    public static String imapQuoted(String s) {
196
197        // First, quote any backslashes by replacing \ with \\
198        // regex Pattern:  \\    (Java string const = \\\\)
199        // Substitute:     \\\\  (Java string const = \\\\\\\\)
200        String result = s.replaceAll("\\\\", "\\\\\\\\");
201
202        // Then, quote any double-quotes by replacing " with \"
203        // regex Pattern:  "    (Java string const = \")
204        // Substitute:     \\"  (Java string const = \\\\\")
205        result = result.replaceAll("\"", "\\\\\"");
206
207        // return string with quotes around it
208        return "\"" + result + "\"";
209    }
210
211    /**
212     * A fast version of  URLDecoder.decode() that works only with UTF-8 and does only two
213     * allocations. This version is around 3x as fast as the standard one and I'm using it
214     * hundreds of times in places that slow down the UI, so it helps.
215     */
216    public static String fastUrlDecode(String s) {
217        try {
218            byte[] bytes = s.getBytes("UTF-8");
219            byte ch;
220            int length = 0;
221            for (int i = 0, count = bytes.length; i < count; i++) {
222                ch = bytes[i];
223                if (ch == '%') {
224                    int h = (bytes[i + 1] - '0');
225                    int l = (bytes[i + 2] - '0');
226                    if (h > 9) {
227                        h -= 7;
228                    }
229                    if (l > 9) {
230                        l -= 7;
231                    }
232                    bytes[length] = (byte) ((h << 4) | l);
233                    i += 2;
234                }
235                else if (ch == '+') {
236                    bytes[length] = ' ';
237                }
238                else {
239                    bytes[length] = bytes[i];
240                }
241                length++;
242            }
243            return new String(bytes, 0, length, "UTF-8");
244        }
245        catch (UnsupportedEncodingException uee) {
246            return null;
247        }
248    }
249
250    /**
251     * Returns true if the specified date is within today. Returns false otherwise.
252     * @param date
253     * @return
254     */
255    public static boolean isDateToday(Date date) {
256        // TODO But Calendar is so slowwwwwww....
257        Date today = new Date();
258        if (date.getYear() == today.getYear() &&
259                date.getMonth() == today.getMonth() &&
260                date.getDate() == today.getDate()) {
261            return true;
262        }
263        return false;
264    }
265
266    /*
267     * TODO disabled this method globally. It is used in all the settings screens but I just
268     * noticed that an unrelated icon was dimmed. Android must share drawables internally.
269     */
270    public static void setCompoundDrawablesAlpha(TextView view, int alpha) {
271//        Drawable[] drawables = view.getCompoundDrawables();
272//        for (Drawable drawable : drawables) {
273//            if (drawable != null) {
274//                drawable.setAlpha(alpha);
275//            }
276//        }
277    }
278
279    // TODO: unit test this
280    public static String buildMailboxIdSelection(ContentResolver resolver, long mailboxId) {
281        // Setup default selection & args, then add to it as necessary
282        StringBuilder selection = new StringBuilder(
283                MessageColumns.FLAG_LOADED + " IN ("
284                + Message.FLAG_LOADED_PARTIAL + "," + Message.FLAG_LOADED_COMPLETE
285                + ") AND ");
286        if (mailboxId == Mailbox.QUERY_ALL_INBOXES
287            || mailboxId == Mailbox.QUERY_ALL_DRAFTS
288            || mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
289            // query for all mailboxes of type INBOX, DRAFTS, or OUTBOX
290            int type;
291            if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
292                type = Mailbox.TYPE_INBOX;
293            } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
294                type = Mailbox.TYPE_DRAFTS;
295            } else {
296                type = Mailbox.TYPE_OUTBOX;
297            }
298            StringBuilder inboxes = new StringBuilder();
299            Cursor c = resolver.query(Mailbox.CONTENT_URI,
300                        EmailContent.ID_PROJECTION,
301                        MailboxColumns.TYPE + "=? AND " + MailboxColumns.FLAG_VISIBLE + "=1",
302                        new String[] { Integer.toString(type) }, null);
303            // build an IN (mailboxId, ...) list
304            // TODO do this directly in the provider
305            while (c.moveToNext()) {
306                if (inboxes.length() != 0) {
307                    inboxes.append(",");
308                }
309                inboxes.append(c.getLong(EmailContent.ID_PROJECTION_COLUMN));
310            }
311            c.close();
312            selection.append(MessageColumns.MAILBOX_KEY + " IN ");
313            selection.append("(").append(inboxes).append(")");
314        } else  if (mailboxId == Mailbox.QUERY_ALL_UNREAD) {
315            selection.append(Message.FLAG_READ + "=0");
316        } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
317            selection.append(Message.FLAG_FAVORITE + "=1");
318        } else {
319            selection.append(MessageColumns.MAILBOX_KEY + "=" + mailboxId);
320        }
321        return selection.toString();
322    }
323
324    // TODO When the UI is settled, cache all strings/drawables
325    // TODO When the UI is settled, write up tests
326    // TODO When the UI is settled, remove backward-compatibility methods
327    public static class FolderProperties {
328
329        private static FolderProperties sInstance;
330
331        private final Context mContext;
332
333        // Caches for frequently accessed resources.
334        private final String[] mSpecialMailbox;
335        private final TypedArray mSpecialMailboxDrawable;
336        private final Drawable mDefaultMailboxDrawable;
337        private final Drawable mSummaryStarredMailboxDrawable;
338        private final Drawable mSummaryCombinedInboxDrawable;
339
340        private FolderProperties(Context context) {
341            mContext = context.getApplicationContext();
342            mSpecialMailbox = context.getResources().getStringArray(R.array.mailbox_display_names);
343            for (int i = 0; i < mSpecialMailbox.length; ++i) {
344                if ("".equals(mSpecialMailbox[i])) {
345                    // there is no localized name, so use the display name from the server
346                    mSpecialMailbox[i] = null;
347                }
348            }
349            mSpecialMailboxDrawable =
350                context.getResources().obtainTypedArray(R.array.mailbox_display_icons);
351            mDefaultMailboxDrawable =
352                context.getResources().getDrawable(R.drawable.ic_list_folder);
353            mSummaryStarredMailboxDrawable =
354                context.getResources().getDrawable(R.drawable.ic_list_starred);
355            mSummaryCombinedInboxDrawable =
356                context.getResources().getDrawable(R.drawable.ic_list_combined_inbox);
357        }
358
359        public static synchronized FolderProperties getInstance(Context context) {
360            if (sInstance == null) {
361                sInstance = new FolderProperties(context);
362            }
363            return sInstance;
364        }
365
366        // For backward compatibility.
367        public String getDisplayName(int type) {
368            return getDisplayName(type, -1);
369        }
370
371        // For backward compatibility.
372        public Drawable getSummaryMailboxIconIds(long id) {
373            return getIcon(-1, id);
374        }
375
376        public Drawable getIconIds(int type) {
377            return getIcon(type, -1);
378        }
379
380        /**
381         * Lookup names of localized special mailboxes
382         */
383        public String getDisplayName(int type, long mailboxId) {
384            // Special combined mailboxes
385            int resId = 0;
386
387            // Can't use long for switch!?
388            if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
389                resId = R.string.account_folder_list_summary_inbox;
390            } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
391                resId = R.string.account_folder_list_summary_starred;
392            } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
393                resId = R.string.account_folder_list_summary_drafts;
394            } else if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
395                resId = R.string.account_folder_list_summary_outbox;
396            }
397            if (resId != 0) {
398                return mContext.getString(resId);
399            }
400
401            if (type < mSpecialMailbox.length) {
402                return mSpecialMailbox[type];
403            }
404            return null;
405        }
406
407        /**
408         * Lookup icons of special mailboxes
409         */
410        public Drawable getIcon(int type, long mailboxId) {
411            if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
412                return mSummaryCombinedInboxDrawable;
413            } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
414                return mSummaryStarredMailboxDrawable;
415            } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
416                return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_DRAFTS);
417            } else if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
418                return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_OUTBOX);
419            }
420            if (0 <= type && type < mSpecialMailboxDrawable.length()) {
421                return mSpecialMailboxDrawable.getDrawable(type);
422            }
423            return mDefaultMailboxDrawable;
424        }
425    }
426
427    private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?"
428            + " and " + HostAuthColumns.LOGIN + " like ?"
429            + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\"";
430    private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?";
431
432    /**
433     * Look for an existing account with the same username & server
434     *
435     * @param context a system context
436     * @param allowAccountId this account Id will not trigger (when editing an existing account)
437     * @param hostName the server's address
438     * @param userLogin the user's login string
439     * @result null = no matching account found.  Account = matching account
440     */
441    public static Account findExistingAccount(Context context, long allowAccountId,
442            String hostName, String userLogin) {
443        ContentResolver resolver = context.getContentResolver();
444        Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION,
445                HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userLogin }, null);
446        try {
447            while (c.moveToNext()) {
448                long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN);
449                // Find account with matching hostauthrecv key, and return it
450                Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
451                        ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null);
452                try {
453                    while (c2.moveToNext()) {
454                        long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN);
455                        if (accountId != allowAccountId) {
456                            Account account = Account.restoreAccountWithId(context, accountId);
457                            if (account != null) {
458                                return account;
459                            }
460                        }
461                    }
462                } finally {
463                    c2.close();
464                }
465            }
466        } finally {
467            c.close();
468        }
469
470        return null;
471    }
472
473    /**
474     * Generate a random message-id header for locally-generated messages.
475     */
476    public static String generateMessageId() {
477        StringBuffer sb = new StringBuffer();
478        sb.append("<");
479        for (int i = 0; i < 24; i++) {
480            sb.append(Integer.toString((int)(Math.random() * 35), 36));
481        }
482        sb.append(".");
483        sb.append(Long.toString(System.currentTimeMillis()));
484        sb.append("@email.android.com>");
485        return sb.toString();
486    }
487
488    /**
489     * Generate a time in milliseconds from a date string that represents a date/time in GMT
490     * @param DateTime date string in format 20090211T180303Z (rfc2445, iCalendar).
491     * @return the time in milliseconds (since Jan 1, 1970)
492     */
493    public static long parseDateTimeToMillis(String date) {
494        GregorianCalendar cal = parseDateTimeToCalendar(date);
495        return cal.getTimeInMillis();
496    }
497
498    /**
499     * Generate a GregorianCalendar from a date string that represents a date/time in GMT
500     * @param DateTime date string in format 20090211T180303Z (rfc2445, iCalendar).
501     * @return the GregorianCalendar
502     */
503    public static GregorianCalendar parseDateTimeToCalendar(String date) {
504        GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
505                Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
506                Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
507                Integer.parseInt(date.substring(13, 15)));
508        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
509        return cal;
510    }
511
512    /**
513     * Generate a time in milliseconds from an email date string that represents a date/time in GMT
514     * @param Email style DateTime string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339)
515     * @return the time in milliseconds (since Jan 1, 1970)
516     */
517    public static long parseEmailDateTimeToMillis(String date) {
518        GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
519                Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
520                Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)),
521                Integer.parseInt(date.substring(17, 19)));
522        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
523        return cal.getTimeInMillis();
524    }
525
526    private static byte[] encode(Charset charset, String s) {
527        if (s == null) {
528            return null;
529        }
530        final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
531        final byte[] bytes = new byte[buffer.limit()];
532        buffer.get(bytes);
533        return bytes;
534    }
535
536    private static String decode(Charset charset, byte[] b) {
537        if (b == null) {
538            return null;
539        }
540        final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
541        return new String(cb.array(), 0, cb.length());
542    }
543
544    /** Converts a String to UTF-8 */
545    public static byte[] toUtf8(String s) {
546        return encode(UTF_8, s);
547    }
548
549    /** Builds a String from UTF-8 bytes */
550    public static String fromUtf8(byte[] b) {
551        return decode(UTF_8, b);
552    }
553
554    /** Converts a String to ASCII bytes */
555    public static byte[] toAscii(String s) {
556        return encode(ASCII, s);
557    }
558
559    /** Builds a String from ASCII bytes */
560    public static String fromAscii(byte[] b) {
561        return decode(ASCII, b);
562    }
563
564    /**
565     * @return true if the input is the first (or only) byte in a UTF-8 character
566     */
567    public static boolean isFirstUtf8Byte(byte b) {
568        // If the top 2 bits is '10', it's not a first byte.
569        return (b & 0xc0) != 0x80;
570    }
571
572    public static String byteToHex(int b) {
573        return byteToHex(new StringBuilder(), b).toString();
574    }
575
576    public static StringBuilder byteToHex(StringBuilder sb, int b) {
577        b &= 0xFF;
578        sb.append("0123456789ABCDEF".charAt(b >> 4));
579        sb.append("0123456789ABCDEF".charAt(b & 0xF));
580        return sb;
581    }
582
583    public static String replaceBareLfWithCrlf(String str) {
584        return str.replace("\r", "").replace("\n", "\r\n");
585    }
586
587    /**
588     * Cancel an {@link AsyncTask}.  If it's already running, it'll be interrupted.
589     */
590    public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
591        cancelTask(task, true);
592    }
593
594    /**
595     * Cancel an {@link AsyncTask}.
596     *
597     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
598     *        task should be interrupted; otherwise, in-progress tasks are allowed
599     *        to complete.
600     */
601    public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
602        if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
603            task.cancel(mayInterruptIfRunning);
604        }
605    }
606
607    /**
608     * @return Device's unique ID if available.  null if the device has no unique ID.
609     */
610    public static String getConsistentDeviceId(Context context) {
611        final String deviceId;
612        try {
613            TelephonyManager tm =
614                    (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
615            if (tm == null) {
616                return null;
617            }
618            deviceId = tm.getDeviceId();
619            if (deviceId == null) {
620                return null;
621            }
622        } catch (Exception e) {
623            Log.d(Email.LOG_TAG, "Error in TelephonyManager.getDeviceId(): " + e.getMessage());
624            return null;
625        }
626        final MessageDigest sha;
627        try {
628            sha = MessageDigest.getInstance("SHA-1");
629        } catch (NoSuchAlgorithmException impossible) {
630            return null;
631        }
632        sha.update(Utility.toUtf8(deviceId));
633        final int hash = getSmallHashFromSha1(sha.digest());
634        return Integer.toString(hash);
635    }
636
637    /**
638     * @return a non-negative integer generated from 20 byte SHA-1 hash.
639     */
640    /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) {
641        final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes.
642        return ((sha1[offset]  & 0x7f) << 24)
643                | ((sha1[offset + 1] & 0xff) << 16)
644                | ((sha1[offset + 2] & 0xff) << 8)
645                | ((sha1[offset + 3] & 0xff));
646    }
647
648    /**
649     * Try to make a date MIME(RFC 2822/5322)-compliant.
650     *
651     * It fixes:
652     * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
653     *   (4 digit zone value can't be preceded by "GMT")
654     *   We got a report saying eBay sends a date in this format
655     */
656    public static String cleanUpMimeDate(String date) {
657        if (TextUtils.isEmpty(date)) {
658            return date;
659        }
660        date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
661        return date;
662    }
663
664    public static ByteArrayInputStream streamFromAsciiString(String ascii) {
665        return new ByteArrayInputStream(toAscii(ascii));
666    }
667
668    /**
669     * A thread safe way to show a Toast.  This method uses {@link Activity#runOnUiThread}, so it
670     * can be called on any thread.
671     *
672     * @param activity Parent activity.
673     * @param resId Resource ID of the message string.
674     */
675    public static void showToast(Activity activity, int resId) {
676        showToast(activity, activity.getResources().getString(resId));
677    }
678
679    /**
680     * A thread safe way to show a Toast.  This method uses {@link Activity#runOnUiThread}, so it
681     * can be called on any thread.
682     *
683     * @param activity Parent activity.
684     * @param message Message to show.
685     */
686    public static void showToast(final Activity activity, final String message) {
687        activity.runOnUiThread(new Runnable() {
688            public void run() {
689                Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
690            }
691        });
692    }
693
694    /**
695     * Run {@code r} on a worker thread.
696     */
697    public static void runAsync(final Runnable r) {
698        new AsyncTask<Void, Void, Void>() {
699            @Override protected Void doInBackground(Void... params) {
700                r.run();
701                return null;
702            }
703        }.execute();
704    }
705
706    /**
707     * Formats the given size as a String in bytes, kB, MB or GB.  Ex: 12,315,000 = 11 MB
708     */
709    public static String formatSize(Context context, long size) {
710        final Resources res = context.getResources();
711        final long KB = 1024;
712        final long MB = (KB * 1024);
713        final long GB  = (MB * 1024);
714
715        int resId;
716        int value;
717
718        if (size < KB) {
719            resId = R.plurals.message_view_attachment_bytes;
720            value = (int) size;
721        } else if (size < MB) {
722            resId = R.plurals.message_view_attachment_kilobytes;
723            value = (int) (size / KB);
724        } else if (size < GB) {
725            resId = R.plurals.message_view_attachment_megabytes;
726            value = (int) (size / MB);
727        } else {
728            resId = R.plurals.message_view_attachment_gigabytes;
729            value = (int) (size / GB);
730        }
731        return res.getQuantityString(resId, value, value);
732    }
733
734    /**
735     * Interface used in {@link #createUniqueFile} instead of {@link File#createNewFile()} to make
736     * it testable.
737     */
738    /* package */ interface NewFileCreator {
739        public static final NewFileCreator DEFAULT = new NewFileCreator() {
740                    @Override public boolean createNewFile(File f) throws IOException {
741                        return f.createNewFile();
742                    }
743        };
744        public boolean createNewFile(File f) throws IOException ;
745    }
746
747    /**
748     * Creates a new empty file with a unique name in the given directory by appending a hyphen and
749     * a number to the given filename.
750     *
751     * @return a new File object, or null if one could not be created
752     */
753    public static File createUniqueFile(File directory, String filename) throws IOException {
754        return createUniqueFileInternal(NewFileCreator.DEFAULT, directory, filename);
755    }
756
757    /* package */ static File createUniqueFileInternal(NewFileCreator nfc,
758            File directory, String filename) throws IOException {
759        File file = new File(directory, filename);
760        if (nfc.createNewFile(file)) {
761            return file;
762        }
763        // Get the extension of the file, if any.
764        int index = filename.lastIndexOf('.');
765        String format;
766        if (index != -1) {
767            String name = filename.substring(0, index);
768            String extension = filename.substring(index);
769            format = name + "-%d" + extension;
770        } else {
771            format = filename + "-%d";
772        }
773
774        for (int i = 2; i < Integer.MAX_VALUE; i++) {
775            file = new File(directory, String.format(format, i));
776            if (nfc.createNewFile(file)) {
777                return file;
778            }
779        }
780        return null;
781    }
782
783    /**
784     * @return a long in column {@code column} of the first result row, if the query returns at
785     * least 1 row.  Otherwise returns {@code defaultValue}.
786     */
787    public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
788            String selection, String[] selectionArgs, String sortOrder, int column,
789            Long defaultValue) {
790        Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs,
791                sortOrder);
792        try {
793            if (c.moveToFirst()) {
794                return c.getLong(column);
795            }
796        } finally {
797            c.close();
798        }
799        return defaultValue;
800    }
801
802    /**
803     * {@link #getFirstRowLong} with null as a default value.
804     */
805    public static Long getFirstRowLong(Context context, Uri uri, String[] projection,
806            String selection, String[] selectionArgs, String sortOrder, int column) {
807        return getFirstRowLong(context, uri, projection, selection, selectionArgs,
808                sortOrder, column, null);
809    }
810
811    /**
812     * @return an integer in column {@code column} of the first result row, if the query returns at
813     * least 1 row.  Otherwise returns {@code defaultValue}.
814     */
815    public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
816            String selection, String[] selectionArgs, String sortOrder, int column,
817            Integer defaultValue) {
818        Long longDefault = (defaultValue == null) ? null : defaultValue.longValue();
819        Long result = getFirstRowLong(context, uri, projection, selection, selectionArgs, sortOrder,
820                column, longDefault);
821        return (result == null) ? null : result.intValue();
822    }
823
824    /**
825     * {@link #getFirstRowInt} with null as a default value.
826     */
827    public static Integer getFirstRowInt(Context context, Uri uri, String[] projection,
828            String selection, String[] selectionArgs, String sortOrder, int column) {
829        return getFirstRowInt(context, uri, projection, selection, selectionArgs,
830                sortOrder, column, null);
831    }
832
833    /**
834     * A class used to restore ListView state (e.g. scroll position) when changing adapter.
835     *
836     * TODO For some reason it doesn't always work.  Investigate and fix it.
837     */
838    public static class ListStateSaver {
839        private final Parcelable mState;
840
841        public ListStateSaver(AbsListView lv) {
842            mState = lv.onSaveInstanceState();
843        }
844
845        public void restore(AbsListView lv) {
846            lv.onRestoreInstanceState(mState);
847        }
848    }
849
850    /**
851     * STOPSHIP Remove this method
852     * Toggle between portrait and landscape.  Developement use only.
853     */
854    public static void changeOrientation(Activity activity) {
855        activity.setRequestedOrientation(
856                (activity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
857                ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
858                : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
859    }
860
861    /**
862     * Class that supports running any operation for each account.
863     */
864    public abstract static class ForEachAccount extends AsyncTask<Void, Void, Long[]> {
865        private final Context mContext;
866
867        public ForEachAccount(Context context) {
868            mContext = context;
869        }
870
871        @Override
872        protected final Long[] doInBackground(Void... params) {
873            ArrayList<Long> ids = new ArrayList<Long>();
874            Cursor c = mContext.getContentResolver().query(EmailContent.Account.CONTENT_URI,
875                    EmailContent.Account.ID_PROJECTION, null, null, null);
876            try {
877                while (c.moveToNext()) {
878                    ids.add(c.getLong(EmailContent.Account.ID_PROJECTION_COLUMN));
879                }
880            } finally {
881                c.close();
882            }
883            return ids.toArray(EMPTY_LONGS);
884        }
885
886        @Override
887        protected final void onPostExecute(Long[] ids) {
888            if (ids != null && !isCancelled()) {
889                for (long id : ids) {
890                    performAction(id);
891                }
892            }
893            onFinished();
894        }
895
896        /**
897         * This method will be called for each account.
898         */
899        protected abstract void performAction(long accountId);
900
901        /**
902         * Called when the iteration is finished.
903         */
904        protected void onFinished() {
905        }
906    }
907}
908