/// <summary> /// Called one at startup, after an initial delay to let outlook finish loading /// </summary> void performDelayedStartupTasks() { // This code is only for diagnosing tricky problems and won't be executed in a normal deployment if (!logger.Enabled) { return; } // iterate over all of today's items logger.Debug("Advanced diagnostics: logging data on items in today's calendar..."); Outlook.Items calItems = findCalendarItems( "[Start] >= '" + (DateTime.Today).ToString("g") + "'" + " AND [Start] <= '" + (DateTime.Today + OneDay).ToString("g") + "'" + " AND [End] >= '" + (DateTime.Today).ToString("g") + "'" // not really necessary but + " AND [End] <= '" + (DateTime.Today + OneDay).ToString("g") + "'" ); var subjectExcludeRegex = getSubjectExcludeRegex(); int count = 0; foreach (Outlook.AppointmentItem item in calItems) { count++; if (string.IsNullOrWhiteSpace(item.Subject)) { logger.Debug("Found meeting with subject '" + item.Subject + "' at " + item.Start); } if (item.AllDayEvent) { continue; // else constructor will throw } var meeting = new UpcomingMeeting(item, item.Start - DefaultReminderTime); if (meeting.GetMeetingUrl() != "") { logger.Debug("Extracted meeting URL from '" + meeting.Subject + "': '" + meeting.GetMeetingUrl() + "'"); } else if (meeting.Body.Trim() != "") { // This is a bit verbose but may be needed sometimes for debugging URL regexes (at least until we build a proper UI for that task) logger.Info("No meeting URL found in '" + meeting.Subject + "': \n-------------------------------------\n" + meeting.Body + "\n-------------------------------------\n"); } if (subjectExcludeRegex != null && subjectExcludeRegex.IsMatch(item.Subject ?? "")) { logger.Info("This item will be ignored due to subject exclude regex: '" + item.Subject + "'"); } } logger.Debug("Completed advanced diagnostics - checked " + count + " calendar items for today\n\n"); }
private void updateUpcomingMeetings() { DateTime now = DateTime.Now; // first update existing items in case there were any changes foreach (UpcomingMeeting m in upcoming.Values) { // if start date was changed in either direction we should recalculate // the reminder - might need a notification sooner, // or if the meeting was postponed then later. // always resetting to defaultremindertime is the safest/most conservative // option since it gives maximum opportunity for the user to decide // whether to defer the meeting if (m.UpdateStartTime()) { logger.Info("Resetting reminder time for " + m + " due to change in start time"); m.NextReminderTime = m.StartTime - DefaultReminderTime; } } // add in any new meetings we're not aware of yet that will start or need to be reminded about in the // next sleep interval, ignoring any that have ended already // this filter is conservative - must not miss any items, but is ok if it contains some extra ones // we use lastMeetingSearchTime as the lower bound to avoid missing stuff, but give up and just // do it from 'now' onwards if we haven't checked for a day or more, since it's too late anyway by then if (now - lastMeetingSearchTime > OneDay) { lastMeetingSearchTime = now; } Outlook.Items calItems = findCalendarItems( "[Start] >= '" + (lastMeetingSearchTime).ToString("g") + "'" + " AND [Start] <= '" + (now + SleepInterval + DefaultReminderTime).ToString("g") + "'" + " AND [End] >= '" + (now).ToString("g") + "'" // not really necessary but + " AND [End] <= '" + (now + OneDay).ToString("g") + "'" ); // next time we'll monitor from now on lastMeetingSearchTime = now; /* * filter = "[Start] >= '" + new DateTime(2016, 1, 17).ToString("g") + "'" + " AND [Start] <= '" + new DateTime(2016, 1, 19).ToString("g") + "'" + " AND [End] >= '" + new DateTime(2016, 1, 17).ToString("g") + "'" + " AND [End] <= '" + new DateTime(2016, 1, 19).ToString("g") + "'" + ; * */ Regex subjectExcludeRegex = getSubjectExcludeRegex(); foreach (Outlook.AppointmentItem item in calItems) { if (item.AllDayEvent) { continue; } if (subjectExcludeRegex != null && subjectExcludeRegex.IsMatch(item.Subject ?? "")) { logger.Debug("Ignoring excluded meeting based on subject: " + item.Subject); continue; } if (UpcomingMeeting.IsAppointmentCancelled(item)) { logger.Debug("Ignoring cancelled meeting: " + item.Subject); continue; } //logger.Debug("Returned: " + new UpcomingMeeting(item, item.Start - DefaultReminderTime)); // TODO; remove // nb: the second check should be unecessary, but prevents bad behaviour if outlook's filtering doesn't work right for some reason if (!upcoming.ContainsKey(item.EntryID) && item.End >= now) { upcoming.Add(item.EntryID, new UpcomingMeeting(item, item.Start - DefaultReminderTime)); } } // finally, expire any meetings that have ended (we can't expire items until they definitely // won't show up in the above filter otherwise we might end up re-adding dismissed reminders) // probably safe to expire them even if not dismissed, if they've ended var expired = new List <string>(upcoming.Values.Where(e => (e.EndTime < now) || e.IsDeleted).Select(e => e.ID)); foreach (var id in expired) { logger.Debug("Removing expired item: " + upcoming[id]); upcoming[id].Dispose(); upcoming.Remove(id); } logger.Debug(upcoming.Count + " upcoming items: " + string.Join("\n ", upcoming.Values)); }
/// <summary> /// Called when the timer wakes up or when a reminder form has been closed to work out /// what to do next /// </summary> private void waitOrRemind() { try { DateTime now = DateTime.Now; updateUpcomingMeetings(); UpcomingMeeting next = null; if (upcoming.Count > 0) { next = upcoming.Values.OrderBy(m => m.NextReminderTime).First(); if (next.IsDismissed) // implies it contains only expired items { next = null; } } DateTime sleepUntil = now + SleepInterval; if (next != null) { // either we wokeup just in time or maybe we have a backlog of reminder(s) to clear // (nb: use 500ms fudge factor to make sure potentially imprecise timers don't hurt us) if (next.NextReminderTime <= now + new TimeSpan(0, 0, 0, 0, 500)) { logger.Info("Showing reminder for: " + next); playReminderSound(); ReminderForm form = new ReminderForm(next); form.FormClosed += ReminderFormClosedEventHandler; form.Show(); // timers etc are disabled until the current window has been closed - // simpler to manage and avoids getting a million popups if you leave it running while on vacation return; } if (next.NextReminderTime < sleepUntil) { sleepUntil = next.NextReminderTime; } } myTimer.Interval = Convert.ToInt32((sleepUntil - now).TotalMilliseconds); if (myTimer.Interval <= 0) { // should never happen logger.Info("Warning: attempted to set invalid interval of " + myTimer.Interval + "; next=" + next); myTimer.Interval = 100; } nextPlannedWakeup = sleepUntil; // used to measure lateness logger.Debug("Setting timer to wait in " + myTimer.Interval + " at " + sleepUntil); myTimer.Start(); if (DateTime.Now - now > new TimeSpan(0, 0, 0, 0, 500)) { logger.Debug("waitOrRemind took a long time: " + (DateTime.Now - now)); } } catch (Exception ex) { logger.Error("Unexpected error in waitOrRemind: ", ex); // don't throw out of here as it could result in the dialog being uncloseable } }
public ReminderForm(UpcomingMeeting meeting) { this.meeting = meeting; this.joinUrl = meeting.GetMeetingUrl(); InitializeComponent(); // populate the combo with a hardcoded list of items whose primary purpose is to illustrate the // syntax of all possible values timeCombo.Items.Add(new SnoozeTime(20, false)); timeCombo.Items.Add(new SnoozeTime(-20, false)); timeCombo.Items.Add(new SnoozeTime(30, true)); timeCombo.Items.Add(new SnoozeTime(60, true)); // set labels for this meeting nameLabel.Text = meeting.Subject; extraInfoLabel.Text = ""; if (meeting.Location.Length > 0) { extraInfoLabel.Text = "Location: " + meeting.Location + "; "; } extraInfoLabel.Text += "Duration " + meeting.OutlookItem.Duration + " mins"; if (meeting.getOrganizer() != null) { extraInfoLabel.Text += "; organizer: " + meeting.getOrganizer(); } IEnumerable <string> attendees = meeting.GetAttendees(); if (attendees.Count() == 1) { extraInfoLabel.Text += "; with: " + string.Join(", ", attendees); } else { extraInfoLabel.Text += "; " + attendees.Count() + " attendees"; } extraInfoLabel.Text += ""; toolTip.SetToolTip(nameLabel, nameLabel.Text); // in case it's too long toolTip.SetToolTip(extraInfoLabel, meeting.Body); joinButton.Visible = joinUrl.Length > 0; // start time timer timer.Enabled = true; updateStartTime(); // reminder times list. stored in MRU order but sorted in time order timeList.Items.Clear(); var list = SnoozeTime.ParseList(Properties.Settings.Default.mruSnoozeTimes); if (list.Count == 0) { // initialize default list of snooze times list.Add(new SnoozeTime(-2 * 60, false)); list.Add(new SnoozeTime(-30, false)); list.Add(new SnoozeTime(30, true)); list.Add(new SnoozeTime(60, true)); list.Add(new SnoozeTime(60 * 5, true)); Properties.Settings.Default.mruSnoozeTimes = SnoozeTime.ListToString(list); Properties.Settings.Default.Save(); } list.Sort(); list.ForEach(st => timeList.Items.Add(st)); // by default, dismiss when dialog is closed. unless snooze is selected meeting.Dismiss(); reactivateTime = DateTime.Now + reactivateTimeSpan; }