summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/java/android/pim/EventRecurrence.java650
-rw-r--r--core/java/android/provider/Settings.java15
-rw-r--r--core/java/android/view/InputEventConsistencyVerifier.java77
-rw-r--r--core/java/android/view/View.java210
-rw-r--r--core/java/android/view/ViewGroup.java341
-rw-r--r--core/java/android/view/accessibility/AccessibilityNodeInfo.java5
-rw-r--r--core/java/android/webkit/WebView.java4
-rw-r--r--core/java/android/widget/GridLayout.java50
-rw-r--r--core/java/android/widget/PopupMenu.java45
-rw-r--r--core/java/android/widget/TextView.java9
-rw-r--r--core/java/com/android/internal/view/menu/ActionMenuPresenter.java1
-rw-r--r--core/java/com/android/internal/view/menu/ListMenuPresenter.java4
-rw-r--r--core/java/com/android/internal/view/menu/MenuItemImpl.java2
-rw-r--r--core/java/com/android/internal/view/menu/MenuPopupHelper.java1
-rw-r--r--core/java/com/android/internal/widget/LockPatternView.java3
-rw-r--r--core/jni/android_pim_EventRecurrence.cpp2
-rw-r--r--core/res/res/drawable-hdpi/ic_notification_ime_default.pngbin0 -> 749 bytes
-rw-r--r--core/res/res/drawable-mdpi/ic_notification_ime_default.pngbin0 -> 749 bytes
-rw-r--r--core/res/res/layout/keyguard_screen_unlock_portrait.xml9
-rw-r--r--core/res/res/values-sw600dp/bools.xml1
-rw-r--r--core/res/res/values/bools.xml1
-rw-r--r--core/tests/coretests/src/android/pim/EventRecurrenceTest.java753
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
new file mode 100644
index 0000000..1a9d88c
--- /dev/null
+++ b/core/res/res/drawable-hdpi/ic_notification_ime_default.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/ic_notification_ime_default.png b/core/res/res/drawable-mdpi/ic_notification_ime_default.png
new file mode 100644
index 0000000..1a9d88c
--- /dev/null
+++ b/core/res/res/drawable-mdpi/ic_notification_ime_default.png
Binary files differ
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);
+ }
+ }
+}