1/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.mail.utils;
18
19import android.annotation.TargetApi;
20import android.app.Activity;
21import android.app.ActivityManager;
22import android.app.Fragment;
23import android.content.ComponentCallbacks;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageManager.NameNotFoundException;
27import android.content.res.Configuration;
28import android.content.res.Resources;
29import android.database.Cursor;
30import android.graphics.Bitmap;
31import android.net.ConnectivityManager;
32import android.net.NetworkInfo;
33import android.net.Uri;
34import android.os.AsyncTask;
35import android.os.Build;
36import android.os.Bundle;
37import android.provider.Browser;
38import android.support.annotation.Nullable;
39import android.support.annotation.VisibleForTesting;
40import android.text.SpannableString;
41import android.text.Spanned;
42import android.text.TextUtils;
43import android.text.style.TextAppearanceSpan;
44import android.view.Menu;
45import android.view.MenuItem;
46import android.view.View;
47import android.view.View.MeasureSpec;
48import android.view.ViewGroup;
49import android.view.ViewGroup.MarginLayoutParams;
50import android.view.Window;
51import android.webkit.WebSettings;
52import android.webkit.WebView;
53
54import com.android.emailcommon.mail.Address;
55import com.android.mail.R;
56import com.android.mail.browse.ConversationCursor;
57import com.android.mail.compose.ComposeActivity;
58import com.android.mail.perf.SimpleTimer;
59import com.android.mail.providers.Account;
60import com.android.mail.providers.Conversation;
61import com.android.mail.providers.Folder;
62import com.android.mail.providers.UIProvider;
63import com.android.mail.providers.UIProvider.EditSettingsExtras;
64import com.android.mail.ui.HelpActivity;
65import com.google.android.mail.common.html.parser.HtmlDocument;
66import com.google.android.mail.common.html.parser.HtmlParser;
67import com.google.android.mail.common.html.parser.HtmlTree;
68import com.google.android.mail.common.html.parser.HtmlTreeBuilder;
69
70import org.json.JSONObject;
71
72import java.io.FileDescriptor;
73import java.io.PrintWriter;
74import java.io.StringWriter;
75import java.util.Locale;
76import java.util.Map;
77
78public 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                        .versionCode);
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@Example.COM
1097     * @return hello@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