blob: f3242b5d6b8232298799721738adc016083ca2ba [file] [log] [blame]
Mindy Pereira7b56a612011-12-14 12:32:28 -08001/**
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 */
Mindy Pereira2c47a112012-02-16 16:08:54 -080016
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.utils;
Mindy Pereira7b56a612011-12-14 12:32:28 -080018
Andy Huangf70fc402012-02-17 15:37:42 -080019import com.google.common.collect.Maps;
20
Mindy Pereira68f2e222012-03-07 10:36:54 -080021import android.app.SearchManager;
Mindy Pereira6f92de62011-12-19 11:31:48 -080022import android.content.Context;
Mindy Pereira8a8c50d2012-02-23 11:09:03 -080023import android.content.Intent;
Paul Westbrook94e440d2012-02-24 11:03:47 -080024import android.content.pm.PackageManager.NameNotFoundException;
Mindy Pereira6f92de62011-12-19 11:31:48 -080025import android.content.res.Resources;
Mindy Pereirab5080d52012-03-09 11:26:44 -080026import android.graphics.Color;
Mindy Pereira6f92de62011-12-19 11:31:48 -080027import android.graphics.Typeface;
Mindy Pereira8a8c50d2012-02-23 11:09:03 -080028import android.net.Uri;
Paul Westbrook94e440d2012-02-24 11:03:47 -080029import android.provider.Browser;
Mindy Pereira3e0426c2011-12-20 11:12:19 -080030import android.text.Html;
31import android.text.Spannable;
Mindy Pereira6f92de62011-12-19 11:31:48 -080032import android.text.SpannableString;
33import android.text.SpannableStringBuilder;
Mindy Pereira3e0426c2011-12-20 11:12:19 -080034import android.text.Spanned;
35import android.text.TextUtils;
36import android.text.TextUtils.SimpleStringSplitter;
Mindy Pereira6f92de62011-12-19 11:31:48 -080037import android.text.style.CharacterStyle;
38import android.text.style.ForegroundColorSpan;
39import android.text.style.StyleSpan;
Mindy Pereira326c6602012-01-04 15:32:42 -080040import android.view.View;
Mindy Pereira326c6602012-01-04 15:32:42 -080041import android.view.View.MeasureSpec;
Andy Huangf70fc402012-02-17 15:37:42 -080042import android.view.ViewGroup;
Mindy Pereira8b99ba42011-12-16 09:57:18 -080043import android.webkit.WebSettings;
44import android.webkit.WebView;
45
Andy Huang30e2c242012-01-06 18:14:30 -080046import com.android.mail.R;
Mindy Pereira8a8c50d2012-02-23 11:09:03 -080047import com.android.mail.providers.Account;
Mindy Pereira9ae8ce02012-02-28 09:28:15 -080048import com.android.mail.providers.Conversation;
Mindy Pereira8a8c50d2012-02-23 11:09:03 -080049import com.android.mail.providers.Folder;
Mindy Pereira3e0426c2011-12-20 11:12:19 -080050
Paul Westbrook94e440d2012-02-24 11:03:47 -080051import java.util.Locale;
Mindy Pereira3e0426c2011-12-20 11:12:19 -080052import java.util.Map;
Mindy Pereira7b56a612011-12-14 12:32:28 -080053
Mindy Pereira6f92de62011-12-19 11:31:48 -080054public class Utils {
55 /**
56 * longest extension we recognize is 4 characters (e.g. "html", "docx")
57 */
58 private static final int FILE_EXTENSION_MAX_CHARS = 4;
Mindy Pereira3e0426c2011-12-20 11:12:19 -080059 private static final Map<Integer, Integer> sPriorityToLength = Maps.newHashMap();
60 public static final String SENDER_LIST_TOKEN_ELIDED = "e";
61 public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n";
62 public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d";
63 public static final String SENDER_LIST_TOKEN_LITERAL = "l";
64 public static final String SENDER_LIST_TOKEN_SENDING = "s";
65 public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f";
66 public static final Character SENDER_LIST_SEPARATOR = '\n';
67 public static final SimpleStringSplitter sSenderListSplitter = new SimpleStringSplitter(
68 SENDER_LIST_SEPARATOR);
69 public static String[] sSenderFragments = new String[8];
Mindy Pereira8b99ba42011-12-16 09:57:18 -080070
Mindy Pereira6349a042012-01-04 11:25:01 -080071 public static final String EXTRA_ACCOUNT = "account";
Mindy Pereira7418e4b2012-02-28 11:32:14 -080072 public static final String EXTRA_COMPOSE_URI = "composeUri";
Mindy Pereira963cded2012-02-28 15:25:21 -080073 public static final String EXTRA_CONVERSATION = "conversationUri";
74 public static final String EXTRA_FOLDER = "folder";
Mindy Pereira8a8c50d2012-02-23 11:09:03 -080075 /*
76 * Notifies that changes happened. Certain UI components, e.g., widgets, can
77 * register for this {@link Intent} and update accordingly. However, this
78 * can be very broad and is NOT the preferred way of getting notification.
79 */
80 // TODO: UI Provider has this notification URI?
Paul Westbrook94e440d2012-02-24 11:03:47 -080081 public static final String ACTION_NOTIFY_DATASET_CHANGED =
82 "com.android.mail.ACTION_NOTIFY_DATASET_CHANGED";
83
84 /** Parameter keys for context-aware help. */
85 private static final String SMART_HELP_LINK_PARAMETER_NAME = "p";
86
87 private static final String SMART_LINK_APP_VERSION = "version";
88 private static String sVersionCode = null;
89
90 private static final String LOG_TAG = new LogUtils().getLogTag();
Mindy Pereira6349a042012-01-04 11:25:01 -080091
Mindy Pereira2c47a112012-02-16 16:08:54 -080092 /**
93 * Sets WebView in a restricted mode suitable for email use.
94 *
95 * @param webView The WebView to restrict
96 */
97 public static void restrictWebView(WebView webView) {
Mindy Pereira8b99ba42011-12-16 09:57:18 -080098 WebSettings webSettings = webView.getSettings();
99 webSettings.setSavePassword(false);
100 webSettings.setSaveFormData(false);
101 webSettings.setJavaScriptEnabled(true);
102 webSettings.setSupportZoom(false);
Mindy Pereira2c47a112012-02-16 16:08:54 -0800103 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800104
Mindy Pereira2c47a112012-02-16 16:08:54 -0800105 /**
106 * Format a plural string.
107 *
108 * @param resource The identity of the resource, which must be a R.plurals
109 * @param count The number of items.
110 */
111 public static String formatPlural(Context context, int resource, int count) {
112 CharSequence formatString = context.getResources().getQuantityText(resource, count);
113 return String.format(formatString.toString(), count);
114 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800115
Mindy Pereira2c47a112012-02-16 16:08:54 -0800116 /**
117 * @return an ellipsized String that's at most maxCharacters long. If the
118 * text passed is longer, it will be abbreviated. If it contains a
119 * suffix, the ellipses will be inserted in the middle and the
120 * suffix will be preserved.
121 */
122 public static String ellipsize(String text, int maxCharacters) {
123 int length = text.length();
124 if (length < maxCharacters)
125 return text;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800126
Mindy Pereira2c47a112012-02-16 16:08:54 -0800127 int realMax = Math.min(maxCharacters, length);
128 // Preserve the suffix if any
129 int index = text.lastIndexOf(".");
130 String extension = "\u2026"; // "...";
131 if (index >= 0) {
132 // Limit the suffix to dot + four characters
133 if (length - index <= FILE_EXTENSION_MAX_CHARS + 1) {
134 extension = extension + text.substring(index + 1);
135 }
136 }
137 realMax -= extension.length();
138 if (realMax < 0)
139 realMax = 0;
140 return text.substring(0, realMax) + extension;
141 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800142
Mindy Pereira2c47a112012-02-16 16:08:54 -0800143 /**
Mindy Pereira4ebb9162012-01-03 11:06:19 -0800144 * Ensures that the given string starts and ends with the double quote
145 * character. The string is not modified in any way except to add the double
146 * quote character to start and end if it's not already there. sample ->
147 * "sample" "sample" -> "sample" ""sample"" -> "sample"
148 * "sample"" -> "sample" sa"mp"le -> "sa"mp"le" "sa"mp"le" -> "sa"mp"le"
149 * (empty string) -> "" " -> ""
150 */
Mindy Pereira2c47a112012-02-16 16:08:54 -0800151 public static String ensureQuotedString(String s) {
152 if (s == null) {
153 return null;
154 }
155 if (!s.matches("^\".*\"$")) {
156 return "\"" + s + "\"";
157 } else {
158 return s;
159 }
160 }
Mindy Pereira4ebb9162012-01-03 11:06:19 -0800161
Mindy Pereira2c47a112012-02-16 16:08:54 -0800162 // TODO: Move this to the UI Provider.
163 private static CharacterStyle sUnreadStyleSpan = null;
164 private static CharacterStyle sReadStyleSpan;
165 private static CharacterStyle sDraftsStyleSpan;
166 private static CharSequence sMeString;
167 private static CharSequence sDraftSingularString;
168 private static CharSequence sDraftPluralString;
169 private static CharSequence sSendingString;
170 private static CharSequence sSendFailedString;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800171
Mindy Pereira2c47a112012-02-16 16:08:54 -0800172 private static int sMaxUnreadCount = -1;
173 private static String sUnreadText;
Mindy Pereira6f92de62011-12-19 11:31:48 -0800174
Mindy Pereira2c47a112012-02-16 16:08:54 -0800175 public static void getStyledSenderSnippet(Context context, String senderInstructions,
176 SpannableStringBuilder senderBuilder, SpannableStringBuilder statusBuilder,
177 int maxChars, boolean forceAllUnread, boolean forceAllRead, boolean allowDraft) {
178 Resources res = context.getResources();
179 if (sUnreadStyleSpan == null) {
180 sUnreadStyleSpan = new StyleSpan(Typeface.BOLD);
181 sReadStyleSpan = new StyleSpan(Typeface.NORMAL);
182 sDraftsStyleSpan = new ForegroundColorSpan(res.getColor(R.color.drafts));
Mindy Pereira6f92de62011-12-19 11:31:48 -0800183
Mindy Pereira2c47a112012-02-16 16:08:54 -0800184 sMeString = context.getText(R.string.me);
185 sDraftSingularString = res.getQuantityText(R.plurals.draft, 1);
186 sDraftPluralString = res.getQuantityText(R.plurals.draft, 2);
187 SpannableString sendingString = new SpannableString(context.getText(R.string.sending));
188 sendingString.setSpan(CharacterStyle.wrap(sDraftsStyleSpan), 0, sendingString.length(),
189 0);
190 sSendingString = sendingString;
191 sSendFailedString = context.getText(R.string.send_failed);
192 }
Mindy Pereira6f92de62011-12-19 11:31:48 -0800193
Mindy Pereira2c47a112012-02-16 16:08:54 -0800194 getSenderSnippet(senderInstructions, senderBuilder, statusBuilder, maxChars,
195 sUnreadStyleSpan, sReadStyleSpan, sDraftsStyleSpan, sMeString,
196 sDraftSingularString, sDraftPluralString, sSendingString, sSendFailedString,
197 forceAllUnread, forceAllRead, allowDraft);
198 }
199
200 /**
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800201 * Uses sender instructions to build a formatted string.
202 * <p>
203 * Sender list instructions contain compact information about the sender
204 * list. Most work that can be done without knowing how much room will be
205 * availble for the sender list is done when creating the instructions.
206 * <p>
207 * The instructions string consists of tokens separated by
208 * SENDER_LIST_SEPARATOR. Here are the tokens, one per line:
209 * <ul>
210 * <li><tt>n</tt></li>
211 * <li><em>int</em>, the number of non-draft messages in the conversation</li>
212 * <li><tt>d</tt</li>
213 * <li><em>int</em>, the number of drafts in the conversation</li>
214 * <li><tt>l</tt></li>
215 * <li><em>literal html to be included in the output</em></li>
216 * <li><tt>s</tt> indicates that the message is sending (in the outbox
217 * without errors)</li>
218 * <li><tt>f</tt> indicates that the message failed to send (in the outbox
219 * with errors)</li>
220 * <li><em>for each message</em>
221 * <ul>
222 * <li><em>int</em>, 0 for read, 1 for unread</li>
223 * <li><em>int</em>, the priority of the message. Zero is the most important
224 * </li>
225 * <li><em>text</em>, the sender text or blank for messages from 'me'</li>
226 * </ul>
227 * </li>
228 * <li><tt>e</tt> to indicate that one or more messages have been elided</li>
229 * <p>
230 * The instructions indicate how many messages and drafts are in the
231 * conversation and then describe the most important messages in order,
232 * indicating the priority of each message and whether the message is
233 * unread.
Andy Huangf70fc402012-02-17 15:37:42 -0800234 *
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800235 * @param instructions instructions as described above
236 * @param senderBuilder the SpannableStringBuilder to append to for sender
237 * information
238 * @param statusBuilder the SpannableStringBuilder to append to for status
239 * @param maxChars the number of characters available to display the text
240 * @param unreadStyle the CharacterStyle for unread messages, or null
241 * @param draftsStyle the CharacterStyle for draft messages, or null
242 * @param sendingString the string to use when there are messages scheduled
243 * to be sent
244 * @param sendFailedString the string to use when there are messages that
245 * mailed to send
246 * @param meString the string to use for messages sent by this user
247 * @param draftString the string to use for "Draft"
248 * @param draftPluralString the string to use for "Drafts"
249 */
Mindy Pereira2c47a112012-02-16 16:08:54 -0800250 public static synchronized void getSenderSnippet(String instructions,
251 SpannableStringBuilder senderBuilder, SpannableStringBuilder statusBuilder,
252 int maxChars, CharacterStyle unreadStyle, CharacterStyle readStyle,
253 CharacterStyle draftsStyle, CharSequence meString, CharSequence draftString,
254 CharSequence draftPluralString, CharSequence sendingString,
255 CharSequence sendFailedString, boolean forceAllUnread, boolean forceAllRead,
256 boolean allowDraft) {
257 assert !(forceAllUnread && forceAllRead);
258 boolean unreadStatusIsForced = forceAllUnread || forceAllRead;
259 boolean forcedUnreadStatus = forceAllUnread;
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800260
Mindy Pereira2c47a112012-02-16 16:08:54 -0800261 // Measure each fragment. It's ok to iterate over the entire set of
262 // fragments because it is
263 // never a long list, even if there are many senders.
264 final Map<Integer, Integer> priorityToLength = sPriorityToLength;
265 priorityToLength.clear();
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800266
Mindy Pereira2c47a112012-02-16 16:08:54 -0800267 int maxFoundPriority = Integer.MIN_VALUE;
268 int numMessages = 0;
269 int numDrafts = 0;
270 CharSequence draftsFragment = "";
271 CharSequence sendingFragment = "";
272 CharSequence sendFailedFragment = "";
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800273
Mindy Pereira2c47a112012-02-16 16:08:54 -0800274 sSenderListSplitter.setString(instructions);
275 int numFragments = 0;
276 String[] fragments = sSenderFragments;
277 int currentSize = fragments.length;
278 while (sSenderListSplitter.hasNext()) {
279 fragments[numFragments++] = sSenderListSplitter.next();
280 if (numFragments == currentSize) {
281 sSenderFragments = new String[2 * currentSize];
282 System.arraycopy(fragments, 0, sSenderFragments, 0, currentSize);
283 currentSize *= 2;
284 fragments = sSenderFragments;
285 }
286 }
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800287
Mindy Pereira2c47a112012-02-16 16:08:54 -0800288 for (int i = 0; i < numFragments;) {
289 String fragment0 = fragments[i++];
290 if ("".equals(fragment0)) {
291 // This should be the final fragment.
292 } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
293 // ignore
294 } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
295 numMessages = Integer.valueOf(fragments[i++]);
296 } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
297 String numDraftsString = fragments[i++];
298 numDrafts = Integer.parseInt(numDraftsString);
299 draftsFragment = numDrafts == 1 ? draftString : draftPluralString + " ("
300 + numDraftsString + ")";
301 } else if (SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) {
302 senderBuilder.append(Html.fromHtml(fragments[i++]));
303 return;
304 } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
305 sendingFragment = sendingString;
306 } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
307 sendFailedFragment = sendFailedString;
308 } else {
309 String priorityString = fragments[i++];
310 CharSequence nameString = fragments[i++];
311 if (nameString.length() == 0)
312 nameString = meString;
313 int priority = Integer.parseInt(priorityString);
314 priorityToLength.put(priority, nameString.length());
315 maxFoundPriority = Math.max(maxFoundPriority, priority);
316 }
317 }
318 String numMessagesFragment = (numMessages != 0) ? " \u00A0"
319 + Integer.toString(numMessages + numDrafts) : "";
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800320
Mindy Pereira2c47a112012-02-16 16:08:54 -0800321 // Don't allocate fixedFragment unless we need it
322 SpannableStringBuilder fixedFragment = null;
323 int fixedFragmentLength = 0;
324 if (draftsFragment.length() != 0 && allowDraft) {
325 if (fixedFragment == null) {
326 fixedFragment = new SpannableStringBuilder();
327 }
328 fixedFragment.append(draftsFragment);
329 if (draftsStyle != null) {
330 fixedFragment.setSpan(CharacterStyle.wrap(draftsStyle), 0, fixedFragment.length(),
331 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
332 }
333 }
334 if (sendingFragment.length() != 0) {
335 if (fixedFragment == null) {
336 fixedFragment = new SpannableStringBuilder();
337 }
338 if (fixedFragment.length() != 0)
339 fixedFragment.append(", ");
340 fixedFragment.append(sendingFragment);
341 }
342 if (sendFailedFragment.length() != 0) {
343 if (fixedFragment == null) {
344 fixedFragment = new SpannableStringBuilder();
345 }
346 if (fixedFragment.length() != 0)
347 fixedFragment.append(", ");
348 fixedFragment.append(sendFailedFragment);
349 }
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800350
Mindy Pereira2c47a112012-02-16 16:08:54 -0800351 if (fixedFragment != null) {
352 fixedFragmentLength = fixedFragment.length();
353 }
354 maxChars -= fixedFragmentLength;
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800355
Mindy Pereira2c47a112012-02-16 16:08:54 -0800356 int maxPriorityToInclude = -1; // inclusive
357 int numCharsUsed = numMessagesFragment.length();
358 int numSendersUsed = 0;
359 while (maxPriorityToInclude < maxFoundPriority) {
360 if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
361 int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1);
362 if (numCharsUsed > 0)
363 length += 2;
364 // We must show at least two senders if they exist. If we don't
365 // have space for both
366 // then we will truncate names.
367 if (length > maxChars && numSendersUsed >= 2) {
368 break;
369 }
370 numCharsUsed = length;
371 numSendersUsed++;
372 }
373 maxPriorityToInclude++;
374 }
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800375
Mindy Pereira2c47a112012-02-16 16:08:54 -0800376 int numCharsToRemovePerWord = 0;
377 if (numCharsUsed > maxChars) {
378 numCharsToRemovePerWord = (numCharsUsed - maxChars) / numSendersUsed;
379 }
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800380
Mindy Pereira2c47a112012-02-16 16:08:54 -0800381 String lastFragment = null;
382 CharacterStyle lastStyle = null;
383 for (int i = 0; i < numFragments;) {
384 String fragment0 = fragments[i++];
385 if ("".equals(fragment0)) {
386 // This should be the final fragment.
387 } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
388 if (lastFragment != null) {
389 addStyledFragment(senderBuilder, lastFragment, lastStyle, false);
390 senderBuilder.append(" ");
391 addStyledFragment(senderBuilder, "..", lastStyle, true);
392 senderBuilder.append(" ");
393 }
394 lastFragment = null;
395 } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
396 i++;
397 } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
398 i++;
399 } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
400 } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
401 } else {
402 final String unreadString = fragment0;
403 final String priorityString = fragments[i++];
404 String nameString = fragments[i++];
405 if (nameString.length() == 0) {
406 nameString = meString.toString();
407 } else {
408 nameString = Html.fromHtml(nameString).toString();
409 }
410 if (numCharsToRemovePerWord != 0) {
411 nameString = nameString.substring(0,
412 Math.max(nameString.length() - numCharsToRemovePerWord, 0));
413 }
414 final boolean unread = unreadStatusIsForced ? forcedUnreadStatus : Integer
415 .parseInt(unreadString) != 0;
416 final int priority = Integer.parseInt(priorityString);
417 if (priority <= maxPriorityToInclude) {
418 if (lastFragment != null && !lastFragment.equals(nameString)) {
419 addStyledFragment(senderBuilder, lastFragment.concat(","), lastStyle,
420 false);
421 senderBuilder.append(" ");
422 }
423 lastFragment = nameString;
424 lastStyle = unread ? unreadStyle : readStyle;
425 } else {
426 if (lastFragment != null) {
427 addStyledFragment(senderBuilder, lastFragment, lastStyle, false);
428 // Adjacent spans can cause the TextView in Gmail widget
429 // confused and leads to weird behavior on scrolling.
430 // Our workaround here is to separate the spans by
431 // spaces.
432 senderBuilder.append(" ");
433 addStyledFragment(senderBuilder, "..", lastStyle, true);
434 senderBuilder.append(" ");
435 }
436 lastFragment = null;
437 }
438 }
439 }
440 if (lastFragment != null) {
441 addStyledFragment(senderBuilder, lastFragment, lastStyle, false);
442 }
443 senderBuilder.append(numMessagesFragment);
444 if (fixedFragmentLength != 0) {
445 statusBuilder.append(fixedFragment);
446 }
447 }
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800448
449 /**
450 * Adds a fragment with given style to a string builder.
Andy Huangf70fc402012-02-17 15:37:42 -0800451 *
Mindy Pereira3e0426c2011-12-20 11:12:19 -0800452 * @param builder the current string builder
453 * @param fragment the fragment to be added
454 * @param style the style of the fragment
455 * @param withSpaces whether to add the whole fragment or to divide it into
456 * smaller ones
457 */
458 private static void addStyledFragment(SpannableStringBuilder builder, String fragment,
459 CharacterStyle style, boolean withSpaces) {
460 if (withSpaces) {
461 int pos = builder.length();
462 builder.append(fragment);
463 builder.setSpan(CharacterStyle.wrap(style), pos, builder.length(),
464 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
465 } else {
466 int start = 0;
467 while (true) {
468 int pos = fragment.substring(start).indexOf(' ');
469 if (pos == -1) {
470 addStyledFragment(builder, fragment.substring(start), style, true);
471 break;
472 } else {
473 pos += start;
474 if (start < pos) {
475 addStyledFragment(builder, fragment.substring(start, pos), style, true);
476 builder.append(' ');
477 }
478 start = pos + 1;
479 if (start >= fragment.length()) {
480 break;
481 }
482 }
483 }
484 }
485 }
486
Mindy Pereira2c47a112012-02-16 16:08:54 -0800487 /**
488 * Returns a boolean indicating whether the table UI should be shown.
489 */
490 public static boolean useTabletUI(Context context) {
491 return context.getResources().getInteger(R.integer.use_tablet_ui) != 0;
492 }
Mindy Pereira4ebb9162012-01-03 11:06:19 -0800493
Mindy Pereira2c47a112012-02-16 16:08:54 -0800494 /**
495 * Perform a simulated measure pass on the given child view, assuming the
496 * child has a ViewGroup parent and that it should be laid out within that
497 * parent with a matching width but variable height. Code largely lifted
498 * from AnimatedAdapter.measureChildHeight().
Andy Huangf70fc402012-02-17 15:37:42 -0800499 *
Mindy Pereira2c47a112012-02-16 16:08:54 -0800500 * @param child a child view that has already been placed within its parent
501 * ViewGroup
502 * @param parent the parent ViewGroup of child
503 * @return measured height of the child in px
504 */
505 public static int measureViewHeight(View child, ViewGroup parent) {
506 int parentWSpec = MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY);
507 int wSpec = ViewGroup.getChildMeasureSpec(parentWSpec,
508 parent.getPaddingLeft() + parent.getPaddingRight(),
509 ViewGroup.LayoutParams.MATCH_PARENT);
510 int hSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
511 child.measure(wSpec, hSpec);
512 return child.getMeasuredHeight();
513 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800514
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800515 /**
516 * Encode the string in HTML.
517 *
518 * @param removeEmptyDoubleQuotes If true, also remove any occurrence of ""
519 * found in the string
520 */
Mindy Pereira2c47a112012-02-16 16:08:54 -0800521 public static Object cleanUpString(String string, boolean removeEmptyDoubleQuotes) {
522 return !TextUtils.isEmpty(string) ? TextUtils.htmlEncode(removeEmptyDoubleQuotes ? string
523 .replace("\"\"", "") : string) : "";
524 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800525
Mindy Pereira2c47a112012-02-16 16:08:54 -0800526 /**
527 * Returns comma seperated strings as an array.
528 */
529 public static String[] splitCommaSeparatedString(String str) {
530 return TextUtils.isEmpty(str) ? new String[0] : TextUtils.split(str, ",");
531 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800532
Mindy Pereira2c47a112012-02-16 16:08:54 -0800533 /**
534 * Get the correct display string for the unread count of a folder.
535 */
536 public static String getUnreadCountString(Context context, int unreadCount) {
537 String unreadCountString;
538 Resources resources = context.getResources();
539 if (sMaxUnreadCount == -1) {
540 sMaxUnreadCount = resources.getInteger(R.integer.maxUnreadCount);
541 }
542 if (unreadCount > sMaxUnreadCount) {
543 if (sUnreadText == null) {
544 sUnreadText = resources.getString(R.string.widget_large_unread_count);
545 }
546 unreadCountString = String.format(sUnreadText, sMaxUnreadCount);
547 } else if (unreadCount <= 0) {
548 unreadCountString = "";
549 } else {
550 unreadCountString = String.valueOf(unreadCount);
551 }
552 return unreadCountString;
553 }
Mindy Pereira28beb842012-02-23 09:27:07 -0800554
555 /**
556 * Get text matching the last sync status.
557 */
558 public static CharSequence getSyncStatusText(Context context, int status) {
559 String[] errors = context.getResources().getStringArray(R.array.sync_status);
560 if (status >= errors.length) {
561 return "";
562 }
563 return errors[status];
564 }
Mindy Pereira8a8c50d2012-02-23 11:09:03 -0800565
566 /**
Mindy Pereira9ae8ce02012-02-28 09:28:15 -0800567 * Create an intent to show a conversation.
568 * @param conversation Conversation to open.
Mindy Pereira161f50d2012-02-28 15:47:19 -0800569 * @param folder
570 * @param account
Mindy Pereira9ae8ce02012-02-28 09:28:15 -0800571 * @return
572 */
Mindy Pereira161f50d2012-02-28 15:47:19 -0800573 public static Intent createViewConversationIntent(Conversation conversation, Folder folder,
574 Account account) {
Mindy Pereira9ae8ce02012-02-28 09:28:15 -0800575 final Intent intent = new Intent(Intent.ACTION_VIEW);
576 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
Mindy Pereira898cd382012-03-06 08:42:47 -0800577 intent.setDataAndType(conversation.uri, account.mimeType);
Mindy Pereira161f50d2012-02-28 15:47:19 -0800578 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
579 intent.putExtra(Utils.EXTRA_FOLDER, folder);
Mindy Pereira963cded2012-02-28 15:25:21 -0800580 intent.putExtra(Utils.EXTRA_CONVERSATION, conversation);
Mindy Pereira9ae8ce02012-02-28 09:28:15 -0800581 return intent;
582 }
583
584 /**
585 * Create an intent to open a folder.
586 * @param folder Folder to open.
Mindy Pereira161f50d2012-02-28 15:47:19 -0800587 * @param account
Mindy Pereira9ae8ce02012-02-28 09:28:15 -0800588 * @return
589 */
Mindy Pereira161f50d2012-02-28 15:47:19 -0800590 public static Intent createViewFolderIntent(Folder folder, Account account) {
Mindy Pereira9ae8ce02012-02-28 09:28:15 -0800591 final Intent intent = new Intent(Intent.ACTION_VIEW);
592 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
Mindy Pereira898cd382012-03-06 08:42:47 -0800593 intent.setDataAndType(folder.uri, account.mimeType);
Mindy Pereira161f50d2012-02-28 15:47:19 -0800594 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Mindy Pereira963cded2012-02-28 15:25:21 -0800595 intent.putExtra(Utils.EXTRA_FOLDER, folder);
Mindy Pereira9ae8ce02012-02-28 09:28:15 -0800596 return intent;
597 }
598
599 /**
Paul Westbrook94e440d2012-02-24 11:03:47 -0800600 * Helper method to show context-aware Gmail help.
601 *
602 * @param context Context to be used to open the help.
603 * @param fromWhere Information about the activity the user was in
604 * when they requested help.
605 */
Mindy Pereiracfb7f332012-02-28 10:23:43 -0800606 public static void showHelp(Context context, Uri accountHelpUrl, String fromWhere) {
607 final Uri uri = addParamsToUrl(context, accountHelpUrl.toString());
Paul Westbrook94e440d2012-02-24 11:03:47 -0800608 Uri.Builder builder = uri.buildUpon();
609 // Add the activity specific information parameter.
610 if (fromWhere != null) {
611 builder = builder.appendQueryParameter(SMART_HELP_LINK_PARAMETER_NAME, fromWhere);
612 }
613
614 openUrl(context, builder.build());
615 }
616
617 /**
618 * Helper method to open a link in a browser.
619 *
620 * @param context Context
621 * @param uri Uri to open.
622 */
623 private static void openUrl(Context context, Uri uri) {
624 if(uri == null || TextUtils.isEmpty(uri.toString())) {
625 LogUtils.wtf(LOG_TAG, "invalid url in Utils.openUrl(): %s", uri);
626 return;
627 }
628 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
629 intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
630 context.startActivity(intent);
631 }
632
633
634 private static Uri addParamsToUrl(Context context, String url) {
635 url = replaceLocale(url);
636 Uri.Builder builder = Uri.parse(url).buildUpon();
637 final String versionCode = getVersionCode(context);
638 if (versionCode != null) {
639 builder = builder.appendQueryParameter(SMART_LINK_APP_VERSION, versionCode);
640 }
641
642 return builder.build();
643 }
644
645 /**
646 * Replaces the language/country of the device into the given string. The pattern "%locale%"
647 * will be replaced with the <language_code>_<country_code> value.
648 *
649 * @param str the string to replace the language/country within
650 *
651 * @return the string with replacement
652 */
653 private static String replaceLocale(String str) {
654 // Substitute locale if present in string
655 if (str.contains("%locale%")) {
656 Locale locale = Locale.getDefault();
657 String tmp = locale.getLanguage() + "_" + locale.getCountry().toLowerCase();
658 str = str.replace("%locale%", tmp);
659 }
660 return str;
661 }
662
663 /**
664 * Returns the version code for the package, or null if it cannot be retrieved.
665 */
666 public static String getVersionCode(Context context) {
667 if (sVersionCode == null) {
668 try {
669 sVersionCode = String.valueOf(context.getPackageManager()
670 .getPackageInfo(context.getPackageName(), 0 /* flags */)
671 .versionCode);
672 } catch (NameNotFoundException e) {
673 LogUtils.e(Utils.LOG_TAG, "Error finding package %s",
674 context.getApplicationInfo().packageName);
675 }
676 }
677 return sVersionCode;
678 }
Mindy Pereira1f936682012-03-02 11:30:33 -0800679
680 /**
681 * Show the settings screen for the supplied account.
682 */
683 public static void showSettings(Context context, Account account) {
684 final Intent settingsIntent = new Intent(Intent.ACTION_EDIT, account.settingsIntentUri);
685 context.startActivity(settingsIntent);
686 }
Mindy Pereira68f2e222012-03-07 10:36:54 -0800687
688 /**
689 * Retrieves the mailbox search query associated with an intent (or null if not available),
690 * doing proper sanitizing (e.g. trims whitespace).
691 */
692 public static String mailSearchQueryForIntent(Intent intent) {
693 String query = intent.getStringExtra(SearchManager.QUERY);
694 return TextUtils.isEmpty(query) ? null : query.trim();
695 }
Andy Huang88fc42e2012-03-08 15:02:43 -0800696
697 /**
698 * Split out a filename's extension and return it.
699 * @param filename a file name
700 * @return the file extension (max of 5 chars including period, like ".docx"), or null
701 */
702 public static String getFileExtension(String filename) {
703 String extension = null;
704 int index = filename.lastIndexOf('.');
705 // Limit the suffix to dot + four characters
706 if (index >= 0 && filename.length() - index <= FILE_EXTENSION_MAX_CHARS + 1) {
707 extension = filename.substring(index);
708 }
709 return extension;
710 }
711
712 /**
713 * (copied from {@link Intent#normalizeMimeType(String)} for pre-J)
714 *
715 * Normalize a MIME data type.
716 *
717 * <p>A normalized MIME type has white-space trimmed,
718 * content-type parameters removed, and is lower-case.
719 * This aligns the type with Android best practices for
720 * intent filtering.
721 *
722 * <p>For example, "text/plain; charset=utf-8" becomes "text/plain".
723 * "text/x-vCard" becomes "text/x-vcard".
724 *
725 * <p>All MIME types received from outside Android (such as user input,
726 * or external sources like Bluetooth, NFC, or the Internet) should
727 * be normalized before they are used to create an Intent.
728 *
729 * @param type MIME data type to normalize
730 * @return normalized MIME data type, or null if the input was null
731 * @see {@link #setType}
732 * @see {@link #setTypeAndNormalize}
733 */
734 public static String normalizeMimeType(String type) {
735 if (type == null) {
736 return null;
737 }
738
739 type = type.trim().toLowerCase(Locale.US);
740
741 final int semicolonIndex = type.indexOf(';');
742 if (semicolonIndex != -1) {
743 type = type.substring(0, semicolonIndex);
744 }
745 return type;
746 }
747
748 /**
749 * (copied from {@link Uri#normalize()} for pre-J)
750 *
751 * Return a normalized representation of this Uri.
752 *
753 * <p>A normalized Uri has a lowercase scheme component.
754 * This aligns the Uri with Android best practices for
755 * intent filtering.
756 *
757 * <p>For example, "HTTP://www.android.com" becomes
758 * "http://www.android.com"
759 *
760 * <p>All URIs received from outside Android (such as user input,
761 * or external sources like Bluetooth, NFC, or the Internet) should
762 * be normalized before they are used to create an Intent.
763 *
764 * <p class="note">This method does <em>not</em> validate bad URI's,
765 * or 'fix' poorly formatted URI's - so do not use it for input validation.
766 * A Uri will always be returned, even if the Uri is badly formatted to
767 * begin with and a scheme component cannot be found.
768 *
769 * @return normalized Uri (never null)
770 * @see {@link android.content.Intent#setData}
771 * @see {@link #setNormalizedData}
772 */
773 public static Uri normalizeUri(Uri uri) {
774 String scheme = uri.getScheme();
775 if (scheme == null) return uri; // give up
776 String lowerScheme = scheme.toLowerCase(Locale.US);
777 if (scheme.equals(lowerScheme)) return uri; // no change
778
779 return uri.buildUpon().scheme(lowerScheme).build();
780 }
781
782 public static Intent setIntentTypeAndNormalize(Intent intent, String type) {
783 return intent.setType(normalizeMimeType(type));
784 }
785
786 public static Intent setIntentDataAndTypeAndNormalize(Intent intent, Uri data, String type) {
787 return intent.setDataAndType(normalizeUri(data), normalizeMimeType(type));
788 }
789
Mindy Pereirab5080d52012-03-09 11:26:44 -0800790 public static int getDefaultFolderBackgroundColor(Context context) {
791 return Integer.parseInt(context.getResources().getString(
792 R.string.default_folder_background_color));
793 }
794
795 public static int getTransparentColor(int color) {
796 return 0x00ffffff & color;
797 }
Mindy Pereira7b56a612011-12-14 12:32:28 -0800798}