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