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
17package com.android.mail.utils;
18
19import com.google.android.mail.common.html.parser.HtmlDocument;
20import com.google.android.mail.common.html.parser.HtmlParser;
21import com.google.android.mail.common.html.parser.HtmlTree;
22import com.google.android.mail.common.html.parser.HtmlTreeBuilder;
23import com.google.common.collect.Maps;
24
25import android.app.Fragment;
26import android.app.SearchManager;
27import android.content.Context;
28import android.content.Intent;
29import android.content.pm.PackageInfo;
30import android.content.pm.PackageManager.NameNotFoundException;
31import android.content.res.Resources;
32import android.content.res.TypedArray;
33import android.database.Cursor;
34import android.graphics.Bitmap;
35import android.graphics.Typeface;
36import android.net.Uri;
37import android.os.AsyncTask;
38import android.os.Build;
39import android.os.Bundle;
40import android.provider.Browser;
41import android.text.Spannable;
42import android.text.SpannableString;
43import android.text.SpannableStringBuilder;
44import android.text.Spanned;
45import android.text.TextUtils;
46import android.text.TextUtils.SimpleStringSplitter;
47import android.text.style.CharacterStyle;
48import android.text.style.ForegroundColorSpan;
49import android.text.style.StyleSpan;
50import android.util.TypedValue;
51import android.view.Menu;
52import android.view.MenuItem;
53import android.view.View;
54import android.view.View.MeasureSpec;
55import android.view.ViewGroup;
56import android.view.ViewGroup.MarginLayoutParams;
57import android.view.Window;
58import android.webkit.WebSettings;
59import android.webkit.WebView;
60
61import com.android.mail.R;
62import com.android.mail.browse.ConversationCursor;
63import com.android.mail.compose.ComposeActivity;
64import com.android.mail.perf.SimpleTimer;
65import com.android.mail.providers.Account;
66import com.android.mail.providers.Conversation;
67import com.android.mail.providers.Folder;
68import com.android.mail.providers.UIProvider;
69import com.android.mail.providers.UIProvider.EditSettingsExtras;
70import com.android.mail.ui.FeedbackEnabledActivity;
71import com.android.mail.ui.ViewMode;
72
73import org.json.JSONObject;
74
75import java.io.FileDescriptor;
76import java.io.PrintWriter;
77import java.io.StringWriter;
78import java.util.Locale;
79import java.util.Map;
80
81public 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@Example.COM
1426     * @return hello@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