Merge "Import translations. DO NOT MERGE" into jb-mr2-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2b0144e..c9f3a2f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -38,13 +38,12 @@
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.mail" />
-    <uses-sdk android:minSdkVersion="15" android:targetSdkVersion="18"></uses-sdk>
+    <uses-sdk android:minSdkVersion="15" android:targetSdkVersion="17"></uses-sdk>
 
     <application android:name="CalendarApplication"
             android:label="@string/app_label" android:icon="@mipmap/ic_launcher_calendar"
             android:taskAffinity="android.task.calendar"
             android:hardwareAccelerated="true"
-            android:requiredAccountType="*"
             android:backupAgent="com.android.calendar.CalendarBackupAgent" >
 
         <meta-data android:name="com.google.android.backup.api_key"
@@ -186,6 +185,9 @@
             </intent-filter>
         </receiver>
 
+        <receiver android:name=".alerts.GlobalDismissManager"
+                  android:exported="false" />
+
         <service android:name=".alerts.AlertService" />
 
         <service android:name=".alerts.DismissAlarmsService" />
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index f4a78a5..b14854a 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -244,4 +244,66 @@
         <item >REPEAT MONTHLY</item>
         <item >REPEAT YEARLY</item>
     </string-array>
+
+    <!-- The following sets of strings describe a monthly recurring event, which will repeat
+         on the Nth WEEKDAY of every month. For example, the 3rd Monday of every month, or
+         the last Sunday. These are set up like this to resolve any gender-matching issues
+         that were present in some languages.
+     -->
+    <!-- Repeat a monthly event on the same nth day of every Sunday. [CHAR LIMIT=30] -->
+    <string-array name="repeat_by_nth_sun">
+        <item >on every first Sunday</item>
+        <item >on every second Sunday</item>
+        <item >on every third Sunday</item>
+        <item >on every fourth Sunday</item>
+        <item >on every last Sunday</item>
+    </string-array>
+    <!-- Repeat a monthly event on the same nth day of every Monday. [CHAR LIMIT=30] -->
+    <string-array name="repeat_by_nth_mon">
+        <item >on every first Monday</item>
+        <item >on every second Monday</item>
+        <item >on every third Monday</item>
+        <item >on every fourth Monday</item>
+        <item >on every last Monday</item>
+    </string-array>
+    <!-- Repeat a monthly event on the same nth day of every Tuesday. [CHAR LIMIT=30] -->
+    <string-array name="repeat_by_nth_tues">
+        <item >on every first Tuesday</item>
+        <item >on every second Tuesday</item>
+        <item >on every third Tuesday</item>
+        <item >on every fourth Tuesday</item>
+        <item >on every last Tuesday</item>
+    </string-array>
+    <!-- Repeat a monthly event on the same nth day of every Wednesday. [CHAR LIMIT=30] -->
+    <string-array name="repeat_by_nth_wed">
+        <item >on every first Wednesday</item>
+        <item >on every second Wednesday</item>
+        <item >on every third Wednesday</item>
+        <item >on every fourth Wednesday</item>
+        <item >on every last Wednesday</item>
+    </string-array>
+    <!-- Repeat a monthly event on the same nth day of every Thursday. [CHAR LIMIT=30] -->
+    <string-array name="repeat_by_nth_thurs">
+        <item >on every first Thursday</item>
+        <item >on every second Thursday</item>
+        <item >on every third Thursday</item>
+        <item >on every fourth Thursday</item>
+        <item >on every last Thursday</item>
+    </string-array>
+    <!-- Repeat a monthly event on the same nth day of every Friday. [CHAR LIMIT=30] -->
+    <string-array name="repeat_by_nth_fri">
+        <item >on every first Friday</item>
+        <item >on every second Friday</item>
+        <item >on every third Friday</item>
+        <item >on every fourth Friday</item>
+        <item >on every last Friday</item>
+    </string-array>
+    <!-- Repeat a monthly event on the same nth day of every Saturday. [CHAR LIMIT=30] -->
+    <string-array name="repeat_by_nth_sat">
+        <item >on every first Saturday</item>
+        <item >on every second Saturday</item>
+        <item >on every third Saturday</item>
+        <item >on every fourth Saturday</item>
+        <item >on every last Saturday</item>
+    </string-array>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 956cdd4..5a71aae 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -429,10 +429,6 @@
         <item quantity="other">Every <xliff:g id="number">%1$d</xliff:g> weeks on <xliff:g id="days_of_week">%2$s</xliff:g></item>
     </plurals>
 
-    <!-- Example: 'Monthly (every first Sunday)' -->
-    <!--   1st parameter is an ordinal number, like 'first' -->
-    <!--   2nd parameter is a day of the week, like 'Sunday' -->
-    <string name="monthly_on_day_count">"Monthly (every <xliff:g id="ordinal_number">%1$s</xliff:g> <xliff:g id="day_of_week">%2$s</xliff:g>)"</string>
     <!-- The common portion of a string describing how often an event repeats,
          example: 'Monthly (on day 2)' -->
     <string name="monthly">Monthly</string>
