1 /** 2 * Copyright (c) 2011, Google Inc. 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.mail.utils; 18 19 import com.google.android.mail.common.html.parser.HtmlDocument; 20 import com.google.android.mail.common.html.parser.HtmlParser; 21 import com.google.android.mail.common.html.parser.HtmlTree; 22 import com.google.android.mail.common.html.parser.HtmlTreeBuilder; 23 import com.google.common.collect.Maps; 24 25 import android.app.Fragment; 26 import android.app.SearchManager; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.PackageInfo; 30 import android.content.pm.PackageManager.NameNotFoundException; 31 import android.content.res.Resources; 32 import android.content.res.TypedArray; 33 import android.database.Cursor; 34 import android.graphics.Bitmap; 35 import android.graphics.Typeface; 36 import android.net.Uri; 37 import android.os.AsyncTask; 38 import android.os.Build; 39 import android.os.Bundle; 40 import android.provider.Browser; 41 import android.text.Spannable; 42 import android.text.SpannableString; 43 import android.text.SpannableStringBuilder; 44 import android.text.Spanned; 45 import android.text.TextUtils; 46 import android.text.TextUtils.SimpleStringSplitter; 47 import android.text.style.CharacterStyle; 48 import android.text.style.ForegroundColorSpan; 49 import android.text.style.StyleSpan; 50 import android.util.TypedValue; 51 import android.view.Menu; 52 import android.view.MenuItem; 53 import android.view.View; 54 import android.view.View.MeasureSpec; 55 import android.view.ViewGroup; 56 import android.view.ViewGroup.MarginLayoutParams; 57 import android.view.Window; 58 import android.webkit.WebSettings; 59 import android.webkit.WebView; 60 61 import com.android.mail.R; 62 import com.android.mail.browse.ConversationCursor; 63 import com.android.mail.compose.ComposeActivity; 64 import com.android.mail.perf.SimpleTimer; 65 import com.android.mail.providers.Account; 66 import com.android.mail.providers.Conversation; 67 import com.android.mail.providers.Folder; 68 import com.android.mail.providers.UIProvider; 69 import com.android.mail.providers.UIProvider.EditSettingsExtras; 70 import com.android.mail.ui.FeedbackEnabledActivity; 71 import com.android.mail.ui.ViewMode; 72 73 import org.json.JSONObject; 74 75 import java.io.FileDescriptor; 76 import java.io.PrintWriter; 77 import java.io.StringWriter; 78 import java.util.Locale; 79 import java.util.Map; 80 81 public class Utils { 82 /** 83 * longest extension we recognize is 4 characters (e.g. "html", "docx") 84 */ 85 private static final int FILE_EXTENSION_MAX_CHARS = 4; 86 private static final Map<Integer, Integer> sPriorityToLength = Maps.newHashMap(); 87 public static final String SENDER_LIST_TOKEN_ELIDED = "e"; 88 public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n"; 89 public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d"; 90 public static final String SENDER_LIST_TOKEN_LITERAL = "l"; 91 public static final String SENDER_LIST_TOKEN_SENDING = "s"; 92 public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f"; 93 public static final Character SENDER_LIST_SEPARATOR = '\n'; 94 public static final SimpleStringSplitter sSenderListSplitter = new SimpleStringSplitter( 95 SENDER_LIST_SEPARATOR); 96 public static String[] sSenderFragments = new String[8]; 97 98 public static final String EXTRA_ACCOUNT = "account"; 99 public static final String EXTRA_ACCOUNT_URI = "accountUri"; 100 public static final String EXTRA_FOLDER_URI = "folderUri"; 101 public static final String EXTRA_FOLDER = "folder"; 102 public static final String EXTRA_COMPOSE_URI = "composeUri"; 103 public static final String EXTRA_CONVERSATION = "conversationUri"; 104 public static final String EXTRA_FROM_NOTIFICATION = "notification"; 105 106 private static final String MAILTO_SCHEME = "mailto"; 107 108 /** Extra tag for debugging the blank fragment problem. */ 109 public static final String VIEW_DEBUGGING_TAG = "MailBlankFragment"; 110 111 /* 112 * Notifies that changes happened. Certain UI components, e.g., widgets, can 113 * register for this {@link Intent} and update accordingly. However, this 114 * can be very broad and is NOT the preferred way of getting notification. 115 */ 116 // TODO: UI Provider has this notification URI? 117 public static final String ACTION_NOTIFY_DATASET_CHANGED = 118 "com.android.mail.ACTION_NOTIFY_DATASET_CHANGED"; 119 120 /** Parameter keys for context-aware help. */ 121 private static final String SMART_HELP_LINK_PARAMETER_NAME = "p"; 122 123 private static final String SMART_LINK_APP_VERSION = "version"; 124 private static int sVersionCode = -1; 125 126 private static final int SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH = 600; 127 128 private static final String APP_VERSION_QUERY_PARAMETER = "appVersion"; 129 private static final String FOLDER_URI_QUERY_PARAMETER = "folderUri"; 130 131 private static final String LOG_TAG = LogTag.getLogTag(); 132 133 public static final boolean ENABLE_CONV_LOAD_TIMER = false; 134 public static final SimpleTimer sConvLoadTimer = 135 new SimpleTimer(ENABLE_CONV_LOAD_TIMER).withSessionName("ConvLoadTimer"); 136 137 private static final int[] STYLE_ATTR = new int[] {android.R.attr.background}; 138 139 public static boolean isRunningJellybeanOrLater() { 140 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; 141 } 142 143 public static boolean isRunningKitkatOrLater() { 144 return Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2; 145 } 146 147 /** 148 * Sets WebView in a restricted mode suitable for email use. 149 * 150 * @param webView The WebView to restrict 151 */ 152 public static void restrictWebView(WebView webView) { 153 WebSettings webSettings = webView.getSettings(); 154 webSettings.setSavePassword(false); 155 webSettings.setSaveFormData(false); 156 webSettings.setJavaScriptEnabled(false); 157 webSettings.setSupportZoom(false); 158 } 159 160 /** 161 * Format a plural string. 162 * 163 * @param resource The identity of the resource, which must be a R.plurals 164 * @param count The number of items. 165 */ 166 public static String formatPlural(Context context, int resource, int count) { 167 final CharSequence formatString = context.getResources().getQuantityText(resource, count); 168 return String.format(formatString.toString(), count); 169 } 170 171 /** 172 * @return an ellipsized String that's at most maxCharacters long. If the 173 * text passed is longer, it will be abbreviated. If it contains a 174 * suffix, the ellipses will be inserted in the middle and the 175 * suffix will be preserved. 176 */ 177 public static String ellipsize(String text, int maxCharacters) { 178 int length = text.length(); 179 if (length < maxCharacters) 180 return text; 181 182 int realMax = Math.min(maxCharacters, length); 183 // Preserve the suffix if any 184 int index = text.lastIndexOf("."); 185 String extension = "\u2026"; // "..."; 186 if (index >= 0) { 187 // Limit the suffix to dot + four characters 188 if (length - index <= FILE_EXTENSION_MAX_CHARS + 1) { 189 extension = extension + text.substring(index + 1); 190 } 191 } 192 realMax -= extension.length(); 193 if (realMax < 0) 194 realMax = 0; 195 return text.substring(0, realMax) + extension; 196 } 197 198 /** 199 * Ensures that the given string starts and ends with the double quote 200 * character. The string is not modified in any way except to add the double 201 * quote character to start and end if it's not already there. sample -> 202 * "sample" "sample" -> "sample" ""sample"" -> "sample" 203 * "sample"" -> "sample" sa"mp"le -> "sa"mp"le" "sa"mp"le" -> "sa"mp"le" 204 * (empty string) -> "" " -> "" 205 */ 206 public static String ensureQuotedString(String s) { 207 if (s == null) { 208 return null; 209 } 210 if (!s.matches("^\".*\"$")) { 211 return "\"" + s + "\""; 212 } else { 213 return s; 214 } 215 } 216 217 // TODO: Move this to the UI Provider. 218 private static CharacterStyle sUnreadStyleSpan = null; 219 private static CharacterStyle sReadStyleSpan; 220 private static CharacterStyle sDraftsStyleSpan; 221 private static CharSequence sMeString; 222 private static CharSequence sDraftSingularString; 223 private static CharSequence sDraftPluralString; 224 private static CharSequence sSendingString; 225 private static CharSequence sSendFailedString; 226 227 private static int sMaxUnreadCount = -1; 228 private static final CharacterStyle ACTION_BAR_UNREAD_STYLE = new StyleSpan(Typeface.BOLD); 229 private static String sUnreadText; 230 private static int sDefaultFolderBackgroundColor = -1; 231 private static int sUseFolderListFragmentTransition = -1; 232 233 public static void getStyledSenderSnippet(Context context, String senderInstructions, 234 SpannableStringBuilder senderBuilder, SpannableStringBuilder statusBuilder, 235 int maxChars, boolean forceAllUnread, boolean forceAllRead, boolean allowDraft) { 236 Resources res = context.getResources(); 237 if (sUnreadStyleSpan == null) { 238 sUnreadStyleSpan = new StyleSpan(Typeface.BOLD); 239 sReadStyleSpan = new StyleSpan(Typeface.NORMAL); 240 sDraftsStyleSpan = new ForegroundColorSpan(res.getColor(R.color.drafts)); 241 242 sMeString = context.getText(R.string.me_subject_pronun); 243 sDraftSingularString = res.getQuantityText(R.plurals.draft, 1); 244 sDraftPluralString = res.getQuantityText(R.plurals.draft, 2); 245 SpannableString sendingString = new SpannableString(context.getText(R.string.sending)); 246 sendingString.setSpan(CharacterStyle.wrap(sDraftsStyleSpan), 0, sendingString.length(), 247 0); 248 sSendingString = sendingString; 249 sSendFailedString = context.getText(R.string.send_failed); 250 } 251 252 getSenderSnippet(senderInstructions, senderBuilder, statusBuilder, maxChars, 253 sUnreadStyleSpan, sReadStyleSpan, sDraftsStyleSpan, sMeString, 254 sDraftSingularString, sDraftPluralString, sSendingString, sSendFailedString, 255 forceAllUnread, forceAllRead, allowDraft); 256 } 257 258 /** 259 * Uses sender instructions to build a formatted string. 260 * <p> 261 * Sender list instructions contain compact information about the sender 262 * list. Most work that can be done without knowing how much room will be 263 * availble for the sender list is done when creating the instructions. 264 * <p> 265 * The instructions string consists of tokens separated by 266 * SENDER_LIST_SEPARATOR. Here are the tokens, one per line: 267 * <ul> 268 * <li><tt>n</tt></li> 269 * <li><em>int</em>, the number of non-draft messages in the conversation</li> 270 * <li><tt>d</tt</li> 271 * <li><em>int</em>, the number of drafts in the conversation</li> 272 * <li><tt>l</tt></li> 273 * <li><em>literal html to be included in the output</em></li> 274 * <li><tt>s</tt> indicates that the message is sending (in the outbox 275 * without errors)</li> 276 * <li><tt>f</tt> indicates that the message failed to send (in the outbox 277 * with errors)</li> 278 * <li><em>for each message</em> 279 * <ul> 280 * <li><em>int</em>, 0 for read, 1 for unread</li> 281 * <li><em>int</em>, the priority of the message. Zero is the most important 282 * </li> 283 * <li><em>text</em>, the sender text or blank for messages from 'me'</li> 284 * </ul> 285 * </li> 286 * <li><tt>e</tt> to indicate that one or more messages have been elided</li> 287 * <p> 288 * The instructions indicate how many messages and drafts are in the 289 * conversation and then describe the most important messages in order, 290 * indicating the priority of each message and whether the message is 291 * unread. 292 * 293 * @param instructions instructions as described above 294 * @param senderBuilder the SpannableStringBuilder to append to for sender 295 * information 296 * @param statusBuilder the SpannableStringBuilder to append to for status 297 * @param maxChars the number of characters available to display the text 298 * @param unreadStyle the CharacterStyle for unread messages, or null 299 * @param draftsStyle the CharacterStyle for draft messages, or null 300 * @param sendingString the string to use when there are messages scheduled 301 * to be sent 302 * @param sendFailedString the string to use when there are messages that 303 * mailed to send 304 * @param meString the string to use for messages sent by this user 305 * @param draftString the string to use for "Draft" 306 * @param draftPluralString the string to use for "Drafts" 307 */ 308 public static synchronized void getSenderSnippet(String instructions, 309 SpannableStringBuilder senderBuilder, SpannableStringBuilder statusBuilder, 310 int maxChars, CharacterStyle unreadStyle, CharacterStyle readStyle, 311 CharacterStyle draftsStyle, CharSequence meString, CharSequence draftString, 312 CharSequence draftPluralString, CharSequence sendingString, 313 CharSequence sendFailedString, boolean forceAllUnread, boolean forceAllRead, 314 boolean allowDraft) { 315 assert !(forceAllUnread && forceAllRead); 316 boolean unreadStatusIsForced = forceAllUnread || forceAllRead; 317 boolean forcedUnreadStatus = forceAllUnread; 318 319 // Measure each fragment. It's ok to iterate over the entire set of 320 // fragments because it is 321 // never a long list, even if there are many senders. 322 final Map<Integer, Integer> priorityToLength = sPriorityToLength; 323 priorityToLength.clear(); 324 325 int maxFoundPriority = Integer.MIN_VALUE; 326 int numMessages = 0; 327 int numDrafts = 0; 328 CharSequence draftsFragment = ""; 329 CharSequence sendingFragment = ""; 330 CharSequence sendFailedFragment = ""; 331 332 sSenderListSplitter.setString(instructions); 333 int numFragments = 0; 334 String[] fragments = sSenderFragments; 335 int currentSize = fragments.length; 336 while (sSenderListSplitter.hasNext()) { 337 fragments[numFragments++] = sSenderListSplitter.next(); 338 if (numFragments == currentSize) { 339 sSenderFragments = new String[2 * currentSize]; 340 System.arraycopy(fragments, 0, sSenderFragments, 0, currentSize); 341 currentSize *= 2; 342 fragments = sSenderFragments; 343 } 344 } 345 346 for (int i = 0; i < numFragments;) { 347 String fragment0 = fragments[i++]; 348 if ("".equals(fragment0)) { 349 // This should be the final fragment. 350 } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) { 351 // ignore 352 } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) { 353 numMessages = Integer.valueOf(fragments[i++]); 354 } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) { 355 String numDraftsString = fragments[i++]; 356 numDrafts = Integer.parseInt(numDraftsString); 357 draftsFragment = numDrafts == 1 ? draftString : draftPluralString + " (" 358 + numDraftsString + ")"; 359 } else if (SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) { 360 senderBuilder.append(Utils.convertHtmlToPlainText(fragments[i++])); 361 return; 362 } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) { 363 sendingFragment = sendingString; 364 } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) { 365 sendFailedFragment = sendFailedString; 366 } else { 367 String priorityString = fragments[i++]; 368 CharSequence nameString = fragments[i++]; 369 if (nameString.length() == 0) 370 nameString = meString; 371 int priority = Integer.parseInt(priorityString); 372 priorityToLength.put(priority, nameString.length()); 373 maxFoundPriority = Math.max(maxFoundPriority, priority); 374 } 375 } 376 String numMessagesFragment = (numMessages != 0) ? " \u00A0" 377 + Integer.toString(numMessages + numDrafts) : ""; 378 379 // Don't allocate fixedFragment unless we need it 380 SpannableStringBuilder fixedFragment = null; 381 int fixedFragmentLength = 0; 382 if (draftsFragment.length() != 0 && allowDraft) { 383 fixedFragment = new SpannableStringBuilder(); 384 fixedFragment.append(draftsFragment); 385 if (draftsStyle != null) { 386 fixedFragment.setSpan(CharacterStyle.wrap(draftsStyle), 0, fixedFragment.length(), 387 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 388 } 389 } 390 if (sendingFragment.length() != 0) { 391 if (fixedFragment == null) { 392 fixedFragment = new SpannableStringBuilder(); 393 } 394 if (fixedFragment.length() != 0) 395 fixedFragment.append(", "); 396 fixedFragment.append(sendingFragment); 397 } 398 if (sendFailedFragment.length() != 0) { 399 if (fixedFragment == null) { 400 fixedFragment = new SpannableStringBuilder(); 401 } 402 if (fixedFragment.length() != 0) 403 fixedFragment.append(", "); 404 fixedFragment.append(sendFailedFragment); 405 } 406 407 if (fixedFragment != null) { 408 fixedFragmentLength = fixedFragment.length(); 409 } 410 maxChars -= fixedFragmentLength; 411 412 int maxPriorityToInclude = -1; // inclusive 413 int numCharsUsed = numMessagesFragment.length(); 414 int numSendersUsed = 0; 415 while (maxPriorityToInclude < maxFoundPriority) { 416 if (priorityToLength.containsKey(maxPriorityToInclude + 1)) { 417 int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1); 418 if (numCharsUsed > 0) 419 length += 2; 420 // We must show at least two senders if they exist. If we don't 421 // have space for both 422 // then we will truncate names. 423 if (length > maxChars && numSendersUsed >= 2) { 424 break; 425 } 426 numCharsUsed = length; 427 numSendersUsed++; 428 } 429 maxPriorityToInclude++; 430 } 431 432 int numCharsToRemovePerWord = 0; 433 if (numCharsUsed > maxChars) { 434 numCharsToRemovePerWord = (numCharsUsed - maxChars) / numSendersUsed; 435 } 436 437 String lastFragment = null; 438 CharacterStyle lastStyle = null; 439 for (int i = 0; i < numFragments;) { 440 String fragment0 = fragments[i++]; 441 if ("".equals(fragment0)) { 442 // This should be the final fragment. 443 } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) { 444 if (lastFragment != null) { 445 addStyledFragment(senderBuilder, lastFragment, lastStyle, false); 446 senderBuilder.append(" "); 447 addStyledFragment(senderBuilder, "..", lastStyle, true); 448 senderBuilder.append(" "); 449 } 450 lastFragment = null; 451 } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) { 452 i++; 453 } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) { 454 i++; 455 } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) { 456 } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) { 457 } else { 458 final String unreadString = fragment0; 459 final String priorityString = fragments[i++]; 460 String nameString = fragments[i++]; 461 if (nameString.length() == 0) { 462 nameString = meString.toString(); 463 } else { 464 nameString = Utils.convertHtmlToPlainText(nameString).toString(); 465 } 466 if (numCharsToRemovePerWord != 0) { 467 nameString = nameString.substring(0, 468 Math.max(nameString.length() - numCharsToRemovePerWord, 0)); 469 } 470 final boolean unread = unreadStatusIsForced ? forcedUnreadStatus : Integer 471 .parseInt(unreadString) != 0; 472 final int priority = Integer.parseInt(priorityString); 473 if (priority <= maxPriorityToInclude) { 474 if (lastFragment != null && !lastFragment.equals(nameString)) { 475 addStyledFragment(senderBuilder, lastFragment.concat(","), lastStyle, 476 false); 477 senderBuilder.append(" "); 478 } 479 lastFragment = nameString; 480 lastStyle = unread ? unreadStyle : readStyle; 481 } else { 482 if (lastFragment != null) { 483 addStyledFragment(senderBuilder, lastFragment, lastStyle, false); 484 // Adjacent spans can cause the TextView in Gmail widget 485 // confused and leads to weird behavior on scrolling. 486 // Our workaround here is to separate the spans by 487 // spaces. 488 senderBuilder.append(" "); 489 addStyledFragment(senderBuilder, "..", lastStyle, true); 490 senderBuilder.append(" "); 491 } 492 lastFragment = null; 493 } 494 } 495 } 496 if (lastFragment != null) { 497 addStyledFragment(senderBuilder, lastFragment, lastStyle, false); 498 } 499 senderBuilder.append(numMessagesFragment); 500 if (fixedFragmentLength != 0) { 501 statusBuilder.append(fixedFragment); 502 } 503 } 504 505 /** 506 * Adds a fragment with given style to a string builder. 507 * 508 * @param builder the current string builder 509 * @param fragment the fragment to be added 510 * @param style the style of the fragment 511 * @param withSpaces whether to add the whole fragment or to divide it into 512 * smaller ones 513 */ 514 private static void addStyledFragment(SpannableStringBuilder builder, String fragment, 515 CharacterStyle style, boolean withSpaces) { 516 if (withSpaces) { 517 int pos = builder.length(); 518 builder.append(fragment); 519 builder.setSpan(CharacterStyle.wrap(style), pos, builder.length(), 520 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 521 } else { 522 int start = 0; 523 while (true) { 524 int pos = fragment.substring(start).indexOf(' '); 525 if (pos == -1) { 526 addStyledFragment(builder, fragment.substring(start), style, true); 527 break; 528 } else { 529 pos += start; 530 if (start < pos) { 531 addStyledFragment(builder, fragment.substring(start, pos), style, true); 532 builder.append(' '); 533 } 534 start = pos + 1; 535 if (start >= fragment.length()) { 536 break; 537 } 538 } 539 } 540 } 541 } 542 543 /** 544 * Returns a boolean indicating whether the table UI should be shown. 545 */ 546 public static boolean useTabletUI(Resources res) { 547 return res.getInteger(R.integer.use_tablet_ui) != 0; 548 } 549 550 /** 551 * @return <code>true</code> if the right edge effect should be displayed on list items 552 */ 553 public static boolean getDisplayListRightEdgeEffect(final boolean tabletDevice, 554 final boolean listCollapsible, final int viewMode) { 555 return tabletDevice && !listCollapsible 556 && (ViewMode.isConversationMode(viewMode) || ViewMode.isAdMode(viewMode)); 557 } 558 559 /** 560 * Returns a boolean indicating whether or not we should animate in the 561 * folder list fragment. 562 */ 563 public static boolean useFolderListFragmentTransition(Context context) { 564 if (sUseFolderListFragmentTransition == -1) { 565 sUseFolderListFragmentTransition = context.getResources().getInteger( 566 R.integer.use_folder_list_fragment_transition); 567 } 568 return sUseFolderListFragmentTransition != 0; 569 } 570 571 /** 572 * Returns displayable text from the provided HTML string. 573 * @param htmlText HTML string 574 * @return Plain text string representation of the specified Html string 575 */ 576 public static String convertHtmlToPlainText(String htmlText) { 577 if (TextUtils.isEmpty(htmlText)) { 578 return ""; 579 } 580 return getHtmlTree(htmlText, new HtmlParser(), new HtmlTreeBuilder()).getPlainText(); 581 } 582 583 public static String convertHtmlToPlainText(String htmlText, HtmlParser parser, 584 HtmlTreeBuilder builder) { 585 if (TextUtils.isEmpty(htmlText)) { 586 return ""; 587 } 588 return getHtmlTree(htmlText, parser, builder).getPlainText(); 589 } 590 591 /** 592 * Returns a {@link HtmlTree} representation of the specified HTML string. 593 */ 594 public static HtmlTree getHtmlTree(String htmlText) { 595 return getHtmlTree(htmlText, new HtmlParser(), new HtmlTreeBuilder()); 596 } 597 598 /** 599 * Returns a {@link HtmlTree} representation of the specified HTML string. 600 */ 601 private static HtmlTree getHtmlTree(String htmlText, HtmlParser parser, 602 HtmlTreeBuilder builder) { 603 final HtmlDocument doc = parser.parse(htmlText); 604 doc.accept(builder); 605 606 return builder.getTree(); 607 } 608 609 /** 610 * Perform a simulated measure pass on the given child view, assuming the 611 * child has a ViewGroup parent and that it should be laid out within that 612 * parent with a matching width but variable height. Code largely lifted 613 * from AnimatedAdapter.measureChildHeight(). 614 * 615 * @param child a child view that has already been placed within its parent 616 * ViewGroup 617 * @param parent the parent ViewGroup of child 618 * @return measured height of the child in px 619 */ 620 public static int measureViewHeight(View child, ViewGroup parent) { 621 final ViewGroup.LayoutParams lp = child.getLayoutParams(); 622 final int childSideMargin; 623 if (lp instanceof MarginLayoutParams) { 624 final MarginLayoutParams mlp = (MarginLayoutParams) lp; 625 childSideMargin = mlp.leftMargin + mlp.rightMargin; 626 } else { 627 childSideMargin = 0; 628 } 629 630 final int parentWSpec = MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY); 631 final int wSpec = ViewGroup.getChildMeasureSpec(parentWSpec, 632 parent.getPaddingLeft() + parent.getPaddingRight() + childSideMargin, 633 ViewGroup.LayoutParams.MATCH_PARENT); 634 final int hSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 635 child.measure(wSpec, hSpec); 636 return child.getMeasuredHeight(); 637 } 638 639 /** 640 * Encode the string in HTML. 641 * 642 * @param removeEmptyDoubleQuotes If true, also remove any occurrence of "" 643 * found in the string 644 */ 645 public static Object cleanUpString(String string, boolean removeEmptyDoubleQuotes) { 646 return !TextUtils.isEmpty(string) ? TextUtils.htmlEncode(removeEmptyDoubleQuotes ? string 647 .replace("\"\"", "") : string) : ""; 648 } 649 650 /** 651 * Get the correct display string for the unread count of a folder. 652 */ 653 public static String getUnreadCountString(Context context, int unreadCount) { 654 final String unreadCountString; 655 final Resources resources = context.getResources(); 656 if (sMaxUnreadCount == -1) { 657 sMaxUnreadCount = resources.getInteger(R.integer.maxUnreadCount); 658 } 659 if (unreadCount > sMaxUnreadCount) { 660 if (sUnreadText == null) { 661 sUnreadText = resources.getString(R.string.widget_large_unread_count); 662 } 663 // Localize "999+" according to the device language 664 unreadCountString = String.format(sUnreadText, sMaxUnreadCount); 665 } else if (unreadCount <= 0) { 666 unreadCountString = ""; 667 } else { 668 // Localize unread count according to the device language 669 unreadCountString = String.format("%d", unreadCount); 670 } 671 return unreadCountString; 672 } 673 674 /** 675 * Get the correct display string for the unread count in the actionbar. 676 */ 677 public static CharSequence getUnreadMessageString(Context context, int unreadCount) { 678 final SpannableString message; 679 final Resources resources = context.getResources(); 680 if (sMaxUnreadCount == -1) { 681 sMaxUnreadCount = resources.getInteger(R.integer.maxUnreadCount); 682 } 683 if (unreadCount > sMaxUnreadCount) { 684 message = new SpannableString( 685 resources.getString(R.string.actionbar_large_unread_count, sMaxUnreadCount)); 686 } else { 687 message = new SpannableString(resources.getQuantityString( 688 R.plurals.actionbar_unread_messages, unreadCount, unreadCount)); 689 } 690 691 message.setSpan(CharacterStyle.wrap(ACTION_BAR_UNREAD_STYLE), 0, 692 message.toString().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 693 694 return message; 695 } 696 697 /** 698 * Get text matching the last sync status. 699 */ 700 public static CharSequence getSyncStatusText(Context context, int packedStatus) { 701 final String[] errors = context.getResources().getStringArray(R.array.sync_status); 702 final int status = packedStatus & 0x0f; 703 if (status >= errors.length) { 704 return ""; 705 } 706 return errors[status]; 707 } 708 709 /** 710 * Create an intent to show a conversation. 711 * @param conversation Conversation to open. 712 * @param folder 713 * @param account 714 * @return 715 */ 716 public static Intent createViewConversationIntent(final Context context, 717 Conversation conversation, final Uri folderUri, Account account) { 718 final Intent intent = new Intent(Intent.ACTION_VIEW); 719 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK 720 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); 721 final Uri versionedUri = appendVersionQueryParameter(context, conversation.uri); 722 // We need the URI to be unique, even if it's for the same message, so append the folder URI 723 final Uri uniqueUri = versionedUri.buildUpon().appendQueryParameter( 724 FOLDER_URI_QUERY_PARAMETER, folderUri.toString()).build(); 725 intent.setDataAndType(uniqueUri, account.mimeType); 726 intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize()); 727 intent.putExtra(Utils.EXTRA_FOLDER_URI, folderUri); 728 intent.putExtra(Utils.EXTRA_CONVERSATION, conversation); 729 return intent; 730 } 731 732 /** 733 * Create an intent to open a folder. 734 * 735 * @param folder Folder to open. 736 * @param account 737 * @return 738 */ 739 public static Intent createViewFolderIntent(final Context context, final Uri folderUri, 740 Account account) { 741 if (folderUri == null || account == null) { 742 LogUtils.wtf(LOG_TAG, "Utils.createViewFolderIntent(%s,%s): Bad input", folderUri, 743 account); 744 return null; 745 } 746 final Intent intent = new Intent(Intent.ACTION_VIEW); 747 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK 748 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); 749 intent.setDataAndType(appendVersionQueryParameter(context, folderUri), account.mimeType); 750 intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize()); 751 intent.putExtra(Utils.EXTRA_FOLDER_URI, folderUri); 752 return intent; 753 } 754 755 /** 756 * Creates an intent to open the default inbox for the given account. 757 * 758 * @param account 759 * @return 760 */ 761 public static Intent createViewInboxIntent(Account account) { 762 if (account == null) { 763 LogUtils.wtf(LOG_TAG, "Utils.createViewInboxIntent(%s): Bad input", account); 764 return null; 765 } 766 final Intent intent = new Intent(Intent.ACTION_VIEW); 767 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK 768 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); 769 intent.setDataAndType(account.settings.defaultInbox, account.mimeType); 770 intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize()); 771 return intent; 772 } 773 774 /** 775 * Helper method to show context-aware Gmail help. 776 * 777 * @param context Context to be used to open the help. 778 * @param fromWhere Information about the activity the user was in 779 * when they requested help. 780 */ 781 public static void showHelp(Context context, Account account, String fromWhere) { 782 final String urlString = (account != null && account.helpIntentUri != null) ? 783 account.helpIntentUri.toString() : null; 784 if (TextUtils.isEmpty(urlString) ) { 785 LogUtils.e(LOG_TAG, "unable to show help for account: %s", account); 786 return; 787 } 788 final Uri uri = addParamsToUrl(context, urlString); 789 Uri.Builder builder = uri.buildUpon(); 790 // Add the activity specific information parameter. 791 if (!TextUtils.isEmpty(fromWhere)) { 792 builder = builder.appendQueryParameter(SMART_HELP_LINK_PARAMETER_NAME, fromWhere); 793 } 794 795 openUrl(context, builder.build(), null); 796 } 797 798 /** 799 * Helper method to open a link in a browser. 800 * 801 * @param context Context 802 * @param uri Uri to open. 803 */ 804 private static void openUrl(Context context, Uri uri, Bundle optionalExtras) { 805 if(uri == null || TextUtils.isEmpty(uri.toString())) { 806 LogUtils.wtf(LOG_TAG, "invalid url in Utils.openUrl(): %s", uri); 807 return; 808 } 809 final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 810 // Fill in any of extras that have been requested. 811 if (optionalExtras != null) { 812 intent.putExtras(optionalExtras); 813 } 814 intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); 815 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 816 817 context.startActivity(intent); 818 } 819 820 821 private static Uri addParamsToUrl(Context context, String url) { 822 url = replaceLocale(url); 823 Uri.Builder builder = Uri.parse(url).buildUpon(); 824 final int versionCode = getVersionCode(context); 825 if (versionCode != -1) { 826 builder = builder.appendQueryParameter(SMART_LINK_APP_VERSION, 827 String.valueOf(versionCode)); 828 } 829 830 return builder.build(); 831 } 832 833 /** 834 * Replaces the language/country of the device into the given string. The pattern "%locale%" 835 * will be replaced with the <language_code>_<country_code> value. 836 * 837 * @param str the string to replace the language/country within 838 * 839 * @return the string with replacement 840 */ 841 private static String replaceLocale(String str) { 842 // Substitute locale if present in string 843 if (str.contains("%locale%")) { 844 Locale locale = Locale.getDefault(); 845 String tmp = locale.getLanguage() + "_" + locale.getCountry().toLowerCase(); 846 str = str.replace("%locale%", tmp); 847 } 848 return str; 849 } 850 851 /** 852 * Returns the version code for the package, or -1 if it cannot be retrieved. 853 */ 854 public static int getVersionCode(Context context) { 855 if (sVersionCode == -1) { 856 try { 857 sVersionCode = 858 context.getPackageManager().getPackageInfo(context.getPackageName(), 859 0 /* flags */).versionCode; 860 } catch (NameNotFoundException e) { 861 LogUtils.e(Utils.LOG_TAG, "Error finding package %s", 862 context.getApplicationInfo().packageName); 863 } 864 } 865 return sVersionCode; 866 } 867 868 /** 869 * Show the top level settings screen for the supplied account. 870 */ 871 public static void showSettings(Context context, Account account) { 872 if (account == null) { 873 LogUtils.e(LOG_TAG, "Invalid attempt to show setting screen with null account"); 874 return; 875 } 876 final Intent settingsIntent = new Intent(Intent.ACTION_EDIT, account.settingsIntentUri); 877 settingsIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 878 context.startActivity(settingsIntent); 879 } 880 881 /** 882 * Show the account level settings screen for the supplied account. 883 */ 884 public static void showAccountSettings(Context context, Account account) { 885 if (account == null) { 886 LogUtils.e(LOG_TAG, "Invalid attempt to show setting screen with null account"); 887 return; 888 } 889 final Intent settingsIntent = new Intent(Intent.ACTION_EDIT, 890 appendVersionQueryParameter(context, account.settingsIntentUri)); 891 892 settingsIntent.putExtra(EditSettingsExtras.EXTRA_ACCOUNT, account); 893 settingsIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 894 context.startActivity(settingsIntent); 895 } 896 897 /** 898 * Show the settings screen for the supplied account. 899 */ 900 public static void showFolderSettings(Context context, Account account, Folder folder) { 901 if (account == null || folder == null) { 902 LogUtils.e(LOG_TAG, "Invalid attempt to show folder settings. account: %s folder: %s", 903 account, folder); 904 return; 905 } 906 final Intent settingsIntent = new Intent(Intent.ACTION_EDIT, 907 appendVersionQueryParameter(context, account.settingsIntentUri)); 908 909 settingsIntent.putExtra(EditSettingsExtras.EXTRA_ACCOUNT, account); 910 settingsIntent.putExtra(EditSettingsExtras.EXTRA_FOLDER, folder); 911 settingsIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 912 context.startActivity(settingsIntent); 913 } 914 915 /** 916 * Show the settings screen for managing all folders. 917 */ 918 public static void showManageFolder(Context context, Account account) { 919 if (account == null) { 920 LogUtils.e(LOG_TAG, "Invalid attempt to the manage folders screen with null account"); 921 return; 922 } 923 final Intent settingsIntent = new Intent(Intent.ACTION_EDIT, account.settingsIntentUri); 924 925 settingsIntent.putExtra(EditSettingsExtras.EXTRA_ACCOUNT, account); 926 settingsIntent.putExtra(EditSettingsExtras.EXTRA_MANAGE_FOLDERS, true); 927 settingsIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 928 context.startActivity(settingsIntent); 929 } 930 931 /** 932 * Show the feedback screen for the supplied account. 933 */ 934 public static void sendFeedback(FeedbackEnabledActivity activity, Account account, 935 boolean reportingProblem) { 936 if (activity != null && account != null) { 937 sendFeedback(activity, account.sendFeedbackIntentUri, reportingProblem); 938 } 939 } 940 public static void sendFeedback(FeedbackEnabledActivity activity, Uri feedbackIntentUri, 941 boolean reportingProblem) { 942 if (activity != null && !isEmpty(feedbackIntentUri)) { 943 final Bundle optionalExtras = new Bundle(2); 944 optionalExtras.putBoolean( 945 UIProvider.SendFeedbackExtras.EXTRA_REPORTING_PROBLEM, reportingProblem); 946 final Bitmap screenBitmap = getReducedSizeBitmap(activity); 947 if (screenBitmap != null) { 948 optionalExtras.putParcelable( 949 UIProvider.SendFeedbackExtras.EXTRA_SCREEN_SHOT, screenBitmap); 950 } 951 openUrl(activity.getActivityContext(), feedbackIntentUri, optionalExtras); 952 } 953 } 954 955 956 public static Bitmap getReducedSizeBitmap(FeedbackEnabledActivity activity) { 957 final Window activityWindow = activity.getWindow(); 958 final View currentView = activityWindow != null ? activityWindow.getDecorView() : null; 959 final View rootView = currentView != null ? currentView.getRootView() : null; 960 if (rootView != null) { 961 rootView.setDrawingCacheEnabled(true); 962 final Bitmap drawingCache = rootView.getDrawingCache(); 963 // Null check to avoid NPE discovered from monkey crash: 964 if (drawingCache != null) { 965 final Bitmap originalBitmap = drawingCache.copy(Bitmap.Config.RGB_565, false); 966 double originalHeight = originalBitmap.getHeight(); 967 double originalWidth = originalBitmap.getWidth(); 968 int newHeight = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH; 969 int newWidth = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH; 970 double scaleX, scaleY; 971 scaleX = newWidth / originalWidth; 972 scaleY = newHeight / originalHeight; 973 final double scale = Math.min(scaleX, scaleY); 974 newWidth = (int)Math.round(originalWidth * scale); 975 newHeight = (int)Math.round(originalHeight * scale); 976 return Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true); 977 } 978 } 979 return null; 980 } 981 982 /** 983 * Retrieves the mailbox search query associated with an intent (or null if not available), 984 * doing proper sanitizing (e.g. trims whitespace). 985 */ 986 public static String mailSearchQueryForIntent(Intent intent) { 987 String query = intent.getStringExtra(SearchManager.QUERY); 988 return TextUtils.isEmpty(query) ? null : query.trim(); 989 } 990 991 /** 992 * Split out a filename's extension and return it. 993 * @param filename a file name 994 * @return the file extension (max of 5 chars including period, like ".docx"), or null 995 */ 996 public static String getFileExtension(String filename) { 997 String extension = null; 998 int index = !TextUtils.isEmpty(filename) ? filename.lastIndexOf('.') : -1; 999 // Limit the suffix to dot + four characters 1000 if (index >= 0 && filename.length() - index <= FILE_EXTENSION_MAX_CHARS + 1) { 1001 extension = filename.substring(index); 1002 } 1003 return extension; 1004 } 1005 1006 /** 1007 * (copied from {@link Intent#normalizeMimeType(String)} for pre-J) 1008 * 1009 * Normalize a MIME data type. 1010 * 1011 * <p>A normalized MIME type has white-space trimmed, 1012 * content-type parameters removed, and is lower-case. 1013 * This aligns the type with Android best practices for 1014 * intent filtering. 1015 * 1016 * <p>For example, "text/plain; charset=utf-8" becomes "text/plain". 1017 * "text/x-vCard" becomes "text/x-vcard". 1018 * 1019 * <p>All MIME types received from outside Android (such as user input, 1020 * or external sources like Bluetooth, NFC, or the Internet) should 1021 * be normalized before they are used to create an Intent. 1022 * 1023 * @param type MIME data type to normalize 1024 * @return normalized MIME data type, or null if the input was null 1025 * @see {@link #setType} 1026 * @see {@link #setTypeAndNormalize} 1027 */ 1028 public static String normalizeMimeType(String type) { 1029 if (type == null) { 1030 return null; 1031 } 1032 1033 type = type.trim().toLowerCase(Locale.US); 1034 1035 final int semicolonIndex = type.indexOf(';'); 1036 if (semicolonIndex != -1) { 1037 type = type.substring(0, semicolonIndex); 1038 } 1039 return type; 1040 } 1041 1042 /** 1043 * (copied from {@link Uri#normalize()} for pre-J) 1044 * 1045 * Return a normalized representation of this Uri. 1046 * 1047 * <p>A normalized Uri has a lowercase scheme component. 1048 * This aligns the Uri with Android best practices for 1049 * intent filtering. 1050 * 1051 * <p>For example, "HTTP://www.android.com" becomes 1052 * "http://www.android.com" 1053 * 1054 * <p>All URIs received from outside Android (such as user input, 1055 * or external sources like Bluetooth, NFC, or the Internet) should 1056 * be normalized before they are used to create an Intent. 1057 * 1058 * <p class="note">This method does <em>not</em> validate bad URI's, 1059 * or 'fix' poorly formatted URI's - so do not use it for input validation. 1060 * A Uri will always be returned, even if the Uri is badly formatted to 1061 * begin with and a scheme component cannot be found. 1062 * 1063 * @return normalized Uri (never null) 1064 * @see {@link android.content.Intent#setData} 1065 * @see {@link #setNormalizedData} 1066 */ 1067 public static Uri normalizeUri(Uri uri) { 1068 String scheme = uri.getScheme(); 1069 if (scheme == null) return uri; // give up 1070 String lowerScheme = scheme.toLowerCase(Locale.US); 1071 if (scheme.equals(lowerScheme)) return uri; // no change 1072 1073 return uri.buildUpon().scheme(lowerScheme).build(); 1074 } 1075 1076 public static Intent setIntentTypeAndNormalize(Intent intent, String type) { 1077 return intent.setType(normalizeMimeType(type)); 1078 } 1079 1080 public static Intent setIntentDataAndTypeAndNormalize(Intent intent, Uri data, String type) { 1081 return intent.setDataAndType(normalizeUri(data), normalizeMimeType(type)); 1082 } 1083 1084 public static int getTransparentColor(int color) { 1085 return 0x00ffffff & color; 1086 } 1087 1088 public static void setMenuItemVisibility(Menu menu, int itemId, boolean shouldShow) { 1089 final MenuItem item = menu.findItem(itemId); 1090 if (item == null) { 1091 return; 1092 } 1093 item.setVisible(shouldShow); 1094 } 1095 1096 /** 1097 * Parse a string (possibly null or empty) into a URI. If the string is null 1098 * or empty, null is returned back. Otherwise an empty URI is returned. 1099 * 1100 * @param uri 1101 * @return a valid URI, possibly {@link android.net.Uri#EMPTY} 1102 */ 1103 public static Uri getValidUri(String uri) { 1104 if (TextUtils.isEmpty(uri) || uri == JSONObject.NULL) 1105 return Uri.EMPTY; 1106 return Uri.parse(uri); 1107 } 1108 1109 public static boolean isEmpty(Uri uri) { 1110 return uri == null || uri.equals(Uri.EMPTY); 1111 } 1112 1113 public static String dumpFragment(Fragment f) { 1114 final StringWriter sw = new StringWriter(); 1115 f.dump("", new FileDescriptor(), new PrintWriter(sw), new String[0]); 1116 return sw.toString(); 1117 } 1118 1119 public static void dumpViewTree(ViewGroup root) { 1120 dumpViewTree(root, ""); 1121 } 1122 1123 private static void dumpViewTree(ViewGroup g, String prefix) { 1124 LogUtils.i(LOG_TAG, "%sVIEWGROUP: %s childCount=%s", prefix, g, g.getChildCount()); 1125 final String childPrefix = prefix + " "; 1126 for (int i = 0; i < g.getChildCount(); i++) { 1127 final View child = g.getChildAt(i); 1128 if (child instanceof ViewGroup) { 1129 dumpViewTree((ViewGroup) child, childPrefix); 1130 } else { 1131 LogUtils.i(LOG_TAG, "%sCHILD #%s: %s", childPrefix, i, child); 1132 } 1133 } 1134 } 1135 1136 /** 1137 * Executes an out-of-band command on the cursor. 1138 * @param cursor 1139 * @param request Bundle with all keys and values set for the command. 1140 * @param key The string value against which we will check for success or failure 1141 * @return true if the operation was a success. 1142 */ 1143 private static boolean executeConversationCursorCommand( 1144 Cursor cursor, Bundle request, String key) { 1145 final Bundle response = cursor.respond(request); 1146 final String result = response.getString(key, 1147 UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_FAILED); 1148 1149 return UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK.equals(result); 1150 } 1151 1152 /** 1153 * Commands a cursor representing a set of conversations to indicate that an item is being shown 1154 * in the UI. 1155 * 1156 * @param cursor a conversation cursor 1157 * @param position position of the item being shown. 1158 */ 1159 public static boolean notifyCursorUIPositionChange(Cursor cursor, int position) { 1160 final Bundle request = new Bundle(); 1161 final String key = 1162 UIProvider.ConversationCursorCommand.COMMAND_NOTIFY_CURSOR_UI_POSITION_CHANGE; 1163 request.putInt(key, position); 1164 return executeConversationCursorCommand(cursor, request, key); 1165 } 1166 1167 /** 1168 * Commands a cursor representing a set of conversations to set its visibility state. 1169 * 1170 * @param cursor a conversation cursor 1171 * @param visible true if the conversation list is visible, false otherwise. 1172 * @param isFirstSeen true if you want to notify the cursor that this conversation list was seen 1173 * for the first time: the user launched the app into it, or the user switched from some 1174 * other folder into it. 1175 */ 1176 public static void setConversationCursorVisibility( 1177 Cursor cursor, boolean visible, boolean isFirstSeen) { 1178 new MarkConversationCursorVisibleTask(cursor, visible, isFirstSeen).execute(); 1179 } 1180 1181 /** 1182 * Async task for marking conversations "seen" and informing the cursor that the folder was 1183 * seen for the first time by the UI. 1184 */ 1185 private static class MarkConversationCursorVisibleTask extends AsyncTask<Void, Void, Void> { 1186 private final Cursor mCursor; 1187 private final boolean mVisible; 1188 private final boolean mIsFirstSeen; 1189 1190 /** 1191 * Create a new task with the given cursor, with the given visibility and 1192 * 1193 * @param cursor 1194 * @param isVisible true if the conversation list is visible, false otherwise. 1195 * @param isFirstSeen true if the folder was shown for the first time: either the user has 1196 * just switched to it, or the user started the app in this folder. 1197 */ 1198 public MarkConversationCursorVisibleTask( 1199 Cursor cursor, boolean isVisible, boolean isFirstSeen) { 1200 mCursor = cursor; 1201 mVisible = isVisible; 1202 mIsFirstSeen = isFirstSeen; 1203 } 1204 1205 @Override 1206 protected Void doInBackground(Void... params) { 1207 if (mCursor == null) { 1208 return null; 1209 } 1210 final Bundle request = new Bundle(); 1211 if (mIsFirstSeen) { 1212 request.putBoolean( 1213 UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER, true); 1214 } 1215 final String key = UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY; 1216 request.putBoolean(key, mVisible); 1217 executeConversationCursorCommand(mCursor, request, key); 1218 return null; 1219 } 1220 } 1221 1222 1223 /** 1224 * This utility method returns the conversation ID at the current cursor position. 1225 * @return the conversation id at the cursor. 1226 */ 1227 public static long getConversationId(ConversationCursor cursor) { 1228 return cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN); 1229 } 1230 1231 /** 1232 * This utility method returns the conversation Uri at the current cursor position. 1233 * @return the conversation id at the cursor. 1234 */ 1235 public static String getConversationUri(ConversationCursor cursor) { 1236 return cursor.getString(UIProvider.CONVERSATION_URI_COLUMN); 1237 } 1238 1239 /** 1240 * @return whether to show two pane or single pane search results. 1241 */ 1242 public static boolean showTwoPaneSearchResults(Context context) { 1243 return context.getResources().getBoolean(R.bool.show_two_pane_search_results); 1244 } 1245 1246 /** 1247 * Sets the layer type of a view to hardware if the view is attached and hardware acceleration 1248 * is enabled. Does nothing otherwise. 1249 */ 1250 public static void enableHardwareLayer(View v) { 1251 if (v != null && v.isHardwareAccelerated()) { 1252 v.setLayerType(View.LAYER_TYPE_HARDWARE, null); 1253 v.buildLayer(); 1254 } 1255 } 1256 1257 /** 1258 * Return whether menus should show the disabled archive menu item or just 1259 * remove it when archive is not available. 1260 */ 1261 public static boolean shouldShowDisabledArchiveIcon(Context context) { 1262 return context.getResources().getBoolean(R.bool.show_disabled_archive_menu_item); 1263 } 1264 1265 public static int getDefaultFolderBackgroundColor(Context context) { 1266 if (sDefaultFolderBackgroundColor == -1) { 1267 sDefaultFolderBackgroundColor = context.getResources().getColor( 1268 R.color.default_folder_background_color); 1269 } 1270 return sDefaultFolderBackgroundColor; 1271 } 1272 1273 /** 1274 * Returns the count that should be shown for the specified folder. This method should be used 1275 * when the UI wants to display an "unread" count. For most labels, the returned value will be 1276 * the unread count, but for some folder types (outbox, drafts, trash) this will return the 1277 * total count. 1278 */ 1279 public static int getFolderUnreadDisplayCount(final Folder folder) { 1280 if (folder != null) { 1281 if (folder.isUnreadCountHidden()) { 1282 return folder.totalCount; 1283 } else { 1284 return folder.unreadCount; 1285 } 1286 } 1287 return 0; 1288 } 1289 1290 /** 1291 * @return an intent which, if launched, will reply to the conversation 1292 */ 1293 public static Intent createReplyIntent(final Context context, final Account account, 1294 final Uri messageUri, final boolean isReplyAll) { 1295 final Intent intent = 1296 ComposeActivity.createReplyIntent(context, account, messageUri, isReplyAll); 1297 return intent; 1298 } 1299 1300 /** 1301 * @return an intent which, if launched, will forward the conversation 1302 */ 1303 public static Intent createForwardIntent( 1304 final Context context, final Account account, final Uri messageUri) { 1305 final Intent intent = ComposeActivity.createForwardIntent(context, account, messageUri); 1306 return intent; 1307 } 1308 1309 public static Uri appendVersionQueryParameter(final Context context, final Uri uri) { 1310 int appVersion = 0; 1311 1312 try { 1313 final PackageInfo packageInfo = 1314 context.getPackageManager().getPackageInfo(context.getPackageName(), 0); 1315 appVersion = packageInfo.versionCode; 1316 } catch (final NameNotFoundException e) { 1317 LogUtils.wtf(LOG_TAG, e, "Couldn't find our own PackageInfo"); 1318 } 1319 1320 return uri.buildUpon().appendQueryParameter(APP_VERSION_QUERY_PARAMETER, 1321 Integer.toString(appVersion)).build(); 1322 } 1323 1324 /** 1325 * Convenience method for diverting mailto: uris directly to our compose activity. Using this 1326 * method ensures that the Account object is not accidentally sent to a different process. 1327 * 1328 * @param context for sending the intent 1329 * @param uri mailto: or other uri 1330 * @param account desired account for potential compose activity 1331 * @return true if a compose activity was started, false if uri should be sent to a view intent 1332 */ 1333 public static boolean divertMailtoUri(final Context context, final Uri uri, 1334 final Account account) { 1335 final String scheme = normalizeUri(uri).getScheme(); 1336 if (TextUtils.equals(MAILTO_SCHEME, scheme)) { 1337 ComposeActivity.composeToAddress(context, account, uri.getSchemeSpecificPart()); 1338 return true; 1339 } 1340 return false; 1341 } 1342 1343 /** 1344 * Gets the specified {@link Folder} object. 1345 * 1346 * @param folderUri The {@link Uri} for the folder 1347 * @param allowHidden <code>true</code> to allow a hidden folder to be returned, 1348 * <code>false</code> to return <code>null</code> instead 1349 * @return the specified {@link Folder} object, or <code>null</code> 1350 */ 1351 public static Folder getFolder(final Context context, final Uri folderUri, 1352 final boolean allowHidden) { 1353 final Uri uri = folderUri 1354 .buildUpon() 1355 .appendQueryParameter(UIProvider.ALLOW_HIDDEN_FOLDERS_QUERY_PARAM, 1356 Boolean.toString(allowHidden)) 1357 .build(); 1358 1359 final Cursor cursor = context.getContentResolver().query(uri, 1360 UIProvider.FOLDERS_PROJECTION, null, null, null); 1361 1362 if (cursor == null) { 1363 return null; 1364 } 1365 1366 try { 1367 if (cursor.moveToFirst()) { 1368 return new Folder(cursor); 1369 } else { 1370 return null; 1371 } 1372 } finally { 1373 cursor.close(); 1374 } 1375 } 1376 1377 /** 1378 * Begins systrace tracing for a given tag. No-op on unsupported platform versions. 1379 * 1380 * @param tag systrace tag to use 1381 * 1382 * @see android.os.Trace#beginSection(String) 1383 */ 1384 public static void traceBeginSection(String tag) { 1385 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { 1386 android.os.Trace.beginSection(tag); 1387 } 1388 } 1389 1390 /** 1391 * Ends systrace tracing for the most recently begun section. No-op on unsupported platform 1392 * versions. 1393 * 1394 * @see android.os.Trace#endSection() 1395 */ 1396 public static void traceEndSection() { 1397 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { 1398 android.os.Trace.endSection(); 1399 } 1400 } 1401 1402 /** 1403 * Get the background color of Gmail's action bar. 1404 */ 1405 public static int getActionBarBackgroundResource(final Context context) { 1406 final TypedValue actionBarStyle = new TypedValue(); 1407 if (context.getTheme().resolveAttribute(android.R.attr.actionBarStyle, actionBarStyle, true) 1408 && actionBarStyle.type == TypedValue.TYPE_REFERENCE) { 1409 final TypedValue backgroundValue = new TypedValue(); 1410 final TypedArray attr = context.obtainStyledAttributes(actionBarStyle.resourceId, 1411 STYLE_ATTR); 1412 attr.getValue(0, backgroundValue); 1413 attr.recycle(); 1414 return backgroundValue.resourceId; 1415 } else { 1416 // Default color 1417 return context.getResources().getColor(R.color.list_background_color); 1418 } 1419 } 1420 1421 /** 1422 * Email addresses are supposed to be treated as case-insensitive for the host-part and 1423 * case-sensitive for the local-part, but nobody really wants email addresses to match 1424 * case-sensitive on the local-part, so just smash everything to lower case. 1425 * @param email Hello (at) Example.COM 1426 * @return hello (at) example.com 1427 */ 1428 public static String normalizeEmailAddress(String email) { 1429 /* 1430 // The RFC5321 version 1431 if (TextUtils.isEmpty(email)) { 1432 return email; 1433 } 1434 String[] parts = email.split("@"); 1435 if (parts.length != 2) { 1436 LogUtils.d(LOG_TAG, "Tried to normalize a malformed email address: ", email); 1437 return email; 1438 } 1439 1440 return parts[0] + "@" + parts[1].toLowerCase(Locale.US); 1441 */ 1442 if (TextUtils.isEmpty(email)) { 1443 return email; 1444 } else { 1445 // Doing this for other locales might really screw things up, so do US-version only 1446 return email.toLowerCase(Locale.US); 1447 } 1448 } 1449 } 1450