package com.jens.automation2; import static com.jens.automation2.Trigger.triggerParameter2Split; import android.annotation.SuppressLint; import android.content.Context; import android.os.AsyncTask; import android.os.Build; import android.os.Looper; import android.util.Log; import android.widget.Toast; import androidx.annotation.Nullable; import com.google.android.gms.location.DetectedActivity; import com.jens.automation2.receivers.ActivityDetectionReceiver; import com.jens.automation2.receivers.BroadcastListener; import com.jens.automation2.receivers.CalendarReceiver; import com.jens.automation2.receivers.NotificationListener; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; public class Rule implements Comparable { protected static ArrayList ruleCollection = new ArrayList(); protected static List ruleRunHistory = new ArrayList(); public static List getRuleRunHistory() { return ruleRunHistory; } protected ArrayList triggerSet; protected ArrayList actionSet; protected String name; protected boolean ruleActive = true; // rules can be deactivated, so they won't fire if you don't want them temporarily protected boolean ruleToggle = false; // rule will run again and do the opposite of its actions if applicable protected Calendar lastExecution; protected static Date lastActivatedRuleActivationTime; public Calendar getLastExecution() { return lastExecution; } public void setLastExecution(Calendar lastExecution) { this.lastExecution = lastExecution; } public boolean isRuleToggle() { return ruleToggle; } public void setRuleToggle(boolean ruleToggle) { this.ruleToggle = ruleToggle; } public static ArrayList getRuleCollection() { return ruleCollection; } public boolean isRuleActive() { return ruleActive; } public void setRuleActive(boolean ruleActive) { this.ruleActive = ruleActive; } public static void setRuleCollection(ArrayList ruleCollection) { Rule.ruleCollection = ruleCollection; } public static Date getLastActivatedRuleActivationTime() { return lastActivatedRuleActivationTime; } public static Rule getLastActivatedRule() { if(ruleRunHistory.size() > 0) return ruleRunHistory.get(0); else return null; } public ArrayList getTriggerSet() { return triggerSet; } public void setTriggerSet(ArrayList triggerSet) { this.triggerSet = triggerSet; } public ArrayList getActionSet() { return actionSet; } public void setActionSet(ArrayList actionSet) { this.actionSet = actionSet; } public String getName() { return name; } public void setName(String name) { this.name = name.trim(); } public static void readFromFile() { ruleCollection = XmlFileInterface.ruleCollection; } @Override public String toString() { return this.getName(); } @SuppressLint("NewApi") public String toStringLong() { String returnString = ""; if(isRuleActive()) returnString += "Active: "; else returnString += "Inactive: "; returnString += this.getName() + ": If "; for(int i=0; i 0) { if(this.getLastExecution() != null) { Calendar now = Calendar.getInstance(); if (this.getLastExecution().getTimeInMillis() + oneTrigger.getTimeFrame().getRepetition() * 1000 <= now.getTimeInMillis()) return true; } else return true; } } else if(oneTrigger.getTriggerType().equals(Trigger.Trigger_Enum.broadcastReceived)) { return oneTrigger.getTriggerParameter() == BroadcastListener.getInstance().hasBroadcastOccurredSince(oneTrigger.getTriggerParameter2(), getLastExecution()); } } return false; } public boolean getsGreenLight(Context context) { if(applies(context)) { if(hasNotAppliedSinceLastExecution()) { Miscellaneous.logEvent("i", "getsGreenLight()", "Rule \"" + getName() + "\" applies and has flipped since its last execution.", 4); return true; } // else if(hasTriggerOfType(Trigger.Trigger_Enum.notification) && NotificationListener.mayRuleStillBeActivatedForPendingNotifications(this)) // { // Miscellaneous.logEvent("i", "getsGreenLight()", "Rule \"" + getName() + "\" applies, has not flipped since its last execution, but may still be executed for other notifications.", 4); // return true; // } else if(hasTriggerOfType(Trigger.Trigger_Enum.calendarEvent) && CalendarReceiver.mayRuleStillBeActivatedForPendingCalendarEvents(this)) { Miscellaneous.logEvent("i", "getsGreenLight()", "Rule \"" + getName() + "\" applies, has not flipped since its last execution, but may still be executed for other calendar events.", 4); return true; } else Miscellaneous.logEvent("i", "getsGreenLight()", "Rule \"" + getName() + "\" has not flipped since its last execution.", 4); } else Miscellaneous.logEvent("i", "getsGreenLight()", "Rule \"" + getName() + "\" does not apply.", 4); return false; } public boolean applies(Context context) { if(AutomationService.getInstance() == null) { Miscellaneous.logEvent("i", "RuleCheck", "Automation service not running. Rule \"" + getName() + "\" cannot apply.", 3); return false; } if(this.ruleActive) { for(Trigger oneTrigger : this.getTriggerSet()) { if (!oneTrigger.applies(null, context)) return false; } Miscellaneous.logEvent("i", String.format(context.getResources().getString(R.string.ruleCheckOf), this.getName()), String.format("Rule \"%1$s\" generally applies currently. Checking if it's really due, yet will be done separately.", this.getName()), 3); return true; } Miscellaneous.logEvent("i", String.format(context.getResources().getString(R.string.ruleCheckOf), this.getName()), String.format(context.getResources().getString(R.string.ruleIsDeactivatedCantApply), this.getName()), 4); return false; } /** * This is actually a function of the class Trigger, but Rule is already distinguished by flavors, Trigger is not. * Hence it is here. * @param oneTrigger * @return */ boolean checkActivityDetection(Trigger oneTrigger) { if (ActivityDetectionReceiver.getActivityDetectionLastResult() != null) { boolean found = false; for (DetectedActivity oneDetectedActivity : ActivityDetectionReceiver.getActivityDetectionLastResult().getProbableActivities()) { if (oneDetectedActivity.getType() == oneTrigger.getActivityDetectionType()) found = true; } if (!found) { Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getName()), String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleDoesntApplyActivityNotPresent), getName(), ActivityDetectionReceiver.getDescription(oneTrigger.getActivityDetectionType())), 3); return false; } else { for (DetectedActivity oneDetectedActivity : ActivityDetectionReceiver.getActivityDetectionLastResult().getProbableActivities()) { if (oneDetectedActivity.getType() == oneTrigger.getActivityDetectionType() && oneDetectedActivity.getConfidence() < Settings.activityDetectionRequiredProbability) { Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getName()), String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleDoesntApplyActivityGivenButTooLowProbability), getName(), ActivityDetectionReceiver.getDescription(oneDetectedActivity.getType()), String.valueOf(oneDetectedActivity.getConfidence()), String.valueOf(Settings.activityDetectionRequiredProbability)), 3); return false; } } } } return true; } private class ActivateRuleTask extends AsyncTask { boolean wasActivated = false; @Override protected Void doInBackground(Object... params) { // Miscellaneous.logEvent("i", "Rule", ((Context) params[0]).getResources().getString(R.string.usingNewThreadForRuleExecution), 5); Thread.setDefaultUncaughtExceptionHandler(Miscellaneous.uncaughtExceptionHandler); // without this line the debugger will - for some reason - skip all breakpoints in this class if(android.os.Debug.isDebuggerConnected()) android.os.Debug.waitForDebugger(); if (Looper.myLooper() == null) Looper.prepare(); setLastExecution(Calendar.getInstance()); wasActivated = activateInternally((AutomationService)params[0]); return null; } @Override protected void onProgressUpdate(String... messages) { AutomationService service = AutomationService.getInstance(); service.speak(messages[0], false); if(Settings.showToasts) Toast.makeText(service, messages[0], Toast.LENGTH_LONG).show(); super.onProgressUpdate(messages); } @Override protected void onPostExecute(Void result) { /* Only update if the rules was actually executed. Became necessary for the notification trigger. If a user created a rule with a notification trigger and this app creates a notification itself this will otherwise end in an infinite loop. */ if(wasActivated) { // setLastExecution(Calendar.getInstance()); AutomationService.updateNotification(); ActivityMainScreen.updateMainScreen(); super.onPostExecute(result); } } /** * Will activate the rule. Should be called by a separate execution thread * @param automationService */ protected boolean activateInternally(AutomationService automationService) { boolean isActuallyToggleable = isActuallyToggable(); // boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); boolean doToggle = ruleToggle && isActuallyToggleable; String message; if(!doToggle) message = String.format(automationService.getResources().getString(R.string.ruleActivate), Rule.this.getName()); else message = String.format(automationService.getResources().getString(R.string.ruleActivateToggle), Rule.this.getName()); Miscellaneous.logEvent("i", "Rule", message, 2); if(Settings.startNewThreadForRuleActivation) publishProgress(message); /* Make a note of Rule/CalendarEvent executed combinations */ if(Rule.this.hasTriggerOfType(Trigger.Trigger_Enum.calendarEvent)) { for(CalendarReceiver.CalendarEvent event : CalendarReceiver.getApplyingCalendarEvents(Rule.this)) { if(!CalendarReceiver.hasEventBeenUsedInRule(Rule.this, event)) { /* Record only the first calendar event that matched because the rule may be executed once for every matching event. */ Miscellaneous.logEvent("i", "Rule", "Executing this rule run for calender event: " + event, 5); CalendarReceiver.addUsedPair(new CalendarReceiver.RuleEventPair(Rule.this, event)); break; } } } /* Run actions one after another */ for(int i = 0; i< Rule.this.getActionSet().size(); i++) { try { Rule.this.getActionSet().get(i).run(automationService, doToggle); } catch(Exception e) { Miscellaneous.logEvent("e", "RuleExecution", "Error running action of rule " + Rule.this.getName() + ": " + Log.getStackTraceString(e), 1); } } // Keep log of last x rule activations (Settings) try { Rule.ruleRunHistory.add(0, Rule.this); // add at beginning for better visualization Rule.lastActivatedRuleActivationTime = new Date(); while(ruleRunHistory.size() > Settings.rulesThatHaveBeenRanHistorySize) ruleRunHistory.remove(ruleRunHistory.size()-1); String history = ""; for(Rule rule : ruleRunHistory) history += rule.getName() + ", "; if(history.length() > 0) history = history.substring(0, history.length()-2); Miscellaneous.logEvent("i", "Rule history", "Most recent first: " + history, 4); } catch(Exception e) { Miscellaneous.logEvent("e", "Rule history error", Log.getStackTraceString(e), 3); } Miscellaneous.logEvent("i", "Rule", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleActivationComplete), Rule.this.getName()), 2); return true; } } public void activate(AutomationService automationService, boolean force) { ActivateRuleTask task = new ActivateRuleTask(); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, automationService, force); else task.execute(automationService, force); } public static ArrayList findRuleCandidates(Trigger.Trigger_Enum triggerType) { ArrayList ruleCandidates = new ArrayList(); for(Rule oneRule : ruleCollection) { innerLoop: for(Trigger oneTrigger : oneRule.getTriggerSet()) { if(oneTrigger.getTriggerType().equals(triggerType)) { ruleCandidates.add(oneRule); break innerLoop; // we don't need to check the other triggers in the same rule } } } return ruleCandidates; } public static ArrayList findRuleCandidates(Action.Action_Enum actionType) { ArrayList ruleCandidates = new ArrayList(); for(Rule oneRule : ruleCollection) { innerloop: for(Action oneAction : oneRule.getActionSet()) { if(oneAction.getAction().equals(actionType)) { ruleCandidates.add(oneRule); break innerloop; // we don't need to check the other actions in the same rule } } } return ruleCandidates; } public static ArrayList findRuleCandidatesByPoi(PointOfInterest searchPoi, boolean triggerParameter) { Miscellaneous.logEvent("i", "RuleSearch", "Searching for rules referencing POI " + searchPoi.getName() + ". Total size of ruleset: " + String.valueOf(ruleCollection.size()), 4); ArrayList ruleCandidates = new ArrayList(); for(int i=0; i findRuleCandidatesByPoi(PointOfInterest searchPoi) { ArrayList ruleCandidates = new ArrayList(); for(Rule oneRule : ruleCollection) { innerloop: for(Trigger oneTrigger : oneRule.getTriggerSet()) { if(oneTrigger.getTriggerType() == Trigger.Trigger_Enum.pointOfInterest) { if(oneTrigger.getPointOfInterest() != null && oneTrigger.getPointOfInterest().equals(searchPoi)) // != null to exclude those who are referring all locations ("entering any location") { ruleCandidates.add(oneRule); break innerloop; //if the poi is found we don't need to search the other triggers in the same rule } } } } return ruleCandidates; } public static ArrayList findRuleCandidatesByTriggerProfile(Profile profile) { ArrayList ruleCandidates = new ArrayList(); for(Rule oneRule : ruleCollection) { innerloop: for(Trigger oneTrigger : oneRule.getTriggerSet()) { if(oneTrigger.getTriggerType() == Trigger.Trigger_Enum.profileActive) { String profileName = oneTrigger.getTriggerParameter2().split(triggerParameter2Split)[0]; if(profileName.equals(profile.getName())) { ruleCandidates.add(oneRule); break innerloop; //if the profile is found we don't need to search the other triggers in the same rule } } } } return ruleCandidates; } public static ArrayList findRuleCandidatesByActionProfile(Profile profile) { ArrayList ruleCandidates = new ArrayList(); for(Rule oneRule : ruleCollection) { innerloop: for(Action oneAction : oneRule.getActionSet()) { if(oneAction.getAction() == Action.Action_Enum.changeSoundProfile) { if(oneAction.getParameter2().equals(profile.getOldName())) // != null to exclude those who are referring all locations ("entering any location") { ruleCandidates.add(oneRule); break innerloop; //if the profile is found we don't need to search the other triggers in the same rule } } } } return ruleCandidates; } public static boolean isAnyRuleUsing(Trigger.Trigger_Enum triggerType) { for(Rule rule: ruleCollection) { if(rule.isRuleActive()) { for(Trigger trigger : rule.getTriggerSet()) { if(trigger.getTriggerType().equals(triggerType)) { Miscellaneous.logEvent("i", "Rule->isAnyRuleUsing()", String.format(Miscellaneous.getAnyContext().getString(R.string.atLeastRuleXisUsingY), rule.getName(), triggerType.getFullName(Miscellaneous.getAnyContext())), 5); return true; } } } } return false; } public static boolean isAnyRuleUsing(Action.Action_Enum actionType) { for(Rule rule: ruleCollection) { if(rule.isRuleActive()) { for(Action action : rule.getActionSet()) { if(action.getAction().equals(actionType)) { Miscellaneous.logEvent("i", "Rule->isAnyRuleUsing()", String.format(Miscellaneous.getAnyContext().getString(R.string.atLeastRuleXisUsingY), rule.getName(), actionType.getFullName(Miscellaneous.getAnyContext())), 5); return true; } } } } return false; } @Override public int compareTo(Rule another) { return this.getName().compareTo(another.getName()); } public boolean haveEnoughPermissions() { return ActivityPermissions.havePermissionsForRule(this, Miscellaneous.getAnyContext()); } public static Rule getByName(String ruleName) { for(Rule r : Rule.getRuleCollection()) { if(r.getName().equals(ruleName)) return r; } return null; } @Override public boolean equals(@Nullable Object obj) { return this.getName().equals(((Rule)obj).getName()); } public boolean hasTriggerOfType(Trigger.Trigger_Enum queryType) { for(Trigger t : getTriggerSet()) { if(t.getTriggerType().equals(queryType)) return true; } return false; } public boolean hasActionOfType(Action.Action_Enum queryType) { for(Action a : getActionSet()) { if(a.getAction().equals(queryType)) return true; } return false; } public int getAmountOfTriggersForType(Trigger.Trigger_Enum type) { int amount = 0; for(Trigger t : getTriggerSet()) { if(t.getTriggerType().equals(type)) amount++; } return amount; } public int getAmountOfActionsForType(Action.Action_Enum type) { int amount = 0; for(Action a : getActionSet()) { if(a.getAction().equals(type)) amount++; } return amount; } }