@@ -721,9 +717,6 @@
 
     <!-- Repeat an monthly event on the same day of every month [CHAR LIMIT=20] -->
     <string name="recurrence_month_pattern_by_day">on the same day each month</string>
-    <!-- Repeat an monthly event on the same nth day of the week of every month.
-         For example, on every second Tuesday [CHAR LIMIT=30] -->
-    <string name="recurrence_month_pattern_by_day_of_week">on every <xliff:g id="nth">%1$s</xliff:g> <xliff:g id="day_of_week">%2$s</xliff:g></string>
 
     <!-- Specifies that a repeating event to repeat forever (based on the defined frequency) instead of ending at a future date[CHAR LIMIT=25] -->
     <string name="recurrence_end_continously">Forever</string>
@@ -743,4 +736,6 @@
     <!-- Description of the selected marker for accessibility support [CHAR LIMIT = NONE]-->
     <string name="acessibility_recurrence_choose_end_date_description">change end date</string>
 
+    <!-- Do Not Translate.  Sender identity for global notification synchronization. -->
+    <string name="notification_sender_id"></string>
 </resources>
diff --git a/src/com/android/calendar/CloudNotificationBackplane.java b/src/com/android/calendar/CloudNotificationBackplane.java
new file mode 100644
index 0000000..d9ff0cc
--- /dev/null
+++ b/src/com/android/calendar/CloudNotificationBackplane.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 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 com.android.calendar;
+
+import java.io.IOException;
+
+import android.content.Context;
+import android.os.Bundle;
+
+public interface CloudNotificationBackplane {
+    public boolean open(Context context);
+    public boolean subscribeToGroup(String senderId, String account, String groupId)
+            throws IOException;
+    public void send(String to, String msgId, Bundle data) throws IOException;
+    public void close();
+}
diff --git a/src/com/android/calendar/EventInfoFragment.java b/src/com/android/calendar/EventInfoFragment.java
index a45c1be..613ba6b 100644
--- a/src/com/android/calendar/EventInfoFragment.java
+++ b/src/com/android/calendar/EventInfoFragment.java
@@ -846,6 +846,7 @@
 
         if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) {
             mEditResponseHelper.setWhichEvents(UPDATE_ALL);
+            mWhichEvents = mEditResponseHelper.getWhichEvents();
         }
         mHandler = new QueryHandler(activity);
         if (!mIsDialog) {
@@ -1338,6 +1339,9 @@
             return true;
         }
 
