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