2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.email;
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;
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;
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;
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");
61 public static final String[] EMPTY_STRINGS = new String[0];
63 // "GMT" + "+" or "-" + 4 digits
64 private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
65 Pattern.compile("GMT([-+]\\d{4})$");
67 public final static String readInputStream(InputStream in, String encoding) throws IOException {
68 InputStreamReader reader = new InputStreamReader(in, encoding);
69 StringBuffer sb = new StringBuffer();
71 char[] buf = new char[512];
72 while ((count = reader.read(buf)) != -1) {
73 sb.append(buf, 0, count);
78 public final static boolean arrayContains(Object[] a, Object o) {
79 for (int i = 0, count = a.length; i < count; i++) {
88 * Combines the given array of Objects into a single string using the
89 * seperator character and each Object's toString() method. between each
96 public static String combine(Object[] parts, char seperator) {
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);
107 return sb.toString();
109 public static String base64Decode(String encoded) {
110 if (encoded == null) {
113 byte[] decoded = Base64.decode(encoded, Base64.DEFAULT);
114 return new String(decoded);
117 public static String base64Encode(String s) {
121 return Base64.encodeToString(s.getBytes(), Base64.NO_WRAP);
124 public static boolean requiredFieldValid(TextView view) {
125 return view.getText() != null && view.getText().length() > 0;
128 public static boolean requiredFieldValid(Editable s) {
129 return s != null && s.length() > 0;
132 public static boolean isPortFieldValid(TextView view) {
133 CharSequence chars = view.getText();
134 if (TextUtils.isEmpty(chars)) return false;
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.
139 port = Integer.parseInt(chars.toString());
140 } catch (NumberFormatException e) {
143 return port > 0 && port < 65536;
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.
150 * TODO: Rename this, because "quoteString()" can mean so many different things.
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) -> ""
163 public static String quoteString(String s) {
167 if (!s.matches("^\".*\"$")) {
168 return "\"" + s + "\"";
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 / "\"
181 * This is used primarily for IMAP login, but might be useful elsewhere.
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.
186 * @param s The string to be quoted.
187 * @return A copy of the string, having undergone quoting as described above
189 public static String imapQuoted(String s) {
191 // First, quote any backslashes by replacing \ with \\
192 // regex Pattern: \\ (Java string const = \\\\)
193 // Substitute: \\\\ (Java string const = \\\\\\\\)
194 String result = s.replaceAll("\\\\", "\\\\\\\\");
196 // Then, quote any double-quotes by replacing " with \"
197 // regex Pattern: " (Java string const = \")
198 // Substitute: \\" (Java string const = \\\\\")
199 result = result.replaceAll("\"", "\\\\\"");
201 // return string with quotes around it
202 return "\"" + result + "\"";
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.
210 public static String fastUrlDecode(String s) {
212 byte[] bytes = s.getBytes("UTF-8");
215 for (int i = 0, count = bytes.length; i < count; i++) {
218 int h = (bytes[i + 1] - '0');
219 int l = (bytes[i + 2] - '0');
226 bytes[length] = (byte) ((h << 4) | l);
229 else if (ch == '+') {
233 bytes[length] = bytes[i];
237 return new String(bytes, 0, length, "UTF-8");
239 catch (UnsupportedEncodingException uee) {
245 * Returns true if the specified date is within today. Returns false otherwise.
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()) {
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.
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);
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
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
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;
290 type = Mailbox.TYPE_OUTBOX;
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) {
303 inboxes.append(c.getLong(EmailContent.ID_PROJECTION_COLUMN));
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");
313 selection.append(MessageColumns.MAILBOX_KEY + "=" + mailboxId);
315 return selection.toString();
318 public static class FolderProperties {
320 private static FolderProperties sInstance;
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;
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;
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);
347 public static FolderProperties getInstance(Context context) {
348 if (sInstance == null) {
349 synchronized (FolderProperties.class) {
350 if (sInstance == null) {
351 sInstance = new FolderProperties(context);
359 * Lookup names of localized special mailboxes
361 * @return Localized strings
363 public String getDisplayName(int type) {
364 if (type < mSpecialMailbox.length) {
365 return mSpecialMailbox[type];
371 * Lookup icons of special mailboxes
373 * @return icon's drawable
375 public Drawable getIconIds(int type) {
376 if (type < mSpecialMailboxDrawable.length()) {
377 return mSpecialMailboxDrawable.getDrawable(type);
379 return mDefaultMailboxDrawable;
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);
392 return mDefaultMailboxDrawable;
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 + "=?";
402 * Look for an existing account with the same username & server
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
410 public static String findDuplicateAccount(Context context, long allowAccountId, String hostName,
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);
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);
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;
443 * Generate a random message-id header for locally-generated messages.
445 public static String generateMessageId() {
446 StringBuffer sb = new StringBuffer();
448 for (int i = 0; i < 24; i++) {
449 sb.append(Integer.toString((int)(Math.random() * 35), 36));
452 sb.append(Long.toString(System.currentTimeMillis()));
453 sb.append("@email.android.com>");
454 return sb.toString();
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)
462 public static long parseDateTimeToMillis(String date) {
463 GregorianCalendar cal = parseDateTimeToCalendar(date);
464 return cal.getTimeInMillis();
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
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"));
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)
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();
495 private static byte[] encode(Charset charset, String s) {
499 final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
500 final byte[] bytes = new byte[buffer.limit()];
505 private static String decode(Charset charset, byte[] b) {
509 final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
510 return new String(cb.array(), 0, cb.length());
513 /** Converts a String to UTF-8 */
514 public static byte[] toUtf8(String s) {
515 return encode(UTF_8, s);
518 /** Builds a String from UTF-8 bytes */
519 public static String fromUtf8(byte[] b) {
520 return decode(UTF_8, b);
523 /** Converts a String to ASCII bytes */
524 public static byte[] toAscii(String s) {
525 return encode(ASCII, s);
528 /** Builds a String from ASCII bytes */
529 public static String fromAscii(byte[] b) {
530 return decode(ASCII, b);
534 * @return true if the input is the first (or only) byte in a UTF-8 character
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;
541 public static String byteToHex(int b) {
542 return byteToHex(new StringBuilder(), b).toString();
545 public static StringBuilder byteToHex(StringBuilder sb, int b) {
547 sb.append("0123456789ABCDEF".charAt(b >> 4));
548 sb.append("0123456789ABCDEF".charAt(b & 0xF));
552 public static String replaceBareLfWithCrlf(String str) {
553 return str.replace("\r", "").replace("\n", "\r\n");
557 * Cancel an {@link AsyncTask}. If it's already running, it'll be interrupted.
559 public static void cancelTaskInterrupt(AsyncTask<?, ?, ?> task) {
560 cancelTask(task, true);
564 * Cancel an {@link AsyncTask}.
566 * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
567 * task should be interrupted; otherwise, in-progress tasks are allowed
570 public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
571 if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
572 task.cancel(mayInterruptIfRunning);
577 * @return Device's unique ID if available. null if the device has no unique ID.
579 public static String getConsistentDeviceId(Context context) {
580 final String deviceId;
582 TelephonyManager tm =
583 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
587 deviceId = tm.getDeviceId();
588 if (deviceId == null) {
591 } catch (Exception e) {
592 Log.d(Email.LOG_TAG, "Error in TelephonyManager.getDeviceId(): " + e.getMessage());
595 final MessageDigest sha;
597 sha = MessageDigest.getInstance("SHA-1");
598 } catch (NoSuchAlgorithmException impossible) {
601 sha.update(Utility.toUtf8(deviceId));
602 final int hash = getSmallHashFromSha1(sha.digest());
603 return Integer.toString(hash);
607 * @return a non-negative integer generated from 20 byte SHA-1 hash.
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));
618 * Try to make a date MIME(RFC 2822/5322)-compliant.
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
625 public static String cleanUpMimeDate(String date) {
626 if (TextUtils.isEmpty(date)) {
629 date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
633 public static ByteArrayInputStream streamFromAsciiString(String ascii) {
634 return new ByteArrayInputStream(toAscii(ascii));