+        if (DEBUG) {
+            Log.d(TAG, "Repeating event: mWhichEvents=" + mWhichEvents);
+        }
         // This is a repeating event
         switch (mWhichEvents) {
             case -1:
diff --git a/src/com/android/calendar/EventRecurrenceFormatter.java b/src/com/android/calendar/EventRecurrenceFormatter.java
index 1d3df70..b9e33fd 100644
--- a/src/com/android/calendar/EventRecurrenceFormatter.java
+++ b/src/com/android/calendar/EventRecurrenceFormatter.java
@@ -28,6 +28,10 @@
 
 public class EventRecurrenceFormatter
 {
+
+    private static int[] mMonthRepeatByDayOfWeekIds;
+    private static String[][] mMonthRepeatByDayOfWeekStrs;
+
     public static String getRepeatString(Context context, Resources r, EventRecurrence recurrence,
             boolean includeEndString) {
         String endString = "";
@@ -99,11 +103,17 @@
             }
             case EventRecurrence.MONTHLY: {
                 if (recurrence.bydayCount == 1) {
-                    String[] ordinals = r.getStringArray(R.array.ordinal_labels);
+                    int weekday = recurrence.startDate.weekDay;
+                    // Cache this stuff so we won't have to redo work again later.
+                    cacheMonthRepeatStrings(r, weekday);
                     int dayNumber = (recurrence.startDate.monthDay - 1) / 7;
-                    int day = EventRecurrence.timeDay2Day(recurrence.startDate.weekDay);
-                    return r.getString(R.string.monthly_on_day_count, ordinals[dayNumber],
-                            dayToString(day, DateUtils.LENGTH_LONG)) + endString;
+                    StringBuilder sb = new StringBuilder();
+                    sb.append(r.getString(R.string.monthly));
+                    sb.append(" (");
+                    sb.append(mMonthRepeatByDayOfWeekStrs[weekday][dayNumber]);
+                    sb.append(")");
+                    sb.append(endString);
+                    return sb.toString();
                 }
                 return r.getString(R.string.monthly) + endString;
             }
@@ -114,6 +124,26 @@
         return null;
     }
 
+    private static void cacheMonthRepeatStrings(Resources r, int weekday) {
+        if (mMonthRepeatByDayOfWeekIds == null) {
+            mMonthRepeatByDayOfWeekIds = new int[7];
+            mMonthRepeatByDayOfWeekIds[0] = R.array.repeat_by_nth_sun;
+            mMonthRepeatByDayOfWeekIds[1] = R.array.repeat_by_nth_mon;
+            mMonthRepeatByDayOfWeekIds[2] = R.array.repeat_by_nth_tues;
+            mMonthRepeatByDayOfWeekIds[3] = R.array.repeat_by_nth_wed;
+            mMonthRepeatByDayOfWeekIds[4] = R.array.repeat_by_nth_thurs;
+            mMonthRepeatByDayOfWeekIds[5] = R.array.repeat_by_nth_fri;
+            mMonthRepeatByDayOfWeekIds[6] = R.array.repeat_by_nth_sat;
+        }
+        if (mMonthRepeatByDayOfWeekStrs == null) {
+            mMonthRepeatByDayOfWeekStrs = new String[7][];
+        }
+        if (mMonthRepeatByDayOfWeekStrs[weekday] == null) {
+            mMonthRepeatByDayOfWeekStrs[weekday] =
+                    r.getStringArray(mMonthRepeatByDayOfWeekIds[weekday]);
+        }
+    }
+
     /**
      * Converts day of week to a String.
      * @param day a EventRecurrence constant
diff --git a/src/com/android/calendar/ExtensionsFactory.java b/src/com/android/calendar/ExtensionsFactory.java
index aaf7b01..c323e16 100644
--- a/src/com/android/calendar/ExtensionsFactory.java
+++ b/src/com/android/calendar/ExtensionsFactory.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.res.AssetManager;
+import android.os.Bundle;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -27,10 +28,12 @@
 import java.io.InputStream;
 import java.util.Properties;
 
+
 /*
  * Skeleton for additional options in the AllInOne menu.
  */
 public class ExtensionsFactory {
+
     private static String TAG = "ExtensionsFactory";
 
     // Config filename for mappings of various class names to their custom
@@ -38,6 +41,7 @@
     private static String EXTENSIONS_PROPERTIES = "calendar_extensions.properties";
 
     private static String ALL_IN_ONE_MENU_KEY = "AllInOneMenuExtensions";
+    private static String CLOUD_NOTIFICATION_KEY = "CloudNotificationChannel";
 
     private static Properties sProperties = new Properties();
     private static AllInOneMenuExtensionsInterface sAllInOneMenuExtensions = null;
@@ -95,4 +99,39 @@
 
         return sAllInOneMenuExtensions;
     }
+
+    public static CloudNotificationBackplane getCloudNotificationBackplane() {
+        CloudNotificationBackplane cnb = null;
+
+        String className = sProperties.getProperty(CLOUD_NOTIFICATION_KEY);
+        if (className != null) {
+            cnb = createInstance(className);
+        } else {
+            Log.d(TAG, CLOUD_NOTIFICATION_KEY + " not found in properties file.");
+        }
+
+        if (cnb == null) {
+            cnb = new CloudNotificationBackplane() {
+                @Override
+                public boolean open(Context context) {
+                    return true;
+                }
+
+                @Override
+                public boolean subscribeToGroup(String senderId, String account, String groupId)
+                        throws IOException {
+                    return true;}
+
+                @Override
+                public void send(String to, String msgId, Bundle data) {
+                }
+
+                @Override
+                public void close() {
+                }
+            };
+        }
+
+        return cnb;
+    }
 }
diff --git a/src/com/android/calendar/alerts/AlertReceiver.java b/src/com/android/calendar/alerts/AlertReceiver.java
index e9822b1..e1f7dec 100644
--- a/src/com/android/calendar/alerts/AlertReceiver.java
+++ b/src/com/android/calendar/alerts/AlertReceiver.java
@@ -105,9 +105,7 @@
         }
         if (DELETE_ALL_ACTION.equals(intent.getAction())) {
 
-            /* The user has clicked the "Clear All Notifications"
-             * buttons so dismiss all Calendar alerts.
-             */
+            // The user has dismissed a digest notification.
             // TODO Grab a wake lock here?
             Intent serviceIntent = new Intent(context, DismissAlarmsService.class);
             context.startService(serviceIntent);
@@ -466,8 +464,10 @@
         Resources res = context.getResources();
         int numEvents = notificationInfos.size();
         long[] eventIds = new long[notificationInfos.size()];
+        long[] startMillis = new long[notificationInfos.size()];
         for (int i = 0; i < notificationInfos.size(); i++) {
             eventIds[i] = notificationInfos.get(i).eventId;
+            startMillis[i] = notificationInfos.get(i).startMillis;
         }
 
         // Create an intent triggered by clicking on the status icon that shows the alerts list.
@@ -479,6 +479,7 @@
         deleteIntent.setClass(context, DismissAlarmsService.class);
         deleteIntent.setAction(DELETE_ALL_ACTION);
         deleteIntent.putExtra(AlertUtils.EVENT_IDS_KEY, eventIds);
+        deleteIntent.putExtra(AlertUtils.EVENT_STARTS_KEY, startMillis);
         PendingIntent pendingDeleteIntent = PendingIntent.getService(context, 0, deleteIntent,
                 PendingIntent.FLAG_UPDATE_CURRENT);
 
diff --git a/src/com/android/calendar/alerts/AlertService.java b/src/com/android/calendar/alerts/AlertService.java
index 046af04..fbb5ad1 100644
--- a/src/com/android/calendar/alerts/AlertService.java
+++ b/src/com/android/calendar/alerts/AlertService.java
@@ -814,6 +814,8 @@
                     lowPriorityEvents.add(newInfo);
                 }
             }
