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