package com.jens.automation2.receivers; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.CalendarContract; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.jens.automation2.AutomationService; import com.jens.automation2.Miscellaneous; import com.jens.automation2.Rule; import com.jens.automation2.Trigger; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.Timer; import java.util.TimerTask; public class CalendarReceiver extends BroadcastReceiver implements AutomationListenerInterface { static CalendarReceiver calendarReceiverInstance = null; static boolean calendarReceiverActive = false; static IntentFilter calendarIntentFilter = null; private static Intent calendarIntent = null; public static final int AVAILABILITY_OUT_OF_OFFICE = 4; public static final int AVAILABILITY_WORKING_ELSEWHERE = 5; public static final String calendarAlarmAction = "ALARM_FOR_CALENDAR"; static List calendarsCache = null; static List calendarEventsCache = null; static List calendarEventsReoccuringCache = null; static Timer timer = null; static TimerTask timerTask = null; static Calendar nextWakeup = null; static AlarmManager alarmManager = null; static boolean wakeupNeedsToBeScheduled = false; public static class RuleEventPair { Rule rule; CalendarEvent event; public RuleEventPair(Rule rule, CalendarEvent event) { this.rule = rule; this.event = event; } } // To determine for which events which rules have been executed static List calendarEventsUsed = new ArrayList<>(); public static void addUsedPair(RuleEventPair pair) { // Add pair only if it's not in the list already. for(RuleEventPair usedPair : calendarEventsUsed) { if(usedPair.rule.equals(pair.rule) && usedPair.event.equals(pair.event)) return; } calendarEventsUsed.add(pair); } public static CalendarReceiver getInstance() { if(calendarReceiverInstance == null) calendarReceiverInstance = new CalendarReceiver(); return calendarReceiverInstance; } @Override public void onReceive(Context context, Intent intent) { if(intent.getAction().equalsIgnoreCase(Intent.ACTION_PROVIDER_CHANGED)) { Miscellaneous.logEvent("i", "CalendarReceiver", "Received " + intent.getAction(), 5); calendarsCache = null; calendarEventsCache = null; checkForRules(context); armOrRearmTimer(); } else if(intent.getAction().equalsIgnoreCase(calendarAlarmAction)) { Miscellaneous.logEvent("i", "CalendarReceiver", "Received alarm for calendar receiver.", 5); routineAtAlarm(); } } static void checkForRules(Context context) { ArrayList ruleCandidates = Rule.findRuleCandidates(Trigger.Trigger_Enum.calendarEvent); for (int i = 0; i < ruleCandidates.size(); i++) { if (ruleCandidates.get(i).getsGreenLight(context)) ruleCandidates.get(i).activate(AutomationService.getInstance(), false); } } @Override public void startListener(AutomationService automationServiceRef) { startCalendarReceiver(automationServiceRef); } @Override public void stopListener(AutomationService automationService) { if(calendarReceiverActive) { if(calendarReceiverInstance != null) { AutomationService.getInstance().unregisterReceiver(calendarReceiverInstance); calendarReceiverInstance = null; } calendarReceiverActive = false; } } @Override public boolean isListenerRunning() { return calendarReceiverActive; } @Override public Trigger.Trigger_Enum[] getMonitoredTrigger() { return new Trigger.Trigger_Enum[]{Trigger.Trigger_Enum.calendarEvent}; } public static class AndroidCalendar { public int calendarId; public String displayName; public String accountString; @NonNull @Override public String toString() { return displayName + " (" + accountString + ")"; } } public static class CalendarEvent { public AndroidCalendar calendar; public int calendarId; public String eventId; public String title; public String description; public String location; public String availability; public Calendar start, end; public boolean allDay, reoccurring; public boolean isCurrentlyActive() { Calendar now = Calendar.getInstance(); return now.getTimeInMillis() >= start.getTimeInMillis() && now.getTimeInMillis() < end.getTimeInMillis(); } @NonNull @Override public String toString() { return title; } @Override public boolean equals(@Nullable Object obj) { try { CalendarEvent compareEvent = (CalendarEvent) obj; return calendarId == compareEvent.calendarId && eventId.equals(compareEvent.eventId) && title.equals(compareEvent.title) && description.equals(compareEvent.description) && location.equals(compareEvent.location) && availability.equals(compareEvent.availability) && start.getTimeInMillis() == compareEvent.start.getTimeInMillis() && end.getTimeInMillis() == compareEvent.end.getTimeInMillis() && allDay == compareEvent.allDay; } catch (Exception e) { Miscellaneous.logEvent("e", "CalendarReceiver compare()", Log.getStackTraceString(e), 5); return false; } } } public static List readCalendars(Context context) { if(calendarsCache == null) { calendarsCache = new ArrayList<>(); Cursor cursor; cursor = context.getContentResolver().query( Uri.parse("content://com.android.calendar/calendars"), new String[]{ CalendarContract.Calendars._ID, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, CalendarContract.Calendars.OWNER_ACCOUNT, }, null, null, null); cursor.moveToFirst(); // fetching calendars name String CNames[] = new String[cursor.getCount()]; List calendarlist = new ArrayList<>(); for (int i = 0; i < CNames.length; i++) { try { AndroidCalendar calendar = new AndroidCalendar(); calendar.calendarId = Integer.parseInt(cursor.getString(0)); calendar.displayName = cursor.getString(1); calendar.accountString = cursor.getString(2); calendarsCache.add(calendar); } catch (Exception e) { } cursor.moveToNext(); } if (cursor != null) cursor.close(); } return calendarsCache; } public static List readCalendarEvents(Context context, boolean includeReoccurring, boolean includePastEvents) { if(calendarEventsCache == null) { calendarEventsCache = new ArrayList<>(); Cursor cursor; cursor = context.getContentResolver().query( Uri.parse("content://com.android.calendar/events"), new String[] { CalendarContract.Events.CALENDAR_ID, CalendarContract.Events._ID, CalendarContract.Events.TITLE, CalendarContract.Events.DESCRIPTION, CalendarContract.Events.ALL_DAY, CalendarContract.Events.DTSTART, CalendarContract.Events.DTEND, CalendarContract.Events.EVENT_LOCATION, CalendarContract.Events.AVAILABILITY }, null, null, null); cursor.moveToFirst(); // fetching calendars name String CNames[] = new String[cursor.getCount()]; Calendar now = Calendar.getInstance(); for (int i = 0; i < CNames.length; i++) { try { CalendarEvent event = new CalendarEvent(); event.calendarId = Integer.parseInt(cursor.getString(0)); for(AndroidCalendar cal : readCalendars(context)) { if(cal.calendarId == event.calendarId) { event.calendar = cal; break; } } event.eventId = cursor.getString(1); event.title = cursor.getString(2); event.description = cursor.getString(3); event.allDay = cursor.getString(4).equals("1"); event.start = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(5))); event.end = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(6))); event.location = cursor.getString(7); event.availability = cursor.getString(8); event.reoccurring = false; if(includePastEvents || event.end.getTimeInMillis() > now.getTimeInMillis()) calendarEventsCache.add(event); } catch (Exception e) {} cursor.moveToNext(); } if(cursor != null) cursor.close(); } if(includeReoccurring && calendarEventsReoccuringCache == null) { calendarEventsReoccuringCache = new ArrayList<>(); Cursor cursor; Calendar queryStart, queryEnd; if(includePastEvents) queryStart = Miscellaneous.calendarFromLong(0); else queryStart = Calendar.getInstance(); queryEnd = Calendar.getInstance(); queryEnd.add(Calendar.YEAR, 1); cursor = context.getContentResolver().query( Uri.parse("content://com.android.calendar/instances/when/" + queryStart.getTimeInMillis() + "/" + queryEnd.getTimeInMillis()), new String[] { CalendarContract.Instances.CALENDAR_ID, CalendarContract.Instances._ID, CalendarContract.Instances.TITLE, CalendarContract.Instances.DESCRIPTION, CalendarContract.Instances.ALL_DAY, CalendarContract.Instances.BEGIN, CalendarContract.Instances.END, // CalendarContract.Instances.DTSTART, // CalendarContract.Instances.DTEND, CalendarContract.Instances.EVENT_LOCATION, CalendarContract.Instances.AVAILABILITY }, null, null, null); cursor.moveToFirst(); // fetching calendars name String CNames[] = new String[cursor.getCount()]; Calendar now = Calendar.getInstance(); for (int i = 0; i < CNames.length; i++) { try { CalendarEvent event = new CalendarEvent(); event.calendarId = Integer.parseInt(cursor.getString(0)); for(AndroidCalendar cal : readCalendars(context)) { if(cal.calendarId == event.calendarId) { event.calendar = cal; break; } } event.eventId = cursor.getString(1); event.title = cursor.getString(2); event.description = cursor.getString(3); event.allDay = cursor.getString(4).equals("1"); event.start = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(5))); event.end = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(6))); event.location = cursor.getString(7); event.availability = cursor.getString(8); event.reoccurring = true; if(includePastEvents || event.end.getTimeInMillis() > now.getTimeInMillis()) { // For the moment keeping separate records to some extent calendarEventsReoccuringCache.add(event); calendarEventsCache.add(event); } } catch (Exception e) {} cursor.moveToNext(); } if(cursor != null) cursor.close(); } return calendarEventsCache; } protected static void routineAtAlarm() { checkForRules(Miscellaneous.getAnyContext()); // Set next timer calculateNextWakeup(); armOrRearmTimer(); } public static void armOrRearmTimer() { PendingIntent pi = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (alarmManager == null) { alarmManager = (AlarmManager) Miscellaneous.getAnyContext().getSystemService(Context.ALARM_SERVICE); } if(pi == null) { Intent intent = new Intent(Miscellaneous.getAnyContext(), CalendarReceiver.class); intent.setAction(calendarAlarmAction); pi = PendingIntent.getBroadcast(AutomationService.getInstance(), 0, intent, 0); } } else { timerTask = new TimerTask() { @Override public void run() { routineAtAlarm(); } }; if(timer != null) { timer.cancel(); timer.purge(); } timer = new Timer(); } if(nextWakeup == null) { readCalendarEvents(Miscellaneous.getAnyContext(), true,false); calculateNextWakeup(); } // If it's now filled, go on if(nextWakeup != null && wakeupNeedsToBeScheduled) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && alarmManager.canScheduleExactAlarms())) { try { alarmManager.cancel(pi); } catch (Exception e) { } Miscellaneous.logEvent("i", "armOrRearmTimer()", "Scheduling wakeup for calendar at " + Miscellaneous.formatDate(nextWakeup.getTime()), 4); alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextWakeup.getTimeInMillis(), pi); wakeupNeedsToBeScheduled = false; } } else timer.schedule(timerTask, nextWakeup.getTimeInMillis()); } } private static void calculateNextWakeup() { Calendar now = Calendar.getInstance(); List events = readCalendarEvents(Miscellaneous.getAnyContext(), true, false); if (events.size() == 0) { Miscellaneous.logEvent("i", "calculateNextWakeup()", "No future events, nothing to schedule.", 5); } else { List ruleCandidates = Rule.findRuleCandidates(Trigger.Trigger_Enum.calendarEvent); List wakeUpCandidatesList = new ArrayList<>(); for (CalendarEvent event : events) { for (Rule r : ruleCandidates) { for (Trigger t : r.getTriggerSet()) { if (t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent) && t.checkCalendarEvent(event, true)) { /* Device needs to wakeup at start AND end of events, no matter what is specified in triggers. This is because we also need to know when a trigger doesn't apply anymore to make it count for hasStateNotAppliedSinceLastRuleExecution(). Otherwise the same rule would not get executed again even after calendar events have come and gone. */ if(event.start.getTimeInMillis() > now.getTimeInMillis()) wakeUpCandidatesList.add(event.start.getTimeInMillis()); if(event.end.getTimeInMillis() > now.getTimeInMillis()) wakeUpCandidatesList.add(event.end.getTimeInMillis()); } } } } Collections.sort(wakeUpCandidatesList); if(wakeUpCandidatesList.size() == 0) Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Not scheduling any calendar related wakeup as there are no future events that might match a configured trigger.", 4); else { if (nextWakeup == null || nextWakeup.getTimeInMillis() != wakeUpCandidatesList.get(0)) { Calendar newAlarm = Miscellaneous.calendarFromLong(wakeUpCandidatesList.get(0)); if (nextWakeup == null) Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Chose " + Miscellaneous.formatDate(newAlarm.getTime()) + " as next wakeup for calendar triggers. Old was null.", 4); else Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Chose " + Miscellaneous.formatDate(newAlarm.getTime()) + " as next wakeup for calendar triggers. Old was " + Miscellaneous.formatDate(nextWakeup.getTime()), 4); nextWakeup = newAlarm; if (!wakeupNeedsToBeScheduled) wakeupNeedsToBeScheduled = true; } else Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Alarm " + Miscellaneous.formatDate(nextWakeup.getTime()) + " has been selected as next wakeup, but not rescheduling since this was not a change.", 4); } } } public static void startCalendarReceiver(final AutomationService automationServiceRef) { if (!calendarReceiverActive) { if (calendarReceiverInstance == null) calendarReceiverInstance = new CalendarReceiver(); if (calendarIntentFilter == null) { calendarIntentFilter = new IntentFilter(); calendarIntentFilter.addAction(Intent.ACTION_PROVIDER_CHANGED); calendarIntentFilter.addDataScheme("content"); calendarIntentFilter.addDataAuthority("com.android.calendar", null); } calendarIntent = automationServiceRef.registerReceiver(calendarReceiverInstance, calendarIntentFilter); calendarReceiverActive = true; armOrRearmTimer(); } } public static boolean mayRuleStillBeActivatedForPendingCalendarEvents(Rule rule) { for(CalendarEvent event : readCalendarEvents(Miscellaneous.getAnyContext(), true,false)) { for(Trigger t : rule.getTriggerSet()) { if(t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent) && t.checkCalendarEvent(event, false)) { if (!hasEventBeenUsedInRule(rule, event)) return true; } } } return false; } static boolean hasEventBeenUsedInRule(Rule rule, CalendarEvent event) { for (RuleEventPair executedPair : calendarEventsUsed) { if (executedPair.rule.equals(rule) && executedPair.event.equals(event)) return true; } return false; } }