+            // TODO(cwren) add beginTime/startTime
+            GlobalDismissManager.processEventIds(context, eventIds.keySet());
         } finally {
             if (alertCursor != null) {
                 alertCursor.close();
diff --git a/src/com/android/calendar/alerts/AlertUtils.java b/src/com/android/calendar/alerts/AlertUtils.java
index 766d8e4..a9a74ee 100644
--- a/src/com/android/calendar/alerts/AlertUtils.java
+++ b/src/com/android/calendar/alerts/AlertUtils.java
@@ -56,6 +56,7 @@
     public static final String EVENT_END_KEY = "eventend";
     public static final String NOTIFICATION_ID_KEY = "notificationid";
     public static final String EVENT_IDS_KEY = "eventids";
+    public static final String EVENT_STARTS_KEY = "starts";
 
     // A flag for using local storage to save alert state instead of the alerts DB table.
     // This allows the unbundled app to run alongside other calendar apps without eating
diff --git a/src/com/android/calendar/alerts/DismissAlarmsService.java b/src/com/android/calendar/alerts/DismissAlarmsService.java
index b52ffd5..d5dfaf3 100644
--- a/src/com/android/calendar/alerts/DismissAlarmsService.java
+++ b/src/com/android/calendar/alerts/DismissAlarmsService.java
@@ -28,6 +28,10 @@
 import android.support.v4.app.TaskStackBuilder;
 
 import com.android.calendar.EventInfoActivity;
+import com.android.calendar.alerts.GlobalDismissManager.AlarmId;
+
+import java.util.LinkedList;
+import java.util.List;
 
 /**
  * Service for asynchronously marking fired alarms as dismissed.
@@ -55,21 +59,31 @@
         long eventEnd = intent.getLongExtra(AlertUtils.EVENT_END_KEY, -1);
         boolean showEvent = intent.getBooleanExtra(AlertUtils.SHOW_EVENT_KEY, false);
         long[] eventIds = intent.getLongArrayExtra(AlertUtils.EVENT_IDS_KEY);
+        long[] eventStarts = intent.getLongArrayExtra(AlertUtils.EVENT_STARTS_KEY);
         int notificationId = intent.getIntExtra(AlertUtils.NOTIFICATION_ID_KEY, -1);
+        List<AlarmId> alarmIds = new LinkedList<AlarmId>();
 
         Uri uri = CalendarAlerts.CONTENT_URI;
         String selection;
 
         // Dismiss a specific fired alarm if id is present, otherwise, dismiss all alarms
         if (eventId != -1) {
+            alarmIds.add(new AlarmId(eventId, eventStart));
             selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED + " AND " +
             CalendarAlerts.EVENT_ID + "=" + eventId;
-        } else if (eventIds != null && eventIds.length > 0) {
+        } else if (eventIds != null && eventIds.length > 0 &&
+                eventStarts != null && eventIds.length == eventStarts.length) {
             selection = buildMultipleEventsQuery(eventIds);
+            for (int i = 0; i < eventIds.length; i++) {
+                alarmIds.add(new AlarmId(eventIds[i], eventStarts[i]));
+            }
         } else {
+            // NOTE: I don't believe that this ever happens.
             selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED;
         }
 
+        GlobalDismissManager.dismissGlobally(getApplicationContext(), alarmIds);
+
         ContentResolver resolver = getContentResolver();
         ContentValues values = new ContentValues();
         values.put(PROJECTION[COLUMN_INDEX_STATE], CalendarAlerts.STATE_DISMISSED);
@@ -90,9 +104,6 @@
             TaskStackBuilder.create(this)
                     .addParentStack(EventInfoActivity.class).addNextIntent(i).startActivities();
         }
-
-        // Stop this service
-        stopSelf();
     }
 
     private String buildMultipleEventsQuery(long[] eventIds) {
diff --git a/src/com/android/calendar/alerts/GlobalDismissManager.java b/src/com/android/calendar/alerts/GlobalDismissManager.java
new file mode 100644
index 0000000..fb36a3e
--- /dev/null
+++ b/src/com/android/calendar/alerts/GlobalDismissManager.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2013 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 com.android.calendar.alerts;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CalendarContract.CalendarAlerts;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.calendar.CloudNotificationBackplane;
+import com.android.calendar.ExtensionsFactory;
+import com.android.calendar.R;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utilities for managing notification dismissal across devices.
+ */
+public class GlobalDismissManager extends BroadcastReceiver {
+    private static final String TAG = "GlobalDismissManager";
+    private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
+    private static final String GLOBAL_DISMISS_MANAGER_PREFS = "com.android.calendar.alerts.GDM";
+    private static final String ACCOUNT_KEY = "known_accounts";
+    protected static final long FOUR_WEEKS = 60 * 60 * 24 * 7 * 4;
+
+    static final String[] EVENT_PROJECTION = new String[] {
+            Events._ID,
+            Events.CALENDAR_ID
+    };
+    static final String[] EVENT_SYNC_PROJECTION = new String[] {
+            Events._ID,
+            Events._SYNC_ID
+    };
+    static final String[] CALENDARS_PROJECTION = new String[] {
+            Calendars._ID,
+            Calendars.ACCOUNT_NAME,
+            Calendars.ACCOUNT_TYPE
+    };
+
+    public static final String KEY_PREFIX = "com.android.calendar.alerts.";
+    public static final String SYNC_ID = KEY_PREFIX + "sync_id";
+    public static final String START_TIME = KEY_PREFIX + "start_time";
+    public static final String ACCOUNT_NAME = KEY_PREFIX + "account_name";
+    public static final String DISMISS_INTENT = KEY_PREFIX + "DISMISS";
+
+    public static class AlarmId {
+        public long mEventId;
+        public long mStart;
+         public AlarmId(long id, long start) {
+             mEventId = id;
+             mStart = start;
+         }
+    }
+
+    /**
+     * Look for unknown accounts in a set of events and associate with them.
+     * Returns immediately, processing happens in the background.
+     * 
+     * @param context application context
+     * @param eventIds IDs for events that have posted notifications that may be
+     *            dismissed.
+     */
+    public static void processEventIds(final Context context, final Set<Long> eventIds) {
+        final String senderId = context.getResources().getString(R.string.notification_sender_id);
+        if (senderId == null || senderId.isEmpty()) {
+            Log.i(TAG, "no sender configured");
+            return;
+        }
+        new AsyncTask<Void, Void, Void>() {
+
+            @Override
+            protected Void doInBackground(Void... params) {
+
+                Map<Long, Long> eventsToCalendars = lookupEventToCalendarMap(context, eventIds);
+                Set<Long> calendars = new LinkedHashSet<Long>();
+                calendars.addAll(eventsToCalendars.values());
+                if (calendars.isEmpty()) {
+                    Log.d(TAG, "foudn no calendars for events");
+                    return null;
+                }
+
+                Map<Long, Pair<String, String>> calendarsToAccounts =
+                        lookupCalendarToAccountMap(context, calendars);
+
+                if (calendarsToAccounts.isEmpty()) {
+                    Log.d(TAG, "found no accounts for calendars");
+                    return null;
+                }
+
+                // filter out non-google accounts (necessary?)
+                Set<String> accounts = new LinkedHashSet<String>();
+                for (Pair<String, String> accountPair : calendarsToAccounts.values()) {
+                    if (GOOGLE_ACCOUNT_TYPE.equals(accountPair.first)) {
+                        accounts.add(accountPair.second);
+                    }
+                }
+
+                // filter out accounts we already know about
+                SharedPreferences prefs =
+                        context.getSharedPreferences(GLOBAL_DISMISS_MANAGER_PREFS,
+                                Context.MODE_PRIVATE);
+                Set<String> existingAccounts = prefs.getStringSet(ACCOUNT_KEY,
+                        new HashSet<String>());
+                accounts.removeAll(existingAccounts);
+
+                if (accounts.isEmpty()) {
+                    return null;
+                }
+
+                // subscribe to remaining accounts
+                CloudNotificationBackplane cnb =
+                        ExtensionsFactory.getCloudNotificationBackplane();
+                if (cnb.open(context)) {
+                    for (String account : accounts) {
+                        try {
+                            if (cnb.subscribeToGroup(senderId, account, account)) {
+                                existingAccounts.add(account);
+                            }
+                        } catch (IOException e) {
+                            // Try again, next time the account triggers and alert.
+                        }
+                    }
+                    cnb.close();
+                    prefs.edit()
+                    .putStringSet(ACCOUNT_KEY, existingAccounts)
+                    .commit();
+                }
+                return null;
+            }
+        }.execute();
+    }
+
+    /**
+     * Globally dismiss notifications that are backed by the same events.
+     * 
+     * @param context application context
+     * @param alarmIds Unique identifiers for events that have been dismissed by the user.
+     * @return true if notification_sender_id is available
+     */
+    public static void dismissGlobally(final Context context, final List<AlarmId> alarmIds) {
+        final String senderId = context.getResources().getString(R.string.notification_sender_id);
+        if ("".equals(senderId)) {
+            Log.i(TAG, "no sender configured");
+            return;
+        }
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                Set<Long> eventIds = new HashSet<Long>(alarmIds.size());
+                for (AlarmId alarmId: alarmIds) {
+                    eventIds.add(alarmId.mEventId);
+                }
+                // find the mapping between calendars and events
+                Map<Long, Long> eventsToCalendars = lookupEventToCalendarMap(context, eventIds);
+
+                if (eventsToCalendars.isEmpty()) {
+                    Log.d(TAG, "found no calendars for events");
+                    return null;
+                }
+
+                Set<Long> calendars = new LinkedHashSet<Long>();
+                calendars.addAll(eventsToCalendars.values());
+
+                // find the accounts associated with those calendars
+                Map<Long, Pair<String, String>> calendarsToAccounts =
+                        lookupCalendarToAccountMap(context, calendars);
+
+                if (calendarsToAccounts.isEmpty()) {
+                    Log.d(TAG, "found no accounts for calendars");
+                    return null;
+                }
+
+                // TODO group by account to reduce queries
+                Map<String, String> syncIdToAccount = new HashMap<String, String>();
+                Map<Long, String> eventIdToSyncId = new HashMap<Long, String>();
+                ContentResolver resolver = context.getContentResolver();
+                for (Long eventId : eventsToCalendars.keySet()) {
+                    Long calendar = eventsToCalendars.get(eventId);
+                    Pair<String, String> account = calendarsToAccounts.get(calendar);
+                    if (GOOGLE_ACCOUNT_TYPE.equals(account.first)) {
+                        Uri uri = asSync(Events.CONTENT_URI, account.first, account.second);
+                        Cursor cursor = resolver.query(uri, EVENT_SYNC_PROJECTION,
+                                Events._ID + " = " + eventId, null, null);
+                        try {
+                            cursor.moveToPosition(-1);
+                            int sync_id_idx = cursor.getColumnIndex(Events._SYNC_ID);
+                            if (sync_id_idx != -1) {
+                                while (cursor.moveToNext()) {
+                                    String syncId = cursor.getString(sync_id_idx);
+                                    syncIdToAccount.put(syncId, account.second);
+                                    eventIdToSyncId.put(eventId, syncId);
+                                }
+                            }
+                        } finally {
+                            cursor.close();
+                        }
+                    }
+                }
+
+                if (syncIdToAccount.isEmpty()) {
+                    Log.d(TAG, "found no syncIds for events");
+                    return null;
+                }
+
+                // TODO group by account to reduce packets
+                CloudNotificationBackplane cnb = ExtensionsFactory.getCloudNotificationBackplane();
+                if (cnb.open(context)) {
+                    for (AlarmId alarmId: alarmIds) {
+                        String syncId = eventIdToSyncId.get(alarmId.mEventId);
+                        String account = syncIdToAccount.get(syncId);
+                        Bundle data = new Bundle();
+                        data.putString(SYNC_ID, syncId);
+                        data.putString(START_TIME, Long.toString(alarmId.mStart));
+                        data.putString(ACCOUNT_NAME, account);
+                        try {
+                            cnb.send(account, syncId + ":" + alarmId.mStart, data);
+                        } catch (IOException e) {
+                            // TODO save a note to try again later
+                        }
+                    }
+                    cnb.close();
+                }
+                return null;
+            }
+        }.execute();
+    }
+
+    private static Uri asSync(Uri uri, String accountType, String account) {
+        return uri
+                .buildUpon()
+                .appendQueryParameter(
+                        android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, "true")
+                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
+                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
+    }
+
+    /**
+     * build a selection over a set of row IDs
+     * 
+     * @param ids row IDs to select
+     * @param key row name for the table
+     * @return a selection string suitable for a resolver query.
+     */
+    private static String buildMultipleIdQuery(Set<Long> ids, String key) {
+        StringBuilder selection = new StringBuilder();
+        boolean first = true;
+        for (Long id : ids) {
+            if (first) {
+                first = false;
+            } else {
+                selection.append(" OR ");
+            }
+            selection.append(key);
+            selection.append("=");
+            selection.append(id);
+        }
+        return selection.toString();
+    }
+
+    /**
+     * @param context application context
+     * @param eventIds Event row IDs to query.
+     * @return a map from event to calendar
+     */
+    private static Map<Long, Long> lookupEventToCalendarMap(final Context context,
+            final Set<Long> eventIds) {
+        Map<Long, Long> eventsToCalendars = new HashMap<Long, Long>();
+        ContentResolver resolver = context.getContentResolver();
+        String eventSelection = buildMultipleIdQuery(eventIds, Events._ID);
+        Cursor eventCursor = resolver.query(Events.CONTENT_URI, EVENT_PROJECTION,
+                eventSelection, null, null);
+        try {
+            eventCursor.moveToPosition(-1);
+            int calendar_id_idx = eventCursor.getColumnIndex(Events.CALENDAR_ID);
+            int event_id_idx = eventCursor.getColumnIndex(Events._ID);
+            if (calendar_id_idx != -1 && event_id_idx != -1) {
+                while (eventCursor.moveToNext()) {
+                    eventsToCalendars.put(eventCursor.getLong(event_id_idx),
+                            eventCursor.getLong(calendar_id_idx));
+                }
+            }
+        } finally {
+            eventCursor.close();
+        }
+        return eventsToCalendars;
+    }
+
+    /**
+     * @param context application context
+     * @param calendars Calendar row IDs to query.
+     * @return a map from Calendar to a pair (account type, account name)
+     */
+    private static Map<Long, Pair<String, String>> lookupCalendarToAccountMap(final Context context,
+            Set<Long> calendars) {
+        Map<Long, Pair<String, String>> calendarsToAccounts =
+                new HashMap<Long, Pair<String, String>>();
+        ;
+        ContentResolver resolver = context.getContentResolver();
+        String calendarSelection = buildMultipleIdQuery(calendars, Calendars._ID);
+        Cursor calendarCursor = resolver.query(Calendars.CONTENT_URI, CALENDARS_PROJECTION,
+                calendarSelection, null, null);
+        try {
+            calendarCursor.moveToPosition(-1);
+            int calendar_id_idx = calendarCursor.getColumnIndex(Calendars._ID);
+            int account_name_idx = calendarCursor.getColumnIndex(Calendars.ACCOUNT_NAME);
+            int account_type_idx = calendarCursor.getColumnIndex(Calendars.ACCOUNT_TYPE);
+            if (calendar_id_idx != -1 && account_name_idx != -1 && account_type_idx != -1) {
+                while (calendarCursor.moveToNext()) {
+                    Long id = calendarCursor.getLong(calendar_id_idx);
+                    String name = calendarCursor.getString(account_name_idx);
+                    String type = calendarCursor.getString(account_type_idx);
+                    calendarsToAccounts.put(id, new Pair<String, String>(type, name));
+                }
+            }
+        } finally {
+            calendarCursor.close();
+        }
+        return calendarsToAccounts;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        boolean updated = false;
+        if (intent.hasExtra(SYNC_ID) && intent.hasExtra(ACCOUNT_NAME)) {
+            String syncId = intent.getStringExtra(SYNC_ID);
+            long startTime = Long.parseLong(intent.getStringExtra(START_TIME));
+            ContentResolver resolver = context.getContentResolver();
+
+            Uri uri = asSync(Events.CONTENT_URI, GOOGLE_ACCOUNT_TYPE,
+                    intent.getStringExtra(ACCOUNT_NAME));
+            Cursor cursor = resolver.query(uri, EVENT_SYNC_PROJECTION,
+                    Events._SYNC_ID + " = '" + syncId + "'", null, null);
+            try {
+                int event_id_idx = cursor.getColumnIndex(Events._ID);
+                cursor.moveToFirst();
+                if (event_id_idx != -1 && !cursor.isAfterLast()) {
+                    long eventId = cursor.getLong(event_id_idx);
+                    ContentValues values = new ContentValues();
+                    String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED +
+                            " AND " + CalendarAlerts.EVENT_ID + "=" + eventId +
+                            " AND " + CalendarAlerts.BEGIN + "=" + startTime;
+                    values.put(CalendarAlerts.STATE, CalendarAlerts.STATE_DISMISSED);
+                    if (resolver.update(CalendarAlerts.CONTENT_URI, values, selection, null) > 0) {
+                        updated |= true;
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        if (updated) {
+            Log.d(TAG, "updating alarm state");
+            AlertService.updateAlertNotification(context);
+        }
+
+        setResultCode(Activity.RESULT_OK);
+    }
+}
diff --git a/src/com/android/calendar/event/EditEventHelper.java b/src/com/android/calendar/event/EditEventHelper.java
index 8e83be9..b110591 100644
--- a/src/com/android/calendar/event/EditEventHelper.java
+++ b/src/com/android/calendar/event/EditEventHelper.java
@@ -60,6 +60,9 @@
 
     private static final boolean DEBUG = false;
 
+    // Used for parsing rrules for special cases.
+    private EventRecurrence mEventRecurrence = new EventRecurrence();
+
     private static final String NO_EVENT_COLOR = "";
 
     public static final String[] EVENT_PROJECTION = new String[] {
@@ -1221,6 +1224,7 @@
 
         startTime.set(model.mStart);
         endTime.set(model.mEnd);
+        offsetStartTimeIfNecessary(startTime, endTime, rrule, model);
 
         ContentValues values = new ContentValues();
 
@@ -1296,6 +1300,79 @@
     }
 
     /**
+     * If the recurrence rule is such that the event start date doesn't actually fall in one of the
+     * recurrences, then push the start date up to the first actual instance of the event.
+     */
+    private void offsetStartTimeIfNecessary(Time startTime, Time endTime, String rrule,
+            CalendarEventModel model) {
+        if (rrule == null || rrule.isEmpty()) {
+            // No need to waste any time with the parsing if the rule is empty.
+            return;
+        }
+
+        mEventRecurrence.parse(rrule);
+        // Check if we meet the specific special case. It has to:
+        //  * be weekly
+        //  * not recur on the same day of the week that the startTime falls on
+        // In this case, we'll need to push the start time to fall on the first day of the week
+        // that is part of the recurrence.
+        if (mEventRecurrence.freq != EventRecurrence.WEEKLY) {
+            // Not weekly so nothing to worry about.
+            return;
+        }
+        if (mEventRecurrence.byday.length > mEventRecurrence.bydayCount) {
+            // This shouldn't happen, but just in case something is weird about the recurrence.
+            return;
+        }
+
+        // Start to figure out what the nearest weekday is.
+        int closestWeekday = Integer.MAX_VALUE;
+        int weekstart = EventRecurrence.day2TimeDay(mEventRecurrence.wkst);
+        int startDay = startTime.weekDay;
+        for (int i = 0; i < mEventRecurrence.bydayCount; i++) {
+            int day = EventRecurrence.day2TimeDay(mEventRecurrence.byday[i]);
+            if (day == startDay) {
+                // Our start day is one of the recurring days, so we're good.
+                return;
+            }
+
+            if (day < weekstart) {
+                // Let's not make any assumptions about what weekstart can be.
+                day += 7;
+            }
+            // We either want the earliest day that is later in the week than startDay ...
+            if (day > startDay && (day < closestWeekday || closestWeekday < startDay)) {
+                closestWeekday = day;
+            }
+            // ... or if there are no days later than startDay, we want the earliest day that is
+            // earlier in the week than startDay.
+            if (closestWeekday == Integer.MAX_VALUE || closestWeekday < startDay) {
+                // We haven't found a day that's later in the week than startDay yet.
+                if (day < closestWeekday) {
+                    closestWeekday = day;
+                }
+            }
+        }
+
+        // We're here, so unfortunately our event's start day is not included in the days of
+        // the week of the recurrence. To save this event correctly we'll need to push the start
+        // date to the closest weekday that *is* part of the recurrence.
+        if (closestWeekday < startDay) {
+            closestWeekday += 7;
+        }
+        int daysOffset = closestWeekday - startDay;
+        startTime.monthDay += daysOffset;
+        endTime.monthDay += daysOffset;
+        long newStartTime = startTime.normalize(true);
+        long newEndTime = endTime.normalize(true);
+
+        // Later we'll actually be using the values from the model rather than the startTime
+        // and endTime themselves, so we need to make these changes to the model as well.
+        model.mStart = newStartTime;
+        model.mEnd = newEndTime;
+    }
+
+    /**
      * Takes an e-mail address and returns the domain (everything after the last @)
      */
     public static String extractDomain(String email) {
diff --git a/src/com/android/calendar/recurrencepicker/RecurrencePickerDialog.java b/src/com/android/calendar/recurrencepicker/RecurrencePickerDialog.java
index 6de8439..580d211 100644
--- a/src/com/android/calendar/recurrencepicker/RecurrencePickerDialog.java
+++ b/src/com/android/calendar/recurrencepicker/RecurrencePickerDialog.java
@@ -327,8 +327,10 @@
     private LinearLayout mWeekGroup2;
     // Sun = 0
     private ToggleButton[] mWeekByDayButtons = new ToggleButton[7];
-    private String[] mDayOfWeekString;
-    private String[] mOrdinalArray;
+    /** A double array of Strings to hold the 7x5 list of possible strings of the form:
+     *  "on every [Nth] [DAY_OF_WEEK]", e.g. "on every second Monday",
+     *  where [Nth] can be [first, second, third, fourth, last] */
+    private String[][] mMonthRepeatByDayOfWeekStrs;
 
     private LinearLayout mMonthGroup;
     private RadioGroup mMonthRepeatByRadioGroup;
@@ -730,14 +732,18 @@
         mWeekGroup = (LinearLayout) mView.findViewById(R.id.weekGroup);
         mWeekGroup2 = (LinearLayout) mView.findViewById(R.id.weekGroup2);
 
-        mOrdinalArray = mResources.getStringArray(R.array.ordinal_labels);
-
         // In Calendar.java day of week order e.g Sun = 1 ... Sat = 7
         String[] dayOfWeekString = new DateFormatSymbols().getWeekdays();
-        mDayOfWeekString = new String[7];
-        for (int i = 0; i < 7; i++) {
-            mDayOfWeekString[i] = dayOfWeekString[TIME_DAY_TO_CALENDAR_DAY[i]];
-        }
+
+        mMonthRepeatByDayOfWeekStrs = new String[7][];
+        // from Time.SUNDAY as 0 through Time.SATURDAY as 6
+        mMonthRepeatByDayOfWeekStrs[0] = mResources.getStringArray(R.array.repeat_by_nth_sun);
+        mMonthRepeatByDayOfWeekStrs[1] = mResources.getStringArray(R.array.repeat_by_nth_mon);
+        mMonthRepeatByDayOfWeekStrs[2] = mResources.getStringArray(R.array.repeat_by_nth_tues);
+        mMonthRepeatByDayOfWeekStrs[3] = mResources.getStringArray(R.array.repeat_by_nth_wed);
+        mMonthRepeatByDayOfWeekStrs[4] = mResources.getStringArray(R.array.repeat_by_nth_thurs);
+        mMonthRepeatByDayOfWeekStrs[5] = mResources.getStringArray(R.array.repeat_by_nth_fri);
+        mMonthRepeatByDayOfWeekStrs[6] = mResources.getStringArray(R.array.repeat_by_nth_sat);
 
         // In Time.java day of week order e.g. Sun = 0
         int idx = Utils.getFirstDayOfWeek(getActivity());
@@ -932,10 +938,10 @@
                         mModel.monthlyByDayOfWeek = mTime.weekDay;
                     }
 
-                    mMonthRepeatByDayOfWeekStr = mResources.getString(
-                            R.string.recurrence_month_pattern_by_day_of_week,
-                            mOrdinalArray[mModel.monthlyByNthDayOfWeek - 1],
-                            mDayOfWeekString[mModel.monthlyByDayOfWeek]);
+                    String[] monthlyByNthDayOfWeekStrs =
+                            mMonthRepeatByDayOfWeekStrs[mModel.monthlyByDayOfWeek];
+                    mMonthRepeatByDayOfWeekStr =
+                            monthlyByNthDayOfWeekStrs[mModel.monthlyByNthDayOfWeek - 1];
                     mRepeatMonthlyByNthDayOfWeek.setText(mMonthRepeatByDayOfWeekStr);
                 }
                 break;