Consolidate date parsing for calendar events. Use human readable dates in app display. Probably haven't broken anything.

git-svn-id: https://zxing.googlecode.com/svn/trunk@2336 59b500cc-1b3d-0410-9834-0bbf25fbcc57
This commit is contained in:
srowen 2012-06-29 17:40:55 +00:00
parent b3f03373c2
commit 07c8b8282e
7 changed files with 192 additions and 178 deletions

View file

@ -67,7 +67,8 @@ public final class HistoryManager {
private static final String[] ID_COL_PROJECTION = { DBHelper.ID_COL };
private static final String[] ID_DETAIL_COL_PROJECTION = { DBHelper.ID_COL, DBHelper.DETAILS_COL };
private static final DateFormat EXPORT_DATE_TIME_FORMAT = DateFormat.getDateTimeInstance();
private static final DateFormat EXPORT_DATE_TIME_FORMAT =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
private final Activity activity;

View file

@ -27,7 +27,7 @@ import android.text.SpannableString;
import android.text.style.StyleSpan;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@ -45,6 +45,12 @@ public final class AddressBookResultHandler extends ResultHandler {
new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH),
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH),
};
static {
for (DateFormat format : DATE_FORMATS) {
format.setLenient(false);
}
}
private static final int[] BUTTON_TEXTS = {
R.string.button_add_contact,
R.string.button_show_map,
@ -148,13 +154,11 @@ public final class AddressBookResultHandler extends ResultHandler {
}
private static Date parseDate(String s) {
for (DateFormat currentFomat : DATE_FORMATS) {
synchronized (currentFomat) {
currentFomat.setLenient(false);
Date result = currentFomat.parse(s, new ParsePosition(0));
if (result != null) {
return result;
}
for (DateFormat currentFormat : DATE_FORMATS) {
try {
return currentFormat.parse(s);
} catch (ParseException e) {
// continue
}
}
return null;
@ -191,7 +195,7 @@ public final class AddressBookResultHandler extends ResultHandler {
if (birthday != null && birthday.length() > 0) {
Date date = parseDate(birthday);
if (date != null) {
ParsedResult.maybeAppend(DateFormat.getDateInstance().format(date.getTime()), contents);
ParsedResult.maybeAppend(DateFormat.getDateInstance(DateFormat.MEDIUM).format(date.getTime()), contents);
}
}
ParsedResult.maybeAppend(result.getNote(), contents);

View file

@ -23,23 +23,16 @@ import com.google.zxing.client.result.ParsedResult;
import android.app.Activity;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
/**
* Handles calendar entries encoded in QR Codes.
*
* @author dswitkin@google.com (Daniel Switkin)
* @author Sean Owen
*/
public final class CalendarResultHandler extends ResultHandler {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
private static final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
private static final int[] buttons = {
R.string.button_add_calendar
};
@ -64,6 +57,7 @@ public final class CalendarResultHandler extends ResultHandler {
if (index == 0) {
addCalendarEvent(calendarResult.getSummary(),
calendarResult.getStart(),
calendarResult.isStartAllDay(),
calendarResult.getEnd(),
calendarResult.getLocation(),
calendarResult.getDescription());
@ -72,16 +66,25 @@ public final class CalendarResultHandler extends ResultHandler {
@Override
public CharSequence getDisplayContents() {
CalendarParsedResult calResult = (CalendarParsedResult) getResult();
StringBuilder result = new StringBuilder(100);
ParsedResult.maybeAppend(calResult.getSummary(), result);
String startString = calResult.getStart();
appendTime(startString, result, false, false);
String endString = calResult.getEnd();
if (endString != null) {
boolean sameStartEnd = startString.equals(endString);
appendTime(endString, result, true, sameStartEnd);
ParsedResult.maybeAppend(calResult.getSummary(), result);
Date start = calResult.getStart();
ParsedResult.maybeAppend(format(calResult.isStartAllDay(), start), result);
Date end = calResult.getEnd();
if (end != null) {
if (calResult.isEndAllDay() && !start.equals(end)) {
// Show only year/month/day
// if it's all-day and this is the end date, it's exclusive, so show the user
// that it ends on the day before to make more intuitive sense.
// But don't do it if the event already (incorrectly?) specifies the same start/end
end = new Date(end.getTime() - 24 * 60 * 60 * 1000);
}
ParsedResult.maybeAppend(format(calResult.isEndAllDay(), end), result);
}
ParsedResult.maybeAppend(calResult.getLocation(), result);
@ -90,38 +93,14 @@ public final class CalendarResultHandler extends ResultHandler {
return result.toString();
}
private static void appendTime(String when, StringBuilder result, boolean end, boolean sameStartEnd) {
if (when.length() == 8) {
// Show only year/month/day
Date date;
synchronized (DATE_FORMAT) {
date = DATE_FORMAT.parse(when, new ParsePosition(0));
}
// if it's all-day and this is the end date, it's exclusive, so show the user
// that it ends on the day before to make more intuitive sense.
// But don't do it if the event already (incorrectly?) specifies the same start/end
if (end && !sameStartEnd) {
date = new Date(date.getTime() - 24 * 60 * 60 * 1000);
}
ParsedResult.maybeAppend(DateFormat.getDateInstance().format(date.getTime()), result);
} else {
// The when string can be local time, or UTC if it ends with a Z
Date date;
synchronized (DATE_TIME_FORMAT) {
date = DATE_TIME_FORMAT.parse(when.substring(0, 15), new ParsePosition(0));
}
long milliseconds = date.getTime();
if (when.length() == 16 && when.charAt(15) == 'Z') {
Calendar calendar = new GregorianCalendar();
// Account for time zone difference
milliseconds += calendar.get(Calendar.ZONE_OFFSET);
// Might need to correct for daylight savings time, but use target time since
// now might be in DST but not then, or vice versa
calendar.setTime(new Date(milliseconds));
milliseconds += calendar.get(Calendar.DST_OFFSET);
}
ParsedResult.maybeAppend(DateFormat.getDateTimeInstance().format(milliseconds), result);
private static String format(boolean allDay, Date date) {
if (date == null) {
return null;
}
DateFormat format = allDay
? DateFormat.getDateInstance(DateFormat.MEDIUM)
: DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
return format.format(date);
}
@Override

View file

@ -41,15 +41,9 @@ import android.provider.ContactsContract;
import android.util.Log;
import android.view.View;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
/**
* A base class for the Android-specific barcode handlers. These allow the app to polymorphically
@ -60,21 +54,12 @@ import java.util.TimeZone;
* instance is needed to launch an intent.
*
* @author dswitkin@google.com (Daniel Switkin)
* @author Sean Owen
*/
public abstract class ResultHandler {
private static final String TAG = ResultHandler.class.getSimpleName();
private static final DateFormat DATE_FORMAT;
static {
DATE_FORMAT = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
// For dates without a time, for purposes of interacting with Android, the resulting timestamp
// needs to be midnight of that day in GMT. See:
// http://code.google.com/p/android/issues/detail?id=8330
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
}
private static final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
private static final String GOOGLE_SHOPPER_PACKAGE = "com.google.android.apps.shopper";
private static final String GOOGLE_SHOPPER_ACTIVITY = GOOGLE_SHOPPER_PACKAGE +
".results.SearchResultsActivity";
@ -223,21 +208,22 @@ public abstract class ResultHandler {
* versions of the system have a bug where the event title will not be filled out.
*
* @param summary A description of the event
* @param start The start time as yyyyMMdd or yyyyMMdd'T'HHmmss or yyyyMMdd'T'HHmmss'Z'
* @param end The end time as yyyyMMdd or yyyyMMdd'T'HHmmss or yyyyMMdd'T'HHmmss'Z'
* @param start The start time
* @param allDay if true, event is considered to be all day starting from start time
* @param end The end time (optional)
* @param location a text description of the event location
* @param description a text description of the event itself
*/
final void addCalendarEvent(String summary,
String start,
String end,
Date start,
boolean allDay,
Date end,
String location,
String description) {
Intent intent = new Intent(Intent.ACTION_EDIT);
intent.setType("vnd.android.cursor.item/event");
long startMilliseconds = calculateMilliseconds(start);
long startMilliseconds = start.getTime();
intent.putExtra("beginTime", startMilliseconds);
boolean allDay = start.length() == 8;
if (allDay) {
intent.putExtra("allDay", true);
}
@ -250,7 +236,7 @@ public abstract class ResultHandler {
endMilliseconds = startMilliseconds;
}
} else {
endMilliseconds = calculateMilliseconds(end);
endMilliseconds = end.getTime();
}
intent.putExtra("endTime", endMilliseconds);
intent.putExtra("title", summary);
@ -259,31 +245,6 @@ public abstract class ResultHandler {
launchIntent(intent);
}
private static long calculateMilliseconds(String when) {
if (when.length() == 8) {
// Only contains year/month/day
Date date;
synchronized (DATE_FORMAT) {
date = DATE_FORMAT.parse(when, new ParsePosition(0));
}
// Note this will be relative to GMT, not the local time zone
return date.getTime();
} else {
// The when string can be local time, or UTC if it ends with a Z
Date date;
synchronized (DATE_TIME_FORMAT) {
date = DATE_TIME_FORMAT.parse(when.substring(0, 15), new ParsePosition(0));
}
long milliseconds = date.getTime();
if (when.length() == 16 && when.charAt(15) == 'Z') {
Calendar calendar = new GregorianCalendar();
int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
milliseconds += offset;
}
return milliseconds;
}
}
final void addPhoneOnlyContact(String[] phoneNumbers,String[] phoneTypes) {
addContact(null, null, phoneNumbers, phoneTypes, null, null, null, null, null, null, null, null, null, null);
}
@ -495,7 +456,12 @@ public abstract class ResultHandler {
}
final void openURL(String url) {
launchIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
launchIntent(intent);
} catch (ActivityNotFoundException anfe) {
Log.w(TAG, "Nothing available to handle " + intent);
}
}
final void webSearch(String query) {

View file

@ -16,14 +16,37 @@
package com.google.zxing.client.result;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;
/**
* @author Sean Owen
*/
public final class CalendarParsedResult extends ParsedResult {
private static final Pattern DATE_TIME = Pattern.compile("[0-9]{8}(T[0-9]{6}Z?)?");
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
static {
// For dates without a time, for purposes of interacting with Android, the resulting timestamp
// needs to be midnight of that day in GMT. See:
// http://code.google.com/p/android/issues/detail?id=8330
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
}
private static final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
private final String summary;
private final String start;
private final String end;
private final Date start;
private final boolean startAllDay;
private final Date end;
private final boolean endAllDay;
private final String location;
private final String attendee;
private final String description;
@ -31,32 +54,23 @@ public final class CalendarParsedResult extends ParsedResult {
private final double longitude;
public CalendarParsedResult(String summary,
String start,
String end,
String location,
String attendee,
String description) {
this(summary, start, end, location, attendee, description, Double.NaN, Double.NaN);
}
public CalendarParsedResult(String summary,
String start,
String end,
String startString,
String endString,
String location,
String attendee,
String description,
double latitude,
double longitude) {
super(ParsedResultType.CALENDAR);
validateDate(start);
this.summary = summary;
this.start = start;
if (end != null) {
validateDate(end);
this.end = end;
} else {
this.end = null;
try {
this.start = parseDate(startString);
this.end = endString == null ? null : parseDate(endString);
} catch (ParseException pe) {
throw new IllegalArgumentException(pe.toString());
}
this.startAllDay = startString.length() == 8;
this.endAllDay = endString != null && endString.length() == 8;
this.location = location;
this.attendee = attendee;
this.description = description;
@ -69,23 +83,34 @@ public final class CalendarParsedResult extends ParsedResult {
}
/**
* <p>We would return the start and end date as a {@link java.util.Date} except that this code
* needs to work under JavaME / MIDP and there is no date parsing library available there, such
* as {@code java.text.SimpleDateFormat}.</p> See validateDate() for the return format.
*
* @return start time formatted as a RFC 2445 DATE or DATE-TIME.</p>
* @return start time
*/
public String getStart() {
public Date getStart() {
return start;
}
/**
* @see #getStart(). May return null if the event has no duration.
* @return true if start time was specified as a whole day
*/
public String getEnd() {
public boolean isStartAllDay() {
return startAllDay;
}
/**
* May return null if the event has no duration.
* @see #getStart()
*/
public Date getEnd() {
return end;
}
/**
* @return true if end time was specified as a whole day
*/
public boolean isEndAllDay() {
return endAllDay;
}
public String getLocation() {
return location;
}
@ -110,8 +135,8 @@ public final class CalendarParsedResult extends ParsedResult {
public String getDisplayResult() {
StringBuilder result = new StringBuilder(100);
maybeAppend(summary, result);
maybeAppend(start, result);
maybeAppend(end, result);
maybeAppend(format(startAllDay, start), result);
maybeAppend(format(endAllDay, end), result);
maybeAppend(location, result);
maybeAppend(attendee, result);
maybeAppend(description, result);
@ -119,36 +144,48 @@ public final class CalendarParsedResult extends ParsedResult {
}
/**
* RFC 2445 allows the start and end fields to be of type DATE (e.g. 20081021) or DATE-TIME
* (e.g. 20081021T123000 for local time, or 20081021T123000Z for UTC).
* Parses a string as a date. RFC 2445 allows the start and end fields to be of type DATE (e.g. 20081021)
* or DATE-TIME (e.g. 20081021T123000 for local time, or 20081021T123000Z for UTC).
*
* @param date The string to validate
* @param when The string to parse
* @throws ParseException if not able to parse as a date
*/
private static void validateDate(CharSequence date) {
if (date != null) {
int length = date.length();
if (length != 8 && length != 15 && length != 16) {
throw new IllegalArgumentException();
}
for (int i = 0; i < 8; i++) {
if (!Character.isDigit(date.charAt(i))) {
throw new IllegalArgumentException();
}
}
if (length > 8) {
if (date.charAt(8) != 'T') {
throw new IllegalArgumentException();
}
for (int i = 9; i < 15; i++) {
if (!Character.isDigit(date.charAt(i))) {
throw new IllegalArgumentException();
}
}
if (length == 16 && date.charAt(15) != 'Z') {
throw new IllegalArgumentException();
}
private static Date parseDate(String when) throws ParseException {
if (!DATE_TIME.matcher(when).matches()) {
throw new ParseException(when, 0);
}
if (when.length() == 8) {
// Show only year/month/day
return DATE_FORMAT.parse(when);
} else {
// The when string can be local time, or UTC if it ends with a Z
Date date;
if (when.length() == 16 && when.charAt(15) == 'Z') {
date = DATE_TIME_FORMAT.parse(when.substring(0, 15));
Calendar calendar = new GregorianCalendar();
long milliseconds = date.getTime();
// Account for time zone difference
milliseconds += calendar.get(Calendar.ZONE_OFFSET);
// Might need to correct for daylight savings time, but use target time since
// now might be in DST but not then, or vice versa
calendar.setTime(new Date(milliseconds));
milliseconds += calendar.get(Calendar.DST_OFFSET);
date = new Date(milliseconds);
} else {
date = DATE_TIME_FORMAT.parse(when);
}
return date;
}
}
private static String format(boolean allDay, Date date) {
if (date == null) {
return null;
}
DateFormat format = allDay
? DateFormat.getDateInstance(DateFormat.MEDIUM)
: DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
return format.format(date);
}
}

View file

@ -19,8 +19,14 @@ package com.google.zxing.client.result;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.TimeZone;
/**
* Tests {@link CalendarParsedResult}.
*
@ -30,6 +36,17 @@ public final class CalendarParsedResultTestCase extends Assert {
private static final double EPSILON = 0.0000000001;
private static final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
static {
DATE_TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
}
@Before
public void setUp() {
Locale.setDefault(Locale.ENGLISH);
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
}
@Test
public void testStartEnd() {
doTest(
@ -125,8 +142,8 @@ public final class CalendarParsedResultTestCase extends Assert {
"Meeting with a friend\nlook at homepage first\n\n\n \n",
"Summary line",
"Location, with, escaped, commas",
"20111110T110000",
"20111110T120000");
"20111110T110000Z",
"20111110T120000Z");
}
@Test
@ -135,24 +152,24 @@ public final class CalendarParsedResultTestCase extends Assert {
"DTSTART;VALUE=DATE:20111110\n" +
"DTEND;VALUE=DATE:20111110\n" +
"END:VEVENT",
null, null, null, "20111110", "20111110");
null, null, null, "20111110T000000Z", "20111110T000000Z");
}
private static void doTest(String contents,
String description,
String summary,
String location,
String start,
String end) {
doTest(contents, description, summary, location, start, end, null, Double.NaN, Double.NaN);
String startString,
String endString) {
doTest(contents, description, summary, location, startString, endString, null, Double.NaN, Double.NaN);
}
private static void doTest(String contents,
String description,
String summary,
String location,
String start,
String end,
String startString,
String endString,
String attendee,
double latitude,
double longitude) {
@ -163,8 +180,8 @@ public final class CalendarParsedResultTestCase extends Assert {
assertEquals(description, calResult.getDescription());
assertEquals(summary, calResult.getSummary());
assertEquals(location, calResult.getLocation());
assertEquals(start, calResult.getStart());
assertEquals(end, calResult.getEnd());
assertEquals(startString, DATE_TIME_FORMAT.format(calResult.getStart()));
assertEquals(endString, calResult.getEnd() == null ? null : DATE_TIME_FORMAT.format(calResult.getEnd()));
assertEquals(attendee, calResult.getAttendee());
assertEqualOrNaN(latitude, calResult.getLatitude());
assertEqualOrNaN(longitude, calResult.getLongitude());

View file

@ -19,8 +19,12 @@ package com.google.zxing.client.result;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.Locale;
import java.util.TimeZone;
/**
* Tests {@link ParsedResult}.
*
@ -29,6 +33,12 @@ import org.junit.Test;
*/
public final class ParsedReaderResultTestCase extends Assert {
@Before
public void setUp() {
Locale.setDefault(Locale.ENGLISH);
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
}
@Test
public void testTextType() {
doTestResult("", "", ParsedResultType.TEXT);
@ -203,25 +213,25 @@ public final class ParsedReaderResultTestCase extends Assert {
// UTC times
doTestResult("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:foo\r\nDTSTART:20080504T123456Z\r\n" +
"DTEND:20080505T234555Z\r\nEND:VEVENT\r\nEND:VCALENDAR",
"foo\n20080504T123456Z\n20080505T234555Z",
"foo\nMay 4, 2008 12:34:56 PM\nMay 5, 2008 11:45:55 PM",
ParsedResultType.CALENDAR);
doTestResult("BEGIN:VEVENT\r\nSUMMARY:foo\r\nDTSTART:20080504T123456Z\r\n" +
"DTEND:20080505T234555Z\r\nEND:VEVENT", "foo\n20080504T123456Z\n20080505T234555Z",
"DTEND:20080505T234555Z\r\nEND:VEVENT", "foo\nMay 4, 2008 12:34:56 PM\nMay 5, 2008 11:45:55 PM",
ParsedResultType.CALENDAR);
// Local times
doTestResult("BEGIN:VEVENT\r\nSUMMARY:foo\r\nDTSTART:20080504T123456\r\n" +
"DTEND:20080505T234555\r\nEND:VEVENT", "foo\n20080504T123456\n20080505T234555",
"DTEND:20080505T234555\r\nEND:VEVENT", "foo\nMay 4, 2008 12:34:56 PM\nMay 5, 2008 11:45:55 PM",
ParsedResultType.CALENDAR);
// Date only (all day event)
doTestResult("BEGIN:VEVENT\r\nSUMMARY:foo\r\nDTSTART:20080504\r\n" +
"DTEND:20080505\r\nEND:VEVENT", "foo\n20080504\n20080505", ParsedResultType.CALENDAR);
"DTEND:20080505\r\nEND:VEVENT", "foo\nMay 4, 2008\nMay 5, 2008", ParsedResultType.CALENDAR);
// Start time only
doTestResult("BEGIN:VEVENT\r\nSUMMARY:foo\r\nDTSTART:20080504T123456Z\r\nEND:VEVENT",
"foo\n20080504T123456Z", ParsedResultType.CALENDAR);
"foo\nMay 4, 2008 12:34:56 PM", ParsedResultType.CALENDAR);
doTestResult("BEGIN:VEVENT\r\nSUMMARY:foo\r\nDTSTART:20080504T123456\r\nEND:VEVENT",
"foo\n20080504T123456", ParsedResultType.CALENDAR);
"foo\nMay 4, 2008 12:34:56 PM", ParsedResultType.CALENDAR);
doTestResult("BEGIN:VEVENT\r\nSUMMARY:foo\r\nDTSTART:20080504\r\nEND:VEVENT",
"foo\n20080504", ParsedResultType.CALENDAR);
"foo\nMay 4, 2008", ParsedResultType.CALENDAR);
doTestResult("BEGIN:VEVENT\r\nDTEND:20080505T\r\nEND:VEVENT",
"BEGIN:VEVENT\r\nDTEND:20080505T\r\nEND:VEVENT", ParsedResultType.URI);
// Yeah, it's OK that this is thought of as maybe a URI as long as it's not CALENDAR