659 lines
24 KiB
Java

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<AndroidCalendar> calendarsCache = null;
static List<CalendarEvent> calendarEventsCache = null;
static List<CalendarEvent> calendarEventsReoccurringCache = null;
// To determine for which events which rules have been executed
static List<RuleEventPair> calendarEventsUsed = new ArrayList<>();
static Timer timer = null;
static TimerTask timerTask = null;
static Calendar nextWakeup = null;
static AlarmManager alarmManager = null;
static boolean wakeupNeedsToBeScheduledOrRescheduled = false;
public static CalendarEvent getLastTriggeringEvent()
{
if(calendarEventsUsed.size() > 0)
{
return calendarEventsUsed.get(calendarEventsUsed.size() -1).event;
}
return null;
}
public static class RuleEventPair
{
Rule rule;
CalendarEvent event;
public RuleEventPair(Rule rule, CalendarEvent event)
{
this.rule = rule;
this.event = event;
}
}
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)
{
Miscellaneous.logEvent("i", "CalendarReceiver", "Received " + intent.getAction(), 4);
if(intent.getAction().equalsIgnoreCase(Intent.ACTION_PROVIDER_CHANGED))
{
Miscellaneous.logEvent("i", "CalendarReceiver", "Clearing calendar caches.", 4);
clearCaches();
routineAtAlarm();
}
else if(intent.getAction().equalsIgnoreCase(calendarAlarmAction))
{
routineAtAlarm();
}
}
static void checkForRules(Context context)
{
ArrayList<Rule> 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);
}
static void clearCaches()
{
calendarsCache = null;
calendarEventsCache = null;
calendarEventsReoccurringCache = null;
}
@Override
public void stopListener(AutomationService automationService)
{
if(calendarReceiverActive)
{
if(calendarReceiverInstance != null)
{
AutomationService.getInstance().unregisterReceiver(calendarReceiverInstance);
calendarReceiverInstance = null;
}
clearCaches();
calendarEventsUsed.clear();
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: " + title + ", location: " + location + ", description: " + description + ", start: " + Miscellaneous.formatDate(start.getTime()) + ", end: " + Miscellaneous.formatDate(end.getTime()) + ", is currently active: " + String.valueOf(isCurrentlyActive()) + ", all day: " + String.valueOf(allDay) + ", availability: " + availability;
}
@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<AndroidCalendar> 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()];
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<CalendarEvent> 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 && calendarEventsReoccurringCache == null)
{
calendarEventsReoccurringCache = 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.EVENT_LOCATION,
CalendarContract.Instances.AVAILABILITY
},
null, null, null);
cursor.moveToFirst();
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
calendarEventsReoccurringCache.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 && wakeupNeedsToBeScheduledOrRescheduled)
{
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);
wakeupNeedsToBeScheduledOrRescheduled = false;
}
}
else
timer.schedule(timerTask, nextWakeup.getTimeInMillis());
}
}
private static void calculateNextWakeup()
{
Calendar now = Calendar.getInstance();
List<CalendarEvent> events = readCalendarEvents(Miscellaneous.getAnyContext(), true, false);
if (events.size() == 0)
{
Miscellaneous.logEvent("i", "calculateNextWakeup()", "No future events, nothing to schedule.", 4);
}
else
{
List<Rule> ruleCandidates = Rule.findRuleCandidates(Trigger.Trigger_Enum.calendarEvent);
List<Long> 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 (!wakeupNeedsToBeScheduledOrRescheduled)
wakeupNeedsToBeScheduledOrRescheduled = 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(RuleEventPair pair : calendarEventsUsed)
Miscellaneous.logEvent("i", "mayRuleStillBeActivatedForPendingCalendarEvents()", "Existing pair of " + pair.rule.getName() + " and " + pair.event, 5);
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))
{
/*
If there are multiple parallel calendar events and a rule has multiple
triggers of type calendar event, we don't want the rule to fire only once.
*/
if(rule.getAmountOfTriggersForType(Trigger.Trigger_Enum.calendarEvent) == 1)
{
Miscellaneous.logEvent("i", "mayRuleStillBeActivatedForPendingCalendarEvents()", "Rule " + rule.getName() + " has not been used in conjunction with event " + event, 4);
return true;
}
}
}
}
}
return false;
}
public static boolean hasEventBeenUsedInRule(Rule rule, CalendarEvent event)
{
for (RuleEventPair executedPair : calendarEventsUsed)
{
if (executedPair.rule.equals(rule) && executedPair.event.equals(event))
return true;
}
return false;
}
public static List<CalendarReceiver.CalendarEvent> getApplyingCalendarEvents(Rule rule)
{
List<CalendarReceiver.CalendarEvent> returnList = new ArrayList<>();
try
{
List<CalendarReceiver.CalendarEvent> calendarEvents = CalendarReceiver.readCalendarEvents(AutomationService.getInstance(), true,false);
for(Trigger t : rule.getTriggerSet())
{
if(t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent))
{
for (CalendarReceiver.CalendarEvent event : calendarEvents)
{
if (t.checkCalendarEvent(event, false))
returnList.add(event);
}
}
}
}
catch(Exception e)
{
Miscellaneous.logEvent("e", "getApplyingCalendarEvents()", Log.getStackTraceString(e), 1);
}
return returnList;
}
}