OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / packages / apps / Email / src / com / android / email / Utility.java
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.email;
18
19 import com.android.email.provider.EmailContent;
20 import com.android.email.provider.EmailContent.Account;
21 import com.android.email.provider.EmailContent.AccountColumns;
22 import com.android.email.provider.EmailContent.HostAuth;
23 import com.android.email.provider.EmailContent.HostAuthColumns;
24 import com.android.email.provider.EmailContent.Mailbox;
25 import com.android.email.provider.EmailContent.MailboxColumns;
26 import com.android.email.provider.EmailContent.Message;
27 import com.android.email.provider.EmailContent.MessageColumns;
28
29 import android.content.ContentResolver;
30 import android.content.Context;
31 import android.content.res.TypedArray;
32 import android.database.Cursor;
33 import android.graphics.drawable.Drawable;
34 import android.os.AsyncTask;
35 import android.security.MessageDigest;
36 import android.telephony.TelephonyManager;
37 import android.text.Editable;
38 import android.text.TextUtils;
39 import android.util.Base64;
40 import android.util.Log;
41 import android.widget.TextView;
42
43 import java.io.ByteArrayInputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.InputStreamReader;
47 import java.io.UnsupportedEncodingException;
48 import java.nio.ByteBuffer;
49 import java.nio.CharBuffer;
50 import java.nio.charset.Charset;
51 import java.security.NoSuchAlgorithmException;
52 import java.util.Date;
53 import java.util.GregorianCalendar;
54 import java.util.TimeZone;
55 import java.util.regex.Pattern;
56
57 public class Utility {
58     public static final Charset UTF_8 = Charset.forName("UTF-8");
59     public static final Charset ASCII = Charset.forName("US-ASCII");
60
61     public static final String[] EMPTY_STRINGS = new String[0];
62
63     // "GMT" + "+" or "-" + 4 digits
64     private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
65             Pattern.compile("GMT([-+]\\d{4})$");
66
67     public final static String readInputStream(InputStream in, String encoding) throws IOException {
68         InputStreamReader reader = new InputStreamReader(in, encoding);
69         StringBuffer sb = new StringBuffer();
70         int count;
71         char[] buf = new char[512];
72         while ((count = reader.read(buf)) != -1) {
73             sb.append(buf, 0, count);
74         }
75         return sb.toString();
76     }
77
78     public final static boolean arrayContains(Object[] a, Object o) {
79         for (int i = 0, count = a.length; i < count; i++) {
80             if (a[i].equals(o)) {
81                 return true;
82             }
83         }
84         return false;
85     }
86
87     /**
88      * Combines the given array of Objects into a single string using the
89      * seperator character and each Object's toString() method. between each
90      * part.
91      *
92      * @param parts
93      * @param seperator
94      * @return
95      */
96     public static String combine(Object[] parts, char seperator) {
97         if (parts == null) {
98             return null;
99         }
100         StringBuffer sb = new StringBuffer();
101         for (int i = 0; i < parts.length; i++) {
102             sb.append(parts[i].toString());
103             if (i < parts.length - 1) {
104                 sb.append(seperator);
105             }
106         }
107         return sb.toString();
108     }
109     public static String base64Decode(String encoded) {
110         if (encoded == null) {
111             return null;
112         }
113         byte[] decoded = Base64.decode(encoded, Base64.DEFAULT);
114         return new String(decoded);
115     }
116
117     public static String base64Encode(String s) {
118         if (s == null) {
119             return s;
120         }
121         return Base64.encodeToString(s.getBytes(), Base64.NO_WRAP);
122     }
123
124     public static boolean requiredFieldValid(TextView view) {
125         return view.getText() != null && view.getText().length() > 0;
126     }
127
128     public static boolean requiredFieldValid(Editable s) {
129         return s != null && s.length() > 0;
130     }
131
132     public static boolean isPortFieldValid(TextView view) {
133         CharSequence chars = view.getText();
134         if (TextUtils.isEmpty(chars)) return false;
135         Integer port;
136         // In theory, we can't get an illegal value here, since the field is monitored for valid
137         // numeric input. But this might be used elsewhere without such a check.
138         try {
139             port = Integer.parseInt(chars.toString());
140         } catch (NumberFormatException e) {
141             return false;
142         }
143         return port > 0 && port < 65536;
144     }
145
146     /**
147      * Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the
148      * double quote character to start and end if it's not already there.
149      *
150      * TODO: Rename this, because "quoteString()" can mean so many different things.
151      *
152      * sample -> "sample"
153      * "sample" -> "sample"
154      * ""sample"" -> "sample"
155      * "sample"" -> "sample"
156      * sa"mp"le -> "sa"mp"le"
157      * "sa"mp"le" -> "sa"mp"le"
158      * (empty string) -> ""
159      * " -> ""
160      * @param s
161      * @return
162      */
163     public static String quoteString(String s) {
164         if (s == null) {
165             return null;
166         }
167         if (!s.matches("^\".*\"$")) {
168             return "\"" + s + "\"";
169         }
170         else {
171             return s;
172         }
173     }
174
175     /**
176      * Apply quoting rules per IMAP RFC,
177      * quoted          = DQUOTE *QUOTED-CHAR DQUOTE
178      * QUOTED-CHAR     = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
179      * quoted-specials = DQUOTE / "\"
180      *
181      * This is used primarily for IMAP login, but might be useful elsewhere.
182      *
183      * NOTE:  Not very efficient - you may wish to preflight this, or perhaps it should check
184      * for trouble chars before calling the replace functions.
185      *
186      * @param s The string to be quoted.
187      * @return A copy of the string, having undergone quoting as described above
188      */
189     public static String imapQuoted(String s) {
190
191         // First, quote any backslashes by replacing \ with \\
192         // regex Pattern:  \\    (Java string const = \\\\)
193         // Substitute:     \\\\  (Java string const = \\\\\\\\)
194         String result = s.replaceAll("\\\\", "\\\\\\\\");
195
196         // Then, quote any double-quotes by replacing " with \"
197         // regex Pattern:  "    (Java string const = \")
198         // Substitute:     \\"  (Java string const = \\\\\")
199         result = result.replaceAll("\"", "\\\\\"");
200
201         // return string with quotes around it
202         return "\"" + result + "\"";
203     }
204
205     /**
206      * A fast version of  URLDecoder.decode() that works only with UTF-8 and does only two
207      * allocations. This version is around 3x as fast as the standard one and I'm using it
208      * hundreds of times in places that slow down the UI, so it helps.
209      */
210     public static String fastUrlDecode(String s) {
211         try {
212             byte[] bytes = s.getBytes("UTF-8");
213             byte ch;
214             int length = 0;
215             for (int i = 0, count = bytes.length; i < count; i++) {
216                 ch = bytes[i];
217                 if (ch == '%') {
218                     int h = (bytes[i + 1] - '0');
219                     int l = (bytes[i + 2] - '0');
220                     if (h > 9) {
221                         h -= 7;
222                     }
223                     if (l > 9) {
224                         l -= 7;
225                     }
226                     bytes[length] = (byte) ((h << 4) | l);
227                     i += 2;
228                 }
229                 else if (ch == '+') {
230                     bytes[length] = ' ';
231                 }
232                 else {
233                     bytes[length] = bytes[i];
234                 }
235                 length++;
236             }
237             return new String(bytes, 0, length, "UTF-8");
238         }
239         catch (UnsupportedEncodingException uee) {
240             return null;
241         }
242     }
243
244     /**
245      * Returns true if the specified date is within today. Returns false otherwise.
246      * @param date
247      * @return
248      */
249     public static boolean isDateToday(Date date) {
250         // TODO But Calendar is so slowwwwwww....
251         Date today = new Date();
252         if (date.getYear() == today.getYear() &&
253                 date.getMonth() == today.getMonth() &&
254                 date.getDate() == today.getDate()) {
255             return true;
256         }
257         return false;
258     }
259
260     /*
261      * TODO disabled this method globally. It is used in all the settings screens but I just
262      * noticed that an unrelated icon was dimmed. Android must share drawables internally.
263      */
264     public static void setCompoundDrawablesAlpha(TextView view, int alpha) {
265 //        Drawable[] drawables = view.getCompoundDrawables();
266 //        for (Drawable drawable : drawables) {
267 //            if (drawable != null) {
268 //                drawable.setAlpha(alpha);
269 //            }
270 //        }
271     }
272
273     // TODO: unit test this
274     public static String buildMailboxIdSelection(ContentResolver resolver, long mailboxId) {
275         // Setup default selection & args, then add to it as necessary
276         StringBuilder selection = new StringBuilder(
277                 MessageColumns.FLAG_LOADED + " IN ("
278                 + Message.FLAG_LOADED_PARTIAL + "," + Message.FLAG_LOADED_COMPLETE
279                 + ") AND ");
280         if (mailboxId == Mailbox.QUERY_ALL_INBOXES
281             || mailboxId == Mailbox.QUERY_ALL_DRAFTS
282             || mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
283             // query for all mailboxes of type INBOX, DRAFTS, or OUTBOX
284             int type;
285             if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
286                 type = Mailbox.TYPE_INBOX;
287             } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
288                 type = Mailbox.TYPE_DRAFTS;
289             } else {
290                 type = Mailbox.TYPE_OUTBOX;
291             }
292             StringBuilder inboxes = new StringBuilder();
293             Cursor c = resolver.query(Mailbox.CONTENT_URI,
294                         EmailContent.ID_PROJECTION,
295                         MailboxColumns.TYPE + "=? AND " + MailboxColumns.FLAG_VISIBLE + "=1",
296                         new String[] { Integer.toString(type) }, null);
297             // build an IN (mailboxId, ...) list
298             // TODO do this directly in the provider
299             while (c.moveToNext()) {
300                 if (inboxes.length() != 0) {
301                     inboxes.append(",");
302                 }
303                 inboxes.append(c.getLong(EmailContent.ID_PROJECTION_COLUMN));
304             }
305             c.close();
306             selection.append(MessageColumns.MAILBOX_KEY + " IN ");
307             selection.append("(").append(inboxes).append(")");
308         } else  if (mailboxId == Mailbox.QUERY_ALL_UNREAD) {
309             selection.append(Message.FLAG_READ + "=0");
310         } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
311             selection.append(Message.FLAG_FAVORITE + "=1");
312         } else {
313             selection.append(MessageColumns.MAILBOX_KEY + "=" + mailboxId);
314         }
315         return selection.toString();
316     }
317
318     public static class FolderProperties {
319
320         private static FolderProperties sInstance;
321
322         // Caches for frequently accessed resources.
323         private String[] mSpecialMailbox = new String[] {};
324         private TypedArray mSpecialMailboxDrawable;
325         private Drawable mDefaultMailboxDrawable;
326         private Drawable mSummaryStarredMailboxDrawable;
327         private Drawable mSummaryCombinedInboxDrawable;
328
329         private FolderProperties(Context context) {
330             mSpecialMailbox = context.getResources().getStringArray(R.array.mailbox_display_names);
331             for (int i = 0; i < mSpecialMailbox.length; ++i) {
332                 if ("".equals(mSpecialMailbox[i])) {
333                     // there is no localized name, so use the display name from the server
334                     mSpecialMailbox[i] = null;
335                 }
336             }
337             mSpecialMailboxDrawable =
338                 context.getResources().obtainTypedArray(R.array.mailbox_display_icons);
339             mDefaultMailboxDrawable =
340                 context.getResources().getDrawable(R.drawable.ic_list_folder);
341             mSummaryStarredMailboxDrawable =
342                 context.getResources().getDrawable(R.drawable.ic_list_starred);
343             mSummaryCombinedInboxDrawable =
344                 context.getResources().getDrawable(R.drawable.ic_list_combined_inbox);
345         }
346
347         public static FolderProperties getInstance(Context context) {
348             if (sInstance == null) {
349                 synchronized (FolderProperties.class) {
350                     if (sInstance == null) {
351                         sInstance = new FolderProperties(context);
352                     }
353                 }
354             }
355             return sInstance;
356         }
357
358         /**
359          * Lookup names of localized special mailboxes
360          * @param type
361          * @return Localized strings
362          */
363         public String getDisplayName(int type) {
364             if (type < mSpecialMailbox.length) {
365                 return mSpecialMailbox[type];
366             }
367             return null;
368         }
369
370         /**
371          * Lookup icons of special mailboxes
372          * @param type
373          * @return icon's drawable
374          */
375         public Drawable getIconIds(int type) {
376             if (type < mSpecialMailboxDrawable.length()) {
377                 return mSpecialMailboxDrawable.getDrawable(type);
378             }
379             return mDefaultMailboxDrawable;
380         }
381
382         public Drawable getSummaryMailboxIconIds(long mailboxKey) {
383             if (mailboxKey == Mailbox.QUERY_ALL_INBOXES) {
384                 return mSummaryCombinedInboxDrawable;
385             } else if (mailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
386                 return mSummaryStarredMailboxDrawable;
387             } else if (mailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
388                 return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_DRAFTS);
389             } else if (mailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
390                 return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_OUTBOX);
391             }
392             return mDefaultMailboxDrawable;
393         }
394     }
395
396     private final static String HOSTAUTH_WHERE_CREDENTIALS = HostAuthColumns.ADDRESS + " like ?"
397             + " and " + HostAuthColumns.LOGIN + " like ?"
398             + " and " + HostAuthColumns.PROTOCOL + " not like \"smtp\"";
399     private final static String ACCOUNT_WHERE_HOSTAUTH = AccountColumns.HOST_AUTH_KEY_RECV + "=?";
400
401     /**
402      * Look for an existing account with the same username & server
403      *
404      * @param context a system context
405      * @param allowAccountId this account Id will not trigger (when editing an existing account)
406      * @param hostName the server
407      * @param userLogin the user login string
408      * @result null = no dupes found.  non-null = dupe account's display name
409      */
410     public static String findDuplicateAccount(Context context, long allowAccountId, String hostName,
411             String userLogin) {
412         ContentResolver resolver = context.getContentResolver();
413         Cursor c = resolver.query(HostAuth.CONTENT_URI, HostAuth.ID_PROJECTION,
414                 HOSTAUTH_WHERE_CREDENTIALS, new String[] { hostName, userLogin }, null);
415         try {
416             while (c.moveToNext()) {
417                 long hostAuthId = c.getLong(HostAuth.ID_PROJECTION_COLUMN);
418                 // Find account with matching hostauthrecv key, and return its display name
419                 Cursor c2 = resolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
420                         ACCOUNT_WHERE_HOSTAUTH, new String[] { Long.toString(hostAuthId) }, null);
421                 try {
422                     while (c2.moveToNext()) {
423                         long accountId = c2.getLong(Account.ID_PROJECTION_COLUMN);
424                         if (accountId != allowAccountId) {
425                             Account account = Account.restoreAccountWithId(context, accountId);
426                             if (account != null) {
427                                 return account.mDisplayName;
428                             }
429                         }
430                     }
431                 } finally {
432                     c2.close();
433                 }
434             }
435         } finally {
436             c.close();
437         }
438
439         return null;
440     }
441
442     /**
443      * Generate a random message-id header for locally-generated messages.
444      */
445     public static String generateMessageId() {
446         StringBuffer sb = new StringBuffer();
447         sb.append("<");
448         for (int i = 0; i < 24; i++) {
449             sb.append(Integer.toString((int)(Math.random() * 35), 36));
450         }
451         sb.append(".");
452         sb.append(Long.toString(System.currentTimeMillis()));
453         sb.append("@email.android.com>");
454         return sb.toString();
455     }
456
457     /**
458      * Generate a time in milliseconds from a date string that represents a date/time in GMT
459      * @param DateTime date string in format 20090211T180303Z (rfc2445, iCalendar).
460      * @return the time in milliseconds (since Jan 1, 1970)
461      */
462     public static long parseDateTimeToMillis(String date) {
463         GregorianCalendar cal = parseDateTimeToCalendar(date);
464         return cal.getTimeInMillis();
465     }
466
467     /**
468      * Generate a GregorianCalendar from a date string that represents a date/time in GMT
469      * @param DateTime date string in format 20090211T180303Z (rfc2445, iCalendar).
470      * @return the GregorianCalendar
471      */
472     public static GregorianCalendar parseDateTimeToCalendar(String date) {
473         GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
474                 Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
475                 Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
476                 Integer.parseInt(date.substring(13, 15)));
477         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
478         return cal;
479     }
480
481     /**
482      * Generate a time in milliseconds from an email date string that represents a date/time in GMT
483      * @param Email style DateTime string in format 2010-02-23T16:00:00.000Z (ISO 8601, rfc3339)
484      * @return the time in milliseconds (since Jan 1, 1970)
485      */
486     public static long parseEmailDateTimeToMillis(String date) {
487         GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
488                 Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
489                 Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)),
490                 Integer.parseInt(date.substring(17, 19)));
491         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
492         return cal.getTimeInMillis();
493     }
494
495     private static byte[] encode(Charset charset, String s) {
496         if (s == null) {
497             return null;
498         }
499         final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
500         final byte[] bytes = new byte[buffer.limit()];
501         buffer.get(bytes);
502         return bytes;
503     }
504
505     private static String decode(Charset charset, byte[] b) {
506         if (b == null) {
507             return null;
508         }
509         final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
510         return new String(cb.array(), 0, cb.length());
511     }
512
513     /** Converts a String to UTF-8 */
514     public static byte[] toUtf8(String s) {
515         return encode(UTF_8, s);
516     }
517
518     /** Builds a String from UTF-8 bytes */
519     public static String fromUtf8(byte[] b) {
520         return decode(UTF_8, b);
521     }
522
523     /** Converts a String to ASCII bytes */
524     public static byte[] toAscii(String s) {
525         return encode(ASCII, s);
526     }
527
528     /** Builds a String from ASCII bytes */
529     public static String fromAscii(byte[] b) {
530         return decode(ASCII, b);
531     }
532
533     /**
534      * @return true if the input is the first (or only) byte in a UTF-8 character
535      */
536     public static boolean isFirstUtf8Byte(byte b) {
537         // If the top 2 bits is '10', it's not a first byte.
538         return (b & 0xc0) != 0x80;
539     }
540
541     public static String byteToHex(int b) {
542         return byteToHex(new StringBuilder(), b).toString();
543     }
544
545     public static StringBuilder byteToHex(StringBuilder sb, int b) {
546         b &= 0xFF;
547         sb.append("0123456789ABCDEF".charAt(b >> 4));
548         sb.append("0123456789ABCDEF".charAt(b & 0xF));
549         return sb;
550     }
551
552     public static String replaceBareLfWithCrlf(String str) {
553         return str.replace("\r", "").replace("\n", "\r\n");
554     }
555
556     /**
557      * Cancel an {@link AsyncTask}.  If it's already running, it'll be interrupted.
558      */
559     public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
560         cancelTask(task, true);
561     }
562
563     /**
564      * Cancel an {@link AsyncTask}.
565      *
566      * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
567      *        task should be interrupted; otherwise, in-progress tasks are allowed
568      *        to complete.
569      */
570     public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
571         if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
572             task.cancel(mayInterruptIfRunning);
573         }
574     }
575
576     /**
577      * @return Device's unique ID if available.  null if the device has no unique ID.
578      */
579     public static String getConsistentDeviceId(Context context) {
580         final String deviceId;
581         try {
582             TelephonyManager tm =
583                     (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
584             if (tm == null) {
585                 return null;
586             }
587             deviceId = tm.getDeviceId();
588             if (deviceId == null) {
589                 return null;
590             }
591         } catch (Exception e) {
592             Log.d(Email.LOG_TAG, "Error in TelephonyManager.getDeviceId(): " + e.getMessage());
593             return null;
594         }
595         final MessageDigest sha;
596         try {
597             sha = MessageDigest.getInstance("SHA-1");
598         } catch (NoSuchAlgorithmException impossible) {
599             return null;
600         }
601         sha.update(Utility.toUtf8(deviceId));
602         final int hash = getSmallHashFromSha1(sha.digest());
603         return Integer.toString(hash);
604     }
605
606     /**
607      * @return a non-negative integer generated from 20 byte SHA-1 hash.
608      */
609     /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) {
610         final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes.
611         return ((sha1[offset]  & 0x7f) << 24)
612                 | ((sha1[offset + 1] & 0xff) << 16)
613                 | ((sha1[offset + 2] & 0xff) << 8)
614                 | ((sha1[offset + 3] & 0xff));
615     }
616
617     /**
618      * Try to make a date MIME(RFC 2822/5322)-compliant.
619      *
620      * It fixes:
621      * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
622      *   (4 digit zone value can't be preceded by "GMT")
623      *   We got a report saying eBay sends a date in this format
624      */
625     public static String cleanUpMimeDate(String date) {
626         if (TextUtils.isEmpty(date)) {
627             return date;
628         }
629         date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
630         return date;
631     }
632
633     public static ByteArrayInputStream streamFromAsciiString(String ascii) {
634         return new ByteArrayInputStream(toAscii(ascii));
635     }
636 }