From 06a6651fae2a1e3ff52e0bca0d4141ed76f2a55a Mon Sep 17 00:00:00 2001 From: jens Date: Sat, 13 Nov 2021 02:17:36 +0100 Subject: [PATCH] Reoccuring time trigger --- .../java/com/jens/automation2/Rule.java | 29 ++++- .../java/com/jens/automation2/Rule.java | 109 ++++++++++++++---- .../java/com/jens/automation2/Rule.java | 109 ++++++++++++++---- .../java/com/jens/automation2/Action.java | 12 ++ .../jens/automation2/ActivityManageRule.java | 5 + .../java/com/jens/automation2/Trigger.java | 49 ++++++-- .../jens/automation2/XmlFileInterface.java | 4 + .../receivers/DateTimeListener.java | 5 +- app/src/main/res/values/strings.xml | 2 +- 9 files changed, 268 insertions(+), 56 deletions(-) diff --git a/app/src/apkFlavor/java/com/jens/automation2/Rule.java b/app/src/apkFlavor/java/com/jens/automation2/Rule.java index ef093a6..23bba17 100644 --- a/app/src/apkFlavor/java/com/jens/automation2/Rule.java +++ b/app/src/apkFlavor/java/com/jens/automation2/Rule.java @@ -988,6 +988,32 @@ public class Rule implements Comparable } } + public boolean haveTriggersReallyChanged(Object triggeringObject) + { + boolean returnValue = false; + + try + { + for(int i=0; i < triggerSet.size(); i++) + { + Trigger t = (Trigger) triggerSet.get(i); + + if(t.hasStateRecentlyNotApplied(triggeringObject)) + { + Miscellaneous.logEvent("i", "Rule", "Rule \"" + getName() + "\" has trigger that flipped: " + t.toString(), 4); + returnValue = true; // only 1 trigger needs to have flipped recently + } + } + + return returnValue; + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "Rule", "Error while checking if rule \"" + getName() + "\" haveTriggersReallyChanged(): " + Log.getStackTraceString(e), 1); + return false; + } + } + /** * Will activate the rule. Should be called by a separate execution thread * @param automationService @@ -998,8 +1024,9 @@ public class Rule implements Comparable boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); boolean doToggle = ruleToggle && isActuallyToggable; + boolean triggersApplyAnew = haveTriggersReallyChanged(new Date()); - if(notLastActive || force || doToggle) + if(notLastActive || force || doToggle || triggersApplyAnew) { String message; if(!doToggle) diff --git a/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java b/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java index 210615f..e2c1040 100644 --- a/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java +++ b/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java @@ -1,11 +1,14 @@ package com.jens.automation2; import android.annotation.SuppressLint; +import android.app.Notification; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.os.AsyncTask; import android.os.Build; +import android.os.Bundle; import android.os.Looper; +import android.os.Parcelable; import android.service.notification.StatusBarNotification; import android.telephony.TelephonyManager; import android.util.Log; @@ -32,6 +35,11 @@ import static com.jens.automation2.Trigger.triggerParameter2Split; import static com.jens.automation2.receivers.NotificationListener.EXTRA_TEXT; import static com.jens.automation2.receivers.NotificationListener.EXTRA_TITLE; +import androidx.core.app.NotificationCompat; + +import org.apache.commons.lang3.StringUtils; + + public class Rule implements Comparable { private static ArrayList ruleCollection = new ArrayList(); @@ -762,13 +770,13 @@ public class Rule implements Comparable String myApp = params[0]; String myTitleDir = params[1]; - String myTitle = params[2]; + String requiredTitle = params[2]; String myTextDir = params[3]; - String myText; + String requiredText; if (params.length >= 5) - myText = params[4]; + requiredText = params[4]; else - myText = ""; + requiredText = ""; if(oneTrigger.getTriggerParameter()) { @@ -780,38 +788,65 @@ public class Rule implements Comparable { if(getLastExecution() == null || sbn.getPostTime() > this.lastExecution.getTimeInMillis()) { - String app = sbn.getPackageName(); - String title = sbn.getNotification().extras.getString(EXTRA_TITLE); - String text = sbn.getNotification().extras.getString(EXTRA_TEXT); + String notificationApp = sbn.getPackageName(); + String notificationTitle = null; + String notificationText = null; - Miscellaneous.logEvent("i", "NotificationCheck", "Checking if this notification matches our rule " + this.getName() + ". App: " + app + ", title: " + title + ", text: " + text, 5); + Miscellaneous.logEvent("i", "NotificationCheck", "Checking if this notification matches our rule " + this.getName() + ". App: " + notificationApp + ", title: " + notificationTitle + ", text: " + notificationText, 5); if (!myApp.equals("-1")) { - if (!app.equalsIgnoreCase(myApp)) + if (!notificationApp.equalsIgnoreCase(myApp)) { Miscellaneous.logEvent("i", "NotificationCheck", "Notification app name does not match rule.", 5); continue; } } - - if (myTitle.length() > 0) + else { - if (!Miscellaneous.compare(myTitleDir, myTitle, title)) + if(myApp.equals(BuildConfig.APPLICATION_ID)) + { + return false; + } + } + + /* + If there are multiple notifications ("stacked") title or text might be null: + https://stackoverflow.com/questions/28047767/notificationlistenerservice-not-reading-text-of-stacked-notifications + */ + + Bundle extras = sbn.getNotification().extras; + + // T I T L E + if (extras.containsKey(EXTRA_TITLE)) + notificationTitle = sbn.getNotification().extras.getString(EXTRA_TITLE); + + if (!StringUtils.isEmpty(requiredTitle)) + { + if (!Miscellaneous.compare(myTitleDir, requiredTitle, notificationTitle)) { Miscellaneous.logEvent("i", "NotificationCheck", "Notification title does not match rule.", 5); continue; } } + else + Miscellaneous.logEvent("i", "NotificationCheck", "A required title for a notification trigger was not specified.", 5); - if (myText.length() > 0) + // T E X T + + if (extras.containsKey(EXTRA_TEXT)) + notificationText = sbn.getNotification().extras.getString(EXTRA_TEXT); + + if (!StringUtils.isEmpty(requiredText)) { - if (!Miscellaneous.compare(myTextDir, myText, text)) + if (!Miscellaneous.compare(myTextDir, requiredText, notificationText)) { Miscellaneous.logEvent("i", "NotificationCheck", "Notification text does not match rule.", 5); continue; } } + else + Miscellaneous.logEvent("i", "NotificationCheck", "A required text for a notification trigger was not specified.", 5); foundMatch = true; break; @@ -838,16 +873,23 @@ public class Rule implements Comparable if (!app.equalsIgnoreCase(myApp)) return false; } - - if (myTitle.length() > 0) + else { - if (!Miscellaneous.compare(myTitleDir, title, myTitle)) + if(myApp.equals(BuildConfig.APPLICATION_ID)) + { + return false; + } + } + + if (requiredTitle.length() > 0) + { + if (!Miscellaneous.compare(myTitleDir, title, requiredTitle)) return false; } - if (myText.length() > 0) + if (requiredText.length() > 0) { - if (!Miscellaneous.compare(myTextDir, text, myText)) + if (!Miscellaneous.compare(myTextDir, text, requiredText)) return false; } } @@ -915,6 +957,32 @@ public class Rule implements Comparable } } + public boolean haveTriggersReallyChanged(Object triggeringObject) + { + boolean returnValue = false; + + try + { + for(int i=0; i < triggerSet.size(); i++) + { + Trigger t = (Trigger) triggerSet.get(i); + + if(t.hasStateRecentlyNotApplied(triggeringObject)) + { + Miscellaneous.logEvent("i", "Rule", "Rule \"" + getName() + "\" has trigger that flipped: " + t.toString(), 4); + returnValue = true; // only 1 trigger needs to have flipped recently + } + } + + return returnValue; + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "Rule", "Error while checking if rule \"" + getName() + "\" haveTriggersReallyChanged(): " + Log.getStackTraceString(e), 1); + return false; + } + } + /** * Will activate the rule. Should be called by a separate execution thread * @param automationService @@ -925,8 +993,9 @@ public class Rule implements Comparable boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); boolean doToggle = ruleToggle && isActuallyToggable; + boolean triggersApplyAnew = haveTriggersReallyChanged(new Date()); - if(notLastActive || force || doToggle) + if(notLastActive || force || doToggle || triggersApplyAnew) { String message; if(!doToggle) diff --git a/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java b/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java index de35808..d166406 100644 --- a/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java +++ b/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java @@ -1,11 +1,14 @@ package com.jens.automation2; import android.annotation.SuppressLint; +import android.app.Notification; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.os.AsyncTask; import android.os.Build; +import android.os.Bundle; import android.os.Looper; +import android.os.Parcelable; import android.service.notification.StatusBarNotification; import android.telephony.TelephonyManager; import android.util.Log; @@ -34,6 +37,11 @@ import static com.jens.automation2.Trigger.triggerParameter2Split; import static com.jens.automation2.receivers.NotificationListener.EXTRA_TEXT; import static com.jens.automation2.receivers.NotificationListener.EXTRA_TITLE; +import androidx.core.app.NotificationCompat; + +import org.apache.commons.lang3.StringUtils; + + public class Rule implements Comparable { private static ArrayList ruleCollection = new ArrayList(); @@ -793,13 +801,13 @@ public class Rule implements Comparable String myApp = params[0]; String myTitleDir = params[1]; - String myTitle = params[2]; + String requiredTitle = params[2]; String myTextDir = params[3]; - String myText; + String requiredText; if (params.length >= 5) - myText = params[4]; + requiredText = params[4]; else - myText = ""; + requiredText = ""; if(oneTrigger.getTriggerParameter()) { @@ -811,38 +819,65 @@ public class Rule implements Comparable { if(getLastExecution() == null || sbn.getPostTime() > this.lastExecution.getTimeInMillis()) { - String app = sbn.getPackageName(); - String title = sbn.getNotification().extras.getString(EXTRA_TITLE); - String text = sbn.getNotification().extras.getString(EXTRA_TEXT); + String notificationApp = sbn.getPackageName(); + String notificationTitle = null; + String notificationText = null; - Miscellaneous.logEvent("i", "NotificationCheck", "Checking if this notification matches our rule " + this.getName() + ". App: " + app + ", title: " + title + ", text: " + text, 5); + Miscellaneous.logEvent("i", "NotificationCheck", "Checking if this notification matches our rule " + this.getName() + ". App: " + notificationApp + ", title: " + notificationTitle + ", text: " + notificationText, 5); if (!myApp.equals("-1")) { - if (!app.equalsIgnoreCase(myApp)) + if (!notificationApp.equalsIgnoreCase(myApp)) { Miscellaneous.logEvent("i", "NotificationCheck", "Notification app name does not match rule.", 5); continue; } } - - if (myTitle.length() > 0) + else { - if (!Miscellaneous.compare(myTitleDir, myTitle, title)) + if(myApp.equals(BuildConfig.APPLICATION_ID)) + { + return false; + } + } + + /* + If there are multiple notifications ("stacked") title or text might be null: + https://stackoverflow.com/questions/28047767/notificationlistenerservice-not-reading-text-of-stacked-notifications + */ + + Bundle extras = sbn.getNotification().extras; + + // T I T L E + if (extras.containsKey(EXTRA_TITLE)) + notificationTitle = sbn.getNotification().extras.getString(EXTRA_TITLE); + + if (!StringUtils.isEmpty(requiredTitle)) + { + if (!Miscellaneous.compare(myTitleDir, requiredTitle, notificationTitle)) { Miscellaneous.logEvent("i", "NotificationCheck", "Notification title does not match rule.", 5); continue; } } + else + Miscellaneous.logEvent("i", "NotificationCheck", "A required title for a notification trigger was not specified.", 5); - if (myText.length() > 0) + // T E X T + + if (extras.containsKey(EXTRA_TEXT)) + notificationText = sbn.getNotification().extras.getString(EXTRA_TEXT); + + if (!StringUtils.isEmpty(requiredText)) { - if (!Miscellaneous.compare(myTextDir, myText, text)) + if (!Miscellaneous.compare(myTextDir, requiredText, notificationText)) { Miscellaneous.logEvent("i", "NotificationCheck", "Notification text does not match rule.", 5); continue; } } + else + Miscellaneous.logEvent("i", "NotificationCheck", "A required text for a notification trigger was not specified.", 5); foundMatch = true; break; @@ -869,16 +904,23 @@ public class Rule implements Comparable if (!app.equalsIgnoreCase(myApp)) return false; } - - if (myTitle.length() > 0) + else { - if (!Miscellaneous.compare(myTitleDir, title, myTitle)) + if(myApp.equals(BuildConfig.APPLICATION_ID)) + { + return false; + } + } + + if (requiredTitle.length() > 0) + { + if (!Miscellaneous.compare(myTitleDir, title, requiredTitle)) return false; } - if (myText.length() > 0) + if (requiredText.length() > 0) { - if (!Miscellaneous.compare(myTextDir, text, myText)) + if (!Miscellaneous.compare(myTextDir, text, requiredText)) return false; } } @@ -946,6 +988,32 @@ public class Rule implements Comparable } } + public boolean haveTriggersReallyChanged(Object triggeringObject) + { + boolean returnValue = false; + + try + { + for(int i=0; i < triggerSet.size(); i++) + { + Trigger t = (Trigger) triggerSet.get(i); + + if(t.hasStateRecentlyNotApplied(triggeringObject)) + { + Miscellaneous.logEvent("i", "Rule", "Rule \"" + getName() + "\" has trigger that flipped: " + t.toString(), 4); + returnValue = true; // only 1 trigger needs to have flipped recently + } + } + + return returnValue; + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "Rule", "Error while checking if rule \"" + getName() + "\" haveTriggersReallyChanged(): " + Log.getStackTraceString(e), 1); + return false; + } + } + /** * Will activate the rule. Should be called by a separate execution thread * @param automationService @@ -956,8 +1024,9 @@ public class Rule implements Comparable boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); boolean doToggle = ruleToggle && isActuallyToggable; + boolean triggersApplyAnew = haveTriggersReallyChanged(new Date()); - if(notLastActive || force || doToggle) + if(notLastActive || force || doToggle || triggersApplyAnew) { String message; if(!doToggle) diff --git a/app/src/main/java/com/jens/automation2/Action.java b/app/src/main/java/com/jens/automation2/Action.java index fa16fe1..215a085 100644 --- a/app/src/main/java/com/jens/automation2/Action.java +++ b/app/src/main/java/com/jens/automation2/Action.java @@ -13,6 +13,8 @@ import java.util.Locale; public class Action { + Rule parentRule = null; + public static final String actionParameter2Split = "ap2split"; public static final String intentPairSeperator = "intPairSplit"; public static final String vibrateSeparator = ","; @@ -273,6 +275,16 @@ public class Action return returnString.toString(); } + public Rule getParentRule() + { + return parentRule; + } + + public void setParentRule(Rule parentRule) + { + this.parentRule = parentRule; + } + public static CharSequence[] getActionTypesAsArray() { ArrayList actionTypesList = new ArrayList(); diff --git a/app/src/main/java/com/jens/automation2/ActivityManageRule.java b/app/src/main/java/com/jens/automation2/ActivityManageRule.java index 73bfd7d..c69d9e8 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManageRule.java +++ b/app/src/main/java/com/jens/automation2/ActivityManageRule.java @@ -403,6 +403,11 @@ public class ActivityManageRule extends Activity ruleToEdit.setName(etRuleName.getText().toString()); ruleToEdit.setRuleActive(chkRuleActive.isChecked()); ruleToEdit.setRuleToggle(chkRuleToggle.isChecked()); + + for(Trigger t : ruleToEdit.getTriggerSet()) + t.setParentRule(ruleToEdit); + for(Action a : ruleToEdit.getActionSet()) + a.setParentRule(ruleToEdit); } private void loadVariablesIntoGui() diff --git a/app/src/main/java/com/jens/automation2/Trigger.java b/app/src/main/java/com/jens/automation2/Trigger.java index d3b5704..72442a5 100644 --- a/app/src/main/java/com/jens/automation2/Trigger.java +++ b/app/src/main/java/com/jens/automation2/Trigger.java @@ -37,11 +37,38 @@ public class Trigger } catch(Exception e) { - Miscellaneous.logEvent("Error while checking if rule " + getParentRule().getName() + " applies. Error occured in trigger " + this.toString() + "." + Diverse.lineSeparator + Diverse.getStackTraceAsString(e), 1); + Miscellaneous.logEvent("e", "Trigger", "Error while checking if rule " + getParentRule().getName() + " applies. Error occured in trigger " + this.toString() + "." + Miscellaneous.lineSeparator + Log.getStackTraceString(e), 1); return false; } } + public boolean hasStateRecentlyNotApplied(Object triggeringObject) + { + // nur mit einem Trigger? + + // door -> was state different in previous step + + try + { + switch(getTriggerType()) + { + case timeFrame: + if(!checkDateTime(triggeringObject, true)) + return false; + break; + default: + break; + } + + return true; + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "Trigger", "Error while checking if rule " + getParentRule().getName() + " applies. Error occured in trigger " + this.toString() + "." + Miscellaneous.lineSeparator + Log.getStackTraceString(e), 1); + return false; + } + } + public boolean checkDateTime(Object triggeringObject, boolean checkifStateChangedSinceLastRuleExecution) { /* @@ -101,7 +128,7 @@ public class Trigger { if(!isSupposedToRepeatSinceLastExecution(compareCal)) { - Miscellaneous.logEvent("TimeFrame: Trigger of rule " + this.getParentRule().getName() + " applies, but repeated execution is not due, yet.", 4); + Miscellaneous.logEvent("i", "TimeFrame", "TimeFrame: Trigger of rule " + this.getParentRule().getName() + " applies, but repeated execution is not due, yet.", 4); return false; } } @@ -116,14 +143,14 @@ public class Trigger */ if( - getParentRule().getLastExecutionTimestamp().get(Calendar.YEAR) == calNow.get(Calendar.YEAR) + getParentRule().getLastExecution().get(Calendar.YEAR) == calNow.get(Calendar.YEAR) && - getParentRule().getLastExecutionTimestamp().get(Calendar.MONTH) == calNow.get(Calendar.MONTH) + getParentRule().getLastExecution().get(Calendar.MONTH) == calNow.get(Calendar.MONTH) && - getParentRule().getLastExecutionTimestamp().get(Calendar.DAY_OF_MONTH) == calNow.get(Calendar.DAY_OF_MONTH) + getParentRule().getLastExecution().get(Calendar.DAY_OF_MONTH) == calNow.get(Calendar.DAY_OF_MONTH) ) { - Miscellaneous.logEvent("TimeFrame: Trigger of rule " + this.getParentRule().getName() + " applies, but it was already executed today.", 4); + Miscellaneous.logEvent("i", "TimeFrame", "TimeFrame: Trigger of rule " + this.getParentRule().getName() + " applies, but it was already executed today.", 4); return false; } } @@ -170,11 +197,11 @@ public class Trigger */ if( - getParentRule().getLastExecutionTimestamp().get(Calendar.YEAR) == calNow.get(Calendar.YEAR) + getParentRule().getLastExecution().get(Calendar.YEAR) == calNow.get(Calendar.YEAR) && - getParentRule().getLastExecutionTimestamp().get(Calendar.MONTH) == calNow.get(Calendar.MONTH) + getParentRule().getLastExecution().get(Calendar.MONTH) == calNow.get(Calendar.MONTH) && - getParentRule().getLastExecutionTimestamp().get(Calendar.DAY_OF_MONTH) == calNow.get(Calendar.DAY_OF_MONTH) + getParentRule().getLastExecution().get(Calendar.DAY_OF_MONTH) == calNow.get(Calendar.DAY_OF_MONTH) ) { Miscellaneous.logEvent("i", "Trigger", "TimeFrame: Trigger of rule " + this.getParentRule().getName() + " applies, but it was already executed today.", 4); @@ -255,7 +282,7 @@ public class Trigger boolean isSupposedToRepeatSinceLastExecution(Calendar now) { TimeFrame tf = new TimeFrame(getTriggerParameter2()); - Calendar lastExec = getParentRule().getLastExecutionTimestamp(); + Calendar lastExec = getParentRule().getLastExecution(); // the simple stuff: @@ -560,7 +587,7 @@ public class Trigger String repeat = ", no repetition"; if(this.getTimeFrame().getRepetition() > 0) - repeat = ", " + String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.repeatEveryXsecondsWithVariable), this.getTimeFrame().getRepetition()); + repeat = ", " + String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.repeatEveryXsecondsWithVariable), String.valueOf(this.getTimeFrame().getRepetition())); returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.triggerTimeFrame) + ": " + this.getTimeFrame().getTriggerTimeStart().toString() + " " + Miscellaneous.getAnyContext().getResources().getString(R.string.until) + " " + this.getTimeFrame().getTriggerTimeStop().toString() + " on days " + this.getTimeFrame().getDayList().toString() + repeat); break; diff --git a/app/src/main/java/com/jens/automation2/XmlFileInterface.java b/app/src/main/java/com/jens/automation2/XmlFileInterface.java index 2b170b7..5c46930 100644 --- a/app/src/main/java/com/jens/automation2/XmlFileInterface.java +++ b/app/src/main/java/com/jens/automation2/XmlFileInterface.java @@ -764,6 +764,8 @@ public class XmlFileInterface try { newRule.setTriggerSet(readTriggerCollection(parser)); + for(Trigger t : newRule.getTriggerSet()) + t.setParentRule(newRule); } catch (XmlPullParserException e) { @@ -779,6 +781,8 @@ public class XmlFileInterface try { newRule.setActionSet(readActionCollection(parser)); + for(Action a : newRule.getActionSet()) + a.setParentRule(newRule); } catch (XmlPullParserException e) { diff --git a/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java b/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java index fd89f29..3c4cfc1 100644 --- a/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java +++ b/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java @@ -227,7 +227,7 @@ public class DateTimeListener extends BroadcastReceiver implements AutomationLis } catch(Exception e) { - Miscellaneous.logEvent("e", "DateTimeListener","Error checking anything for rule " + oneRule.toString() + " needs to be added to candicates list: " + Diverse.getStackTraceAsString(e), 1); + Miscellaneous.logEvent("e", "DateTimeListener","Error checking anything for rule " + oneRule.toString() + " needs to be added to candicates list: " + Log.getStackTraceString(e), 1); } } } @@ -268,9 +268,8 @@ public class DateTimeListener extends BroadcastReceiver implements AutomationLis SimpleDateFormat sdf = new SimpleDateFormat("E dd.MM.yyyy HH:mm"); Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(scheduleCandidate); + calendar.setTimeInMillis(scheduleCandidate.time.getTimeInMillis()); Miscellaneous.logEvent("i", "AlarmManager", "Chose " + sdf.format(calendar.getTime()) + " as next scheduled alarm.", 4); - } public static void clearAlarms() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18f1888..6d9f0d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -704,6 +704,6 @@ Your rules required permissions which cannot be requested from this installed flavor of Automation. If you do not choose a specific app, but choose \"Any app\" notifications from Automation will be ignored to avoid loops. Repeat every x seconds - repeat every %1$o seconds + repeat every %1$s seconds You need to enter a positive non-decimal value for reptition time. \ No newline at end of file