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 android.annotation.TargetApi;
     20 import android.app.Activity;
     21 import android.app.ActivityManager;
     22 import android.app.Fragment;
     23 import android.app.SearchManager;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.pm.PackageManager.NameNotFoundException;
     27 import android.content.res.Resources;
     28 import android.database.Cursor;
     29 import android.graphics.Bitmap;
     30 import android.graphics.Typeface;
     31 import android.net.ConnectivityManager;
     32 import android.net.NetworkInfo;
     33 import android.net.Uri;
     34 import android.os.AsyncTask;
     35 import android.os.Build;
     36 import android.os.Bundle;
     37 import android.provider.Browser;
     38 import android.text.Spannable;
     39 import android.text.SpannableString;
     40 import android.text.Spanned;
     41 import android.text.TextUtils;
     42 import android.text.style.CharacterStyle;
     43 import android.text.style.StyleSpan;
     44 import android.text.style.TextAppearanceSpan;
     45 import android.view.Menu;
     46 import android.view.MenuItem;
     47 import android.view.View;
     48 import android.view.View.MeasureSpec;
     49 import android.view.ViewGroup;
     50 import android.view.ViewGroup.MarginLayoutParams;
     51 import android.view.Window;
     52 import android.webkit.WebSettings;
     53 import android.webkit.WebView;
     54 
     55 import com.android.emailcommon.mail.Address;
     56 import com.android.mail.R;
     57 import com.android.mail.browse.ConversationCursor;
     58 import com.android.mail.compose.ComposeActivity;
     59 import com.android.mail.perf.SimpleTimer;
     60 import com.android.mail.providers.Account;
     61 import com.android.mail.providers.Conversation;
     62 import com.android.mail.providers.Folder;
     63 import com.android.mail.providers.UIProvider;
     64 import com.android.mail.providers.UIProvider.EditSettingsExtras;
     65 import com.android.mail.ui.HelpActivity;
     66 import com.android.mail.ui.ViewMode;
     67 import com.google.android.mail.common.html.parser.HtmlDocument;
     68 import com.google.android.mail.common.html.parser.HtmlParser;
     69 import com.google.android.mail.common.html.parser.HtmlTree;
     70 import com.google.android.mail.common.html.parser.HtmlTreeBuilder;
     71 
     72 import org.json.JSONObject;
     73 
     74 import java.io.FileDescriptor;
     75 import java.io.PrintWriter;
     76 import java.io.StringWriter;
     77 import java.util.Locale;
     78 import java.util.Map;
     79 
     80 public class Utils {
     81     /**
     82      * longest extension we recognize is 4 characters (e.g. "html", "docx")
     83      */
     84     private static final int FILE_EXTENSION_MAX_CHARS = 4;
     85     public static final String SENDER_LIST_TOKEN_ELIDED = "e";
     86     public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n";
     87     public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d";
     88     public static final String SENDER_LIST_TOKEN_LITERAL = "l";
     89     public static final String SENDER_LIST_TOKEN_SENDING = "s";
     90     public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f";
     91     public static final Character SENDER_LIST_SEPARATOR = '\n';
     92 
     93     public static final String EXTRA_ACCOUNT = "account";
     94     public static final String EXTRA_ACCOUNT_URI = "accountUri";
     95     public static final String EXTRA_FOLDER_URI = "folderUri";
     96     public static final String EXTRA_FOLDER = "folder";
     97     public static final String EXTRA_COMPOSE_URI = "composeUri";
     98     public static final String EXTRA_CONVERSATION = "conversationUri";
     99     public static final String EXTRA_FROM_NOTIFICATION = "notification";
    100     public static final String EXTRA_IGNORE_INITIAL_CONVERSATION_LIMIT =
    101             "ignore-initial-conversation-limit";
    102 
    103     private static final String MAILTO_SCHEME = "mailto";
    104 
    105     /** Extra tag for debugging the blank fragment problem. */
    106     public static final String VIEW_DEBUGGING_TAG = "MailBlankFragment";
    107 
    108     /*
    109      * Notifies that changes happened. Certain UI components, e.g., widgets, can
    110      * register for this {@link Intent} and update accordingly. However, this
    111      * can be very broad and is NOT the preferred way of getting notification.
    112      */
    113     // TODO: UI Provider has this notification URI?
    114     public static final String ACTION_NOTIFY_DATASET_CHANGED =
    115             "com.android.mail.ACTION_NOTIFY_DATASET_CHANGED";
    116 
    117     /** Parameter keys for context-aware help. */
    118     private static final String SMART_HELP_LINK_PARAMETER_NAME = "p";
    119 
    120     private static final String SMART_LINK_APP_VERSION = "version";
    121     private static String sVersionCode = null;
    122 
    123     private static final int SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH = 600;
    124 
    125     private static final String APP_VERSION_QUERY_PARAMETER = "appVersion";
    126     private static final String FOLDER_URI_QUERY_PARAMETER = "folderUri";
    127 
    128     private static final String LOG_TAG = LogTag.getLogTag();
    129 
    130     public static final boolean ENABLE_CONV_LOAD_TIMER = false;
    131     public static final SimpleTimer sConvLoadTimer =
    132             new SimpleTimer(ENABLE_CONV_LOAD_TIMER).withSessionName("ConvLoadTimer");
    133 
    134     private static final int[] STYLE_ATTR = new int[] {android.R.attr.background};
    135 
    136     public static boolean isRunningJellybeanOrLater() {
    137         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
    138     }
    139 
    140     public static boolean isRunningJBMR1OrLater() {
    141         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
    142     }
    143 
    144     public static boolean isRunningKitkatOrLater() {
    145         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    146     }
    147 
    148     public static boolean isRunningLOrLater() {
    149         //TODO: Update this to the L SDK once defined. Right now it is fine to use the watch
    150         // build version number, as this app woll not be running on watch devices
    151         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH;
    152     }
    153 
    154     /**
    155      * @return Whether we are running on a low memory device.  This is used to disable certain
    156      * memory intensive features in the app.
    157      */
    158     @TargetApi(Build.VERSION_CODES.KITKAT)
    159     public static boolean isLowRamDevice(Context context) {
    160         if (isRunningKitkatOrLater()) {
    161             final ActivityManager am = (ActivityManager) context.getSystemService(
    162                     Context.ACTIVITY_SERVICE);
    163             // This will be null when running unit tests
    164             return am != null && am.isLowRamDevice();
    165         } else {
    166             return false;
    167         }
    168     }
    169 
    170     /**
    171      * Sets WebView in a restricted mode suitable for email use.
    172      *
    173      * @param webView The WebView to restrict
    174      */
    175     public static void restrictWebView(WebView webView) {
    176         WebSettings webSettings = webView.getSettings();
    177         webSettings.setSavePassword(false);
    178         webSettings.setSaveFormData(false);
    179         webSettings.setJavaScriptEnabled(false);
    180         webSettings.setSupportZoom(false);
    181     }
    182 
    183     /**
    184      * Sets custom user agent to WebView so we don't get GAIA interstitials b/13990689.
    185      *
    186      * @param webView The WebView to customize.
    187      */
    188     public static void setCustomUserAgent(WebView webView, Context context) {
    189         final WebSettings settings = webView.getSettings();
    190         final String version = getVersionCode(context);
    191         final String originalUserAgent = settings.getUserAgentString();
    192         final String userAgent = context.getResources().getString(
    193                 R.string.user_agent_format, originalUserAgent, version);
    194         settings.setUserAgentString(userAgent);
    195     }
    196 
    197     /**
    198      * Returns the version code for the package, or null if it cannot be retrieved.
    199      */
    200     public static String getVersionCode(Context context) {
    201         if (sVersionCode == null) {
    202             try {
    203                 sVersionCode = String.valueOf(context.getPackageManager()
    204                         .getPackageInfo(context.getPackageName(), 0 /* flags */)
    205                         .versionCode);
    206             } catch (NameNotFoundException e) {
    207                 LogUtils.e(Utils.LOG_TAG, "Error finding package %s",
    208                         context.getApplicationInfo().packageName);
    209             }
    210         }
    211         return sVersionCode;
    212     }
    213 
    214     /**
    215      * Format a plural string.
    216      *
    217      * @param resource The identity of the resource, which must be a R.plurals
    218      * @param count The number of items.
    219      */
    220     public static String formatPlural(Context context, int resource, int count) {
    221         final CharSequence formatString = context.getResources().getQuantityText(resource, count);
    222         return String.format(formatString.toString(), count);
    223     }
    224 
    225     /**
    226      * @return an ellipsized String that's at most maxCharacters long. If the
    227      *         text passed is longer, it will be abbreviated. If it contains a
    228      *         suffix, the ellipses will be inserted in the middle and the
    229      *         suffix will be preserved.
    230      */
    231     public static String ellipsize(String text, int maxCharacters) {
    232         int length = text.length();
    233         if (length < maxCharacters)
    234             return text;
    235 
    236         int realMax = Math.min(maxCharacters, length);
    237         // Preserve the suffix if any
    238         int index = text.lastIndexOf(".");
    239         String extension = "\u2026"; // "...";
    240         if (index >= 0) {
    241             // Limit the suffix to dot + four characters
    242             if (length - index <= FILE_EXTENSION_MAX_CHARS + 1) {
    243                 extension = extension + text.substring(index + 1);
    244             }
    245         }
    246         realMax -= extension.length();
    247         if (realMax < 0)
    248             realMax = 0;
    249         return text.substring(0, realMax) + extension;
    250     }
    251 
    252     private static int sMaxUnreadCount = -1;
    253     private static final CharacterStyle ACTION_BAR_UNREAD_STYLE = new StyleSpan(Typeface.BOLD);
    254     private static String sUnreadText;
    255     private static String sUnseenText;
    256     private static String sLargeUnseenText;
    257     private static int sDefaultFolderBackgroundColor = -1;
    258     private static int sUseFolderListFragmentTransition = -1;
    259 
    260     /**
    261      * Returns a boolean indicating whether the table UI should be shown.
    262      */
    263     public static boolean useTabletUI(Resources res) {
    264         return res.getBoolean(R.bool.use_tablet_ui);
    265     }
    266 
    267     /**
    268      * @return <code>true</code> if the right edge effect should be displayed on list items
    269      */
    270     @Deprecated
    271     // TODO: remove this now that visual design no longer has right-edge caret (which made it so
    272     // the hard right edge was drawn IN list items to ensure the caret didn't get an edge)
    273     public static boolean getDisplayListRightEdgeEffect(final boolean tabletDevice,
    274             final boolean listCollapsible, final int viewMode) {
    275         return tabletDevice && !listCollapsible
    276                 && (ViewMode.isConversationMode(viewMode) || ViewMode.isAdMode(viewMode));
    277     }
    278 
    279     /**
    280      * Returns a boolean indicating whether or not we should animate in the
    281      * folder list fragment.
    282      */
    283     public static boolean useFolderListFragmentTransition(Context context) {
    284         if (sUseFolderListFragmentTransition == -1) {
    285             sUseFolderListFragmentTransition  = context.getResources().getInteger(
    286                     R.integer.use_folder_list_fragment_transition);
    287         }
    288         return sUseFolderListFragmentTransition != 0;
    289     }
    290 
    291     /**
    292      * Returns displayable text from the provided HTML string.
    293      * @param htmlText HTML string
    294      * @return Plain text string representation of the specified Html string
    295      */
    296     public static String convertHtmlToPlainText(String htmlText) {
    297         if (TextUtils.isEmpty(htmlText)) {
    298             return "";
    299         }
    300         return getHtmlTree(htmlText, new HtmlParser(), new HtmlTreeBuilder()).getPlainText();
    301     }
    302 
    303     public static String convertHtmlToPlainText(String htmlText, HtmlParser parser,
    304             HtmlTreeBuilder builder) {
    305         if (TextUtils.isEmpty(htmlText)) {
    306             return "";
    307         }
    308         return getHtmlTree(htmlText, parser, builder).getPlainText();
    309     }
    310 
    311     /**
    312      * Returns a {@link HtmlTree} representation of the specified HTML string.
    313      */
    314     public static HtmlTree getHtmlTree(String htmlText) {
    315         return getHtmlTree(htmlText, new HtmlParser(), new HtmlTreeBuilder());
    316     }
    317 
    318     /**
    319      * Returns a {@link HtmlTree} representation of the specified HTML string.
    320      */
    321     private static HtmlTree getHtmlTree(String htmlText, HtmlParser parser,
    322             HtmlTreeBuilder builder) {
    323         final HtmlDocument doc = parser.parse(htmlText);
    324         doc.accept(builder);
    325 
    326         return builder.getTree();
    327     }
    328 
    329     /**
    330      * Perform a simulated measure pass on the given child view, assuming the
    331      * child has a ViewGroup parent and that it should be laid out within that
    332      * parent with a matching width but variable height. Code largely lifted
    333      * from AnimatedAdapter.measureChildHeight().
    334      *
    335      * @param child a child view that has already been placed within its parent
    336      *            ViewGroup
    337      * @param parent the parent ViewGroup of child
    338      * @return measured height of the child in px
    339      */
    340     public static int measureViewHeight(View child, ViewGroup parent) {
    341         final ViewGroup.LayoutParams lp = child.getLayoutParams();
    342         final int childSideMargin;
    343         if (lp instanceof MarginLayoutParams) {
    344             final MarginLayoutParams mlp = (MarginLayoutParams) lp;
    345             childSideMargin = mlp.leftMargin + mlp.rightMargin;
    346         } else {
    347             childSideMargin = 0;
    348         }
    349 
    350         final int parentWSpec = MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY);
    351         final int wSpec = ViewGroup.getChildMeasureSpec(parentWSpec,
    352                 parent.getPaddingLeft() + parent.getPaddingRight() + childSideMargin,
    353                 ViewGroup.LayoutParams.MATCH_PARENT);
    354         final int hSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    355         child.measure(wSpec, hSpec);
    356         return child.getMeasuredHeight();
    357     }
    358 
    359     /**
    360      * Encode the string in HTML.
    361      *
    362      * @param removeEmptyDoubleQuotes If true, also remove any occurrence of ""
    363      *            found in the string
    364      */
    365     public static Object cleanUpString(String string, boolean removeEmptyDoubleQuotes) {
    366         return !TextUtils.isEmpty(string) ? TextUtils.htmlEncode(removeEmptyDoubleQuotes ? string
    367                 .replace("\"\"", "") : string) : "";
    368     }
    369 
    370     /**
    371      * Get the correct display string for the unread count of a folder.
    372      */
    373     public static String getUnreadCountString(Context context, int unreadCount) {
    374         final String unreadCountString;
    375         final Resources resources = context.getResources();
    376         if (sMaxUnreadCount == -1) {
    377             sMaxUnreadCount = resources.getInteger(R.integer.maxUnreadCount);
    378         }
    379         if (unreadCount > sMaxUnreadCount) {
    380             if (sUnreadText == null) {
    381                 sUnreadText = resources.getString(R.string.widget_large_unread_count);
    382             }
    383             // Localize "99+" according to the device language
    384             unreadCountString = String.format(sUnreadText, sMaxUnreadCount);
    385         } else if (unreadCount <= 0) {
    386             unreadCountString = "";
    387         } else {
    388             // Localize unread count according to the device language
    389             unreadCountString = String.format("%d", unreadCount);
    390         }
    391         return unreadCountString;
    392     }
    393 
    394     /**
    395      * Get the correct display string for the unseen count of a folder.
    396      */
    397     public static String getUnseenCountString(Context context, int unseenCount) {
    398         final String unseenCountString;
    399         final Resources resources = context.getResources();
    400         if (sMaxUnreadCount == -1) {
    401             sMaxUnreadCount = resources.getInteger(R.integer.maxUnreadCount);
    402         }
    403         if (unseenCount > sMaxUnreadCount) {
    404             if (sLargeUnseenText == null) {
    405                 sLargeUnseenText = resources.getString(R.string.large_unseen_count);
    406             }
    407             // Localize "99+" according to the device language
    408             unseenCountString = String.format(sLargeUnseenText, sMaxUnreadCount);
    409         } else if (unseenCount <= 0) {
    410             unseenCountString = "";
    411         } else {
    412             if (sUnseenText == null) {
    413                 sUnseenText = resources.getString(R.string.unseen_count);
    414             }
    415             // Localize unseen count according to the device language
    416             unseenCountString = String.format(sUnseenText, unseenCount);
    417         }
    418         return unseenCountString;
    419     }
    420 
    421     /**
    422      * Get the correct display string for the unread count in the actionbar.
    423      */
    424     public static CharSequence getUnreadMessageString(Context context, int unreadCount) {
    425         final SpannableString message;
    426         final Resources resources = context.getResources();
    427         if (sMaxUnreadCount == -1) {
    428             sMaxUnreadCount = resources.getInteger(R.integer.maxUnreadCount);
    429         }
    430         if (unreadCount > sMaxUnreadCount) {
    431             message = new SpannableString(
    432                     resources.getString(R.string.actionbar_large_unread_count, sMaxUnreadCount));
    433         } else {
    434              message = new SpannableString(resources.getQuantityString(
    435                      R.plurals.actionbar_unread_messages, unreadCount, unreadCount));
    436         }
    437 
    438         message.setSpan(CharacterStyle.wrap(ACTION_BAR_UNREAD_STYLE), 0,
    439                 message.toString().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    440 
    441         return message;
    442     }
    443 
    444     /**
    445      * Get text matching the last sync status.
    446      */
    447     public static CharSequence getSyncStatusText(Context context, int packedStatus) {
    448         final String[] errors = context.getResources().getStringArray(R.array.sync_status);
    449         final int status = packedStatus & 0x0f;
    450         if (status >= errors.length) {
    451             return "";
    452         }
    453         return errors[status];
    454     }
    455 
    456     /**
    457      * Create an intent to show a conversation.
    458      * @param conversation Conversation to open.
    459      * @param folderUri
    460      * @param account
    461      * @return
    462      */
    463     public static Intent createViewConversationIntent(final Context context,
    464             Conversation conversation, final Uri folderUri, Account account) {
    465         final Intent intent = new Intent(Intent.ACTION_VIEW);
    466         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
    467                 | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
    468         final Uri versionedUri = appendVersionQueryParameter(context, conversation.uri);
    469         // We need the URI to be unique, even if it's for the same message, so append the folder URI
    470         final Uri uniqueUri = versionedUri.buildUpon().appendQueryParameter(
    471                 FOLDER_URI_QUERY_PARAMETER, folderUri.toString()).build();
    472         intent.setDataAndType(uniqueUri, account.mimeType);
    473         intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
    474         intent.putExtra(Utils.EXTRA_FOLDER_URI, folderUri);
    475         intent.putExtra(Utils.EXTRA_CONVERSATION, conversation);
    476         return intent;
    477     }
    478 
    479     /**
    480      * Create an intent to open a folder.
    481      *
    482      * @param folderUri Folder to open.
    483      * @param account
    484      * @return
    485      */
    486     public static Intent createViewFolderIntent(final Context context, final Uri folderUri,
    487             Account account) {
    488         if (folderUri == null || account == null) {
    489             LogUtils.wtf(LOG_TAG, "Utils.createViewFolderIntent(%s,%s): Bad input", folderUri,
    490                     account);
    491             return null;
    492         }
    493         final Intent intent = new Intent(Intent.ACTION_VIEW);
    494         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
    495                 | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
    496         intent.setDataAndType(appendVersionQueryParameter(context, folderUri), account.mimeType);
    497         intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
    498         intent.putExtra(Utils.EXTRA_FOLDER_URI, folderUri);
    499         return intent;
    500     }
    501 
    502     /**
    503      * Creates an intent to open the default inbox for the given account.
    504      *
    505      * @param account
    506      * @return
    507      */
    508     public static Intent createViewInboxIntent(Account account) {
    509         if (account == null) {
    510             LogUtils.wtf(LOG_TAG, "Utils.createViewInboxIntent(%s): Bad input", account);
    511             return null;
    512         }
    513         final Intent intent = new Intent(Intent.ACTION_VIEW);
    514         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
    515                 | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
    516         intent.setDataAndType(account.settings.defaultInbox, account.mimeType);
    517         intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
    518         return intent;
    519     }
    520 
    521     /**
    522      * Helper method to show context-aware help.
    523      *
    524      * @param context Context to be used to open the help.
    525      * @param account Account from which the help URI is extracted
    526      * @param helpTopic Information about the activity the user was in
    527      *      when they requested help which specifies the help topic to display
    528      */
    529     public static void showHelp(Context context, Account account, String helpTopic) {
    530         final String urlString = account.helpIntentUri != null ?
    531                 account.helpIntentUri.toString() : null;
    532         if (TextUtils.isEmpty(urlString)) {
    533             LogUtils.e(LOG_TAG, "unable to show help for account: %s", account);
    534             return;
    535         }
    536         showHelp(context, account.helpIntentUri, helpTopic);
    537     }
    538 
    539     /**
    540      * Helper method to show context-aware help.
    541      *
    542      * @param context Context to be used to open the help.
    543      * @param helpIntentUri URI of the help content to display
    544      * @param helpTopic Information about the activity the user was in
    545      *      when they requested help which specifies the help topic to display
    546      */
    547     public static void showHelp(Context context, Uri helpIntentUri, String helpTopic) {
    548         final String urlString = helpIntentUri == null ? null : helpIntentUri.toString();
    549         if (TextUtils.isEmpty(urlString)) {
    550             LogUtils.e(LOG_TAG, "unable to show help for help URI: %s", helpIntentUri);
    551             return;
    552         }
    553 
    554         // generate the full URL to the requested help section
    555         final Uri helpUrl = HelpUrl.getHelpUrl(context, helpIntentUri, helpTopic);
    556 
    557         final boolean useBrowser = context.getResources().getBoolean(R.bool.openHelpWithBrowser);
    558         if (useBrowser) {
    559             // open a browser with the full help URL
    560             openUrl(context, helpUrl, null);
    561         } else {
    562             // start the help activity with the full help URL
    563             final Intent intent = new Intent(context, HelpActivity.class);
    564             intent.putExtra(HelpActivity.PARAM_HELP_URL, helpUrl);
    565             context.startActivity(intent);
    566         }
    567     }
    568 
    569     /**
    570      * Helper method to open a link in a browser.
    571      *
    572      * @param context Context
    573      * @param uri Uri to open.
    574      */
    575     private static void openUrl(Context context, Uri uri, Bundle optionalExtras) {
    576         if(uri == null || TextUtils.isEmpty(uri.toString())) {
    577             LogUtils.wtf(LOG_TAG, "invalid url in Utils.openUrl(): %s", uri);
    578             return;
    579         }
    580         final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    581         // Fill in any of extras that have been requested.
    582         if (optionalExtras != null) {
    583             intent.putExtras(optionalExtras);
    584         }
    585         intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
    586         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    587 
    588         context.startActivity(intent);
    589     }
    590 
    591     /**
    592      * Show the top level settings screen for the supplied account.
    593      */
    594     public static void showSettings(Context context, Account account) {
    595         if (account == null) {
    596             LogUtils.e(LOG_TAG, "Invalid attempt to show setting screen with null account");
    597             return;
    598         }
    599         final Intent settingsIntent = new Intent(Intent.ACTION_EDIT, account.settingsIntentUri);
    600 
    601         settingsIntent.setPackage(context.getPackageName());
    602         settingsIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    603 
    604         context.startActivity(settingsIntent);
    605     }
    606 
    607     /**
    608      * Show the account level settings screen for the supplied account.
    609      */
    610     public static void showAccountSettings(Context context, Account account) {
    611         if (account == null) {
    612             LogUtils.e(LOG_TAG, "Invalid attempt to show setting screen with null account");
    613             return;
    614         }
    615         final Intent settingsIntent = new Intent(Intent.ACTION_EDIT,
    616                 appendVersionQueryParameter(context, account.settingsIntentUri));
    617 
    618         settingsIntent.setPackage(context.getPackageName());
    619         settingsIntent.putExtra(EditSettingsExtras.EXTRA_ACCOUNT, account);
    620         settingsIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    621 
    622         context.startActivity(settingsIntent);
    623     }
    624 
    625     /**
    626      * Show the feedback screen for the supplied account.
    627      */
    628     public static void sendFeedback(Activity activity, Account account, boolean reportingProblem) {
    629         if (activity != null && account != null) {
    630             sendFeedback(activity, account.sendFeedbackIntentUri, reportingProblem);
    631         }
    632     }
    633 
    634     public static void sendFeedback(Activity activity, Uri feedbackIntentUri,
    635             boolean reportingProblem) {
    636         if (activity != null &&  !isEmpty(feedbackIntentUri)) {
    637             final Bundle optionalExtras = new Bundle(2);
    638             optionalExtras.putBoolean(
    639                     UIProvider.SendFeedbackExtras.EXTRA_REPORTING_PROBLEM, reportingProblem);
    640             final Bitmap screenBitmap = getReducedSizeBitmap(activity);
    641             if (screenBitmap != null) {
    642                 optionalExtras.putParcelable(
    643                         UIProvider.SendFeedbackExtras.EXTRA_SCREEN_SHOT, screenBitmap);
    644             }
    645             openUrl(activity, feedbackIntentUri, optionalExtras);
    646         }
    647     }
    648 
    649     private static Bitmap getReducedSizeBitmap(Activity activity) {
    650         final Window activityWindow = activity.getWindow();
    651         final View currentView = activityWindow != null ? activityWindow.getDecorView() : null;
    652         final View rootView = currentView != null ? currentView.getRootView() : null;
    653         if (rootView != null) {
    654             rootView.setDrawingCacheEnabled(true);
    655             final Bitmap drawingCache = rootView.getDrawingCache();
    656             // Null check to avoid NPE discovered from monkey crash:
    657             if (drawingCache != null) {
    658                 try {
    659                     final Bitmap originalBitmap = drawingCache.copy(Bitmap.Config.RGB_565, false);
    660                     double originalHeight = originalBitmap.getHeight();
    661                     double originalWidth = originalBitmap.getWidth();
    662                     int newHeight = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
    663                     int newWidth = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
    664                     double scaleX, scaleY;
    665                     scaleX = newWidth  / originalWidth;
    666                     scaleY = newHeight / originalHeight;
    667                     final double scale = Math.min(scaleX, scaleY);
    668                     newWidth = (int)Math.round(originalWidth * scale);
    669                     newHeight = (int)Math.round(originalHeight * scale);
    670                     return Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true);
    671                 } catch (OutOfMemoryError e) {
    672                     LogUtils.e(LOG_TAG, e, "OOME when attempting to scale screenshot");
    673                 }
    674             }
    675         }
    676         return null;
    677     }
    678 
    679     /**
    680      * Retrieves the mailbox search query associated with an intent (or null if not available),
    681      * doing proper sanitizing (e.g. trims whitespace).
    682      */
    683     public static String mailSearchQueryForIntent(Intent intent) {
    684         String query = intent.getStringExtra(SearchManager.QUERY);
    685         return TextUtils.isEmpty(query) ? null : query.trim();
    686    }
    687 
    688     /**
    689      * Split out a filename's extension and return it.
    690      * @param filename a file name
    691      * @return the file extension (max of 5 chars including period, like ".docx"), or null
    692      */
    693     public static String getFileExtension(String filename) {
    694         String extension = null;
    695         int index = !TextUtils.isEmpty(filename) ? filename.lastIndexOf('.') : -1;
    696         // Limit the suffix to dot + four characters
    697         if (index >= 0 && filename.length() - index <= FILE_EXTENSION_MAX_CHARS + 1) {
    698             extension = filename.substring(index);
    699         }
    700         return extension;
    701     }
    702 
    703    /**
    704     * (copied from {@link Intent#normalizeMimeType(String)} for pre-J)
    705     *
    706     * Normalize a MIME data type.
    707     *
    708     * <p>A normalized MIME type has white-space trimmed,
    709     * content-type parameters removed, and is lower-case.
    710     * This aligns the type with Android best practices for
    711     * intent filtering.
    712     *
    713     * <p>For example, "text/plain; charset=utf-8" becomes "text/plain".
    714     * "text/x-vCard" becomes "text/x-vcard".
    715     *
    716     * <p>All MIME types received from outside Android (such as user input,
    717     * or external sources like Bluetooth, NFC, or the Internet) should
    718     * be normalized before they are used to create an Intent.
    719     *
    720     * @param type MIME data type to normalize
    721     * @return normalized MIME data type, or null if the input was null
    722     * @see {@link android.content.Intent#setType}
    723     * @see {@link android.content.Intent#setTypeAndNormalize}
    724     */
    725    public static String normalizeMimeType(String type) {
    726        if (type == null) {
    727            return null;
    728        }
    729 
    730        type = type.trim().toLowerCase(Locale.US);
    731 
    732        final int semicolonIndex = type.indexOf(';');
    733        if (semicolonIndex != -1) {
    734            type = type.substring(0, semicolonIndex);
    735        }
    736        return type;
    737    }
    738 
    739    /**
    740     * (copied from {@link android.net.Uri#normalizeScheme()} for pre-J)
    741     *
    742     * Return a normalized representation of this Uri.
    743     *
    744     * <p>A normalized Uri has a lowercase scheme component.
    745     * This aligns the Uri with Android best practices for
    746     * intent filtering.
    747     *
    748     * <p>For example, "HTTP://www.android.com" becomes
    749     * "http://www.android.com"
    750     *
    751     * <p>All URIs received from outside Android (such as user input,
    752     * or external sources like Bluetooth, NFC, or the Internet) should
    753     * be normalized before they are used to create an Intent.
    754     *
    755     * <p class="note">This method does <em>not</em> validate bad URI's,
    756     * or 'fix' poorly formatted URI's - so do not use it for input validation.
    757     * A Uri will always be returned, even if the Uri is badly formatted to
    758     * begin with and a scheme component cannot be found.
    759     *
    760     * @return normalized Uri (never null)
    761     * @see {@link android.content.Intent#setData}
    762     */
    763    public static Uri normalizeUri(Uri uri) {
    764        String scheme = uri.getScheme();
    765        if (scheme == null) return uri;  // give up
    766        String lowerScheme = scheme.toLowerCase(Locale.US);
    767        if (scheme.equals(lowerScheme)) return uri;  // no change
    768 
    769        return uri.buildUpon().scheme(lowerScheme).build();
    770    }
    771 
    772    public static Intent setIntentTypeAndNormalize(Intent intent, String type) {
    773        return intent.setType(normalizeMimeType(type));
    774    }
    775 
    776    public static Intent setIntentDataAndTypeAndNormalize(Intent intent, Uri data, String type) {
    777        return intent.setDataAndType(normalizeUri(data), normalizeMimeType(type));
    778    }
    779 
    780    public static int getTransparentColor(int color) {
    781        return 0x00ffffff & color;
    782    }
    783 
    784     public static void setMenuItemVisibility(Menu menu, int itemId, boolean shouldShow) {
    785         final MenuItem item = menu.findItem(itemId);
    786         if (item == null) {
    787             return;
    788         }
    789         item.setVisible(shouldShow);
    790     }
    791 
    792     /**
    793      * Parse a string (possibly null or empty) into a URI. If the string is null
    794      * or empty, null is returned back. Otherwise an empty URI is returned.
    795      *
    796      * @param uri
    797      * @return a valid URI, possibly {@link android.net.Uri#EMPTY}
    798      */
    799     public static Uri getValidUri(String uri) {
    800         if (TextUtils.isEmpty(uri) || uri == JSONObject.NULL)
    801             return Uri.EMPTY;
    802         return Uri.parse(uri);
    803     }
    804 
    805     public static boolean isEmpty(Uri uri) {
    806         return uri == null || Uri.EMPTY.equals(uri);
    807     }
    808 
    809     public static String dumpFragment(Fragment f) {
    810         final StringWriter sw = new StringWriter();
    811         f.dump("", new FileDescriptor(), new PrintWriter(sw), new String[0]);
    812         return sw.toString();
    813     }
    814 
    815     public static void dumpViewTree(ViewGroup root) {
    816         dumpViewTree(root, "");
    817     }
    818 
    819     private static void dumpViewTree(ViewGroup g, String prefix) {
    820         LogUtils.i(LOG_TAG, "%sVIEWGROUP: %s childCount=%s", prefix, g, g.getChildCount());
    821         final String childPrefix = prefix + "  ";
    822         for (int i = 0; i < g.getChildCount(); i++) {
    823             final View child = g.getChildAt(i);
    824             if (child instanceof ViewGroup) {
    825                 dumpViewTree((ViewGroup) child, childPrefix);
    826             } else {
    827                 LogUtils.i(LOG_TAG, "%sCHILD #%s: %s", childPrefix, i, child);
    828             }
    829         }
    830     }
    831 
    832     /**
    833      * Executes an out-of-band command on the cursor.
    834      * @param cursor
    835      * @param request Bundle with all keys and values set for the command.
    836      * @param key The string value against which we will check for success or failure
    837      * @return true if the operation was a success.
    838      */
    839     private static boolean executeConversationCursorCommand(
    840             Cursor cursor, Bundle request, String key) {
    841         final Bundle response = cursor.respond(request);
    842         final String result = response.getString(key,
    843                 UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_FAILED);
    844 
    845         return UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK.equals(result);
    846     }
    847 
    848     /**
    849      * Commands a cursor representing a set of conversations to indicate that an item is being shown
    850      * in the UI.
    851      *
    852      * @param cursor a conversation cursor
    853      * @param position position of the item being shown.
    854      */
    855     public static boolean notifyCursorUIPositionChange(Cursor cursor, int position) {
    856         final Bundle request = new Bundle();
    857         final String key =
    858                 UIProvider.ConversationCursorCommand.COMMAND_NOTIFY_CURSOR_UI_POSITION_CHANGE;
    859         request.putInt(key, position);
    860         return executeConversationCursorCommand(cursor, request, key);
    861     }
    862 
    863     /**
    864      * Commands a cursor representing a set of conversations to set its visibility state.
    865      *
    866      * @param cursor a conversation cursor
    867      * @param visible true if the conversation list is visible, false otherwise.
    868      * @param isFirstSeen true if you want to notify the cursor that this conversation list was seen
    869      *        for the first time: the user launched the app into it, or the user switched from some
    870      *        other folder into it.
    871      */
    872     public static void setConversationCursorVisibility(
    873             Cursor cursor, boolean visible, boolean isFirstSeen) {
    874         new MarkConversationCursorVisibleTask(cursor, visible, isFirstSeen).execute();
    875     }
    876 
    877     /**
    878      * Async task for  marking conversations "seen" and informing the cursor that the folder was
    879      * seen for the first time by the UI.
    880      */
    881     private static class MarkConversationCursorVisibleTask extends AsyncTask<Void, Void, Void> {
    882         private final Cursor mCursor;
    883         private final boolean mVisible;
    884         private final boolean mIsFirstSeen;
    885 
    886         /**
    887          * Create a new task with the given cursor, with the given visibility and
    888          *
    889          * @param cursor
    890          * @param isVisible true if the conversation list is visible, false otherwise.
    891          * @param isFirstSeen true if the folder was shown for the first time: either the user has
    892          *        just switched to it, or the user started the app in this folder.
    893          */
    894         public MarkConversationCursorVisibleTask(
    895                 Cursor cursor, boolean isVisible, boolean isFirstSeen) {
    896             mCursor = cursor;
    897             mVisible = isVisible;
    898             mIsFirstSeen = isFirstSeen;
    899         }
    900 
    901         @Override
    902         protected Void doInBackground(Void... params) {
    903             if (mCursor == null) {
    904                 return null;
    905             }
    906             final Bundle request = new Bundle();
    907             if (mIsFirstSeen) {
    908                 request.putBoolean(
    909                         UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER, true);
    910             }
    911             final String key = UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY;
    912             request.putBoolean(key, mVisible);
    913             executeConversationCursorCommand(mCursor, request, key);
    914             return null;
    915         }
    916     }
    917 
    918 
    919     /**
    920      * This utility method returns the conversation ID at the current cursor position.
    921      * @return the conversation id at the cursor.
    922      */
    923     public static long getConversationId(ConversationCursor cursor) {
    924         return cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
    925     }
    926 
    927     /**
    928      * This utility method returns the conversation Uri at the current cursor position.
    929      * @return the conversation id at the cursor.
    930      */
    931     public static String getConversationUri(ConversationCursor cursor) {
    932         return cursor.getString(UIProvider.CONVERSATION_URI_COLUMN);
    933     }
    934 
    935     /**
    936      * @return whether to show two pane or single pane search results.
    937      */
    938     public static boolean showTwoPaneSearchResults(Context context) {
    939         return context.getResources().getBoolean(R.bool.show_two_pane_search_results);
    940     }
    941 
    942     /**
    943      * Sets the layer type of a view to hardware if the view is attached and hardware acceleration
    944      * is enabled. Does nothing otherwise.
    945      */
    946     public static void enableHardwareLayer(View v) {
    947         if (v != null && v.isHardwareAccelerated()) {
    948             v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    949             v.buildLayer();
    950         }
    951     }
    952 
    953     public static int getDefaultFolderBackgroundColor(Context context) {
    954         if (sDefaultFolderBackgroundColor == -1) {
    955             sDefaultFolderBackgroundColor = context.getResources().getColor(
    956                     R.color.default_folder_background_color);
    957         }
    958         return sDefaultFolderBackgroundColor;
    959     }
    960 
    961     /**
    962      * Returns the count that should be shown for the specified folder.  This method should be used
    963      * when the UI wants to display an "unread" count.  For most labels, the returned value will be
    964      * the unread count, but for some folder types (outbox, drafts, trash) this will return the
    965      * total count.
    966      */
    967     public static int getFolderUnreadDisplayCount(final Folder folder) {
    968         if (folder != null) {
    969             if (folder.supportsCapability(UIProvider.FolderCapabilities.UNSEEN_COUNT_ONLY)) {
    970                 return 0;
    971             } else if (folder.isUnreadCountHidden()) {
    972                 return folder.totalCount;
    973             } else {
    974                 return folder.unreadCount;
    975             }
    976         }
    977         return 0;
    978     }
    979 
    980     /**
    981      * @return an intent which, if launched, will reply to the conversation
    982      */
    983     public static Intent createReplyIntent(final Context context, final Account account,
    984             final Uri messageUri, final boolean isReplyAll) {
    985         final Intent intent =
    986                 ComposeActivity.createReplyIntent(context, account, messageUri, isReplyAll);
    987         return intent;
    988     }
    989 
    990     /**
    991      * @return an intent which, if launched, will forward the conversation
    992      */
    993     public static Intent createForwardIntent(
    994             final Context context, final Account account, final Uri messageUri) {
    995         final Intent intent = ComposeActivity.createForwardIntent(context, account, messageUri);
    996         return intent;
    997     }
    998 
    999     public static Uri appendVersionQueryParameter(final Context context, final Uri uri) {
   1000         return uri.buildUpon().appendQueryParameter(APP_VERSION_QUERY_PARAMETER,
   1001                 getVersionCode(context)).build();
   1002     }
   1003 
   1004     /**
   1005      * Convenience method for diverting mailto: uris directly to our compose activity. Using this
   1006      * method ensures that the Account object is not accidentally sent to a different process.
   1007      *
   1008      * @param context for sending the intent
   1009      * @param uri mailto: or other uri
   1010      * @param account desired account for potential compose activity
   1011      * @return true if a compose activity was started, false if uri should be sent to a view intent
   1012      */
   1013     public static boolean divertMailtoUri(final Context context, final Uri uri,
   1014             final Account account) {
   1015         final String scheme = normalizeUri(uri).getScheme();
   1016         if (TextUtils.equals(MAILTO_SCHEME, scheme)) {
   1017             ComposeActivity.composeMailto(context, account, uri);
   1018             return true;
   1019         }
   1020         return false;
   1021     }
   1022 
   1023     /**
   1024      * Gets the specified {@link Folder} object.
   1025      *
   1026      * @param folderUri The {@link Uri} for the folder
   1027      * @param allowHidden <code>true</code> to allow a hidden folder to be returned,
   1028      *        <code>false</code> to return <code>null</code> instead
   1029      * @return the specified {@link Folder} object, or <code>null</code>
   1030      */
   1031     public static Folder getFolder(final Context context, final Uri folderUri,
   1032             final boolean allowHidden) {
   1033         final Uri uri = folderUri
   1034                 .buildUpon()
   1035                 .appendQueryParameter(UIProvider.ALLOW_HIDDEN_FOLDERS_QUERY_PARAM,
   1036                         Boolean.toString(allowHidden))
   1037                 .build();
   1038 
   1039         final Cursor cursor = context.getContentResolver().query(uri,
   1040                 UIProvider.FOLDERS_PROJECTION, null, null, null);
   1041 
   1042         if (cursor == null) {
   1043             return null;
   1044         }
   1045 
   1046         try {
   1047             if (cursor.moveToFirst()) {
   1048                 return new Folder(cursor);
   1049             } else {
   1050                 return null;
   1051             }
   1052         } finally {
   1053             cursor.close();
   1054         }
   1055     }
   1056 
   1057     /**
   1058      * Begins systrace tracing for a given tag. No-op on unsupported platform versions.
   1059      *
   1060      * @param tag systrace tag to use
   1061      *
   1062      * @see android.os.Trace#beginSection(String)
   1063      */
   1064     public static void traceBeginSection(String tag) {
   1065         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
   1066             android.os.Trace.beginSection(tag);
   1067         }
   1068     }
   1069 
   1070     /**
   1071      * Ends systrace tracing for the most recently begun section. No-op on unsupported platform
   1072      * versions.
   1073      *
   1074      * @see android.os.Trace#endSection()
   1075      */
   1076     public static void traceEndSection() {
   1077         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
   1078             android.os.Trace.endSection();
   1079         }
   1080     }
   1081 
   1082     /**
   1083      * Given a value and a set of upper-bounds to use as buckets, return the smallest upper-bound
   1084      * that is greater than the value.<br>
   1085      * <br>
   1086      * Useful for turning a continuous value into one of a set of discrete ones.
   1087      *
   1088      * @param value a value to bucketize
   1089      * @param upperBounds list of upper-bound buckets to clamp to, sorted from smallest-greatest
   1090      * @return the smallest upper-bound larger than the value, or -1 if the value is larger than
   1091      * all upper-bounds
   1092      */
   1093     public static long getUpperBound(long value, long[] upperBounds) {
   1094         for (long ub : upperBounds) {
   1095             if (value < ub) {
   1096                 return ub;
   1097             }
   1098         }
   1099         return -1;
   1100     }
   1101 
   1102     public static Address getAddress(Map<String, Address> cache, String emailStr) {
   1103         Address addr;
   1104         synchronized (cache) {
   1105             addr = cache.get(emailStr);
   1106             if (addr == null) {
   1107                 addr = Address.getEmailAddress(emailStr);
   1108                 if (addr != null) {
   1109                     cache.put(emailStr, addr);
   1110                 }
   1111             }
   1112         }
   1113         return addr;
   1114     }
   1115 
   1116     /**
   1117      * Applies the given appearance on the given subString, and inserts that as a parameter in the
   1118      * given parentString.
   1119      */
   1120     public static Spanned insertStringWithStyle(Context context,
   1121             String entireString, String subString, int appearance) {
   1122         final Resources resources = context.getResources();
   1123         final int index = entireString.indexOf(subString);
   1124         final SpannableString descriptionText = new SpannableString(entireString);
   1125         descriptionText.setSpan(
   1126                 new TextAppearanceSpan(context, appearance),
   1127                 index,
   1128                 index + subString.length(),
   1129                 0);
   1130         return descriptionText;
   1131     }
   1132 
   1133     /**
   1134      * Email addresses are supposed to be treated as case-insensitive for the host-part and
   1135      * case-sensitive for the local-part, but nobody really wants email addresses to match
   1136      * case-sensitive on the local-part, so just smash everything to lower case.
   1137      * @param email Hello (at) Example.COM
   1138      * @return hello (at) example.com
   1139      */
   1140     public static String normalizeEmailAddress(String email) {
   1141         /*
   1142         // The RFC5321 version
   1143         if (TextUtils.isEmpty(email)) {
   1144             return email;
   1145         }
   1146         String[] parts = email.split("@");
   1147         if (parts.length != 2) {
   1148             LogUtils.d(LOG_TAG, "Tried to normalize a malformed email address: ", email);
   1149             return email;
   1150         }
   1151 
   1152         return parts[0] + "@" + parts[1].toLowerCase(Locale.US);
   1153         */
   1154         if (TextUtils.isEmpty(email)) {
   1155             return email;
   1156         } else {
   1157             // Doing this for other locales might really screw things up, so do US-version only
   1158             return email.toLowerCase(Locale.US);
   1159         }
   1160     }
   1161 
   1162     /**
   1163      * Returns whether the device currently has network connection. This does not guarantee that
   1164      * the connection is reliable.
   1165      */
   1166     public static boolean isConnected(final Context context) {
   1167         final ConnectivityManager connectivityManager =
   1168                 ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
   1169         final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
   1170         return (networkInfo != null) && networkInfo.isConnected();
   1171     }
   1172 }
   1173