summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Blitzstein <sblitz@google.com>2012-11-13 10:02:41 -0800
committerSam Blitzstein <sblitz@google.com>2012-11-15 14:30:10 -0800
commit7e19bf984bc280c0cc034adf1dfa8840c75a698d (patch)
treec3dc355674a9bc86629961f2025ac0bd5a416aee
parentad83b9aa618d52952f98bf8d7ec876140e7fb404 (diff)
downloadandroid_packages_apps_Calendar-7e19bf984bc280c0cc034adf1dfa8840c75a698d.tar.gz
android_packages_apps_Calendar-7e19bf984bc280c0cc034adf1dfa8840c75a698d.tar.bz2
android_packages_apps_Calendar-7e19bf984bc280c0cc034adf1dfa8840c75a698d.zip
Added new notification actions for map and call.
Bug: 7525552 Change-Id: I6a175a270f4049897ee73c005f4f73420fc635d9
-rw-r--r--res/drawable-hdpi/ic_call.pngbin0 -> 1998 bytes
-rw-r--r--res/drawable-hdpi/ic_map.pngbin0 -> 1191 bytes
-rw-r--r--res/drawable-mdpi/ic_call.pngbin0 -> 1470 bytes
-rw-r--r--res/drawable-mdpi/ic_map.pngbin0 -> 893 bytes
-rw-r--r--res/drawable-xhdpi/ic_call.pngbin0 -> 2929 bytes
-rw-r--r--res/drawable-xhdpi/ic_map.pngbin0 -> 1584 bytes
-rw-r--r--res/layout/notification.xml18
-rw-r--r--res/values/strings.xml5
-rw-r--r--src/com/android/calendar/EventInfoFragment.java328
-rw-r--r--src/com/android/calendar/Utils.java337
-rw-r--r--src/com/android/calendar/alerts/AlertReceiver.java282
-rw-r--r--tests/src/com/android/calendar/UtilsTests.java2
12 files changed, 622 insertions, 350 deletions
diff --git a/res/drawable-hdpi/ic_call.png b/res/drawable-hdpi/ic_call.png
new file mode 100644
index 00000000..4a3a05f3
--- /dev/null
+++ b/res/drawable-hdpi/ic_call.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_map.png b/res/drawable-hdpi/ic_map.png
new file mode 100644
index 00000000..2daebc56
--- /dev/null
+++ b/res/drawable-hdpi/ic_map.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_call.png b/res/drawable-mdpi/ic_call.png
new file mode 100644
index 00000000..a10fd770
--- /dev/null
+++ b/res/drawable-mdpi/ic_call.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_map.png b/res/drawable-mdpi/ic_map.png
new file mode 100644
index 00000000..9f52789c
--- /dev/null
+++ b/res/drawable-mdpi/ic_map.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call.png b/res/drawable-xhdpi/ic_call.png
new file mode 100644
index 00000000..564e1892
--- /dev/null
+++ b/res/drawable-xhdpi/ic_call.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_map.png b/res/drawable-xhdpi/ic_map.png
new file mode 100644
index 00000000..d4c57200
--- /dev/null
+++ b/res/drawable-xhdpi/ic_map.png
Binary files differ
diff --git a/res/layout/notification.xml b/res/layout/notification.xml
index 4a723c75..b56d5c68 100644
--- a/res/layout/notification.xml
+++ b/res/layout/notification.xml
@@ -56,6 +56,24 @@
</LinearLayout>
<ImageButton
+ android:id="@+id/map_button"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical|right"
+ android:src="@drawable/ic_map"
+ android:background="?android:attr/selectableItemBackground"
+ android:padding="8dip"
+ android:contentDescription="@string/map_label" />
+ <ImageButton
+ android:id="@+id/call_button"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical|right"
+ android:src="@drawable/ic_call"
+ android:background="?android:attr/selectableItemBackground"
+ android:padding="8dip"
+ android:contentDescription="@string/call_label" />
+ <ImageButton
android:id="@+id/email_button"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5a6472f4..9c577802 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -294,6 +294,11 @@
<!-- Toast message displayed when the event id was not found LIMIT=30] -->
<string name="event_not_found">"Event not found."</string>
+ <!-- Notification label for getting a map of the location. -->
+ <string name="map_label">Map</string>
+ <!-- Notification label for calling a phone number in the location. -->
+ <string name="call_label">Call</string>
+
<!-- Title of the quick response item in Settings [CHAR LIMIT=18]-->
<string name="quick_response_settings">Quick responses</string>
<!-- Summary of the quick response item in Settings [CHAR LIMIT=80]-->
diff --git a/src/com/android/calendar/EventInfoFragment.java b/src/com/android/calendar/EventInfoFragment.java
index 88e58355..d3caae0b 100644
--- a/src/com/android/calendar/EventInfoFragment.java
+++ b/src/com/android/calendar/EventInfoFragment.java
@@ -254,10 +254,6 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange
static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?";
static final String CALENDARS_VISIBLE_WHERE = Calendars.VISIBLE + "=?";
- private static final String NANP_ALLOWED_SYMBOLS = "()+-*#.";
- private static final int NANP_MIN_DIGITS = 7;
- private static final int NANP_MAX_DIGITS = 11;
-
private View mView;
@@ -320,8 +316,6 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange
private boolean mNoCrossFade = false; // Used to prevent repeated cross-fade
- private static final Pattern mWildcardPattern = Pattern.compile("^.*$");
-
ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
@@ -1233,7 +1227,7 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange
textView.setAutoLinkMask(0);
textView.setText(location.trim());
try {
- linkifyTextView(textView);
+ Utils.linkifyTextView(textView, true);
} catch (Exception ex) {
// unexpected
Log.e(TAG, "Linkification failed", ex);
@@ -1337,326 +1331,6 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange
return;
}
- /**
- * Finds North American Numbering Plan (NANP) phone numbers in the input text.
- *
- * @param text The text to scan.
- * @return A list of [start, end) pairs indicating the positions of phone numbers in the input.
- */
- // @VisibleForTesting
- static int[] findNanpPhoneNumbers(CharSequence text) {
- ArrayList<Integer> list = new ArrayList<Integer>();
-
- int startPos = 0;
- int endPos = text.length() - NANP_MIN_DIGITS + 1;
- if (endPos < 0) {
- return new int[] {};
- }
-
- /*
- * We can't just strip the whitespace out and crunch it down, because the whitespace
- * is significant. March through, trying to figure out where numbers start and end.
- */
- while (startPos < endPos) {
- // skip whitespace
- while (Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
- startPos++;
- }
- if (startPos == endPos) {
- break;
- }
-
- // check for a match at this position
- int matchEnd = findNanpMatchEnd(text, startPos);
- if (matchEnd > startPos) {
- list.add(startPos);
- list.add(matchEnd);
- startPos = matchEnd; // skip past match
- } else {
- // skip to next whitespace char
- while (!Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
- startPos++;
- }
- }
- }
-
- int[] result = new int[list.size()];
- for (int i = list.size() - 1; i >= 0; i--) {
- result[i] = list.get(i);
- }
- return result;
- }
-
- /**
- * Checks to see if there is a valid phone number in the input, starting at the specified
- * offset. If so, the index of the last character + 1 is returned. The input is assumed
- * to begin with a non-whitespace character.
- *
- * @return Exclusive end position, or -1 if not a match.
- */
- private static int findNanpMatchEnd(CharSequence text, int startPos) {
- /*
- * A few interesting cases:
- * 94043 # too short, ignore
- * 123456789012 # too long, ignore
- * +1 (650) 555-1212 # 11 digits, spaces
- * (650) 555 5555 # Second space, only when first is present.
- * (650) 555-1212, (650) 555-1213 # two numbers, return first
- * 1-650-555-1212 # 11 digits with leading '1'
- * *#650.555.1212#*! # 10 digits, include #*, ignore trailing '!'
- * 555.1212 # 7 digits
- *
- * For the most part we want to break on whitespace, but it's common to leave a space
- * between the initial '1' and/or after the area code.
- */
-
- // Check for "tel:" URI prefix.
- if (text.length() > startPos+4
- && text.subSequence(startPos, startPos+4).toString().equalsIgnoreCase("tel:")) {
- startPos += 4;
- }
-
- int endPos = text.length();
- int curPos = startPos;
- int foundDigits = 0;
- char firstDigit = 'x';
- boolean foundWhiteSpaceAfterAreaCode = false;
-
- while (curPos <= endPos) {
- char ch;
- if (curPos < endPos) {
- ch = text.charAt(curPos);
- } else {
- ch = 27; // fake invalid symbol at end to trigger loop break
- }
-
- if (Character.isDigit(ch)) {
- if (foundDigits == 0) {
- firstDigit = ch;
- }
- foundDigits++;
- if (foundDigits > NANP_MAX_DIGITS) {
- // too many digits, stop early
- return -1;
- }
- } else if (Character.isWhitespace(ch)) {
- if ( (firstDigit == '1' && foundDigits == 4) ||
- (foundDigits == 3)) {
- foundWhiteSpaceAfterAreaCode = true;
- } else if (firstDigit == '1' && foundDigits == 1) {
- } else if (foundWhiteSpaceAfterAreaCode
- && ( (firstDigit == '1' && (foundDigits == 7)) || (foundDigits == 6))) {
- } else {
- break;
- }
- } else if (NANP_ALLOWED_SYMBOLS.indexOf(ch) == -1) {
- break;
- }
- // else it's an allowed symbol
-
- curPos++;
- }
-
- if ((firstDigit != '1' && (foundDigits == 7 || foundDigits == 10)) ||
- (firstDigit == '1' && foundDigits == 11)) {
- // match
- return curPos;
- }
-
- return -1;
- }
-
- private static int indexFirstNonWhitespaceChar(CharSequence str) {
- for (int i = 0; i < str.length(); i++) {
- if (!Character.isWhitespace(str.charAt(i))) {
- return i;
- }
- }
- return -1;
- }
-
- private static int indexLastNonWhitespaceChar(CharSequence str) {
- for (int i = str.length() - 1; i >= 0; i--) {
- if (!Character.isWhitespace(str.charAt(i))) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * Replaces stretches of text that look like addresses and phone numbers with clickable
- * links.
- * <p>
- * This is really just an enhanced version of Linkify.addLinks().
- */
- private static void linkifyTextView(TextView textView) {
- /*
- * If the text includes a street address like "1600 Amphitheater Parkway, 94043",
- * the current Linkify code will identify "94043" as a phone number and invite
- * you to dial it (and not provide a map link for the address). For outside US,
- * use Linkify result iff it spans the entire text. Otherwise send the user to maps.
- */
- String defaultPhoneRegion = System.getProperty("user.region", "US");
- if (!defaultPhoneRegion.equals("US")) {
- CharSequence origText = textView.getText();
- Linkify.addLinks(textView, Linkify.ALL);
-
- // If Linkify links the entire text, use that result.
- if (textView.getText() instanceof Spannable) {
- Spannable spanText = (Spannable) textView.getText();
- URLSpan[] spans = spanText.getSpans(0, spanText.length(), URLSpan.class);
- if (spans.length == 1) {
- int linkStart = spanText.getSpanStart(spans[0]);
- int linkEnd = spanText.getSpanEnd(spans[0]);
- if (linkStart <= indexFirstNonWhitespaceChar(origText) &&
- linkEnd >= indexLastNonWhitespaceChar(origText) + 1) {
- return;
- }
- }
- }
-
- // Otherwise default to geo.
- textView.setText(origText);
- Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
- return;
- }
-
- /*
- * For within US, we want to have better recognition of phone numbers without losing
- * any of the existing annotations. Ideally this would be addressed by improving Linkify.
- * For now we manage it as a second pass over the text.
- *
- * URIs and e-mail addresses are pretty easy to pick out of text. Phone numbers
- * are a bit tricky because they have radically different formats in different
- * countries, in terms of both the digits and the way in which they are commonly
- * written or presented (e.g. the punctuation and spaces in "(650) 555-1212").
- * The expected format of a street address is defined in WebView.findAddress(). It's
- * pretty narrowly defined, so it won't often match.
- *
- * The RFC 3966 specification defines the format of a "tel:" URI.
- *
- * Start by letting Linkify find anything that isn't a phone number. We have to let it
- * run first because every invocation removes all previous URLSpan annotations.
- *
- * Ideally we'd use the external/libphonenumber routines, but those aren't available
- * to unbundled applications.
- */
- boolean linkifyFoundLinks = Linkify.addLinks(textView,
- Linkify.ALL & ~(Linkify.PHONE_NUMBERS));
-
- /*
- * Search for phone numbers.
- *
- * Some URIs contain strings of digits that look like phone numbers. If both the URI
- * scanner and the phone number scanner find them, we want the URI link to win. Since
- * the URI scanner runs first, we just need to avoid creating overlapping spans.
- */
- CharSequence text = textView.getText();
- int[] phoneSequences = findNanpPhoneNumbers(text);
-
- /*
- * If the contents of the TextView are already Spannable (which will be the case if
- * Linkify found stuff, but might not be otherwise), we can just add annotations
- * to what's there. If it's not, and we find phone numbers, we need to convert it to
- * a Spannable form. (This mimics the behavior of Linkable.addLinks().)
- */
- Spannable spanText;
- if (text instanceof SpannableString) {
- spanText = (SpannableString) text;
- } else {
- spanText = SpannableString.valueOf(text);
- }
-
- /*
- * Get a list of any spans created by Linkify, for the overlapping span check.
- */
- URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class);
-
- /*
- * Insert spans for the numbers we found. We generate "tel:" URIs.
- */
- int phoneCount = 0;
- for (int match = 0; match < phoneSequences.length / 2; match++) {
- int start = phoneSequences[match*2];
- int end = phoneSequences[match*2 + 1];
-
- if (spanWillOverlap(spanText, existingSpans, start, end)) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- CharSequence seq = text.subSequence(start, end);
- Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap");
- }
- continue;
- }
-
- /*
- * The Linkify code takes the matching span and strips out everything that isn't a
- * digit or '+' sign. We do the same here. Extension numbers will get appended
- * without a separator, but the dialer wasn't doing anything useful with ";ext="
- * anyway.
- */
-
- //String dialStr = phoneUtil.format(match.number(),
- // PhoneNumberUtil.PhoneNumberFormat.RFC3966);
- StringBuilder dialBuilder = new StringBuilder();
- for (int i = start; i < end; i++) {
- char ch = spanText.charAt(i);
- if (ch == '+' || Character.isDigit(ch)) {
- dialBuilder.append(ch);
- }
- }
- URLSpan span = new URLSpan("tel:" + dialBuilder.toString());
-
- spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- phoneCount++;
- }
-
- if (phoneCount != 0) {
- // If we had to "upgrade" to Spannable, store the object into the TextView.
- if (spanText != text) {
- textView.setText(spanText);
- }
-
- // Linkify.addLinks() sets the TextView movement method if it finds any links. We
- // want to do the same here. (This is cloned from Linkify.addLinkMovementMethod().)
- MovementMethod mm = textView.getMovementMethod();
-
- if ((mm == null) || !(mm instanceof LinkMovementMethod)) {
- if (textView.getLinksClickable()) {
- textView.setMovementMethod(LinkMovementMethod.getInstance());
- }
- }
- }
-
- if (!linkifyFoundLinks && phoneCount == 0) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "No linkification matches, using geo default");
- }
- Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
- }
- }
-
- /**
- * Determines whether a new span at [start,end) will overlap with any existing span.
- */
- private static boolean spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start,
- int end) {
- if (start == end) {
- // empty span, ignore
- return false;
- }
- for (URLSpan span : spanList) {
- int existingStart = spanText.getSpanStart(span);
- int existingEnd = spanText.getSpanEnd(span);
- if ((start >= existingStart && start < existingEnd) ||
- end > existingStart && end <= existingEnd) {
- return true;
- }
- }
-
- return false;
- }
-
private void sendAccessibilityEvent() {
AccessibilityManager am =
(AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE);
diff --git a/src/com/android/calendar/Utils.java b/src/com/android/calendar/Utils.java
index a3198cd2..a3c6abe6 100644
--- a/src/com/android/calendar/Utils.java
+++ b/src/com/android/calendar/Utils.java
@@ -39,12 +39,20 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.CalendarContract.Calendars;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.format.Time;
+import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
+import android.text.style.URLSpan;
+import android.text.util.Linkify;
import android.util.Log;
import android.widget.SearchView;
+import android.widget.TextView;
import com.android.calendar.CalendarController.ViewType;
import com.android.calendar.CalendarUtils.TimeZoneUtils;
@@ -62,6 +70,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
+import java.util.regex.Pattern;
public class Utils {
private static final boolean DEBUG = false;
@@ -128,6 +137,12 @@ public class Utils {
private static long mTardis = 0;
private static String sVersion = null;
+ private static final Pattern mWildcardPattern = Pattern.compile("^.*$");
+ private static final String NANP_ALLOWED_SYMBOLS = "()+-*#.";
+ private static final int NANP_MIN_DIGITS = 7;
+ private static final int NANP_MAX_DIGITS = 11;
+
+
/**
* Returns whether the SDK is the Jellybean release or later.
*/
@@ -1550,4 +1565,326 @@ public class Utils {
extras.putBoolean("metafeedonly", true);
ContentResolver.requestSync(account, Calendars.CONTENT_URI.getAuthority(), extras);
}
+
+ /**
+ * Replaces stretches of text that look like addresses and phone numbers with clickable
+ * links. If lastDitchGeo is true, then if no links are found in the textview, the entire
+ * string will be converted to a single geo link.
+ * <p>
+ * This is really just an enhanced version of Linkify.addLinks().
+ */
+ public static void linkifyTextView(TextView textView, boolean lastDitchGeo) {
+ /*
+ * If the text includes a street address like "1600 Amphitheater Parkway, 94043",
+ * the current Linkify code will identify "94043" as a phone number and invite
+ * you to dial it (and not provide a map link for the address). For outside US,
+ * use Linkify result iff it spans the entire text. Otherwise send the user to maps.
+ */
+ String defaultPhoneRegion = System.getProperty("user.region", "US");
+ if (!defaultPhoneRegion.equals("US")) {
+ CharSequence origText = textView.getText();
+ Linkify.addLinks(textView, Linkify.ALL);
+
+ // If Linkify links the entire text, use that result.
+ if (textView.getText() instanceof Spannable) {
+ Spannable spanText = (Spannable) textView.getText();
+ URLSpan[] spans = spanText.getSpans(0, spanText.length(), URLSpan.class);
+ if (spans.length == 1) {
+ int linkStart = spanText.getSpanStart(spans[0]);
+ int linkEnd = spanText.getSpanEnd(spans[0]);
+ if (linkStart <= indexFirstNonWhitespaceChar(origText) &&
+ linkEnd >= indexLastNonWhitespaceChar(origText) + 1) {
+ return;
+ }
+ }
+ }
+
+ // Otherwise default to geo.
+ textView.setText(origText);
+ Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
+ return;
+ }
+
+ /*
+ * For within US, we want to have better recognition of phone numbers without losing
+ * any of the existing annotations. Ideally this would be addressed by improving Linkify.
+ * For now we manage it as a second pass over the text.
+ *
+ * URIs and e-mail addresses are pretty easy to pick out of text. Phone numbers
+ * are a bit tricky because they have radically different formats in different
+ * countries, in terms of both the digits and the way in which they are commonly
+ * written or presented (e.g. the punctuation and spaces in "(650) 555-1212").
+ * The expected format of a street address is defined in WebView.findAddress(). It's
+ * pretty narrowly defined, so it won't often match.
+ *
+ * The RFC 3966 specification defines the format of a "tel:" URI.
+ *
+ * Start by letting Linkify find anything that isn't a phone number. We have to let it
+ * run first because every invocation removes all previous URLSpan annotations.
+ *
+ * Ideally we'd use the external/libphonenumber routines, but those aren't available
+ * to unbundled applications.
+ */
+ boolean linkifyFoundLinks = Linkify.addLinks(textView,
+ Linkify.ALL & ~(Linkify.PHONE_NUMBERS));
+
+ /*
+ * Search for phone numbers.
+ *
+ * Some URIs contain strings of digits that look like phone numbers. If both the URI
+ * scanner and the phone number scanner find them, we want the URI link to win. Since
+ * the URI scanner runs first, we just need to avoid creating overlapping spans.
+ */
+ CharSequence text = textView.getText();
+ int[] phoneSequences = findNanpPhoneNumbers(text);
+
+ /*
+ * If the contents of the TextView are already Spannable (which will be the case if
+ * Linkify found stuff, but might not be otherwise), we can just add annotations
+ * to what's there. If it's not, and we find phone numbers, we need to convert it to
+ * a Spannable form. (This mimics the behavior of Linkable.addLinks().)
+ */
+ Spannable spanText;
+ if (text instanceof SpannableString) {
+ spanText = (SpannableString) text;
+ } else {
+ spanText = SpannableString.valueOf(text);
+ }
+
+ /*
+ * Get a list of any spans created by Linkify, for the overlapping span check.
+ */
+ URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class);
+
+ /*
+ * Insert spans for the numbers we found. We generate "tel:" URIs.
+ */
+ int phoneCount = 0;
+ for (int match = 0; match < phoneSequences.length / 2; match++) {
+ int start = phoneSequences[match*2];
+ int end = phoneSequences[match*2 + 1];
+
+ if (spanWillOverlap(spanText, existingSpans, start, end)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ CharSequence seq = text.subSequence(start, end);
+ Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap");
+ }
+ continue;
+ }
+
+ /*
+ * The Linkify code takes the matching span and strips out everything that isn't a
+ * digit or '+' sign. We do the same here. Extension numbers will get appended
+ * without a separator, but the dialer wasn't doing anything useful with ";ext="
+ * anyway.
+ */
+
+ //String dialStr = phoneUtil.format(match.number(),
+ // PhoneNumberUtil.PhoneNumberFormat.RFC3966);
+ StringBuilder dialBuilder = new StringBuilder();
+ for (int i = start; i < end; i++) {
+ char ch = spanText.charAt(i);
+ if (ch == '+' || Character.isDigit(ch)) {
+ dialBuilder.append(ch);
+ }
+ }
+ URLSpan span = new URLSpan("tel:" + dialBuilder.toString());
+
+ spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ phoneCount++;
+ }
+
+ if (phoneCount != 0) {
+ // If we had to "upgrade" to Spannable, store the object into the TextView.
+ if (spanText != text) {
+ textView.setText(spanText);
+ }
+
+ // Linkify.addLinks() sets the TextView movement method if it finds any links. We
+ // want to do the same here. (This is cloned from Linkify.addLinkMovementMethod().)
+ MovementMethod mm = textView.getMovementMethod();
+
+ if ((mm == null) || !(mm instanceof LinkMovementMethod)) {
+ if (textView.getLinksClickable()) {
+ textView.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ }
+
+ if (lastDitchGeo && !linkifyFoundLinks && phoneCount == 0) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "No linkification matches, using geo default");
+ }
+ Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
+ }
+ }
+
+ private static int indexFirstNonWhitespaceChar(CharSequence str) {
+ for (int i = 0; i < str.length(); i++) {
+ if (!Character.isWhitespace(str.charAt(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static int indexLastNonWhitespaceChar(CharSequence str) {
+ for (int i = str.length() - 1; i >= 0; i--) {
+ if (!Character.isWhitespace(str.charAt(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Finds North American Numbering Plan (NANP) phone numbers in the input text.
+ *
+ * @param text The text to scan.
+ * @return A list of [start, end) pairs indicating the positions of phone numbers in the input.
+ */
+ // @VisibleForTesting
+ static int[] findNanpPhoneNumbers(CharSequence text) {
+ ArrayList<Integer> list = new ArrayList<Integer>();
+
+ int startPos = 0;
+ int endPos = text.length() - NANP_MIN_DIGITS + 1;
+ if (endPos < 0) {
+ return new int[] {};
+ }
+
+ /*
+ * We can't just strip the whitespace out and crunch it down, because the whitespace
+ * is significant. March through, trying to figure out where numbers start and end.
+ */
+ while (startPos < endPos) {
+ // skip whitespace
+ while (Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
+ startPos++;
+ }
+ if (startPos == endPos) {
+ break;
+ }
+
+ // check for a match at this position
+ int matchEnd = findNanpMatchEnd(text, startPos);
+ if (matchEnd > startPos) {
+ list.add(startPos);
+ list.add(matchEnd);
+ startPos = matchEnd; // skip past match
+ } else {
+ // skip to next whitespace char
+ while (!Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
+ startPos++;
+ }
+ }
+ }
+
+ int[] result = new int[list.size()];
+ for (int i = list.size() - 1; i >= 0; i--) {
+ result[i] = list.get(i);
+ }
+ return result;
+ }
+
+ /**
+ * Checks to see if there is a valid phone number in the input, starting at the specified
+ * offset. If so, the index of the last character + 1 is returned. The input is assumed
+ * to begin with a non-whitespace character.
+ *
+ * @return Exclusive end position, or -1 if not a match.
+ */
+ private static int findNanpMatchEnd(CharSequence text, int startPos) {
+ /*
+ * A few interesting cases:
+ * 94043 # too short, ignore
+ * 123456789012 # too long, ignore
+ * +1 (650) 555-1212 # 11 digits, spaces
+ * (650) 555 5555 # Second space, only when first is present.
+ * (650) 555-1212, (650) 555-1213 # two numbers, return first
+ * 1-650-555-1212 # 11 digits with leading '1'
+ * *#650.555.1212#*! # 10 digits, include #*, ignore trailing '!'
+ * 555.1212 # 7 digits
+ *
+ * For the most part we want to break on whitespace, but it's common to leave a space
+ * between the initial '1' and/or after the area code.
+ */
+
+ // Check for "tel:" URI prefix.
+ if (text.length() > startPos+4
+ && text.subSequence(startPos, startPos+4).toString().equalsIgnoreCase("tel:")) {
+ startPos += 4;
+ }
+
+ int endPos = text.length();
+ int curPos = startPos;
+ int foundDigits = 0;
+ char firstDigit = 'x';
+ boolean foundWhiteSpaceAfterAreaCode = false;
+
+ while (curPos <= endPos) {
+ char ch;
+ if (curPos < endPos) {
+ ch = text.charAt(curPos);
+ } else {
+ ch = 27; // fake invalid symbol at end to trigger loop break
+ }
+
+ if (Character.isDigit(ch)) {
+ if (foundDigits == 0) {
+ firstDigit = ch;
+ }
+ foundDigits++;
+ if (foundDigits > NANP_MAX_DIGITS) {
+ // too many digits, stop early
+ return -1;
+ }
+ } else if (Character.isWhitespace(ch)) {
+ if ( (firstDigit == '1' && foundDigits == 4) ||
+ (foundDigits == 3)) {
+ foundWhiteSpaceAfterAreaCode = true;
+ } else if (firstDigit == '1' && foundDigits == 1) {
+ } else if (foundWhiteSpaceAfterAreaCode
+ && ( (firstDigit == '1' && (foundDigits == 7)) || (foundDigits == 6))) {
+ } else {
+ break;
+ }
+ } else if (NANP_ALLOWED_SYMBOLS.indexOf(ch) == -1) {
+ break;
+ }
+ // else it's an allowed symbol
+
+ curPos++;
+ }
+
+ if ((firstDigit != '1' && (foundDigits == 7 || foundDigits == 10)) ||
+ (firstDigit == '1' && foundDigits == 11)) {
+ // match
+ return curPos;
+ }
+
+ return -1;
+ }
+
+ /**
+ * Determines whether a new span at [start,end) will overlap with any existing span.
+ */
+ private static boolean spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start,
+ int end) {
+ if (start == end) {
+ // empty span, ignore
+ return false;
+ }
+ for (URLSpan span : spanList) {
+ int existingStart = spanText.getSpanStart(span);
+ int existingEnd = spanText.getSpanEnd(span);
+ if ((start >= existingStart && start < existingEnd) ||
+ end > existingStart && end <= existingEnd) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
}
diff --git a/src/com/android/calendar/alerts/AlertReceiver.java b/src/com/android/calendar/alerts/AlertReceiver.java
index a0a82d54..f3a45978 100644
--- a/src/com/android/calendar/alerts/AlertReceiver.java
+++ b/src/com/android/calendar/alerts/AlertReceiver.java
@@ -23,6 +23,7 @@ import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
@@ -32,13 +33,18 @@ import android.os.PowerManager;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
+import android.telephony.TelephonyManager;
+import android.text.Spannable;
+import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.RelativeSizeSpan;
import android.text.style.TextAppearanceSpan;
+import android.text.style.URLSpan;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
+import android.widget.TextView;
import com.android.calendar.R;
import com.android.calendar.Utils;
@@ -66,6 +72,8 @@ public class AlertReceiver extends BroadcastReceiver {
private static final String TAG = "AlertReceiver";
private static final String DELETE_ALL_ACTION = "com.android.calendar.DELETEALL";
+ private static final String MAP_ACTION = "com.android.calendar.MAP";
+ private static final String CALL_ACTION = "com.android.calendar.CALL";
private static final String MAIL_ACTION = "com.android.calendar.MAIL";
private static final String EXTRA_EVENT_ID = "eventid";
@@ -77,6 +85,10 @@ public class AlertReceiver extends BroadcastReceiver {
public static final String ACTION_DISMISS_OLD_REMINDERS = "removeOldReminders";
private static final int NOTIFICATION_DIGEST_MAX_LENGTH = 3;
+ private static final String GEO_PREFIX = "geo:";
+ private static final String TEL_PREFIX = "tel:";
+ private static final int MAX_NOTIF_ACTIONS = 3;
+
private static Handler sAsyncHandler;
static {
HandlerThread thr = new HandlerThread("AlertReceiver async");
@@ -97,10 +109,46 @@ public class AlertReceiver extends BroadcastReceiver {
// TODO Grab a wake lock here?
Intent serviceIntent = new Intent(context, DismissAlarmsService.class);
context.startService(serviceIntent);
+ } else if (MAP_ACTION.equals(intent.getAction())) {
+ // Try starting the map action.
+ // If no map location is found (something changed since the notification was originally
+ // fired), update the notifications to express this change.
+ final long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1);
+ if (eventId != -1) {
+ URLSpan[] urlSpans = getURLSpans(context, eventId);
+ Intent geoIntent = createMapActivityIntent(context, urlSpans);
+ if (geoIntent != null) {
+ // Location was successfully found, so dismiss the shade and start maps.
+ context.startActivity(geoIntent);
+ closeNotificationShade(context);
+ } else {
+ // No location was found, so update all notifications.
+ // Our alert service does not currently allow us to specify only one
+ // specific notification to refresh.
+ AlertService.updateAlertNotification(context);
+ }
+ }
+ } else if (CALL_ACTION.equals(intent.getAction())) {
+ // Try starting the call action.
+ // If no call location is found (something changed since the notification was originally
+ // fired), update the notifications to express this change.
+ final long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1);
+ if (eventId != -1) {
+ URLSpan[] urlSpans = getURLSpans(context, eventId);
+ Intent callIntent = createCallActivityIntent(context, urlSpans);
+ if (callIntent != null) {
+ // Call location was successfully found, so dismiss the shade and start dialer.
+ context.startActivity(callIntent);
+ closeNotificationShade(context);
+ } else {
+ // No call location was found, so update all notifications.
+ // Our alert service does not currently allow us to specify only one
+ // specific notification to refresh.
+ AlertService.updateAlertNotification(context);
+ }
+ }
} else if (MAIL_ACTION.equals(intent.getAction())) {
- // Close the notification shade.
- Intent closeNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
- context.sendBroadcast(closeNotificationShadeIntent);
+ closeNotificationShade(context);
// Now start the email intent.
final long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1);
@@ -254,15 +302,24 @@ public class AlertReceiver extends BroadcastReceiver {
notificationBuilder.setFullScreenIntent(createAlertActivityIntent(context), true);
}
- PendingIntent snoozeIntent = null;
- PendingIntent emailIntent = null;
+ PendingIntent mapIntent = null, callIntent = null, snoozeIntent = null, emailIntent = null;
if (addActionButtons) {
- // Create snooze intent. TODO: change snooze to 10 minutes.
- snoozeIntent = createSnoozeIntent(context, eventId, startMillis, endMillis,
- notificationId);
+ // Send map, call, and email intent back to ourself first for a couple reasons:
+ // 1) Workaround issue where clicking action button in notification does
+ // not automatically close the notification shade.
+ // 2) Event information will always be up to date.
+
+ // Create map and/or call intents.
+ URLSpan[] urlSpans = getURLSpans(context, eventId);
+ mapIntent = createMapBroadcastIntent(context, urlSpans, eventId);
+ callIntent = createCallBroadcastIntent(context, urlSpans, eventId);
// Create email intent for emailing attendees.
emailIntent = createBroadcastMailIntent(context, eventId, title);
+
+ // Create snooze intent. TODO: change snooze to 10 minutes.
+ snoozeIntent = createSnoozeIntent(context, eventId, startMillis, endMillis,
+ notificationId);
}
if (Utils.isJellybeanOrLater()) {
@@ -273,14 +330,34 @@ public class AlertReceiver extends BroadcastReceiver {
// A higher priority will encourage notification manager to expand it.
notificationBuilder.setPriority(priority);
- // Add action buttons.
- if (snoozeIntent != null) {
- notificationBuilder.addAction(R.drawable.ic_alarm_holo_dark,
- resources.getString(R.string.snooze_label), snoozeIntent);
+ // Add action buttons. Show at most three, using the following priority ordering:
+ // 1. Map
+ // 2. Call
+ // 3. Email
+ // 4. Snooze
+ // Actions will only be shown if they are applicable; i.e. with no location, map will
+ // not be shown, and with no recipients, snooze will not be shown.
+ // TODO: Get icons, get strings. Maybe show preview of actual location/number?
+ int numActions = 0;
+ if (mapIntent != null && numActions < MAX_NOTIF_ACTIONS) {
+ notificationBuilder.addAction(R.drawable.ic_map,
+ resources.getString(R.string.map_label), mapIntent);
+ numActions++;
}
- if (emailIntent != null) {
+ if (callIntent != null && numActions < MAX_NOTIF_ACTIONS) {
+ notificationBuilder.addAction(R.drawable.ic_call,
+ resources.getString(R.string.call_label), callIntent);
+ numActions++;
+ }
+ if (emailIntent != null && numActions < MAX_NOTIF_ACTIONS) {
notificationBuilder.addAction(R.drawable.ic_menu_email_holo_dark,
resources.getString(R.string.email_guests_label), emailIntent);
+ numActions++;
+ }
+ if (snoozeIntent != null && numActions < MAX_NOTIF_ACTIONS) {
+ notificationBuilder.addAction(R.drawable.ic_alarm_holo_dark,
+ resources.getString(R.string.snooze_label), snoozeIntent);
+ numActions++;
}
return notificationBuilder.getNotification();
@@ -295,20 +372,41 @@ public class AlertReceiver extends BroadcastReceiver {
contentView.setImageViewResource(R.id.image, R.drawable.stat_notify_calendar);
contentView.setTextViewText(R.id.title, title);
contentView.setTextViewText(R.id.text, summaryText);
- if (snoozeIntent == null) {
- contentView.setViewVisibility(R.id.email_button, View.GONE);
+
+ int numActions = 0;
+ if (mapIntent == null || numActions >= MAX_NOTIF_ACTIONS) {
+ contentView.setViewVisibility(R.id.map_button, View.GONE);
} else {
- contentView.setViewVisibility(R.id.snooze_button, View.VISIBLE);
- contentView.setOnClickPendingIntent(R.id.snooze_button, snoozeIntent);
+ contentView.setViewVisibility(R.id.map_button, View.VISIBLE);
+ contentView.setOnClickPendingIntent(R.id.map_button, mapIntent);
contentView.setViewVisibility(R.id.end_padding, View.GONE);
+ numActions++;
}
- if (emailIntent == null) {
+ if (callIntent == null || numActions >= MAX_NOTIF_ACTIONS) {
+ contentView.setViewVisibility(R.id.call_button, View.GONE);
+ } else {
+ contentView.setViewVisibility(R.id.call_button, View.VISIBLE);
+ contentView.setOnClickPendingIntent(R.id.call_button, callIntent);
+ contentView.setViewVisibility(R.id.end_padding, View.GONE);
+ numActions++;
+ }
+ if (emailIntent == null || numActions >= MAX_NOTIF_ACTIONS) {
contentView.setViewVisibility(R.id.email_button, View.GONE);
} else {
contentView.setViewVisibility(R.id.email_button, View.VISIBLE);
contentView.setOnClickPendingIntent(R.id.email_button, emailIntent);
contentView.setViewVisibility(R.id.end_padding, View.GONE);
+ numActions++;
}
+ if (snoozeIntent == null || numActions >= MAX_NOTIF_ACTIONS) {
+ contentView.setViewVisibility(R.id.snooze_button, View.GONE);
+ } else {
+ contentView.setViewVisibility(R.id.snooze_button, View.VISIBLE);
+ contentView.setOnClickPendingIntent(R.id.snooze_button, snoozeIntent);
+ contentView.setViewVisibility(R.id.end_padding, View.GONE);
+ numActions++;
+ }
+
n.contentView = contentView;
return n;
@@ -486,6 +584,11 @@ public class AlertReceiver extends BroadcastReceiver {
return nw;
}
+ private void closeNotificationShade(Context context) {
+ Intent closeNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ context.sendBroadcast(closeNotificationShadeIntent);
+ }
+
private static final String[] ATTENDEES_PROJECTION = new String[] {
Attendees.ATTENDEE_EMAIL, // 0
Attendees.ATTENDEE_STATUS, // 1
@@ -519,6 +622,12 @@ public class AlertReceiver extends BroadcastReceiver {
ATTENDEES_SORT_ORDER);
}
+ private static Cursor getLocationCursor(Context context, long eventId) {
+ return context.getContentResolver().query(
+ ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+ new String[] { Events.EVENT_LOCATION }, null, null, null);
+ }
+
/**
* Creates a broadcast pending intent that fires to AlertReceiver when the email button
* is clicked.
@@ -545,10 +654,6 @@ public class AlertReceiver extends BroadcastReceiver {
do {
String email = attendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
if (Utils.isEmailableFrom(email, syncAccount)) {
- // Send intent back to ourself first for a couple reasons:
- // 1) Workaround issue where clicking action button in notification does
- // not automatically close the notification shade.
- // 2) Attendees list in email will always be up to date.
Intent broadcastIntent = new Intent(MAIL_ACTION);
broadcastIntent.setClass(context, AlertReceiver.class);
broadcastIntent.putExtra(EXTRA_EVENT_ID, eventId);
@@ -647,4 +752,137 @@ public class AlertReceiver extends BroadcastReceiver {
emailList.add(email);
}
}
+
+ /**
+ * Using the linkify magic, get a list of URLs from the event's location. If no such links
+ * are found, we should end up with a single geo link of the entire string.
+ */
+ private static URLSpan[] getURLSpans(Context context, long eventId) {
+ Cursor locationCursor = getLocationCursor(context, eventId);
+ if (locationCursor != null && locationCursor.moveToFirst()) {
+ String location = locationCursor.getString(0); // Only one item in this cursor.
+ if (location == null || location.isEmpty()) {
+ // Return an empty list if we know there was nothing in the location field.
+ return new URLSpan[0];
+ }
+
+ TextView locationTV = new TextView(context);
+ locationTV.setText(location);
+ Utils.linkifyTextView(locationTV, false);
+ CharSequence text = locationTV.getText();
+
+ // The linkify method should have found at least one link, at the very least.
+ // If no smart links were found, it should have set the whole string as a geo link.
+ if (text instanceof Spannable) {
+ Spannable spanText = (SpannableString) locationTV.getText();
+ URLSpan[] urlSpans =
+ spanText.getSpans(0, spanText.length(), URLSpan.class);
+ return urlSpans;
+ }
+ }
+
+ // If no links were found or location was empty, return an empty list.
+ return new URLSpan[0];
+ }
+
+ /**
+ * Create a pending intent to send ourself a broadcast to start maps, using the first map
+ * link available.
+ * If no links are found, return null.
+ */
+ private static PendingIntent createMapBroadcastIntent(Context context, URLSpan[] urlSpans,
+ long eventId) {
+ for (int span_i = 0; span_i < urlSpans.length; span_i++) {
+ URLSpan urlSpan = urlSpans[span_i];
+ String urlString = urlSpan.getURL();
+ if (urlString.startsWith(GEO_PREFIX)) {
+ Intent broadcastIntent = new Intent(MAP_ACTION);
+ broadcastIntent.setClass(context, AlertReceiver.class);
+ broadcastIntent.putExtra(EXTRA_EVENT_ID, eventId);
+ return PendingIntent.getBroadcast(context,
+ Long.valueOf(eventId).hashCode(), broadcastIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+ }
+
+ // No geo link was found, so return null;
+ return null;
+ }
+
+ /**
+ * Create an intent to take the user to maps, using the first map link available.
+ * If no links are found, return null.
+ */
+ private static Intent createMapActivityIntent(Context context, URLSpan[] urlSpans) {
+ for (int span_i = 0; span_i < urlSpans.length; span_i++) {
+ URLSpan urlSpan = urlSpans[span_i];
+ String urlString = urlSpan.getURL();
+ if (urlString.startsWith(GEO_PREFIX)) {
+ Intent geoIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlString));
+ geoIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return geoIntent;
+ }
+ }
+
+ // No geo link was found, so return null;
+ return null;
+ }
+
+ /**
+ * Create a pending intent to send ourself a broadcast to take the user to dialer, or any other
+ * app capable of making phone calls. Use the first phone number available. If no phone number
+ * is found, or if the device is not capable of making phone calls (i.e. a tablet), return null.
+ */
+ private static PendingIntent createCallBroadcastIntent(Context context, URLSpan[] urlSpans,
+ long eventId) {
+ // Return null if the device is unable to make phone calls.
+ TelephonyManager tm =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE) {
+ return null;
+ }
+
+ for (int span_i = 0; span_i < urlSpans.length; span_i++) {
+ URLSpan urlSpan = urlSpans[span_i];
+ String urlString = urlSpan.getURL();
+ if (urlString.startsWith(TEL_PREFIX)) {
+ Intent broadcastIntent = new Intent(CALL_ACTION);
+ broadcastIntent.setClass(context, AlertReceiver.class);
+ broadcastIntent.putExtra(EXTRA_EVENT_ID, eventId);
+ return PendingIntent.getBroadcast(context,
+ Long.valueOf(eventId).hashCode(), broadcastIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+ }
+
+ // No tel link was found, so return null;
+ return null;
+ }
+
+ /**
+ * Create an intent to take the user to dialer, or any other app capable of making phone calls.
+ * Use the first phone number available. If no phone number is found, or if the device is
+ * not capable of making phone calls (i.e. a tablet), return null.
+ */
+ private static Intent createCallActivityIntent(Context context, URLSpan[] urlSpans) {
+ // Return null if the device is unable to make phone calls.
+ TelephonyManager tm =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE) {
+ return null;
+ }
+
+ for (int span_i = 0; span_i < urlSpans.length; span_i++) {
+ URLSpan urlSpan = urlSpans[span_i];
+ String urlString = urlSpan.getURL();
+ if (urlString.startsWith(TEL_PREFIX)) {
+ Intent callIntent = new Intent(Intent.ACTION_DIAL, Uri.parse(urlString));
+ callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return callIntent;
+ }
+ }
+
+ // No tel link was found, so return null;
+ return null;
+ }
}
diff --git a/tests/src/com/android/calendar/UtilsTests.java b/tests/src/com/android/calendar/UtilsTests.java
index 6beac971..02798e49 100644
--- a/tests/src/com/android/calendar/UtilsTests.java
+++ b/tests/src/com/android/calendar/UtilsTests.java
@@ -363,7 +363,7 @@ public class UtilsTests extends TestCase {
* @param matches Pairs of start/end positions.
*/
private static void findPhoneNumber(String text, String[] matches) {
- int[] results = EventInfoFragment.findNanpPhoneNumbers(text);
+ int[] results = Utils.findNanpPhoneNumbers(text);
assertEquals(results.length % 2, 0);