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 androidx.annotation.NonNull; 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 AutomationService automationServiceRef; 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 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(); } } private 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 automationService) { if(!calendarReceiverActive) { if(calendarReceiverInstance == null) calendarReceiverInstance = new CalendarReceiver(); if(calendarIntentFilter == null) { calendarIntentFilter = new IntentFilter(); calendarIntentFilter.addAction(Intent.ACTION_PROVIDER_CHANGED); // calendarIntentFilter.addDataScheme("content"); } AutomationService.getInstance().registerReceiver(calendarReceiverInstance, calendarIntentFilter); calendarReceiverActive = true; } } @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; } } 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(); } private 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()), 5); 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(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.", 5); else Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Chose " + Miscellaneous.formatDate(newAlarm.getTime()) + " as next wakeup for calendar triggers. Old was " + Miscellaneous.formatDate(nextWakeup.getTime()), 5); 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.", 5); } } public static void startCalendarReceiver(final AutomationService automationServiceRef) { if (!calendarReceiverActive) { CalendarReceiver.automationServiceRef = automationServiceRef; 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(); } } }