diff options
Diffstat (limited to 'core')
22 files changed, 1931 insertions, 252 deletions
diff --git a/core/java/android/pim/EventRecurrence.java b/core/java/android/pim/EventRecurrence.java index 56c4f7a..cde7dac 100644 --- a/core/java/android/pim/EventRecurrence.java +++ b/core/java/android/pim/EventRecurrence.java @@ -18,36 +18,18 @@ package android.pim; import android.text.TextUtils; import android.text.format.Time; +import android.util.Log; +import android.util.TimeFormatException; import java.util.Calendar; +import java.util.HashMap; -public class EventRecurrence -{ - /** - * Thrown when a recurrence string provided can not be parsed according - * to RFC2445. - */ - public static class InvalidFormatException extends RuntimeException - { - InvalidFormatException(String s) { - super(s); - } - } - - public EventRecurrence() - { - wkst = MO; - } - - /** - * Parse an iCalendar/RFC2445 recur type according to Section 4.3.10. - */ - public native void parse(String recur); +/** + * Event recurrence utility functions. + */ +public class EventRecurrence { + private static String TAG = "EventRecur"; - public void setStartDate(Time date) { - startDate = date; - } - public static final int SECONDLY = 1; public static final int MINUTELY = 2; public static final int HOURLY = 3; @@ -64,13 +46,15 @@ public class EventRecurrence public static final int FR = 0x00200000; public static final int SA = 0x00400000; - public Time startDate; - public int freq; + public Time startDate; // set by setStartDate(), not parse() + + public int freq; // SECONDLY, MINUTELY, etc. public String until; public int count; public int interval; - public int wkst; + public int wkst; // SU, MO, TU, etc. + /* lists with zero entries may be null references */ public int[] bysecond; public int bysecondCount; public int[] byminute; @@ -79,7 +63,7 @@ public class EventRecurrence public int byhourCount; public int[] byday; public int[] bydayNum; - public int bydayCount; + public int bydayCount; public int[] bymonthday; public int bymonthdayCount; public int[] byyearday; @@ -91,6 +75,134 @@ public class EventRecurrence public int[] bysetpos; public int bysetposCount; + /** maps a part string to a parser object */ + private static HashMap<String,PartParser> sParsePartMap; + static { + sParsePartMap = new HashMap<String,PartParser>(); + sParsePartMap.put("FREQ", new ParseFreq()); + sParsePartMap.put("UNTIL", new ParseUntil()); + sParsePartMap.put("COUNT", new ParseCount()); + sParsePartMap.put("INTERVAL", new ParseInterval()); + sParsePartMap.put("BYSECOND", new ParseBySecond()); + sParsePartMap.put("BYMINUTE", new ParseByMinute()); + sParsePartMap.put("BYHOUR", new ParseByHour()); + sParsePartMap.put("BYDAY", new ParseByDay()); + sParsePartMap.put("BYMONTHDAY", new ParseByMonthDay()); + sParsePartMap.put("BYYEARDAY", new ParseByYearDay()); + sParsePartMap.put("BYWEEKNO", new ParseByWeekNo()); + sParsePartMap.put("BYMONTH", new ParseByMonth()); + sParsePartMap.put("BYSETPOS", new ParseBySetPos()); + sParsePartMap.put("WKST", new ParseWkst()); + } + + /* values for bit vector that keeps track of what we have already seen */ + private static final int PARSED_FREQ = 1 << 0; + private static final int PARSED_UNTIL = 1 << 1; + private static final int PARSED_COUNT = 1 << 2; + private static final int PARSED_INTERVAL = 1 << 3; + private static final int PARSED_BYSECOND = 1 << 4; + private static final int PARSED_BYMINUTE = 1 << 5; + private static final int PARSED_BYHOUR = 1 << 6; + private static final int PARSED_BYDAY = 1 << 7; + private static final int PARSED_BYMONTHDAY = 1 << 8; + private static final int PARSED_BYYEARDAY = 1 << 9; + private static final int PARSED_BYWEEKNO = 1 << 10; + private static final int PARSED_BYMONTH = 1 << 11; + private static final int PARSED_BYSETPOS = 1 << 12; + private static final int PARSED_WKST = 1 << 13; + + /** maps a FREQ value to an integer constant */ + private static final HashMap<String,Integer> sParseFreqMap = new HashMap<String,Integer>(); + static { + sParseFreqMap.put("SECONDLY", SECONDLY); + sParseFreqMap.put("MINUTELY", MINUTELY); + sParseFreqMap.put("HOURLY", HOURLY); + sParseFreqMap.put("DAILY", DAILY); + sParseFreqMap.put("WEEKLY", WEEKLY); + sParseFreqMap.put("MONTHLY", MONTHLY); + sParseFreqMap.put("YEARLY", YEARLY); + } + + /** maps a two-character weekday string to an integer constant */ + private static final HashMap<String,Integer> sParseWeekdayMap = new HashMap<String,Integer>(); + static { + sParseWeekdayMap.put("SU", SU); + sParseWeekdayMap.put("MO", MO); + sParseWeekdayMap.put("TU", TU); + sParseWeekdayMap.put("WE", WE); + sParseWeekdayMap.put("TH", TH); + sParseWeekdayMap.put("FR", FR); + sParseWeekdayMap.put("SA", SA); + } + + /** If set, allow lower-case recurrence rule strings. Minor performance impact. */ + private static final boolean ALLOW_LOWER_CASE = false; + + /** If set, validate the value of UNTIL parts. Minor performance impact. */ + private static final boolean VALIDATE_UNTIL = false; + + /** If set, require that only one of {UNTIL,COUNT} is present. Breaks compat w/ old parser. */ + private static final boolean ONLY_ONE_UNTIL_COUNT = false; + + + /** + * Thrown when a recurrence string provided can not be parsed according + * to RFC2445. + */ + public static class InvalidFormatException extends RuntimeException { + InvalidFormatException(String s) { + super(s); + } + } + + /** + * Parse an iCalendar/RFC2445 recur type according to Section 4.3.10. The string is + * parsed twice, by the old and new parsers, and the results are compared. + * <p> + * TODO: this will go away, and what is now parse2() will simply become parse(). + */ + public void parse(String recur) { + InvalidFormatException newExcep = null; + try { + parse2(recur); + } catch (InvalidFormatException ife) { + newExcep = ife; + } + + boolean oldThrew = false; + try { + EventRecurrence check = new EventRecurrence(); + check.parseNative(recur); + if (newExcep == null) { + // Neither threw, check to see if results match. + if (!equals(check)) { + throw new InvalidFormatException("Recurrence rule parse does not match [" + + recur + "]"); + } + } + } catch (InvalidFormatException ife) { + oldThrew = true; + if (newExcep == null) { + // Old threw, but new didn't. Log a warning, but don't throw. + Log.d(TAG, "NOTE: old parser rejected [" + recur + "]: " + ife.getMessage()); + } + } + + if (newExcep != null) { + if (!oldThrew) { + // New threw, but old didn't. Log a warning and throw the exception. + Log.d(TAG, "NOTE: new parser rejected [" + recur + "]: " + newExcep.getMessage()); + } + throw newExcep; + } + } + + native void parseNative(String recur); + + public void setStartDate(Time date) { + startDate = date; + } + /** * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc. * constants. btw, I think we should switch to those here too, to @@ -118,7 +230,7 @@ public class EventRecurrence throw new RuntimeException("bad day of week: " + day); } } - + public static int timeDay2Day(int day) { switch (day) @@ -191,16 +303,16 @@ public class EventRecurrence throw new RuntimeException("bad day of week: " + day); } } - + /** * Converts one of the internal day constants (SU, MO, etc.) to the * two-letter string representing that constant. - * - * @throws IllegalArgumentException Thrown if the day argument is not one of - * the defined day constants. - * + * * @param day one the internal constants SU, MO, etc. * @return the two-letter string for the day ("SU", "MO", etc.) + * + * @throws IllegalArgumentException Thrown if the day argument is not one of + * the defined day constants. */ private static String day2String(int day) { switch (day) { @@ -283,7 +395,7 @@ public class EventRecurrence s.append(";UNTIL="); s.append(until); } - + if (this.count != 0) { s.append(";COUNT="); s.append(this.count); @@ -323,36 +435,484 @@ public class EventRecurrence return s.toString(); } - + public boolean repeatsOnEveryWeekDay() { if (this.freq != WEEKLY) { - return false; + return false; } - + int count = this.bydayCount; if (count != 5) { return false; } - + for (int i = 0 ; i < count ; i++) { int day = byday[i]; if (day == SU || day == SA) { return false; } } - + return true; } - + public boolean repeatsMonthlyOnDayCount() { if (this.freq != MONTHLY) { return false; } - + if (bydayCount != 1 || bymonthdayCount != 0) { return false; } - + + return true; + } + + /** + * Determines whether two integer arrays contain identical elements. + * <p> + * The native implementation over-allocated the arrays (and may have stuff left over from + * a previous run), so we can't just check the arrays -- the separately-maintained count + * field also matters. We assume that a null array will have a count of zero, and that the + * array can hold as many elements as the associated count indicates. + * <p> + * TODO: replace this with Arrays.equals() when the old parser goes away. + */ + private static boolean arraysEqual(int[] array1, int count1, int[] array2, int count2) { + if (count1 != count2) { + return false; + } + + for (int i = 0; i < count1; i++) { + if (array1[i] != array2[i]) + return false; + } + return true; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof EventRecurrence)) { + return false; + } + + EventRecurrence er = (EventRecurrence) obj; + return (startDate == null ? + er.startDate == null : Time.compare(startDate, er.startDate) == 0) && + freq == er.freq && + (until == null ? er.until == null : until.equals(er.until)) && + count == er.count && + interval == er.interval && + wkst == er.wkst && + arraysEqual(bysecond, bysecondCount, er.bysecond, er.bysecondCount) && + arraysEqual(byminute, byminuteCount, er.byminute, er.byminuteCount) && + arraysEqual(byhour, byhourCount, er.byhour, er.byhourCount) && + arraysEqual(byday, bydayCount, er.byday, er.bydayCount) && + arraysEqual(bydayNum, bydayCount, er.bydayNum, er.bydayCount) && + arraysEqual(bymonthday, bymonthdayCount, er.bymonthday, er.bymonthdayCount) && + arraysEqual(byyearday, byyeardayCount, er.byyearday, er.byyeardayCount) && + arraysEqual(byweekno, byweeknoCount, er.byweekno, er.byweeknoCount) && + arraysEqual(bymonth, bymonthCount, er.bymonth, er.bymonthCount) && + arraysEqual(bysetpos, bysetposCount, er.bysetpos, er.bysetposCount); + } + + @Override public int hashCode() { + // We overrode equals, so we must override hashCode(). Nobody seems to need this though. + throw new UnsupportedOperationException(); + } + + /** + * Resets parser-modified fields to their initial state. Does not alter startDate. + * <p> + * The original parser always set all of the "count" fields, "wkst", and "until", + * essentially allowing the same object to be used multiple times by calling parse(). + * It's unclear whether this behavior was intentional. For now, be paranoid and + * preserve the existing behavior by resetting the fields. + * <p> + * We don't need to touch the integer arrays; they will either be ignored or + * overwritten. The "startDate" field is not set by the parser, so we ignore it here. + */ + private void resetFields() { + until = null; + freq = count = interval = bysecondCount = byminuteCount = byhourCount = + bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount = + bysetposCount = 0; + } + + /** + * Parses an rfc2445 recurrence rule string into its component pieces. Attempting to parse + * malformed input will result in an EventRecurrence.InvalidFormatException. + * + * @param recur The recurrence rule to parse (in un-folded form). + */ + void parse2(String recur) { + /* + * From RFC 2445 section 4.3.10: + * + * recur = "FREQ"=freq *( + * ; either UNTIL or COUNT may appear in a 'recur', + * ; but UNTIL and COUNT MUST NOT occur in the same 'recur' + * + * ( ";" "UNTIL" "=" enddate ) / + * ( ";" "COUNT" "=" 1*DIGIT ) / + * + * ; the rest of these keywords are optional, + * ; but MUST NOT occur more than once + * + * ( ";" "INTERVAL" "=" 1*DIGIT ) / + * ( ";" "BYSECOND" "=" byseclist ) / + * ( ";" "BYMINUTE" "=" byminlist ) / + * ( ";" "BYHOUR" "=" byhrlist ) / + * ( ";" "BYDAY" "=" bywdaylist ) / + * ( ";" "BYMONTHDAY" "=" bymodaylist ) / + * ( ";" "BYYEARDAY" "=" byyrdaylist ) / + * ( ";" "BYWEEKNO" "=" bywknolist ) / + * ( ";" "BYMONTH" "=" bymolist ) / + * ( ";" "BYSETPOS" "=" bysplist ) / + * ( ";" "WKST" "=" weekday ) / + * ( ";" x-name "=" text ) + * ) + * + * Examples: + * FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU + * FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8 + * + * Strategy: + * (1) Split the string at ';' boundaries to get an array of rule "parts". + * (2) For each part, find substrings for left/right sides of '=' (name/value). + * (3) Call a <name>-specific parsing function to parse the <value> into an + * output field. + * + * By keeping track of which names we've seen in a bit vector, we can verify the + * constraints indicated above (FREQ appears first, none of them appear more than once -- + * though x-[name] would require special treatment), and we have either UNTIL or COUNT + * but not both. + * + * In general, RFC 2445 property names (e.g. "FREQ") and enumerations ("TU") must + * be handled in a case-insensitive fashion, but case may be significant for other + * properties. We don't have any case-sensitive values in RRULE, except possibly + * for the custom "X-" properties, but we ignore those anyway. Thus, we can trivially + * convert the entire string to upper case and then use simple comparisons. + * + * Differences from previous version: + * - allows lower-case property and enumeration values [optional] + * - enforces that FREQ appears first + * - enforces that only one of UNTIL and COUNT may be specified + * - allows (but ignores) X-* parts + * - improved validation on various values (e.g. UNTIL timestamps) + * - error messages are more specific + */ + + /* TODO: replace with "if (freq != 0) throw" if nothing requires this */ + resetFields(); + + int parseFlags = 0; + String[] parts; + if (ALLOW_LOWER_CASE) { + parts = recur.toUpperCase().split(";"); + } else { + parts = recur.split(";"); + } + for (String part : parts) { + int equalIndex = part.indexOf('='); + if (equalIndex <= 0) { + /* no '=' or no LHS */ + throw new InvalidFormatException("Missing LHS in " + part); + } + + String lhs = part.substring(0, equalIndex); + String rhs = part.substring(equalIndex + 1); + if (rhs.length() == 0) { + throw new InvalidFormatException("Missing RHS in " + part); + } + + /* + * In lieu of a "switch" statement that allows string arguments, we use a + * map from strings to parsing functions. + */ + PartParser parser = sParsePartMap.get(lhs); + if (parser == null) { + if (lhs.startsWith("X-")) { + //Log.d(TAG, "Ignoring custom part " + lhs); + continue; + } + throw new InvalidFormatException("Couldn't find parser for " + lhs); + } else { + int flag = parser.parsePart(rhs, this); + if ((parseFlags & flag) != 0) { + throw new InvalidFormatException("Part " + lhs + " was specified twice"); + } + if (parseFlags == 0 && flag != PARSED_FREQ) { + throw new InvalidFormatException("FREQ must be specified first"); + } + parseFlags |= flag; + } + } + + // If not specified, week starts on Monday. + if ((parseFlags & PARSED_WKST) == 0) { + wkst = MO; + } + + // FREQ is mandatory. + if ((parseFlags & PARSED_FREQ) == 0) { + throw new InvalidFormatException("Must specify a FREQ value"); + } + + // Can't have both UNTIL and COUNT. + if ((parseFlags & (PARSED_UNTIL | PARSED_COUNT)) == (PARSED_UNTIL | PARSED_COUNT)) { + if (ONLY_ONE_UNTIL_COUNT) { + throw new InvalidFormatException("Must not specify both UNTIL and COUNT: " + recur); + } else { + Log.w(TAG, "Warning: rrule has both UNTIL and COUNT: " + recur); + } + } + } + + /** + * Base class for the RRULE part parsers. + */ + abstract static class PartParser { + /** + * Parses a single part. + * + * @param value The right-hand-side of the part. + * @param er The EventRecurrence into which the result is stored. + * @return A bit value indicating which part was parsed. + */ + public abstract int parsePart(String value, EventRecurrence er); + + /** + * Parses an integer, with range-checking. + * + * @param str The string to parse. + * @param minVal Minimum allowed value. + * @param maxVal Maximum allowed value. + * @param allowZero Is 0 allowed? + * @return The parsed value. + */ + public static int parseIntRange(String str, int minVal, int maxVal, boolean allowZero) { + try { + if (str.charAt(0) == '+') { + // Integer.parseInt does not allow a leading '+', so skip it manually. + str = str.substring(1); + } + int val = Integer.parseInt(str); + if (val < minVal || val > maxVal || (val == 0 && !allowZero)) { + throw new InvalidFormatException("Integer value out of range: " + str); + } + return val; + } catch (NumberFormatException nfe) { + throw new InvalidFormatException("Invalid integer value: " + str); + } + } + + /** + * Parses a comma-separated list of integers, with range-checking. + * + * @param listStr The string to parse. + * @param minVal Minimum allowed value. + * @param maxVal Maximum allowed value. + * @param allowZero Is 0 allowed? + * @return A new array with values, sized to hold the exact number of elements. + */ + public static int[] parseNumberList(String listStr, int minVal, int maxVal, + boolean allowZero) { + int[] values; + + if (listStr.indexOf(",") < 0) { + // Common case: only one entry, skip split() overhead. + values = new int[1]; + values[0] = parseIntRange(listStr, minVal, maxVal, allowZero); + } else { + String[] valueStrs = listStr.split(","); + int len = valueStrs.length; + values = new int[len]; + for (int i = 0; i < len; i++) { + values[i] = parseIntRange(valueStrs[i], minVal, maxVal, allowZero); + } + } + return values; + } + } + + /** parses FREQ={SECONDLY,MINUTELY,...} */ + private static class ParseFreq extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + Integer freq = sParseFreqMap.get(value); + if (freq == null) { + throw new InvalidFormatException("Invalid FREQ value: " + value); + } + er.freq = freq; + return PARSED_FREQ; + } + } + /** parses UNTIL=enddate, e.g. "19970829T021400" */ + private static class ParseUntil extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + if (VALIDATE_UNTIL) { + try { + // Parse the time to validate it. The result isn't retained. + Time until = new Time(); + until.parse(value); + } catch (TimeFormatException tfe) { + throw new InvalidFormatException("Invalid UNTIL value: " + value); + } + } + er.until = value; + return PARSED_UNTIL; + } + } + /** parses COUNT=[non-negative-integer] */ + private static class ParseCount extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + er.count = parseIntRange(value, 0, Integer.MAX_VALUE, true); + return PARSED_COUNT; + } + } + /** parses INTERVAL=[non-negative-integer] */ + private static class ParseInterval extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + er.interval = parseIntRange(value, 1, Integer.MAX_VALUE, false); + return PARSED_INTERVAL; + } + } + /** parses BYSECOND=byseclist */ + private static class ParseBySecond extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] bysecond = parseNumberList(value, 0, 59, true); + er.bysecond = bysecond; + er.bysecondCount = bysecond.length; + return PARSED_BYSECOND; + } + } + /** parses BYMINUTE=byminlist */ + private static class ParseByMinute extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] byminute = parseNumberList(value, 0, 59, true); + er.byminute = byminute; + er.byminuteCount = byminute.length; + return PARSED_BYMINUTE; + } + } + /** parses BYHOUR=byhrlist */ + private static class ParseByHour extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] byhour = parseNumberList(value, 0, 23, true); + er.byhour = byhour; + er.byhourCount = byhour.length; + return PARSED_BYHOUR; + } + } + /** parses BYDAY=bywdaylist, e.g. "1SU,-1SU" */ + private static class ParseByDay extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] byday; + int[] bydayNum; + int bydayCount; + + if (value.indexOf(",") < 0) { + /* only one entry, skip split() overhead */ + bydayCount = 1; + byday = new int[1]; + bydayNum = new int[1]; + parseWday(value, byday, bydayNum, 0); + } else { + String[] wdays = value.split(","); + int len = wdays.length; + bydayCount = len; + byday = new int[len]; + bydayNum = new int[len]; + for (int i = 0; i < len; i++) { + parseWday(wdays[i], byday, bydayNum, i); + } + } + er.byday = byday; + er.bydayNum = bydayNum; + er.bydayCount = bydayCount; + return PARSED_BYDAY; + } + + /** parses [int]weekday, putting the pieces into parallel array entries */ + private static void parseWday(String str, int[] byday, int[] bydayNum, int index) { + int wdayStrStart = str.length() - 2; + String wdayStr; + + if (wdayStrStart > 0) { + /* number is included; parse it out and advance to weekday */ + String numPart = str.substring(0, wdayStrStart); + int num = parseIntRange(numPart, -53, 53, false); + bydayNum[index] = num; + wdayStr = str.substring(wdayStrStart); + } else { + /* just the weekday string */ + wdayStr = str; + } + Integer wday = sParseWeekdayMap.get(wdayStr); + if (wday == null) { + throw new InvalidFormatException("Invalid BYDAY value: " + str); + } + byday[index] = wday; + } + } + /** parses BYMONTHDAY=bymodaylist */ + private static class ParseByMonthDay extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] bymonthday = parseNumberList(value, -31, 31, false); + er.bymonthday = bymonthday; + er.bymonthdayCount = bymonthday.length; + return PARSED_BYMONTHDAY; + } + } + /** parses BYYEARDAY=byyrdaylist */ + private static class ParseByYearDay extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] byyearday = parseNumberList(value, -366, 366, false); + er.byyearday = byyearday; + er.byyeardayCount = byyearday.length; + return PARSED_BYYEARDAY; + } + } + /** parses BYWEEKNO=bywknolist */ + private static class ParseByWeekNo extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] byweekno = parseNumberList(value, -53, 53, false); + er.byweekno = byweekno; + er.byweeknoCount = byweekno.length; + return PARSED_BYWEEKNO; + } + } + /** parses BYMONTH=bymolist */ + private static class ParseByMonth extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] bymonth = parseNumberList(value, 1, 12, false); + er.bymonth = bymonth; + er.bymonthCount = bymonth.length; + return PARSED_BYMONTH; + } + } + /** parses BYSETPOS=bysplist */ + private static class ParseBySetPos extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + int[] bysetpos = parseNumberList(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true); + er.bysetpos = bysetpos; + er.bysetposCount = bysetpos.length; + return PARSED_BYSETPOS; + } + } + /** parses WKST={SU,MO,...} */ + private static class ParseWkst extends PartParser { + @Override public int parsePart(String value, EventRecurrence er) { + Integer wkst = sParseWeekdayMap.get(value); + if (wkst == null) { + throw new InvalidFormatException("Invalid WKST value: " + value); + } + er.wkst = wkst; + return PARSED_WKST; + } + } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 603edf0..19e9a67 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -300,6 +300,21 @@ public final class Settings { "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS"; /** + * Activity Action: Show a dialog to select input method. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SHOW_INPUT_METHOD_PICKER = + "android.settings.SHOW_INPUT_METHOD_PICKER"; + + /** * Activity Action: Show settings to manage the user input dictionary. * <p> * In some cases, a matching Activity may not exist, so ensure you diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java index e14b975..49f3bbe 100644 --- a/core/java/android/view/InputEventConsistencyVerifier.java +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -32,6 +32,11 @@ import android.util.Log; public final class InputEventConsistencyVerifier { private static final boolean IS_ENG_BUILD = "eng".equals(Build.TYPE); + private static final String EVENT_TYPE_KEY = "KeyEvent"; + private static final String EVENT_TYPE_TRACKBALL = "TrackballEvent"; + private static final String EVENT_TYPE_TOUCH = "TouchEvent"; + private static final String EVENT_TYPE_GENERIC_MOTION = "GenericMotionEvent"; + // The number of recent events to log when a problem is detected. // Can be set to 0 to disable logging recent events but the runtime overhead of // this feature is negligible on current hardware. @@ -54,6 +59,7 @@ public final class InputEventConsistencyVerifier { // It does not make sense to examine the contents of the last event since it may have // been recycled. private InputEvent mLastEvent; + private String mLastEventType; private int mLastNestingLevel; // Copy of the most recent events. @@ -185,7 +191,7 @@ public final class InputEventConsistencyVerifier { * and both dispatching methods call into the consistency verifier. */ public void onKeyEvent(KeyEvent event, int nestingLevel) { - if (!startEvent(event, nestingLevel, "KeyEvent")) { + if (!startEvent(event, nestingLevel, EVENT_TYPE_KEY)) { return; } @@ -247,7 +253,7 @@ public final class InputEventConsistencyVerifier { * and both dispatching methods call into the consistency verifier. */ public void onTrackballEvent(MotionEvent event, int nestingLevel) { - if (!startEvent(event, nestingLevel, "TrackballEvent")) { + if (!startEvent(event, nestingLevel, EVENT_TYPE_TRACKBALL)) { return; } @@ -310,23 +316,19 @@ public final class InputEventConsistencyVerifier { * and both dispatching methods call into the consistency verifier. */ public void onTouchEvent(MotionEvent event, int nestingLevel) { - if (!startEvent(event, nestingLevel, "TouchEvent")) { + if (!startEvent(event, nestingLevel, EVENT_TYPE_TOUCH)) { return; } final int action = event.getAction(); final boolean newStream = action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_CANCEL; - if (mTouchEventStreamIsTainted || mTouchEventStreamUnhandled) { - if (newStream) { - mTouchEventStreamIsTainted = false; - mTouchEventStreamUnhandled = false; - mTouchEventStreamPointers = 0; - } else { - finishEvent(mTouchEventStreamIsTainted); - return; - } + if (newStream && (mTouchEventStreamIsTainted || mTouchEventStreamUnhandled)) { + mTouchEventStreamIsTainted = false; + mTouchEventStreamUnhandled = false; + mTouchEventStreamPointers = 0; } + final boolean wasTainted = mTouchEventStreamIsTainted; try { ensureMetaStateIsNormalized(event.getMetaState()); @@ -439,7 +441,7 @@ public final class InputEventConsistencyVerifier { problem("Source was not SOURCE_CLASS_POINTER."); } } finally { - finishEvent(false); + finishEvent(wasTainted); } } @@ -453,7 +455,7 @@ public final class InputEventConsistencyVerifier { * and both dispatching methods call into the consistency verifier. */ public void onGenericMotionEvent(MotionEvent event, int nestingLevel) { - if (!startEvent(event, nestingLevel, "GenericMotionEvent")) { + if (!startEvent(event, nestingLevel, EVENT_TYPE_GENERIC_MOTION)) { return; } @@ -568,21 +570,19 @@ public final class InputEventConsistencyVerifier { } private boolean startEvent(InputEvent event, int nestingLevel, String eventType) { - // Ignore the event if it is already tainted. - if (event.isTainted()) { - return false; - } - // Ignore the event if we already checked it at a higher nesting level. - if (event == mLastEvent && nestingLevel < mLastNestingLevel) { + if (event == mLastEvent && nestingLevel < mLastNestingLevel + && eventType == mLastEventType) { return false; } if (nestingLevel > 0) { mLastEvent = event; + mLastEventType = eventType; mLastNestingLevel = nestingLevel; } else { mLastEvent = null; + mLastEventType = null; mLastNestingLevel = 0; } @@ -593,27 +593,30 @@ public final class InputEventConsistencyVerifier { private void finishEvent(boolean tainted) { if (mViolationMessage != null && mViolationMessage.length() != 0) { - mViolationMessage.append("\n in ").append(mCaller); - mViolationMessage.append("\n "); - appendEvent(mViolationMessage, 0, mCurrentEvent, false); - - if (RECENT_EVENTS_TO_LOG != 0 && mRecentEvents != null) { - mViolationMessage.append("\n -- recent events --"); - for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) { - final int index = (mMostRecentEventIndex + RECENT_EVENTS_TO_LOG - i) - % RECENT_EVENTS_TO_LOG; - final InputEvent event = mRecentEvents[index]; - if (event == null) { - break; + if (!tainted) { + // Write a log message only if the event was not already tainted. + mViolationMessage.append("\n in ").append(mCaller); + mViolationMessage.append("\n "); + appendEvent(mViolationMessage, 0, mCurrentEvent, false); + + if (RECENT_EVENTS_TO_LOG != 0 && mRecentEvents != null) { + mViolationMessage.append("\n -- recent events --"); + for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) { + final int index = (mMostRecentEventIndex + RECENT_EVENTS_TO_LOG - i) + % RECENT_EVENTS_TO_LOG; + final InputEvent event = mRecentEvents[index]; + if (event == null) { + break; + } + mViolationMessage.append("\n "); + appendEvent(mViolationMessage, i + 1, event, mRecentEventsUnhandled[index]); } - mViolationMessage.append("\n "); - appendEvent(mViolationMessage, i + 1, event, mRecentEventsUnhandled[index]); } - } - Log.d(mLogTag, mViolationMessage.toString()); + Log.d(mLogTag, mViolationMessage.toString()); + tainted = true; + } mViolationMessage.setLength(0); - tainted = true; } if (tainted) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 4403591..f70ca90 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -2262,6 +2262,8 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit private OnTouchListener mOnTouchListener; + private OnHoverListener mOnHoverListener; + private OnGenericMotionListener mOnGenericMotionListener; private OnDragListener mOnDragListener; @@ -2484,6 +2486,12 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit Rect mLocalDirtyRect; /** + * Set to true when the view is sending hover accessibility events because it + * is the innermost hovered view. + */ + private boolean mSendingHoverAccessibilityEvents; + + /** * Consistency verifier for debugging purposes. * @hide */ @@ -2503,6 +2511,9 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED | LAYOUT_DIRECTION_INHERIT; mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS); + mUserPaddingStart = -1; + mUserPaddingEnd = -1; + mUserPaddingRelative = false; } /** @@ -2864,13 +2875,16 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit mUserPaddingRelative = (startPadding >= 0 || endPadding >= 0); + // Cache user padding as we cannot fully resolve padding here (we dont have yet the resolved + // layout direction). Those cached values will be used later during padding resolution. + mUserPaddingStart = startPadding; + mUserPaddingEnd = endPadding; + if (padding >= 0) { leftPadding = padding; topPadding = padding; rightPadding = padding; bottomPadding = padding; - startPadding = padding; - endPadding = padding; } // If the user specified the padding (either with android:padding or @@ -2882,11 +2896,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit rightPadding >= 0 ? rightPadding : mPaddingRight, bottomPadding >= 0 ? bottomPadding : mPaddingBottom); - // Cache user padding as we cannot fully resolve padding here (we dont have yet the resolved - // layout direction). Those cached values will be used later during padding resolution. - mUserPaddingStart = startPadding; - mUserPaddingEnd = endPadding; - if (viewFlagMasks != 0) { setFlags(viewFlagValues, viewFlagMasks); } @@ -5117,9 +5126,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit return true; } - if (mInputEventConsistencyVerifier != null) { - mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); - } return false; } @@ -5147,6 +5153,12 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit || action == MotionEvent.ACTION_HOVER_MOVE || action == MotionEvent.ACTION_HOVER_EXIT) { if (dispatchHoverEvent(event)) { + // For compatibility with existing applications that handled HOVER_MOVE + // events in onGenericMotionEvent, dispatch the event there. The + // onHoverEvent method did not exist at the time. + if (action == MotionEvent.ACTION_HOVER_MOVE) { + dispatchGenericMotionEventInternal(event); + } return true; } } else if (dispatchGenericPointerEvent(event)) { @@ -5156,6 +5168,17 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit return true; } + if (dispatchGenericMotionEventInternal(event)) { + return true; + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + return false; + } + + private boolean dispatchGenericMotionEventInternal(MotionEvent event) { //noinspection SimplifiableIfStatement if (mOnGenericMotionListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnGenericMotionListener.onGenericMotion(this, event)) { @@ -5181,13 +5204,42 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. - * @hide */ protected boolean dispatchHoverEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + if (!hasHoveredChild() && !mSendingHoverAccessibilityEvents) { + mSendingHoverAccessibilityEvents = true; + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + } + break; + case MotionEvent.ACTION_HOVER_EXIT: + if (mSendingHoverAccessibilityEvents) { + mSendingHoverAccessibilityEvents = false; + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + break; + } + + if (mOnHoverListener != null && (mViewFlags & ENABLED_MASK) == ENABLED + && mOnHoverListener.onHover(this, event)) { + return true; + } + return onHoverEvent(event); } /** + * Returns true if the view has a child to which it has recently sent + * {@link MotionEvent#ACTION_HOVER_ENTER}. If this view is hovered and + * it does not have a hovered child, then it must be the innermost hovered view. + * @hide + */ + protected boolean hasHoveredChild() { + return false; + } + + /** * Dispatch a generic motion event to the view under the first pointer. * <p> * Do not call this method directly. @@ -5196,7 +5248,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. - * @hide */ protected boolean dispatchGenericPointerEvent(MotionEvent event) { return false; @@ -5211,7 +5262,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. - * @hide */ protected boolean dispatchGenericFocusedEvent(MotionEvent event) { return false; @@ -5788,71 +5838,129 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit /** * Implement this method to handle hover events. * <p> - * Hover events are pointer events with action {@link MotionEvent#ACTION_HOVER_ENTER}, - * {@link MotionEvent#ACTION_HOVER_MOVE}, or {@link MotionEvent#ACTION_HOVER_EXIT}. - * </p><p> - * The view receives hover enter as the pointer enters the bounds of the view and hover - * exit as the pointer exits the bound of the view or just before the pointer goes down - * (which implies that {@link #onTouchEvent(MotionEvent)} will be called soon). - * </p><p> - * If the view would like to handle the hover event itself and prevent its children - * from receiving hover, it should return true from this method. If this method returns - * true and a child has already received a hover enter event, the child will - * automatically receive a hover exit event. + * This method is called whenever a pointer is hovering into, over, or out of the + * bounds of a view and the view is not currently being touched. + * Hover events are represented as pointer events with action + * {@link MotionEvent#ACTION_HOVER_ENTER}, {@link MotionEvent#ACTION_HOVER_MOVE}, + * or {@link MotionEvent#ACTION_HOVER_EXIT}. + * </p> + * <ul> + * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_ENTER} + * when the pointer enters the bounds of the view.</li> + * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_MOVE} + * when the pointer has already entered the bounds of the view and has moved.</li> + * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_EXIT} + * when the pointer has exited the bounds of the view or when the pointer is + * about to go down due to a button click, tap, or similar user action that + * causes the view to be touched.</li> + * </ul> + * <p> + * The view should implement this method to return true to indicate that it is + * handling the hover event, such as by changing its drawable state. * </p><p> - * The default implementation sets the hovered state of the view if the view is - * clickable. + * The default implementation calls {@link #setHovered} to update the hovered state + * of the view when a hover enter or hover exit event is received, if the view + * is enabled and is clickable. * </p> * * @param event The motion event that describes the hover. - * @return True if this view handled the hover event and does not want its children - * to receive the hover event. + * @return True if the view handled the hover event. + * + * @see #isHovered + * @see #setHovered + * @see #onHoverChanged */ public boolean onHoverEvent(MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_HOVER_ENTER: - setHovered(true); - break; + if (isHoverable()) { + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + setHovered(true); + break; + case MotionEvent.ACTION_HOVER_EXIT: + setHovered(false); + break; + } + return true; + } + return false; + } - case MotionEvent.ACTION_HOVER_EXIT: - setHovered(false); - break; + /** + * Returns true if the view should handle {@link #onHoverEvent} + * by calling {@link #setHovered} to change its hovered state. + * + * @return True if the view is hoverable. + */ + private boolean isHoverable() { + final int viewFlags = mViewFlags; + if ((viewFlags & ENABLED_MASK) == DISABLED) { + return false; } - return false; + return (viewFlags & CLICKABLE) == CLICKABLE + || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE; } /** * Returns true if the view is currently hovered. * * @return True if the view is currently hovered. + * + * @see #setHovered + * @see #onHoverChanged */ + @ViewDebug.ExportedProperty public boolean isHovered() { return (mPrivateFlags & HOVERED) != 0; } /** * Sets whether the view is currently hovered. + * <p> + * Calling this method also changes the drawable state of the view. This + * enables the view to react to hover by using different drawable resources + * to change its appearance. + * </p><p> + * The {@link #onHoverChanged} method is called when the hovered state changes. + * </p> * * @param hovered True if the view is hovered. + * + * @see #isHovered + * @see #onHoverChanged */ public void setHovered(boolean hovered) { if (hovered) { if ((mPrivateFlags & HOVERED) == 0) { mPrivateFlags |= HOVERED; refreshDrawableState(); - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + onHoverChanged(true); } } else { if ((mPrivateFlags & HOVERED) != 0) { mPrivateFlags &= ~HOVERED; refreshDrawableState(); - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + onHoverChanged(false); } } } /** + * Implement this method to handle hover state changes. + * <p> + * This method is called whenever the hover state changes as a result of a + * call to {@link #setHovered}. + * </p> + * + * @param hovered The current hover state, as returned by {@link #isHovered}. + * + * @see #isHovered + * @see #setHovered + */ + public void onHoverChanged(boolean hovered) { + } + + /** * Implement this method to handle touch screen motion events. * * @param event The motion event. @@ -8860,12 +8968,12 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit // Start user padding override Left user padding. Otherwise, if Left user // padding is not defined, use the default left padding. If Left user padding // is defined, just use it. - if (mUserPaddingStart >= 0) { + if (mUserPaddingStart > 0) { mUserPaddingLeft = mUserPaddingStart; } else if (mUserPaddingLeft < 0) { mUserPaddingLeft = mPaddingLeft; } - if (mUserPaddingEnd >= 0) { + if (mUserPaddingEnd > 0) { mUserPaddingRight = mUserPaddingEnd; } else if (mUserPaddingRight < 0) { mUserPaddingRight = mPaddingRight; @@ -11026,6 +11134,10 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit */ public void setPaddingRelative(int start, int top, int end, int bottom) { mUserPaddingRelative = true; + + mUserPaddingStart = start; + mUserPaddingEnd = end; + switch(getResolvedLayoutDirection()) { case LAYOUT_DIRECTION_RTL: setPadding(end, top, start, bottom); @@ -13097,6 +13209,24 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit } /** + * Interface definition for a callback to be invoked when a hover event is + * dispatched to this view. The callback will be invoked before the hover + * event is given to the view. + */ + public interface OnHoverListener { + /** + * Called when a hover event is dispatched to a view. This allows listeners to + * get a chance to respond before the target view. + * + * @param v The view the hover event has been dispatched to. + * @param event The MotionEvent object containing full information about + * the event. + * @return True if the listener has consumed the event, false otherwise. + */ + boolean onHover(View v, MotionEvent event); + } + + /** * Interface definition for a callback to be invoked when a generic motion event is * dispatched to this view. The callback will be invoked before the generic motion * event is given to the view. diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index a6bce75..e928f80 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -143,8 +143,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @ViewDebug.ExportedProperty(category = "events") private float mLastTouchDownY; - // Child which last received ACTION_HOVER_ENTER and ACTION_HOVER_MOVE. - private View mHoveredChild; + // First hover target in the linked list of hover targets. + // The hover targets are children which have received ACTION_HOVER_ENTER. + // They might not have actually handled the hover event, but we will + // continue sending hover events to them as long as the pointer remains over + // their bounds and the view group does not intercept hover. + private HoverTarget mFirstHoverTarget; + + // True if the view group itself received a hover event. + // It might not have actually handled the hover event. + private boolean mHoveredSelf; /** * Internal flags. @@ -1222,56 +1230,31 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return false; } - /** @hide */ + /** + * {@inheritDoc} + */ @Override protected boolean dispatchHoverEvent(MotionEvent event) { - // Send the hover enter or hover move event to the view group first. - // If it handles the event then a hovered child should receive hover exit. - boolean handled = false; - final boolean interceptHover; final int action = event.getAction(); - if (action == MotionEvent.ACTION_HOVER_EXIT) { - interceptHover = true; - } else { - handled = super.dispatchHoverEvent(event); - interceptHover = handled; - } - // Send successive hover events to the hovered child as long as the pointer - // remains within the child's bounds. + // First check whether the view group wants to intercept the hover event. + final boolean interceptHover = onInterceptHoverEvent(event); + event.setAction(action); // restore action in case it was changed + MotionEvent eventNoHistory = event; - if (mHoveredChild != null) { + boolean handled = false; + + // Send events to the hovered children and build a new list of hover targets until + // one is found that handles the event. + HoverTarget firstOldHoverTarget = mFirstHoverTarget; + mFirstHoverTarget = null; + if (!interceptHover && action != MotionEvent.ACTION_HOVER_EXIT) { final float x = event.getX(); final float y = event.getY(); - - if (interceptHover - || !isTransformedTouchPointInView(x, y, mHoveredChild, null)) { - // Pointer exited the child. - // Send it a hover exit with only the most recent coordinates. We could - // try to find the exact point in history when the pointer left the view - // but it is not worth the effort. - eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); - eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT); - handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, mHoveredChild); - eventNoHistory.setAction(action); - mHoveredChild = null; - } else { - // Pointer is still within the child. - //noinspection ConstantConditions - handled |= dispatchTransformedGenericPointerEvent(event, mHoveredChild); - } - } - - // Find a new hovered child if needed. - if (!interceptHover && mHoveredChild == null - && (action == MotionEvent.ACTION_HOVER_ENTER - || action == MotionEvent.ACTION_HOVER_MOVE)) { final int childrenCount = mChildrenCount; if (childrenCount != 0) { final View[] children = mChildren; - final float x = event.getX(); - final float y = event.getY(); - + HoverTarget lastHoverTarget = null; for (int i = childrenCount - 1; i >= 0; i--) { final View child = children[i]; if (!canViewReceivePointerEvents(child) @@ -1279,24 +1262,140 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager continue; } - // Found the hovered child. - mHoveredChild = child; + // Obtain a hover target for this child. Dequeue it from the + // old hover target list if the child was previously hovered. + HoverTarget hoverTarget = firstOldHoverTarget; + final boolean wasHovered; + for (HoverTarget predecessor = null; ;) { + if (hoverTarget == null) { + hoverTarget = HoverTarget.obtain(child); + wasHovered = false; + break; + } + + if (hoverTarget.child == child) { + if (predecessor != null) { + predecessor.next = hoverTarget.next; + } else { + firstOldHoverTarget = hoverTarget.next; + } + hoverTarget.next = null; + wasHovered = true; + break; + } + + predecessor = hoverTarget; + hoverTarget = hoverTarget.next; + } + + // Enqueue the hover target onto the new hover target list. + if (lastHoverTarget != null) { + lastHoverTarget.next = hoverTarget; + } else { + lastHoverTarget = hoverTarget; + mFirstHoverTarget = hoverTarget; + } + + // Dispatch the event to the child. + if (action == MotionEvent.ACTION_HOVER_ENTER) { + if (!wasHovered) { + // Send the enter as is. + handled |= dispatchTransformedGenericPointerEvent( + event, child); // enter + } + } else if (action == MotionEvent.ACTION_HOVER_MOVE) { + if (!wasHovered) { + // Synthesize an enter from a move. + eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); + eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER); + handled |= dispatchTransformedGenericPointerEvent( + eventNoHistory, child); // enter + eventNoHistory.setAction(action); + + handled |= dispatchTransformedGenericPointerEvent( + eventNoHistory, child); // move + } else { + // Send the move as is. + handled |= dispatchTransformedGenericPointerEvent(event, child); + } + } + if (handled) { + break; + } + } + } + } + + // Send exit events to all previously hovered children that are no longer hovered. + while (firstOldHoverTarget != null) { + final View child = firstOldHoverTarget.child; + + // Exit the old hovered child. + if (action == MotionEvent.ACTION_HOVER_EXIT) { + // Send the exit as is. + handled |= dispatchTransformedGenericPointerEvent( + event, child); // exit + } else { + // Synthesize an exit from a move or enter. + // Ignore the result because hover focus has moved to a different view. + if (action == MotionEvent.ACTION_HOVER_MOVE) { + dispatchTransformedGenericPointerEvent( + event, child); // move + } + eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); + eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT); + dispatchTransformedGenericPointerEvent( + eventNoHistory, child); // exit + eventNoHistory.setAction(action); + } + + final HoverTarget nextOldHoverTarget = firstOldHoverTarget.next; + firstOldHoverTarget.recycle(); + firstOldHoverTarget = nextOldHoverTarget; + } + + // Send events to the view group itself if no children have handled it. + boolean newHoveredSelf = !handled; + if (newHoveredSelf == mHoveredSelf) { + if (newHoveredSelf) { + // Send event to the view group as before. + handled |= super.dispatchHoverEvent(event); + } + } else { + if (mHoveredSelf) { + // Exit the view group. + if (action == MotionEvent.ACTION_HOVER_EXIT) { + // Send the exit as is. + handled |= super.dispatchHoverEvent(event); // exit + } else { + // Synthesize an exit from a move or enter. + // Ignore the result because hover focus is moving to a different view. if (action == MotionEvent.ACTION_HOVER_MOVE) { - // Pointer was moving within the view group and entered the child. - // Send it a hover enter and hover move with only the most recent - // coordinates. We could try to find the exact point in history when - // the pointer entered the view but it is not worth the effort. - eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); - eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER); - handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child); - eventNoHistory.setAction(action); - - handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child); - } else { /* must be ACTION_HOVER_ENTER */ - // Pointer entered the child. - handled |= dispatchTransformedGenericPointerEvent(event, child); + super.dispatchHoverEvent(event); // move } - break; + eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); + eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT); + super.dispatchHoverEvent(eventNoHistory); // exit + eventNoHistory.setAction(action); + } + mHoveredSelf = false; + } + + if (newHoveredSelf) { + // Enter the view group. + if (action == MotionEvent.ACTION_HOVER_ENTER) { + // Send the enter as is. + handled |= super.dispatchHoverEvent(event); // enter + mHoveredSelf = true; + } else if (action == MotionEvent.ACTION_HOVER_MOVE) { + // Synthesize an enter from a move. + eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); + eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER); + handled |= super.dispatchHoverEvent(eventNoHistory); // enter + eventNoHistory.setAction(action); + + handled |= super.dispatchHoverEvent(eventNoHistory); // move + mHoveredSelf = true; } } } @@ -1306,25 +1405,55 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager eventNoHistory.recycle(); } - // Send hover exit to the view group. If there was a child, we will already have - // sent the hover exit to it. - if (action == MotionEvent.ACTION_HOVER_EXIT) { - handled |= super.dispatchHoverEvent(event); - } - // Done. return handled; } + /** @hide */ @Override - public boolean onHoverEvent(MotionEvent event) { - // Handle the event only if leaf. This guarantees that - // the leafs (or any custom class that returns true from - // this method) will get a change to process the hover. - //noinspection SimplifiableIfStatement - if (getChildCount() == 0) { - return super.onHoverEvent(event); - } + protected boolean hasHoveredChild() { + return mFirstHoverTarget != null; + } + + /** + * Implement this method to intercept hover events before they are handled + * by child views. + * <p> + * This method is called before dispatching a hover event to a child of + * the view group or to the view group's own {@link #onHoverEvent} to allow + * the view group a chance to intercept the hover event. + * This method can also be used to watch all pointer motions that occur within + * the bounds of the view group even when the pointer is hovering over + * a child of the view group rather than over the view group itself. + * </p><p> + * The view group can prevent its children from receiving hover events by + * implementing this method and returning <code>true</code> to indicate + * that it would like to intercept hover events. The view group must + * continuously return <code>true</code> from {@link #onInterceptHoverEvent} + * for as long as it wishes to continue intercepting hover events from + * its children. + * </p><p> + * Interception preserves the invariant that at most one view can be + * hovered at a time by transferring hover focus from the currently hovered + * child to the view group or vice-versa as needed. + * </p><p> + * If this method returns <code>true</code> and a child is already hovered, then the + * child view will first receive a hover exit event and then the view group + * itself will receive a hover enter event in {@link #onHoverEvent}. + * Likewise, if this method had previously returned <code>true</code> to intercept hover + * events and instead returns <code>false</code> while the pointer is hovering + * within the bounds of one of a child, then the view group will first receive a + * hover exit event in {@link #onHoverEvent} and then the hovered child will + * receive a hover enter event. + * </p><p> + * The default implementation always returns false. + * </p> + * + * @param event The motion event that describes the hover. + * @return True if the view group would like to intercept the hover event + * and prevent its children from receiving it. + */ + public boolean onInterceptHoverEvent(MotionEvent event) { return false; } @@ -1335,7 +1464,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return MotionEvent.obtainNoHistory(event); } - /** @hide */ + /** + * {@inheritDoc} + */ @Override protected boolean dispatchGenericPointerEvent(MotionEvent event) { // Send the event to the child under the pointer. @@ -1362,7 +1493,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return super.dispatchGenericPointerEvent(event); } - /** @hide */ + /** + * {@inheritDoc} + */ @Override protected boolean dispatchGenericFocusedEvent(MotionEvent event) { // Send the event to the focused child or to this view group if it has focus. @@ -3337,10 +3470,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mTransition.removeChild(this, view); } - if (view == mHoveredChild) { - mHoveredChild = null; - } - boolean clearChildFocus = false; if (view == mFocused) { view.clearFocusForRemoval(); @@ -3404,7 +3533,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final OnHierarchyChangeListener onHierarchyChangeListener = mOnHierarchyChangeListener; final boolean notifyListener = onHierarchyChangeListener != null; final View focused = mFocused; - final View hoveredChild = mHoveredChild; final boolean detach = mAttachInfo != null; View clearChildFocus = null; @@ -3418,10 +3546,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mTransition.removeChild(this, view); } - if (view == hoveredChild) { - mHoveredChild = null; - } - if (view == focused) { view.clearFocusForRemoval(); clearChildFocus = view; @@ -3479,7 +3603,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final OnHierarchyChangeListener listener = mOnHierarchyChangeListener; final boolean notify = listener != null; final View focused = mFocused; - final View hoveredChild = mHoveredChild; final boolean detach = mAttachInfo != null; View clearChildFocus = null; @@ -3492,10 +3615,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mTransition.removeChild(this, view); } - if (view == hoveredChild) { - mHoveredChild = null; - } - if (view == focused) { view.clearFocusForRemoval(); clearChildFocus = view; @@ -5242,4 +5361,50 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } } + + /* Describes a hovered view. */ + private static final class HoverTarget { + private static final int MAX_RECYCLED = 32; + private static final Object sRecycleLock = new Object(); + private static HoverTarget sRecycleBin; + private static int sRecycledCount; + + // The hovered child view. + public View child; + + // The next target in the target list. + public HoverTarget next; + + private HoverTarget() { + } + + public static HoverTarget obtain(View child) { + final HoverTarget target; + synchronized (sRecycleLock) { + if (sRecycleBin == null) { + target = new HoverTarget(); + } else { + target = sRecycleBin; + sRecycleBin = target.next; + sRecycledCount--; + target.next = null; + } + } + target.child = child; + return target; + } + + public void recycle() { + synchronized (sRecycleLock) { + if (sRecycledCount < MAX_RECYCLED) { + next = sRecycleBin; + sRecycleBin = this; + sRecycledCount += 1; + } else { + next = null; + } + child = null; + } + } + } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 18ef38a..555667b 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -252,7 +252,8 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Finds {@link AccessibilityNodeInfo}s by text. The match is case - * insensitive containment. + * insensitive containment. The search is relative to this info i.e. + * this info is the root of the traversed tree. * * @param text The searched text. * @return A list of node info. @@ -260,7 +261,7 @@ public class AccessibilityNodeInfo implements Parcelable { public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text) { enforceSealed(); if (!canPerformRequestOverConnection(mAccessibilityViewId)) { - return null; + return Collections.emptyList(); } try { return mConnection.findAccessibilityNodeInfosByViewText(text, mAccessibilityWindowId, diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index c56e6db..9f632d1 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -5707,8 +5707,8 @@ public class WebView extends AbsoluteLayout return false; } WebViewCore.CursorData data = cursorDataNoPosition(); - data.mX = viewToContentX((int) event.getX()); - data.mY = viewToContentY((int) event.getY()); + data.mX = viewToContentX((int) event.getX() + mScrollX); + data.mY = viewToContentY((int) event.getY() + mScrollY); mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, data); return true; } diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 0e52869..e88d257 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -535,7 +535,7 @@ public class GridLayout extends ViewGroup { return result; } - private int getDefaultMargin(View c, boolean leading, boolean horizontal) { + private int getDefaultMargin(View c, boolean horizontal, boolean leading) { // In the absence of any other information, calculate a default gap such // that, in a grid of identical components, the heights and the vertical // gaps are in the proportion of the golden ratio. @@ -544,12 +544,12 @@ public class GridLayout extends ViewGroup { return (int) (c.getMeasuredHeight() / GOLDEN_RATIO / 2); } - private int getDefaultMargin(View c, boolean isAtEdge, boolean leading, boolean horizontal) { + private int getDefaultMargin(View c, boolean isAtEdge, boolean horizontal, boolean leading) { // todo remove DEFAULT_CONTAINER_MARGIN. Use padding? Seek advice on Themes/Styles, etc. - return isAtEdge ? DEFAULT_CONTAINER_MARGIN : getDefaultMargin(c, leading, horizontal); + return isAtEdge ? DEFAULT_CONTAINER_MARGIN : getDefaultMargin(c, horizontal, leading); } - private int getDefaultMarginValue(View c, LayoutParams p, boolean leading, boolean horizontal) { + private int getDefaultMarginValue(View c, LayoutParams p, boolean horizontal, boolean leading) { if (!mUseDefaultMargins) { return 0; } @@ -558,15 +558,19 @@ public class GridLayout extends ViewGroup { Interval span = group.span; boolean isAtEdge = leading ? (span.min == 0) : (span.max == axis.getCount()); - return getDefaultMargin(c, isAtEdge, leading, horizontal); + return getDefaultMargin(c, isAtEdge, horizontal, leading); } - private int getMargin(View view, boolean leading, boolean horizontal) { + private int getMargin(View view, boolean horizontal, boolean leading) { LayoutParams lp = getLayoutParams(view); int margin = horizontal ? (leading ? lp.leftMargin : lp.rightMargin) : (leading ? lp.topMargin : lp.bottomMargin); - return margin == UNDEFINED ? getDefaultMarginValue(view, lp, leading, horizontal) : margin; + return margin == UNDEFINED ? getDefaultMarginValue(view, lp, horizontal, leading) : margin; + } + + private int getTotalMargin(View child, boolean horizontal) { + return getMargin(child, horizontal, true) + getMargin(child, horizontal, false); } private static int valueIfDefined(int value, int defaultValue) { @@ -749,8 +753,8 @@ public class GridLayout extends ViewGroup { View c = getChildAt(i); drawRectangle(canvas, c.getLeft() - getMargin(c, true, true), - c.getTop() - getMargin(c, true, false), - c.getRight() + getMargin(c, false, true), + c.getTop() - getMargin(c, false, true), + c.getRight() + getMargin(c, true, false), c.getBottom() + getMargin(c, false, false), paint); } } @@ -794,17 +798,12 @@ public class GridLayout extends ViewGroup { return c.getVisibility() == View.GONE; } - private void measureChildWithMargins(View child, - int parentWidthMeasureSpec, int parentHeightMeasureSpec) { - + private void measureChildWithMargins(View child, int widthMeasureSpec, int heightMeasureSpec) { LayoutParams lp = getLayoutParams(child); - int hMargins = getMargin(child, true, true) + getMargin(child, false, true); - int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, - mPaddingLeft + mPaddingRight + hMargins, lp.width); - int vMargins = getMargin(child, true, false) + getMargin(child, false, false); - int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, - mPaddingTop + mPaddingBottom + vMargins, lp.height); - + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + mPaddingLeft + mPaddingRight + getTotalMargin(child, true), lp.width); + int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + mPaddingTop + mPaddingBottom + getTotalMargin(child, false), lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @@ -842,9 +841,7 @@ public class GridLayout extends ViewGroup { private int getMeasurementIncludingMargin(View c, boolean horizontal, int measurementType) { int result = getMeasurement(c, horizontal, measurementType); if (mAlignmentMode == ALIGN_MARGINS) { - int leadingMargin = getMargin(c, true, horizontal); - int trailingMargin = getMargin(c, false, horizontal); - return result + leadingMargin + trailingMargin; + return result + getTotalMargin(c, horizontal); } return result; } @@ -919,8 +916,8 @@ public class GridLayout extends ViewGroup { if (mAlignmentMode == ALIGN_MARGINS) { int leftMargin = getMargin(c, true, true); - int topMargin = getMargin(c, true, false); - int rightMargin = getMargin(c, false, true); + int topMargin = getMargin(c, false, true); + int rightMargin = getMargin(c, true, false); int bottomMargin = getMargin(c, false, false); // Same calculation as getMeasurementIncludingMargin() @@ -1387,7 +1384,7 @@ public class GridLayout extends ViewGroup { Group g = horizontal ? lp.columnGroup : lp.rowGroup; Interval span = g.span; int index = leading ? span.min : span.max; - margins[index] = max(margins[index], getMargin(c, leading, horizontal)); + margins[index] = max(margins[index], getMargin(c, horizontal, leading)); } } @@ -1817,7 +1814,8 @@ public class GridLayout extends ViewGroup { } private int getDefaultWeight(int size) { - return (size == MATCH_PARENT) ? DEFAULT_WEIGHT_1 : DEFAULT_WEIGHT_0; + //return (size == MATCH_PARENT) ? DEFAULT_WEIGHT_1 : DEFAULT_WEIGHT_0; + return DEFAULT_WEIGHT_0; } private void init(Context context, AttributeSet attrs, int defaultGravity) { diff --git a/core/java/android/widget/PopupMenu.java b/core/java/android/widget/PopupMenu.java index 82770ad..17512d8 100644 --- a/core/java/android/widget/PopupMenu.java +++ b/core/java/android/widget/PopupMenu.java @@ -18,6 +18,7 @@ package android.widget; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuPopupHelper; +import com.android.internal.view.menu.MenuPresenter; import com.android.internal.view.menu.SubMenuBuilder; import android.content.Context; @@ -32,12 +33,25 @@ import android.view.View; * If the IME is visible the popup will not overlap it until it is touched. Touching outside * of the popup will dismiss it. */ -public class PopupMenu implements MenuBuilder.Callback { +public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { private Context mContext; private MenuBuilder mMenu; private View mAnchor; private MenuPopupHelper mPopup; private OnMenuItemClickListener mMenuItemClickListener; + private OnDismissListener mDismissListener; + + /** + * Callback interface used to notify the application that the menu has closed. + */ + public interface OnDismissListener { + /** + * Called when the associated menu has been dismissed. + * + * @param menu The PopupMenu that was dismissed. + */ + public void onDismiss(PopupMenu menu); + } /** * Construct a new PopupMenu. @@ -53,6 +67,7 @@ public class PopupMenu implements MenuBuilder.Callback { mMenu.setCallback(this); mAnchor = anchor; mPopup = new MenuPopupHelper(context, mMenu, anchor); + mPopup.setCallback(this); } /** @@ -77,6 +92,15 @@ public class PopupMenu implements MenuBuilder.Callback { } /** + * Inflate a menu resource into this PopupMenu. This is equivalent to calling + * popupMenu.getMenuInflater().inflate(menuRes, popupMenu.getMenu()). + * @param menuRes Menu resource to inflate + */ + public void inflate(int menuRes) { + getMenuInflater().inflate(menuRes, mMenu); + } + + /** * Show the menu popup anchored to the view specified during construction. * @see #dismiss() */ @@ -92,11 +116,25 @@ public class PopupMenu implements MenuBuilder.Callback { mPopup.dismiss(); } + /** + * Set a listener that will be notified when the user selects an item from the menu. + * + * @param listener Listener to notify + */ public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { mMenuItemClickListener = listener; } /** + * Set a listener that will be notified when this menu is dismissed. + * + * @param listener Listener to notify + */ + public void setOnDismissListener(OnDismissListener listener) { + mDismissListener = listener; + } + + /** * @hide */ public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { @@ -110,12 +148,15 @@ public class PopupMenu implements MenuBuilder.Callback { * @hide */ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (mDismissListener != null) { + mDismissListener.onDismiss(this); + } } /** * @hide */ - public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + public boolean onOpenSubMenu(MenuBuilder subMenu) { if (!subMenu.hasVisibleItems()) { return true; } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 02c2b8f..77df7c8 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -7658,12 +7658,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - public void findViewsWithText(ArrayList<View> outViews, CharSequence text) { + public void findViewsWithText(ArrayList<View> outViews, CharSequence searched) { + if (TextUtils.isEmpty(searched)) { + return; + } CharSequence thisText = getText(); if (TextUtils.isEmpty(thisText)) { return; } - if (thisText.toString().toLowerCase().contains(text)) { + String searchedLowerCase = searched.toString().toLowerCase(); + String thisTextLowerCase = thisText.toString().toLowerCase(); + if (thisTextLowerCase.contains(searched)) { outViews.add(this); } } diff --git a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java index 98c2747..322a854 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java +++ b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java @@ -336,6 +336,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter { if (groupId != 0) { seenGroups.put(groupId, true); } + item.setIsActionButton(true); } else if (item.requestsActionButton()) { // Items in a group with other items that already have an action slot // can break the max actions rule, but not the width limit. diff --git a/core/java/com/android/internal/view/menu/ListMenuPresenter.java b/core/java/com/android/internal/view/menu/ListMenuPresenter.java index cc09927..27e4191 100644 --- a/core/java/com/android/internal/view/menu/ListMenuPresenter.java +++ b/core/java/com/android/internal/view/menu/ListMenuPresenter.java @@ -184,12 +184,12 @@ public class ListMenuPresenter implements MenuPresenter, AdapterView.OnItemClick private class MenuAdapter extends BaseAdapter { public int getCount() { - ArrayList<MenuItemImpl> items = mMenu.getVisibleItems(); + ArrayList<MenuItemImpl> items = mMenu.getNonActionItems(); return items.size() - mItemIndexOffset; } public MenuItemImpl getItem(int position) { - ArrayList<MenuItemImpl> items = mMenu.getVisibleItems(); + ArrayList<MenuItemImpl> items = mMenu.getNonActionItems(); return items.get(position + mItemIndexOffset); } diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java index 1a6cc54..253511c 100644 --- a/core/java/com/android/internal/view/menu/MenuItemImpl.java +++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java @@ -507,7 +507,7 @@ public final class MenuItemImpl implements MenuItem { } public boolean isActionButton() { - return (mFlags & IS_ACTION) == IS_ACTION || requiresActionButton(); + return (mFlags & IS_ACTION) == IS_ACTION; } public boolean requestsActionButton() { diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java index 8db7e3c..cffbb4e 100644 --- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java +++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java @@ -126,6 +126,7 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On public void onDismiss() { mPopup = null; + mMenu.close(); if (mTreeObserver != null) { if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver(); mTreeObserver.removeGlobalOnLayoutListener(this); diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java index cbb110a..0c0205c 100644 --- a/core/java/com/android/internal/widget/LockPatternView.java +++ b/core/java/com/android/internal/widget/LockPatternView.java @@ -464,8 +464,7 @@ public class LockPatternView extends View { break; case MeasureSpec.EXACTLY: default: - // use the specified size, if non-zero - result = specSize != 0 ? specSize : desired; + result = specSize; } return result; } diff --git a/core/jni/android_pim_EventRecurrence.cpp b/core/jni/android_pim_EventRecurrence.cpp index 44e898d..3e11569 100644 --- a/core/jni/android_pim_EventRecurrence.cpp +++ b/core/jni/android_pim_EventRecurrence.cpp @@ -147,7 +147,7 @@ EventRecurrence_parse(JNIEnv* env, jobject This, jstring jstr) */
static JNINativeMethod METHODS[] = {
/* name, signature, funcPtr */
- { "parse", "(Ljava/lang/String;)V", (void*)EventRecurrence_parse }
+ { "parseNative", "(Ljava/lang/String;)V", (void*)EventRecurrence_parse }
};
static const char*const CLASS_NAME = "android/pim/EventRecurrence";
diff --git a/core/res/res/drawable-hdpi/ic_notification_ime_default.png b/core/res/res/drawable-hdpi/ic_notification_ime_default.png Binary files differnew file mode 100644 index 0000000..1a9d88c --- /dev/null +++ b/core/res/res/drawable-hdpi/ic_notification_ime_default.png diff --git a/core/res/res/drawable-mdpi/ic_notification_ime_default.png b/core/res/res/drawable-mdpi/ic_notification_ime_default.png Binary files differnew file mode 100644 index 0000000..1a9d88c --- /dev/null +++ b/core/res/res/drawable-mdpi/ic_notification_ime_default.png diff --git a/core/res/res/layout/keyguard_screen_unlock_portrait.xml b/core/res/res/layout/keyguard_screen_unlock_portrait.xml index dd68d82..03c6022 100644 --- a/core/res/res/layout/keyguard_screen_unlock_portrait.xml +++ b/core/res/res/layout/keyguard_screen_unlock_portrait.xml @@ -101,6 +101,10 @@ android:visibility="gone" /> + <!-- We need MATCH_PARENT here only to force the size of the parent to be passed to + the pattern view for it to compute its size. This is an unusual case, caused by + LockPatternView's requirement to maintain a square aspect ratio based on the width + of the screen. --> <com.android.internal.widget.LockPatternView android:id="@+id/lockPattern" android:layout_width="match_parent" @@ -109,6 +113,8 @@ android:layout_marginRight="8dip" android:layout_marginBottom="4dip" android:layout_marginLeft="8dip" + android:layout_gravity="center|bottom" + android:layout_rowWeight="1" /> <TextView @@ -123,8 +129,7 @@ <!-- Footer: an emergency call button and an initially hidden "Forgot pattern" button --> <LinearLayout android:orientation="horizontal" - android:layout_width="match_parent" - android:layout_gravity="center"> + android:layout_gravity="fill_horizontal"> <Button android:id="@+id/emergencyCallButton" android:layout_width="wrap_content" diff --git a/core/res/res/values-sw600dp/bools.xml b/core/res/res/values-sw600dp/bools.xml index 734031f..d73ff99 100644 --- a/core/res/res/values-sw600dp/bools.xml +++ b/core/res/res/values-sw600dp/bools.xml @@ -16,4 +16,5 @@ <resources> <bool name="preferences_prefer_dual_pane">true</bool> + <bool name="show_ongoing_ime_switcher">false</bool> </resources> diff --git a/core/res/res/values/bools.xml b/core/res/res/values/bools.xml index 9d6309d..9647bb7 100644 --- a/core/res/res/values/bools.xml +++ b/core/res/res/values/bools.xml @@ -19,4 +19,5 @@ <bool name="action_bar_embed_tabs">false</bool> <bool name="split_action_bar_is_narrow">true</bool> <bool name="preferences_prefer_dual_pane">false</bool> + <bool name="show_ongoing_ime_switcher">true</bool> </resources> diff --git a/core/tests/coretests/src/android/pim/EventRecurrenceTest.java b/core/tests/coretests/src/android/pim/EventRecurrenceTest.java new file mode 100644 index 0000000..05000f1 --- /dev/null +++ b/core/tests/coretests/src/android/pim/EventRecurrenceTest.java @@ -0,0 +1,753 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.pim; + +import android.pim.EventRecurrence.InvalidFormatException; +import android.test.suitebuilder.annotation.SmallTest; +import android.test.suitebuilder.annotation.Suppress; + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * Test android.pim.EventRecurrence. + * + * adb shell am instrument -w -e class android.pim.EventRecurrenceTest \ + * com.android.frameworks.coretests/android.test.InstrumentationTestRunner + */ +public class EventRecurrenceTest extends TestCase { + + @SmallTest + public void test0() throws Exception { + verifyRecurType("FREQ=SECONDLY", + /* int freq */ EventRecurrence.SECONDLY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test1() throws Exception { + verifyRecurType("FREQ=MINUTELY", + /* int freq */ EventRecurrence.MINUTELY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test2() throws Exception { + verifyRecurType("FREQ=HOURLY", + /* int freq */ EventRecurrence.HOURLY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test3() throws Exception { + verifyRecurType("FREQ=DAILY", + /* int freq */ EventRecurrence.DAILY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test4() throws Exception { + verifyRecurType("FREQ=WEEKLY", + /* int freq */ EventRecurrence.WEEKLY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test5() throws Exception { + verifyRecurType("FREQ=MONTHLY", + /* int freq */ EventRecurrence.MONTHLY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test6() throws Exception { + verifyRecurType("FREQ=YEARLY", + /* int freq */ EventRecurrence.YEARLY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test7() throws Exception { + // with an until + verifyRecurType("FREQ=DAILY;UNTIL=112233T223344Z", + /* int freq */ EventRecurrence.DAILY, + /* String until */ "112233T223344Z", + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test8() throws Exception { + // with a count + verifyRecurType("FREQ=DAILY;COUNT=334", + /* int freq */ EventRecurrence.DAILY, + /* String until */ null, + /* int count */ 334, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test9() throws Exception { + // with a count + verifyRecurType("FREQ=DAILY;INTERVAL=5000", + /* int freq */ EventRecurrence.DAILY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 5000, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @SmallTest + public void test10() throws Exception { + // verifyRecurType all of the BY* ones with one element + verifyRecurType("FREQ=DAILY" + + ";BYSECOND=0" + + ";BYMINUTE=1" + + ";BYHOUR=2" + + ";BYMONTHDAY=30" + + ";BYYEARDAY=300" + + ";BYWEEKNO=53" + + ";BYMONTH=12" + + ";BYSETPOS=-15" + + ";WKST=SU", + /* int freq */ EventRecurrence.DAILY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ new int[]{0}, + /* int[] byminute */ new int[]{1}, + /* int[] byhour */ new int[]{2}, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ new int[]{30}, + /* int[] byyearday */ new int[]{300}, + /* int[] byweekno */ new int[]{53}, + /* int[] bymonth */ new int[]{12}, + /* int[] bysetpos */ new int[]{-15}, + /* int wkst */ EventRecurrence.SU + ); + } + + @SmallTest + public void test11() throws Exception { + // verifyRecurType all of the BY* ones with one element + verifyRecurType("FREQ=DAILY" + + ";BYSECOND=0,30,59" + + ";BYMINUTE=0,41,59" + + ";BYHOUR=0,4,23" + + ";BYMONTHDAY=-31,-1,1,31" + + ";BYYEARDAY=-366,-1,1,366" + + ";BYWEEKNO=-53,-1,1,53" + + ";BYMONTH=1,12" + + ";BYSETPOS=1,2,3,4,500,10000" + + ";WKST=SU", + /* int freq */ EventRecurrence.DAILY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ new int[]{0, 30, 59}, + /* int[] byminute */ new int[]{0, 41, 59}, + /* int[] byhour */ new int[]{0, 4, 23}, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ new int[]{-31, -1, 1, 31}, + /* int[] byyearday */ new int[]{-366, -1, 1, 366}, + /* int[] byweekno */ new int[]{-53, -1, 1, 53}, + /* int[] bymonth */ new int[]{1, 12}, + /* int[] bysetpos */ new int[]{1, 2, 3, 4, 500, 10000}, + /* int wkst */ EventRecurrence.SU + ); + } + + private static class Check { + Check(String k, int... v) { + key = k; + values = v; + } + + String key; + int[] values; + } + + // this is a negative verifyRecurType case to verifyRecurType the range of the numbers accepted + @SmallTest + public void test12() throws Exception { + Check[] checks = new Check[]{ + new Check("BYSECOND", -100, -1, 60, 100), + new Check("BYMINUTE", -100, -1, 60, 100), + new Check("BYHOUR", -100, -1, 24, 100), + new Check("BYMONTHDAY", -100, -32, 0, 32, 100), + new Check("BYYEARDAY", -400, -367, 0, 367, 400), + new Check("BYWEEKNO", -100, -54, 0, 54, 100), + new Check("BYMONTH", -100, -5, 0, 13, 100) + }; + + for (Check ck : checks) { + for (int n : ck.values) { + String recur = "FREQ=DAILY;" + ck.key + "=" + n; + try { + EventRecurrence er = new EventRecurrence(); + er.parse(recur); + fail("Negative verifyRecurType failed. " + + " parse failed to throw an exception for '" + + recur + "'"); + } catch (EventRecurrence.InvalidFormatException e) { + // expected + } + } + } + } + + // verifyRecurType BYDAY + @SmallTest + public void test13() throws Exception { + verifyRecurType("FREQ=DAILY;BYDAY=1SU,-2MO,+33TU,WE,TH,FR,SA", + /* int freq */ EventRecurrence.DAILY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ new int[] { + EventRecurrence.SU, + EventRecurrence.MO, + EventRecurrence.TU, + EventRecurrence.WE, + EventRecurrence.TH, + EventRecurrence.FR, + EventRecurrence.SA + }, + /* int[] bydayNum */ new int[]{1, -2, 33, 0, 0, 0, 0}, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + @Suppress + // Repro bug #2331761 - this should fail because of the last comma into BYDAY + public void test14() throws Exception { + verifyRecurType("FREQ=WEEKLY;WKST=MO;UNTIL=20100129T130000Z;INTERVAL=1;BYDAY=MO,TU,WE,", + /* int freq */ EventRecurrence.WEEKLY, + /* String until */ "20100129T130000Z", + /* int count */ 0, + /* int interval */ 1, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ new int[] { + EventRecurrence.MO, + EventRecurrence.TU, + EventRecurrence.WE, + }, + /* int[] bydayNum */ new int[]{0, 0, 0}, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + // This test should pass + public void test15() throws Exception { + verifyRecurType("FREQ=WEEKLY;WKST=MO;UNTIL=20100129T130000Z;INTERVAL=1;" + + "BYDAY=MO,TU,WE,TH,FR,SA,SU", + /* int freq */ EventRecurrence.WEEKLY, + /* String until */ "20100129T130000Z", + /* int count */ 0, + /* int interval */ 1, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ new int[] { + EventRecurrence.MO, + EventRecurrence.TU, + EventRecurrence.WE, + EventRecurrence.TH, + EventRecurrence.FR, + EventRecurrence.SA, + EventRecurrence.SU + }, + /* int[] bydayNum */ new int[]{0, 0, 0, 0, 0, 0, 0}, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + // Sample coming from RFC2445 + public void test16() throws Exception { + verifyRecurType("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1", + /* int freq */ EventRecurrence.MONTHLY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ new int[] { + EventRecurrence.MO, + EventRecurrence.TU, + EventRecurrence.WE, + EventRecurrence.TH, + EventRecurrence.FR + }, + /* int[] bydayNum */ new int[] {0, 0, 0, 0, 0}, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ new int[] { -1 }, + /* int wkst */ EventRecurrence.MO + ); + } + + // Sample coming from RFC2445 + public void test17() throws Exception { + verifyRecurType("FREQ=DAILY;COUNT=10;INTERVAL=2", + /* int freq */ EventRecurrence.DAILY, + /* String until */ null, + /* int count */ 10, + /* int interval */ 2, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + // Sample coming from RFC2445 + public void test18() throws Exception { + verifyRecurType("FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10", + /* int freq */ EventRecurrence.YEARLY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ new int[] { + EventRecurrence.SU + }, + /* int[] bydayNum */ new int[] { -1 }, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ new int[] { 10 }, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + // Sample coming from bug #1640517 + public void test19() throws Exception { + verifyRecurType("FREQ=YEARLY;BYMONTH=3;BYDAY=TH", + /* int freq */ EventRecurrence.YEARLY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ new int[] { + EventRecurrence.TH + }, + /* int[] bydayNum */ new int[] { 0 }, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ new int[] { 3 }, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + // for your copying pleasure + public void fakeTestXX() throws Exception { + verifyRecurType("FREQ=DAILY;", + /* int freq */ EventRecurrence.DAILY, + /* String until */ null, + /* int count */ 0, + /* int interval */ 0, + /* int[] bysecond */ null, + /* int[] byminute */ null, + /* int[] byhour */ null, + /* int[] byday */ null, + /* int[] bydayNum */ null, + /* int[] bymonthday */ null, + /* int[] byyearday */ null, + /* int[] byweekno */ null, + /* int[] bymonth */ null, + /* int[] bysetpos */ null, + /* int wkst */ EventRecurrence.MO + ); + } + + private static void cmp(int vlen, int[] v, int[] correct, String name) { + if ((correct == null && v != null) + || (correct != null && v == null)) { + throw new RuntimeException("One is null, one isn't for " + name + + ": correct=" + Arrays.toString(correct) + + " actual=" + Arrays.toString(v)); + } + if ((correct == null && vlen != 0) + || (vlen != (correct == null ? 0 : correct.length))) { + throw new RuntimeException("Reported length mismatch for " + name + + ": correct=" + ((correct == null) ? "null" : correct.length) + + " actual=" + vlen); + } + if (correct == null) { + return; + } + if (v.length < correct.length) { + throw new RuntimeException("Array length mismatch for " + name + + ": correct=" + Arrays.toString(correct) + + " actual=" + Arrays.toString(v)); + } + for (int i = 0; i < correct.length; i++) { + if (v[i] != correct[i]) { + throw new RuntimeException("Array value mismatch for " + name + + ": correct=" + Arrays.toString(correct) + + " actual=" + Arrays.toString(v)); + } + } + } + + private static boolean eq(String a, String b) { + if ((a == null && b != null) || (a != null && b == null)) { + return false; + } else { + return a == b || a.equals(b); + } + } + + private static void verifyRecurType(String recur, + int freq, String until, int count, int interval, + int[] bysecond, int[] byminute, int[] byhour, + int[] byday, int[] bydayNum, int[] bymonthday, + int[] byyearday, int[] byweekno, int[] bymonth, + int[] bysetpos, int wkst) { + EventRecurrence eventRecurrence = new EventRecurrence(); + eventRecurrence.parse(recur); + if (eventRecurrence.freq != freq + || !eq(eventRecurrence.until, until) + || eventRecurrence.count != count + || eventRecurrence.interval != interval + || eventRecurrence.wkst != wkst) { + System.out.println("Error... got:"); + print(eventRecurrence); + System.out.println("expected:"); + System.out.println("{"); + System.out.println(" freq=" + freq); + System.out.println(" until=" + until); + System.out.println(" count=" + count); + System.out.println(" interval=" + interval); + System.out.println(" wkst=" + wkst); + System.out.println(" bysecond=" + Arrays.toString(bysecond)); + System.out.println(" byminute=" + Arrays.toString(byminute)); + System.out.println(" byhour=" + Arrays.toString(byhour)); + System.out.println(" byday=" + Arrays.toString(byday)); + System.out.println(" bydayNum=" + Arrays.toString(bydayNum)); + System.out.println(" bymonthday=" + Arrays.toString(bymonthday)); + System.out.println(" byyearday=" + Arrays.toString(byyearday)); + System.out.println(" byweekno=" + Arrays.toString(byweekno)); + System.out.println(" bymonth=" + Arrays.toString(bymonth)); + System.out.println(" bysetpos=" + Arrays.toString(bysetpos)); + System.out.println("}"); + throw new RuntimeException("Mismatch in fields"); + } + cmp(eventRecurrence.bysecondCount, eventRecurrence.bysecond, bysecond, "bysecond"); + cmp(eventRecurrence.byminuteCount, eventRecurrence.byminute, byminute, "byminute"); + cmp(eventRecurrence.byhourCount, eventRecurrence.byhour, byhour, "byhour"); + cmp(eventRecurrence.bydayCount, eventRecurrence.byday, byday, "byday"); + cmp(eventRecurrence.bydayCount, eventRecurrence.bydayNum, bydayNum, "bydayNum"); + cmp(eventRecurrence.bymonthdayCount, eventRecurrence.bymonthday, bymonthday, "bymonthday"); + cmp(eventRecurrence.byyeardayCount, eventRecurrence.byyearday, byyearday, "byyearday"); + cmp(eventRecurrence.byweeknoCount, eventRecurrence.byweekno, byweekno, "byweekno"); + cmp(eventRecurrence.bymonthCount, eventRecurrence.bymonth, bymonth, "bymonth"); + cmp(eventRecurrence.bysetposCount, eventRecurrence.bysetpos, bysetpos, "bysetpos"); + } + + private static void print(EventRecurrence er) { + System.out.println("{"); + System.out.println(" freq=" + er.freq); + System.out.println(" until=" + er.until); + System.out.println(" count=" + er.count); + System.out.println(" interval=" + er.interval); + System.out.println(" wkst=" + er.wkst); + System.out.println(" bysecond=" + Arrays.toString(er.bysecond)); + System.out.println(" bysecondCount=" + er.bysecondCount); + System.out.println(" byminute=" + Arrays.toString(er.byminute)); + System.out.println(" byminuteCount=" + er.byminuteCount); + System.out.println(" byhour=" + Arrays.toString(er.byhour)); + System.out.println(" byhourCount=" + er.byhourCount); + System.out.println(" byday=" + Arrays.toString(er.byday)); + System.out.println(" bydayNum=" + Arrays.toString(er.bydayNum)); + System.out.println(" bydayCount=" + er.bydayCount); + System.out.println(" bymonthday=" + Arrays.toString(er.bymonthday)); + System.out.println(" bymonthdayCount=" + er.bymonthdayCount); + System.out.println(" byyearday=" + Arrays.toString(er.byyearday)); + System.out.println(" byyeardayCount=" + er.byyeardayCount); + System.out.println(" byweekno=" + Arrays.toString(er.byweekno)); + System.out.println(" byweeknoCount=" + er.byweeknoCount); + System.out.println(" bymonth=" + Arrays.toString(er.bymonth)); + System.out.println(" bymonthCount=" + er.bymonthCount); + System.out.println(" bysetpos=" + Arrays.toString(er.bysetpos)); + System.out.println(" bysetposCount=" + er.bysetposCount); + System.out.println("}"); + } + + + /** A list of valid rules. The parser must accept these. */ + private static final String[] GOOD_RRULES = { + /* extracted wholesale from from RFC 2445 section 4.8.5.4 */ + "FREQ=DAILY;COUNT=10", + "FREQ=DAILY;UNTIL=19971224T000000Z", + "FREQ=DAILY;INTERVAL=2", + "FREQ=DAILY;INTERVAL=10;COUNT=5", + "FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA", + "FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1", + "FREQ=WEEKLY;COUNT=10", + "FREQ=WEEKLY;UNTIL=19971224T000000Z", + "FREQ=WEEKLY;INTERVAL=2;WKST=SU", + "FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH", + "FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH", + "FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR", + "FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH", + "FREQ=MONTHLY;COUNT=10;BYDAY=1FR", + "FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR", + "FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU", + "FREQ=MONTHLY;COUNT=6;BYDAY=-2MO", + "FREQ=MONTHLY;BYMONTHDAY=-3", + "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15", + "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1", + "FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15", + "FREQ=MONTHLY;INTERVAL=2;BYDAY=TU", + "FREQ=YEARLY;COUNT=10;BYMONTH=6,7", + "FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3", + "FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200", + "FREQ=YEARLY;BYDAY=20MO", + "FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO", + "FREQ=YEARLY;BYMONTH=3;BYDAY=TH", + "FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8", + "FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13", + "FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13", + "FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8", + "FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3", + "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2", + "FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z", + "FREQ=MINUTELY;INTERVAL=15;COUNT=6", + "FREQ=MINUTELY;INTERVAL=90;COUNT=4", + "FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40", + "FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16", + "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO", + "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU", + /* a few more */ + "FREQ=SECONDLY;BYSECOND=0,15,59", + "FREQ=MINUTELY;BYMINUTE=0,15,59", + "FREQ=HOURLY;BYHOUR=+0,+15,+23", + "FREQ=DAILY;X-WHATEVER=blah", // fails on old parser + //"freq=daily;wkst=su", // fails on old parser + }; + + /** The parser must reject these. */ + private static final String[] BAD_RRULES = { + "INTERVAL=4;FREQ=YEARLY", // FREQ must come first + "FREQ=MONTHLY;FREQ=MONTHLY", // can't specify twice + "FREQ=MONTHLY;COUNT=1;COUNT=1", // can't specify twice + "FREQ=SECONDLY;BYSECOND=60", // range + "FREQ=MINUTELY;BYMINUTE=-1", // range + "FREQ=HOURLY;BYHOUR=24", // range + "FREQ=YEARLY;BYMONTHDAY=0", // zero not valid + //"FREQ=YEARLY;COUNT=1;UNTIL=12345", // can't have both COUNT and UNTIL + //"FREQ=DAILY;UNTIL=19970829T021400e", // invalid date + }; + + /** + * Simple test of good/bad rules. + */ + @SmallTest + public void testBasicParse() { + for (String rule : GOOD_RRULES) { + EventRecurrence recur = new EventRecurrence(); + recur.parse(rule); + } + + for (String rule : BAD_RRULES) { + EventRecurrence recur = new EventRecurrence(); + boolean didThrow = false; + + try { + recur.parse(rule); + } catch (InvalidFormatException ife) { + didThrow = true; + } + + assertTrue("Expected throw on " + rule, didThrow); + } + } +} |
