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