Home | History | Annotate | Download | only in utils
      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