diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/calendar/alerts/AlarmManagerInterface.java | 10 | ||||
-rw-r--r-- | src/com/android/calendar/alerts/AlertActivity.java | 14 | ||||
-rw-r--r-- | src/com/android/calendar/alerts/AlertReceiver.java | 20 | ||||
-rw-r--r-- | src/com/android/calendar/alerts/AlertService.java | 67 | ||||
-rw-r--r-- | src/com/android/calendar/alerts/AlertUtils.java | 27 | ||||
-rw-r--r-- | src/com/android/calendar/alerts/SnoozeAlarmsService.java | 3 | ||||
-rw-r--r-- | src/com/android/calendar/event/EditEventView.java | 134 |
7 files changed, 212 insertions, 63 deletions
diff --git a/src/com/android/calendar/alerts/AlarmManagerInterface.java b/src/com/android/calendar/alerts/AlarmManagerInterface.java new file mode 100644 index 00000000..5ee83734 --- /dev/null +++ b/src/com/android/calendar/alerts/AlarmManagerInterface.java @@ -0,0 +1,10 @@ +package com.android.calendar.alerts; + +import android.app.PendingIntent; + +/** + * AlarmManager abstracted to an interface for testability. + */ +public interface AlarmManagerInterface { + public void set(int type, long triggerAtMillis, PendingIntent operation); +} diff --git a/src/com/android/calendar/alerts/AlertActivity.java b/src/com/android/calendar/alerts/AlertActivity.java index d22408fd..12d7b10a 100644 --- a/src/com/android/calendar/alerts/AlertActivity.java +++ b/src/com/android/calendar/alerts/AlertActivity.java @@ -124,20 +124,6 @@ public class AlertActivity extends Activity implements OnClickListener { } @Override - protected void onInsertComplete(int token, Object cookie, Uri uri) { - if (uri != null) { - Long alarmTime = (Long) cookie; - - if (alarmTime != 0) { - // Set a new alarm to go off after the snooze delay. - // TODO make provider schedule this automatically when - // inserting an alarm - AlertUtils.scheduleAlarm(AlertActivity.this, null, alarmTime); - } - } - } - - @Override protected void onUpdateComplete(int token, Object cookie, int result) { // Ignore } diff --git a/src/com/android/calendar/alerts/AlertReceiver.java b/src/com/android/calendar/alerts/AlertReceiver.java index 757f0ed4..c837ffe4 100644 --- a/src/com/android/calendar/alerts/AlertReceiver.java +++ b/src/com/android/calendar/alerts/AlertReceiver.java @@ -219,16 +219,16 @@ public class AlertReceiver extends BroadcastReceiver { public static NotificationWrapper makeBasicNotification(Context context, String title, String summaryText, long startMillis, long endMillis, long eventId, - int notificationId, boolean doPopup) { + int notificationId, boolean doPopup, int priority) { Notification n = buildBasicNotification(new Notification.Builder(context), context, title, summaryText, startMillis, endMillis, eventId, notificationId, - doPopup, false, false); + doPopup, priority, false); return new NotificationWrapper(n, notificationId, eventId, startMillis, endMillis, doPopup); } private static Notification buildBasicNotification(Notification.Builder notificationBuilder, Context context, String title, String summaryText, long startMillis, long endMillis, - long eventId, int notificationId, boolean doPopup, boolean highPriority, + long eventId, int notificationId, boolean doPopup, int priority, boolean addActionButtons) { Resources resources = context.getResources(); if (title == null || title.length() == 0) { @@ -258,13 +258,9 @@ public class AlertReceiver extends BroadcastReceiver { notificationBuilder.setWhen(0); if (Utils.isJellybeanOrLater()) { - // Setting to a higher priority will encourage notification manager to expand the - // notification. - if (highPriority) { - notificationBuilder.setPriority(Notification.PRIORITY_HIGH); - } else { - notificationBuilder.setPriority(Notification.PRIORITY_DEFAULT); - } + // Should be one of the values in Notification (ie. Notification.PRIORITY_HIGH, etc). + // A higher priority will encourage notification manager to expand it. + notificationBuilder.setPriority(priority); } if (addActionButtons) { @@ -313,11 +309,11 @@ public class AlertReceiver extends BroadcastReceiver { */ public static NotificationWrapper makeExpandingNotification(Context context, String title, String summaryText, String description, long startMillis, long endMillis, long eventId, - int notificationId, boolean doPopup, boolean highPriority) { + int notificationId, boolean doPopup, int priority) { Notification.Builder basicBuilder = new Notification.Builder(context); Notification notification = buildBasicNotification(basicBuilder, context, title, summaryText, startMillis, endMillis, eventId, notificationId, doPopup, - highPriority, true); + priority, true); if (Utils.isJellybeanOrLater()) { // Create a new-style expanded notification Notification.BigTextStyle expandedBuilder = new Notification.BigTextStyle( diff --git a/src/com/android/calendar/alerts/AlertService.java b/src/com/android/calendar/alerts/AlertService.java index ac1ca26d..9811dabf 100644 --- a/src/com/android/calendar/alerts/AlertService.java +++ b/src/com/android/calendar/alerts/AlertService.java @@ -16,7 +16,6 @@ package com.android.calendar.alerts; -import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.Service; @@ -233,12 +232,13 @@ public class AlertService extends Service { return false; } - return generateAlerts(context, nm, prefs, alertCursor, currentTime, MAX_NOTIFICATIONS); + return generateAlerts(context, nm, AlertUtils.createAlarmManager(context), prefs, + alertCursor, currentTime, MAX_NOTIFICATIONS); } public static boolean generateAlerts(Context context, NotificationMgr nm, - SharedPreferences prefs, Cursor alertCursor, final long currentTime, - final int maxNotifications) { + AlarmManagerInterface alarmMgr, SharedPreferences prefs, Cursor alertCursor, + final long currentTime, final int maxNotifications) { if (DEBUG) { Log.d(TAG, "alertCursor count:" + alertCursor.getCount()); } @@ -311,7 +311,8 @@ public class AlertService extends Service { info.allDay, info.location); notification = AlertReceiver.makeBasicNotification(context, info.eventName, summaryText, info.startMillis, info.endMillis, info.eventId, - AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, false); + AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, false, + Notification.PRIORITY_MIN); } else { // Multiple expired events are listed in a digest. notification = AlertReceiver.makeDigestNotification(context, @@ -349,7 +350,7 @@ public class AlertService extends Service { // Schedule the next silent refresh time so notifications will change // buckets (eg. drop into expired digest, etc). if (nextRefreshTime < Long.MAX_VALUE && nextRefreshTime > currentTime) { - AlertUtils.scheduleNextNotificationRefresh(context, null, nextRefreshTime); + AlertUtils.scheduleNextNotificationRefresh(context, alarmMgr, nextRefreshTime); if (DEBUG) { long minutesBeforeRefresh = (nextRefreshTime - currentTime) / MINUTE_MS; Time time = new Time(); @@ -441,18 +442,27 @@ public class AlertService extends Service { } private static long getNextRefreshTime(NotificationInfo info, long currentTime) { - // We change an event's priority bucket at 15 minutes into the event (so recently started - // concurrent events stay high priority) + long startAdjustedForAllDay = info.startMillis; + long endAdjustedForAllDay = info.endMillis; + if (info.allDay) { + Time t = new Time(); + startAdjustedForAllDay = Utils.convertAlldayUtcToLocal(t, info.startMillis, + Time.getCurrentTimezone()); + endAdjustedForAllDay = Utils.convertAlldayUtcToLocal(t, info.startMillis, + Time.getCurrentTimezone()); + } + + // We change an event's priority bucket at 15 minutes into the event or 1/4 event duration. long nextRefreshTime = Long.MAX_VALUE; - long gracePeriodCutoff = info.startMillis + - getGracePeriodMs(info.startMillis, info.endMillis); + long gracePeriodCutoff = startAdjustedForAllDay + + getGracePeriodMs(startAdjustedForAllDay, endAdjustedForAllDay, info.allDay); if (gracePeriodCutoff > currentTime) { nextRefreshTime = Math.min(nextRefreshTime, gracePeriodCutoff); } // ... and at the end (so expiring ones drop into a digest). - if (info.endMillis > currentTime && info.endMillis > gracePeriodCutoff) { - nextRefreshTime = Math.min(nextRefreshTime, info.endMillis); + if (endAdjustedForAllDay > currentTime && endAdjustedForAllDay > gracePeriodCutoff) { + nextRefreshTime = Math.min(nextRefreshTime, endAdjustedForAllDay); } return nextRefreshTime; } @@ -595,16 +605,12 @@ public class AlertService extends Service { // Adjust for all day events to ensure the right bucket. Don't use the 1/4 event // duration grace period for these. - long gracePeriodMs; long beginTimeAdjustedForAllDay = beginTime; String tz = null; if (allDay) { tz = TimeZone.getDefault().getID(); beginTimeAdjustedForAllDay = Utils.convertAlldayUtcToLocal(null, beginTime, tz); - gracePeriodMs = MIN_DEPRIORITIZE_GRACE_PERIOD_MS; - } else { - gracePeriodMs = getGracePeriodMs(beginTime, endTime); } // Handle multiple alerts for the same event ID. @@ -653,7 +659,8 @@ public class AlertService extends Service { // TODO: Prioritize by "primary" calendar eventIds.put(eventId, newInfo); - long highPriorityCutoff = currentTime - gracePeriodMs; + long highPriorityCutoff = currentTime - + getGracePeriodMs(beginTime, endTime, allDay); if (beginTimeAdjustedForAllDay > highPriorityCutoff) { // High priority = future events or events that just started @@ -676,8 +683,14 @@ public class AlertService extends Service { /** * High priority cutoff should be 1/4 event duration or 15 min, whichever is longer. */ - private static long getGracePeriodMs(long beginTime, long endTime) { - return Math.max(MIN_DEPRIORITIZE_GRACE_PERIOD_MS, ((endTime - beginTime) / 4)); + private static long getGracePeriodMs(long beginTime, long endTime, boolean allDay) { + if (allDay) { + // We don't want all day events to be high priority for hours, so automatically + // demote these after 15 min. + return MIN_DEPRIORITIZE_GRACE_PERIOD_MS; + } else { + return Math.max(MIN_DEPRIORITIZE_GRACE_PERIOD_MS, ((endTime - beginTime) / 4)); + } } private static String getDigestTitle(ArrayList<NotificationInfo> events) { @@ -696,11 +709,15 @@ public class AlertService extends Service { private static void postNotification(NotificationInfo info, String summaryText, Context context, boolean highPriority, NotificationPrefs prefs, NotificationMgr notificationMgr, int notificationId) { + int priorityVal = Notification.PRIORITY_DEFAULT; + if (highPriority) { + priorityVal = Notification.PRIORITY_HIGH; + } + String tickerText = getTickerText(info.eventName, info.location); NotificationWrapper notification = AlertReceiver.makeExpandingNotification(context, info.eventName, summaryText, info.description, info.startMillis, - info.endMillis, info.eventId, notificationId, prefs.getDoPopup(), - highPriority); + info.endMillis, info.eventId, notificationId, prefs.getDoPopup(), priorityVal); boolean quietUpdate = true; String ringtone = NotificationPrefs.EMPTY_RINGTONE; @@ -868,10 +885,8 @@ public class AlertService extends Service { private void doTimeChanged() { ContentResolver cr = getContentResolver(); - Object service = getSystemService(Context.ALARM_SERVICE); - AlarmManager manager = (AlarmManager) service; // TODO Move this into Provider - rescheduleMissedAlarms(cr, this, manager); + rescheduleMissedAlarms(cr, this, AlertUtils.createAlarmManager(this)); updateAlertNotification(this); } @@ -900,8 +915,8 @@ public class AlertService extends Service { * @param context the Context * @param manager the AlarmManager */ - public static final void rescheduleMissedAlarms(ContentResolver cr, Context context, - AlarmManager manager) { + private static final void rescheduleMissedAlarms(ContentResolver cr, Context context, + AlarmManagerInterface manager) { // Get all the alerts that have been scheduled but have not fired // and should have fired by now and are not too old. long now = System.currentTimeMillis(); diff --git a/src/com/android/calendar/alerts/AlertUtils.java b/src/com/android/calendar/alerts/AlertUtils.java index 9f160c6b..f68f5224 100644 --- a/src/com/android/calendar/alerts/AlertUtils.java +++ b/src/com/android/calendar/alerts/AlertUtils.java @@ -80,6 +80,20 @@ public class AlertUtils { private static final int FLUSH_INTERVAL_MS = FLUSH_INTERVAL_DAYS * 24 * 60 * 60 * 1000; /** + * Creates an AlarmManagerInterface that wraps a real AlarmManager. The alarm code + * was abstracted to an interface to make it testable. + */ + public static AlarmManagerInterface createAlarmManager(Context context) { + final AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + return new AlarmManagerInterface() { + @Override + public void set(int type, long triggerAtMillis, PendingIntent operation) { + mgr.set(type, triggerAtMillis, operation); + } + }; + } + + /** * Schedules an alarm intent with the system AlarmManager that will notify * listeners when a reminder should be fired. The provider will keep * scheduled reminders up to date but apps may use this to implement snooze @@ -90,7 +104,8 @@ public class AlertUtils { * @param manager The AlarmManager to use or null * @param alarmTime The time to fire the intent in UTC millis since epoch */ - public static void scheduleAlarm(Context context, AlarmManager manager, long alarmTime) { + public static void scheduleAlarm(Context context, AlarmManagerInterface manager, + long alarmTime) { scheduleAlarmHelper(context, manager, alarmTime, false); } @@ -98,17 +113,13 @@ public class AlertUtils { * Schedules the next alarm to silently refresh the notifications. Note that if there * is a pending silent refresh alarm, it will be replaced with this one. */ - static void scheduleNextNotificationRefresh(Context context, AlarmManager manager, + static void scheduleNextNotificationRefresh(Context context, AlarmManagerInterface manager, long alarmTime) { scheduleAlarmHelper(context, manager, alarmTime, true); } - private static void scheduleAlarmHelper(Context context, AlarmManager manager, long alarmTime, - boolean quietUpdate) { - if (manager == null) { - manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - } - + private static void scheduleAlarmHelper(Context context, AlarmManagerInterface manager, + long alarmTime, boolean quietUpdate) { int alarmType = AlarmManager.RTC_WAKEUP; Intent intent = new Intent(CalendarContract.ACTION_EVENT_REMINDER); intent.setClass(context, AlertReceiver.class); diff --git a/src/com/android/calendar/alerts/SnoozeAlarmsService.java b/src/com/android/calendar/alerts/SnoozeAlarmsService.java index 2eac1a71..6ef983d5 100644 --- a/src/com/android/calendar/alerts/SnoozeAlarmsService.java +++ b/src/com/android/calendar/alerts/SnoozeAlarmsService.java @@ -80,7 +80,8 @@ public class SnoozeAlarmsService extends IntentService { ContentValues values = AlertUtils.makeContentValues(eventId, eventStart, eventEnd, alarmTime, 0); resolver.insert(uri, values); - AlertUtils.scheduleAlarm(SnoozeAlarmsService.this, null, alarmTime); + AlertUtils.scheduleAlarm(SnoozeAlarmsService.this, AlertUtils.createAlarmManager(this), + alarmTime); } AlertService.updateAlertNotification(this); stopSelf(); diff --git a/src/com/android/calendar/event/EditEventView.java b/src/com/android/calendar/event/EditEventView.java index c6f49542..928281ff 100644 --- a/src/com/android/calendar/event/EditEventView.java +++ b/src/com/android/calendar/event/EditEventView.java @@ -24,12 +24,14 @@ import android.app.ProgressDialog; import android.app.Service; import android.app.TimePickerDialog; import android.app.TimePickerDialog.OnTimeSetListener; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; +import android.database.MatrixCursor; import android.graphics.drawable.Drawable; import android.provider.CalendarContract.Attendees; import android.provider.CalendarContract.Calendars; @@ -43,14 +45,17 @@ import android.text.format.DateUtils; import android.text.format.Time; import android.text.util.Rfc822Tokenizer; import android.util.Log; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.CalendarView; import android.widget.CheckBox; @@ -64,6 +69,7 @@ import android.widget.ResourceCursorAdapter; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; import android.widget.TimePicker; import com.android.calendar.CalendarEventModel; @@ -93,6 +99,7 @@ import java.util.Formatter; import java.util.HashMap; import java.util.Locale; import java.util.TimeZone; +import java.util.TreeMap; public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener, DialogInterface.OnClickListener, OnItemSelectedListener { @@ -100,6 +107,16 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa private static final String GOOGLE_SECONDARY_CALENDAR = "calendar.google.com"; private static final String PERIOD_SPACE = ". "; + // Constants used for title autocompletion. + private static final String[] EVENT_PROJECTION = new String[] { + Events._ID, + Events.TITLE, + }; + private static final int EVENT_INDEX_ID = 0; + private static final int EVENT_INDEX_TITLE = 1; + private static final String TITLE_WHERE = Events.TITLE + " LIKE ?"; + private static final int MAX_TITLE_SUGGESTIONS = 4; + ArrayList<View> mEditOnlyList = new ArrayList<View>(); ArrayList<View> mEditViewList = new ArrayList<View>(); ArrayList<View> mViewOnlyList = new ArrayList<View>(); @@ -121,7 +138,7 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa Spinner mAvailabilitySpinner; Spinner mAccessLevelSpinner; RadioGroup mResponseRadioGroup; - TextView mTitleTextView; + AutoCompleteTextView mTitleTextView; TextView mLocationTextView; TextView mDescriptionTextView; TextView mWhenView; @@ -627,6 +644,106 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa } /** + * Adapter for title auto completion. + */ + private static class TitleAdapter extends ResourceCursorAdapter { + private final ContentResolver mContentResolver; + + public TitleAdapter(Context context) { + super(context, android.R.layout.simple_dropdown_item_1line, null, 0); + mContentResolver = context.getContentResolver(); + } + + @Override + public int getCount() { + return Math.min(MAX_TITLE_SUGGESTIONS, super.getCount()); + } + + private static String getTitleAtCursor(Cursor cursor) { + return cursor.getString(EVENT_INDEX_TITLE); + } + + @Override + public final String convertToString(Cursor cursor) { + return getTitleAtCursor(cursor); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView textView = (TextView) view; + textView.setText(getTitleAtCursor(cursor)); + } + + @Override + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + String filter = constraint == null ? "" : constraint.toString() + "%"; + if (filter.isEmpty()) { + return null; + } + long startTime = System.currentTimeMillis(); + + // Query all titles prefixed with the constraint. There is no way to insert + // 'DISTINCT' or 'GROUP BY' to get rid of dupes, so use post-processing to + // remove dupes. We will order query results by descending event ID to show + // results that were most recently inputted. + Cursor tempCursor = mContentResolver.query(Events.CONTENT_URI, EVENT_PROJECTION, + TITLE_WHERE, new String[] { filter }, Events._ID + " DESC"); + if (tempCursor != null) { + try { + // Post process query results. + Cursor c = uniqueTitlesCursor(tempCursor); + + // Log the processing duration. + long duration = System.currentTimeMillis() - startTime; + StringBuilder msg = new StringBuilder(); + msg.append("Autocomplete of "); + msg.append(constraint); + msg.append(": title query match took "); + msg.append(duration); + msg.append("ms."); + Log.d(TAG, msg.toString()); + return c; + } finally { + tempCursor.close(); + } + } else { + return null; + } + } + + /** + * Post-process the query results to return the first MAX_TITLE_SUGGESTIONS + * unique titles in alphabetical order. + */ + private Cursor uniqueTitlesCursor(Cursor cursor) { + TreeMap<String, String[]> titleToQueryResults = + new TreeMap<String, String[]>(String.CASE_INSENSITIVE_ORDER); + int numColumns = cursor.getColumnCount(); + cursor.moveToPosition(-1); + + // Remove dupes. + while ((titleToQueryResults.size() < MAX_TITLE_SUGGESTIONS) && cursor.moveToNext()) { + String title = getTitleAtCursor(cursor).trim(); + String data[] = new String[numColumns]; + if (!titleToQueryResults.containsKey(title)) { + for (int i = 0; i < numColumns; i++) { + data[i] = cursor.getString(i); + } + titleToQueryResults.put(title, data); + } + } + + // Copy the sorted results to a new cursor. + MatrixCursor newCursor = new MatrixCursor(EVENT_PROJECTION); + for (String[] result : titleToQueryResults.values()) { + newCursor.addRow(result); + } + newCursor.moveToFirst(); + return newCursor; + } + } + + /** * Does prep steps for saving a calendar event. * * This triggers a parse of the attendees list and checks if the event is @@ -829,7 +946,7 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa // cache all the widgets mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars_spinner); - mTitleTextView = (TextView) view.findViewById(R.id.title); + mTitleTextView = (AutoCompleteTextView) view.findViewById(R.id.title); mLocationTextView = (TextView) view.findViewById(R.id.location); mDescriptionTextView = (TextView) view.findViewById(R.id.description); mTimezoneLabel = (TextView) view.findViewById(R.id.timezone_label); @@ -863,6 +980,19 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa mAttendeesList = (MultiAutoCompleteTextView) view.findViewById(R.id.attendees); mTitleTextView.setTag(mTitleTextView.getBackground()); + mTitleTextView.setAdapter(new TitleAdapter(activity)); + mTitleTextView.setOnEditorActionListener(new OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + // Dismiss the suggestions dropdown. Return false so the other + // side effects still occur (soft keyboard going away, etc.). + mTitleTextView.dismissDropDown(); + } + return false; + } + }); + mLocationTextView.setTag(mLocationTextView.getBackground()); mDescriptionTextView.setTag(mDescriptionTextView.getBackground()); mRepeatsSpinner.setTag(mRepeatsSpinner.getBackground()); |