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.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 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; } } static List calendarEventsUsed = new ArrayList<>(); // To determine for which events which rules have been executed 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) { /* Kann die selbe Regel mehrfach pro Termin ausgeführt werden? Nein, eh nicht, ne? Am nächsten Tag ist es wieder ein anderer Termin. Wenn zwei zeitgleiche Termine mit gleichen Inhalten in verschiedenen Kalendern sind, würde die Regel so 2x ausgeführt werden. */ //TODO: Second appointment directly one after another or overlapping won't get executed 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; 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[]{ "_id", "calendar_displayName", "ownerAccount", }, 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 includePastEvents) { if(calendarEventsCache == null) { calendarEventsCache = new ArrayList<>(); Cursor cursor; cursor = context.getContentResolver().query( Uri.parse("content://com.android.calendar/events"), new String[] { "calendar_id", "_id", "title", "description", "allDay", "dtstart", "dtend", "eventLocation", "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); if(includePastEvents || event.end.getTimeInMillis() > now.getTimeInMillis()) 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(), 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(), 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(), 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; } }