diff --git a/app/src/apkFlavor/AndroidManifest.xml b/app/src/apkFlavor/AndroidManifest.xml index acac67e7..73643316 100644 --- a/app/src/apkFlavor/AndroidManifest.xml +++ b/app/src/apkFlavor/AndroidManifest.xml @@ -132,7 +132,7 @@ android:scheme="package" />--> - + diff --git a/app/src/apkFlavor/java/com/jens/automation2/Rule.java b/app/src/apkFlavor/java/com/jens/automation2/Rule.java index ef093a68..23bba173 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/AndroidManifest.xml b/app/src/fdroidFlavor/AndroidManifest.xml index b43e4ff8..3037baed 100644 --- a/app/src/fdroidFlavor/AndroidManifest.xml +++ b/app/src/fdroidFlavor/AndroidManifest.xml @@ -129,7 +129,7 @@ android:scheme="package" />--> - + diff --git a/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java b/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java index 210615f6..e2c1040b 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/AndroidManifest.xml b/app/src/googlePlayFlavor/AndroidManifest.xml index 71eee3e7..1c457831 100644 --- a/app/src/googlePlayFlavor/AndroidManifest.xml +++ b/app/src/googlePlayFlavor/AndroidManifest.xml @@ -123,7 +123,7 @@ android:scheme="package" />--> - + diff --git a/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java b/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java index de358084..d1664063 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 fa16fe13..215a085d 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/ActivityMainRules.java b/app/src/main/java/com/jens/automation2/ActivityMainRules.java index fa037e65..afcacbc8 100644 --- a/app/src/main/java/com/jens/automation2/ActivityMainRules.java +++ b/app/src/main/java/com/jens/automation2/ActivityMainRules.java @@ -19,7 +19,7 @@ import android.widget.TextView; import android.widget.Toast; import com.jens.automation2.AutomationService.serviceCommands; -import com.jens.automation2.receivers.AlarmListener; +import com.jens.automation2.receivers.DateTimeListener; import java.util.ArrayList; @@ -254,7 +254,7 @@ public class ActivityMainRules extends ActivityGeneric try { if(AutomationService.isMyServiceRunning(this)) - AlarmListener.reloadAlarms(); + DateTimeListener.reloadAlarms(); } catch(NullPointerException e) { diff --git a/app/src/main/java/com/jens/automation2/ActivityManageRule.java b/app/src/main/java/com/jens/automation2/ActivityManageRule.java index 73bfd7d0..c69d9e8c 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/ActivityManageTriggerTimeFrame.java b/app/src/main/java/com/jens/automation2/ActivityManageTriggerTimeFrame.java index c67edf7f..9bbf6066 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManageTriggerTimeFrame.java +++ b/app/src/main/java/com/jens/automation2/ActivityManageTriggerTimeFrame.java @@ -6,11 +6,16 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; import android.widget.RadioButton; import android.widget.TimePicker; import android.widget.Toast; +import org.apache.commons.lang3.StringUtils; + import java.sql.Time; +import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; @@ -18,8 +23,9 @@ public class ActivityManageTriggerTimeFrame extends Activity { Button bSaveTimeFrame; TimePicker startPicker, stopPicker; - CheckBox checkMonday, checkTuesday, checkWednesday, checkThursday, checkFriday, checkSaturday, checkSunday; + CheckBox checkMonday, checkTuesday, checkWednesday, checkThursday, checkFriday, checkSaturday, checkSunday, chkRepeat; RadioButton radioTimeFrameEntering, radioTimeFrameLeaving; + EditText etRepeatEvery; public static Trigger editedTimeFrameTrigger = null; @@ -44,7 +50,9 @@ public class ActivityManageTriggerTimeFrame extends Activity checkSunday = (CheckBox)findViewById(R.id.checkSunday); radioTimeFrameEntering = (RadioButton)findViewById(R.id.radioTimeFrameEntering); radioTimeFrameLeaving = (RadioButton)findViewById(R.id.radioTimeFrameLeaving); - + chkRepeat = (CheckBox)findViewById(R.id.chkRepeat); + etRepeatEvery = (EditText)findViewById(R.id.etRepeatEvery); + bSaveTimeFrame.setOnClickListener(new OnClickListener() { @Override @@ -92,11 +100,43 @@ public class ActivityManageTriggerTimeFrame extends Activity { Toast.makeText(getBaseContext(), getResources().getString(R.string.selectOneDay), Toast.LENGTH_LONG).show(); return; - } + } + + boolean goOn = false; + if(chkRepeat.isChecked()) + { + if(!StringUtils.isEmpty(etRepeatEvery.getText().toString())) + { + try + { + long value = Long.parseLong(etRepeatEvery.getText().toString()); + if(value > 0) + { + goOn = true; + } + } + catch(Exception e) + { + } + } + } + else + goOn = true; + + if(!goOn) + { + Toast.makeText(getBaseContext(), getResources().getString(R.string.enterRepetitionTime), Toast.LENGTH_LONG).show(); + return; + } if(editedTimeFrameTrigger.getTimeFrame() == null) + { // add new one - editedTimeFrameTrigger.setTimeFrame(new TimeFrame(startTime, stopTime, dayList)); + if(chkRepeat.isChecked()) + editedTimeFrameTrigger.setTimeFrame(new TimeFrame(startTime, stopTime, dayList, Long.parseLong(etRepeatEvery.getText().toString()))); + else + editedTimeFrameTrigger.setTimeFrame(new TimeFrame(startTime, stopTime, dayList, 0)); + } else { // edit one @@ -104,6 +144,11 @@ public class ActivityManageTriggerTimeFrame extends Activity editedTimeFrameTrigger.getTimeFrame().setTriggerTimeStop(stopTime); editedTimeFrameTrigger.getTimeFrame().getDayList().clear(); editedTimeFrameTrigger.getTimeFrame().setDayList(dayList); + + if(chkRepeat.isChecked()) + editedTimeFrameTrigger.getTimeFrame().setRepetition(Long.parseLong(etRepeatEvery.getText().toString())); + else + editedTimeFrameTrigger.getTimeFrame().setRepetition(0); } editedTimeFrameTrigger.setTriggerParameter(radioTimeFrameEntering.isChecked()); @@ -112,6 +157,15 @@ public class ActivityManageTriggerTimeFrame extends Activity finish(); } }); + + chkRepeat.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) + { + etRepeatEvery.setEnabled(isChecked); + } + }); if(editedTimeFrameTrigger.getTimeFrame() != null) loadVariableIntoGui(); @@ -158,6 +212,12 @@ public class ActivityManageTriggerTimeFrame extends Activity break; } } + + if(editedTimeFrameTrigger.getTimeFrame().getRepetition() > 0) + { + chkRepeat.setChecked(true); + etRepeatEvery.setText(String.valueOf(editedTimeFrameTrigger.getTimeFrame().getRepetition())); + } } } diff --git a/app/src/main/java/com/jens/automation2/Miscellaneous.java b/app/src/main/java/com/jens/automation2/Miscellaneous.java index bae4a4c8..0658b465 100644 --- a/app/src/main/java/com/jens/automation2/Miscellaneous.java +++ b/app/src/main/java/com/jens/automation2/Miscellaneous.java @@ -77,6 +77,8 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.sql.Time; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; @@ -1532,4 +1534,21 @@ public class Miscellaneous extends Service else*/ return PhoneNumberUtils.compare(number1, number2); } + + public static String formatDate(Date input) + { + DateFormat sdf = null; + SimpleDateFormat fallBackFormatter = new SimpleDateFormat(Settings.dateFormat); + + if(sdf == null && Settings.dateFormat != null) + sdf = new SimpleDateFormat(Settings.dateFormat); + + String formattedDate; + if(sdf != null) + formattedDate = sdf.format(input); + else + formattedDate = fallBackFormatter.format(input); + + return formattedDate; + } } \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java b/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java index 9921ba13..c36aefe9 100644 --- a/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java +++ b/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java @@ -5,7 +5,7 @@ import android.util.Log; import com.jens.automation2.location.CellLocationChangedReceiver; import com.jens.automation2.location.WifiBroadcastReceiver; -import com.jens.automation2.receivers.AlarmListener; +import com.jens.automation2.receivers.DateTimeListener; import com.jens.automation2.receivers.AutomationListenerInterface; import com.jens.automation2.receivers.BatteryReceiver; import com.jens.automation2.receivers.BluetoothReceiver; @@ -42,7 +42,7 @@ public class ReceiverCoordinator Class adClass = Class.forName("ActivityDetectionReceiver"); allImplementers = new Class[] { adClass, - AlarmListener.class, + DateTimeListener.class, BatteryReceiver.class, BluetoothReceiver.class, ConnectivityReceiver.class, @@ -59,7 +59,7 @@ public class ReceiverCoordinator // e.printStackTrace(); allImplementers = new Class[] { - AlarmListener.class, + DateTimeListener.class, BatteryReceiver.class, BluetoothReceiver.class, ConnectivityReceiver.class, @@ -155,7 +155,7 @@ public class ReceiverCoordinator BatteryReceiver.startBatteryReceiver(AutomationService.getInstance()); // startAlarmListener - AlarmListener.startAlarmListener(AutomationService.getInstance()); + DateTimeListener.startAlarmListener(AutomationService.getInstance()); TimeZoneListener.startTimeZoneListener(AutomationService.getInstance()); // startNoiseListener @@ -199,7 +199,7 @@ public class ReceiverCoordinator WifiBroadcastReceiver.stopWifiReceiver(); BatteryReceiver.stopBatteryReceiver(); TimeZoneListener.stopTimeZoneListener(); - AlarmListener.stopAlarmListener(AutomationService.getInstance()); + DateTimeListener.stopAlarmListener(AutomationService.getInstance()); NoiseListener.stopNoiseListener(); ProcessListener.stopProcessListener(AutomationService.getInstance()); diff --git a/app/src/main/java/com/jens/automation2/TimeFrame.java b/app/src/main/java/com/jens/automation2/TimeFrame.java index 757d85b8..a15059e0 100644 --- a/app/src/main/java/com/jens/automation2/TimeFrame.java +++ b/app/src/main/java/com/jens/automation2/TimeFrame.java @@ -6,70 +6,90 @@ import java.util.ArrayList; public class TimeFrame { // Defines a timeframe - private Time triggerTimeStart; - private Time triggerTimeStop; + protected Time triggerTimeStart; + protected Time triggerTimeStop; + protected long repetition; - private ArrayList dayList = new ArrayList(); - public ArrayList getDayList() + protected final static String separator = "/"; + + private ArrayList dayList = new ArrayList(); + public ArrayList getDayList() + { + return dayList; + } + public void setDayList(ArrayList dayList) + { + this.dayList = dayList; + } + public void setDayListFromString(String dayListString) + { +// Log.i("Parsing", "Full string: " + dayListString); + char[] dayListCharArray = dayListString.toCharArray(); + + dayList = new ArrayList(); + for(char item : dayListCharArray) { - return dayList; - } - public void setDayList(ArrayList dayList) - { - this.dayList = dayList; - } - public void setDayListFromString(String dayListString) - { -// Log.i("Parsing", "Full string: " + dayListString); - char[] dayListCharArray = dayListString.toCharArray(); - - dayList = new ArrayList(); - for(char item : dayListCharArray) - { // Log.i("Parsing", String.valueOf(item)); - dayList.add(Integer.parseInt(String.valueOf(item))); - } + dayList.add(Integer.parseInt(String.valueOf(item))); } + } - - public Time getTriggerTimeStart() - { - return triggerTimeStart; - } - public void setTriggerTimeStart(Time triggerTimeStart) - { - this.triggerTimeStart = triggerTimeStart; - } - public Time getTriggerTimeStop() - { - return triggerTimeStop; - } - public void setTriggerTimeStop(Time triggerTimeStop) - { - this.triggerTimeStop = triggerTimeStop; - } - - public TimeFrame (Time timeStart, Time timeEnd, ArrayList dayList2) - { - this.setTriggerTimeStart(timeStart); - this.setTriggerTimeStop(timeEnd); - this.setDayList(dayList2); - } - TimeFrame (String fileContent) - { - String[] dateArray = fileContent.split("/"); // example: timestart/timestop/days[int] - this.setTriggerTimeStart(Time.valueOf(dateArray[0])); - this.setTriggerTimeStop(Time.valueOf(dateArray[1])); - this.setDayListFromString(dateArray[2]); - } - @Override - public String toString() - { - String returnString = this.getTriggerTimeStart().toString() + "/" + this.getTriggerTimeStop().toString() + "/"; - - for(Integer oneDay : this.getDayList()) - returnString += String.valueOf(oneDay); - - return returnString; - } -} + public Time getTriggerTimeStart() + { + return triggerTimeStart; + } + public void setTriggerTimeStart(Time triggerTimeStart) + { + this.triggerTimeStart = triggerTimeStart; + } + + public Time getTriggerTimeStop() + { + return triggerTimeStop; + } + public void setTriggerTimeStop(Time triggerTimeStop) + { + this.triggerTimeStop = triggerTimeStop; + } + + public long getRepetition() + { + return repetition; + } + + public void setRepetition(long repetition) + { + this.repetition = repetition; + } + + public TimeFrame (Time timeStart, Time timeEnd, ArrayList dayList2, long repetition) + { + this.setTriggerTimeStart(timeStart); + this.setTriggerTimeStop(timeEnd); + this.setDayList(dayList2); + this.setRepetition(repetition); + } + + public TimeFrame (String fileContent) + { + String[] dateArray = fileContent.split(separator); // example: timestart/timestop/days[int]/repetition + this.setTriggerTimeStart(Time.valueOf(dateArray[0])); + this.setTriggerTimeStop(Time.valueOf(dateArray[1])); + this.setDayListFromString(dateArray[2]); + if(dateArray.length > 3) // may not exist in old config files + this.setRepetition(Long.parseLong(dateArray[3])); + } + + @Override + public String toString() + { + String returnString = this.getTriggerTimeStart().toString() + separator + this.getTriggerTimeStop().toString() + separator; + + for(Integer oneDay : this.getDayList()) + returnString += String.valueOf(oneDay); + + returnString += separator + String.valueOf(repetition); + + return returnString; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/Trigger.java b/app/src/main/java/com/jens/automation2/Trigger.java index 04e41983..72442a51 100644 --- a/app/src/main/java/com/jens/automation2/Trigger.java +++ b/app/src/main/java/com/jens/automation2/Trigger.java @@ -3,24 +3,315 @@ package com.jens.automation2; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.os.Build; +import android.util.Log; import androidx.annotation.RequiresApi; import com.jens.automation2.receivers.BluetoothReceiver; +import java.sql.Time; import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; public class Trigger { - /* + Rule parentRule = null; + + public boolean applies(Object triggeringObject) + { + try + { + switch(this.getTriggerType()) + { + case timeFrame: + if(!checkDateTime(triggeringObject, false)) + 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 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) + { + /* + * Use format known from Automation + * 07:30:00/17:30:00/23456/300 <-- last parameter is optional: repetition in seconds + * Also required: inside or outside that interval + */ + + Date triggeringTime; + if(triggeringObject instanceof Date) + triggeringTime = (Date)triggeringObject; + else + triggeringTime = new Date(); + + String timeString = String.valueOf(triggeringTime.getHours()) + ":" + String.valueOf(triggeringTime.getMinutes()) + ":" + String.valueOf(triggeringTime.getSeconds()); + Time nowTime = Time.valueOf(timeString); + Calendar calNow = Calendar.getInstance(); + + try + { + TimeFrame tf = new TimeFrame(getTriggerParameter2()); + + if(tf.getDayList().contains(calNow.get(Calendar.DAY_OF_WEEK))) + { + if( + // Regular case, start time is lower than end time + ( + Miscellaneous.compareTimes(tf.getTriggerTimeStart(), nowTime) >= 0 + && + Miscellaneous.compareTimes(nowTime, tf.getTriggerTimeStop()) > 0 + ) + | + // Other case, start time higher than end time, timeframe goes over midnight + ( + Miscellaneous.compareTimes(tf.getTriggerTimeStart(), tf.getTriggerTimeStop()) < 0 + && + (Miscellaneous.compareTimes(tf.getTriggerTimeStart(), nowTime) >= 0 + | + Miscellaneous.compareTimes(nowTime, tf.getTriggerTimeStop()) > 0) + ) + + ) + { + // We are in the timeframe + Miscellaneous.logEvent("i", "Trigger", "TimeFrame: We're currently (" + calNow.getTime().toString() + ") in the specified TimeFrame (" + tf.toString() + ").", 4); + if(getTriggerParameter()) + { + if(checkifStateChangedSinceLastRuleExecution) + { + /* + * Was there a target repetition time between last execution and now? + * If not -> return false. + */ + Calendar compareCal = Calendar.getInstance(); + compareCal.setTimeInMillis(triggeringTime.getTime()); + if(tf.getRepetition() > 0) + { + if(!isSupposedToRepeatSinceLastExecution(compareCal)) + { + Miscellaneous.logEvent("i", "TimeFrame", "TimeFrame: Trigger of rule " + this.getParentRule().getName() + " applies, but repeated execution is not due, yet.", 4); + return false; + } + } + else + { + /* + * This is not a repeating rule. Have we left + * the relevant timeframe since the last run? + * Determine if it has ran today already. If yes + * return false because every rule that is not + * repeating can only be executed once per day. + */ + + if( + getParentRule().getLastExecution().get(Calendar.YEAR) == calNow.get(Calendar.YEAR) + && + getParentRule().getLastExecution().get(Calendar.MONTH) == calNow.get(Calendar.MONTH) + && + getParentRule().getLastExecution().get(Calendar.DAY_OF_MONTH) == calNow.get(Calendar.DAY_OF_MONTH) + ) + { + Miscellaneous.logEvent("i", "TimeFrame", "TimeFrame: Trigger of rule " + this.getParentRule().getName() + " applies, but it was already executed today.", 4); + return false; + } + } + } + + Miscellaneous.logEvent("i", "Trigger", "TimeFrame: That's what's specified. Trigger of rule " + this.getParentRule().getName() + " applies.", 4); + return true; + } + else + { + Miscellaneous.logEvent("i", "Trigger", "TimeFrame: That's not what's specified. Trigger of rule " + this.getParentRule().getName() + " doesn't apply.", 4); + return false; + } + } + else + { + Miscellaneous.logEvent("i", "Trigger", "TimeFrame: We're currently (" + calNow.getTime().toString() + ", Day: " + String.valueOf(calNow.get(Calendar.DAY_OF_WEEK)) + ") not in the specified TimeFrame (" + tf.toString() + ") because of the time.", 5); + if(!getTriggerParameter()) + { + if(checkifStateChangedSinceLastRuleExecution) + { + /* + * Was there a target repetition time between last execution and now? + * If not -> return false. + */ + Calendar compareCal = Calendar.getInstance(); + compareCal.setTimeInMillis(triggeringTime.getTime()); + if(tf.getRepetition() > 0) + { + if(!isSupposedToRepeatSinceLastExecution(compareCal)) + { + Miscellaneous.logEvent("i", "Trigger", "TimeFrame: Trigger of rule " + this.getParentRule().getName() + " applies, but repeated execution is not due, yet.", 4); + return false; + } + } + else + { + /* + * This is not a repeating rule. Have we left + * the relevant timeframe since the last run? + * Determine if it has ran today already. If yes + * return false because every rule that is not + * repeating can only be executed once per day. + */ + + if( + getParentRule().getLastExecution().get(Calendar.YEAR) == calNow.get(Calendar.YEAR) + && + getParentRule().getLastExecution().get(Calendar.MONTH) == calNow.get(Calendar.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); + return false; + } + } + } + Miscellaneous.logEvent("i", "Trigger", "TimeFrame: That's what's specified. Trigger of rule " + this.getParentRule().getName() + " applies.", 5); + return true; + } + else + { + Miscellaneous.logEvent("i", "Trigger", "TimeFrame: That's not what's specified. Trigger of rule " + this.getParentRule().getName() + " doesn't apply.", 5); + return false; + } + } + } + else + { + Miscellaneous.logEvent("i", "Trigger", "TimeFrame: We're currently (" + calNow.getTime().toString() + ", Day: " + String.valueOf(calNow.get(Calendar.DAY_OF_WEEK)) + ") not in the specified TimeFrame (" + tf.toString() + ") because of the day.", 5); + return false; + } + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "Trigger", "There was an error while checking if the time based trigger applies: " + Log.getStackTraceString(e), 1); + return false; + } + } + + public static Calendar getNextRepeatedExecutionAfter(Trigger trigger, Calendar now) + { + Calendar calSet; + Time setTime; + TimeFrame tf = new TimeFrame(trigger.getTriggerParameter2()); + + if(tf.getRepetition() > 0) + { + if(trigger.getTriggerParameter()) + setTime = tf.getTriggerTimeStart(); + else + setTime = tf.getTriggerTimeStop(); + + calSet = (Calendar) now.clone(); + calSet.set(Calendar.HOUR_OF_DAY, setTime.getHours()); + calSet.set(Calendar.MINUTE, setTime.getMinutes()); + calSet.set(Calendar.SECOND, 0); + calSet.set(Calendar.MILLISECOND, 0); + +// if(this.applies(null)) +// { + // If the starting time is a day ahead remove 1 day. + if(calSet.getTimeInMillis() > now.getTimeInMillis()) + calSet.add(Calendar.DAY_OF_MONTH, -1); + + long differenceInSeconds = Math.abs(now.getTimeInMillis() - calSet.getTimeInMillis()) / 1000; + long nextExecutionMultiplier = Math.floorDiv(differenceInSeconds, tf.getRepetition()) + 1; + long nextScheduleTimestamp = (calSet.getTimeInMillis() / 1000) + (nextExecutionMultiplier * tf.getRepetition()); + Calendar calSchedule = Calendar.getInstance(); + calSchedule.setTimeInMillis(nextScheduleTimestamp * 1000); + + /* + * Das war mal aktiviert. Allerdings: Die ganze Funktion liefert zurück, wenn die Regel NOCH nicht + * zutrifft, aber wir z.B. gleich den zeitlichen Bereich betreten. + */ +// if(trigger.checkDateTime(calSchedule.getTime(), false)) +// { + return calSchedule; +// } +// } + } + else + Miscellaneous.logEvent("i", "Trigger", "Trigger " + trigger.toString() + " is not executed repeatedly.", 5); + + return null; + } + + boolean isSupposedToRepeatSinceLastExecution(Calendar now) + { + TimeFrame tf = new TimeFrame(getTriggerParameter2()); + Calendar lastExec = getParentRule().getLastExecution(); + + // the simple stuff: + + if(lastExec == null) // rule never run, go any way + return true; + else if(tf.getRepetition() <= 0) // is not set to repeat at all + return false; + + /* + * We don't need to check if the trigger currently applies, that has + * been done externally via the applies() function. We can safely assume + * we're inside the specified timeframe. + */ + + Calendar timeSupposedToRunNext = getNextRepeatedExecutionAfter(this, lastExec); + if(now.getTimeInMillis() > timeSupposedToRunNext.getTimeInMillis()) + return true; + + return false; + } + + /* * Can be several things: * -PointOfInterest * -TimeFrame * -Event (like charging, cable plugged, etc.) */ - public enum Trigger_Enum { + public enum Trigger_Enum { pointOfInterest, timeFrame, charging, batteryLevel, usb_host_connection, speed, noiseLevel, wifiConnection, process_started_stopped, airplaneMode, roaming, nfcTag, activityDetection, bluetoothConnection, headsetPlugged, notification, phoneCall; //phoneCall always needs to be at the very end because of Google's shitty so called privacy public String getFullName(Context context) @@ -294,7 +585,11 @@ public class Trigger else returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.leaving) + " "); - 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()); + String repeat = ", no repetition"; + if(this.getTimeFrame().getRepetition() > 0) + 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; case speed: if(getTriggerParameter()) @@ -611,5 +906,14 @@ public class Trigger { return this.bluetoothEvent; } - + + public Rule getParentRule() + { + return parentRule; + } + + public void setParentRule(Rule parentRule) + { + this.parentRule = parentRule; + } } \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/XmlFileInterface.java b/app/src/main/java/com/jens/automation2/XmlFileInterface.java index 2b170b74..5c469307 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/AlarmListener.java b/app/src/main/java/com/jens/automation2/receivers/AlarmListener.java deleted file mode 100644 index 4c7bf102..00000000 --- a/app/src/main/java/com/jens/automation2/receivers/AlarmListener.java +++ /dev/null @@ -1,301 +0,0 @@ -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 com.jens.automation2.AutomationService; -import com.jens.automation2.Miscellaneous; -import com.jens.automation2.Rule; -import com.jens.automation2.Trigger; -import com.jens.automation2.Trigger.Trigger_Enum; - -import java.sql.Time; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; - -public class AlarmListener extends BroadcastReceiver implements AutomationListenerInterface -{ - private static AutomationService automationServiceRef; - private static AlarmManager centralAlarmManagerInstance; -// private static Intent alarmIntent; -// private static PendingIntent alarmPendingIntent; - private static boolean alarmListenerActive=false; - private static ArrayList alarmCandidates = new ArrayList(); - - private static ArrayList requestCodeList = new ArrayList(); - - public static void startAlarmListener(final AutomationService automationServiceRef) - { - AlarmListener.startAlarmListenerInternal(automationServiceRef); - } - public static void stopAlarmListener(Context context) - { - AlarmListener.stopAlarmListenerInternal(); - } - - public static boolean isAlarmListenerActive() - { - return alarmListenerActive; - } - - @Override - public void onReceive(Context context, Intent intent) - { - Miscellaneous.logEvent("i", "AlarmListener", "Alarm received", 2); - Date now = new Date(); - String timeString = String.valueOf(now.getHours()) + ":" + String.valueOf(now.getMinutes()) + ":" + String.valueOf(now.getSeconds()); - Time passTime = Time.valueOf(timeString); - - ArrayList allRulesWithNowInTimeFrame = Rule.findRuleCandidatesByTime(passTime); - for(int i=0; i allRulesWithTimeFrames = new ArrayList(); - allRulesWithTimeFrames = Rule.findRuleCandidatesByTimeFrame(); - for(Rule oneRule : allRulesWithTimeFrames) - { - for(Trigger oneTrigger : oneRule.getTriggerSet()) - { - if(oneTrigger.getTriggerType() == Trigger_Enum.timeFrame) - { - Calendar calNow, calSet; - Time setTime; - - if(oneTrigger.getTriggerParameter()) - setTime = oneTrigger.getTimeFrame().getTriggerTimeStart(); - else - setTime = oneTrigger.getTimeFrame().getTriggerTimeStop(); - - calNow = Calendar.getInstance(); - calSet = (Calendar) calNow.clone(); - calSet.set(Calendar.HOUR_OF_DAY, setTime.getHours()); - calSet.set(Calendar.MINUTE, setTime.getMinutes()); - calSet.set(Calendar.SECOND, 0); - calSet.set(Calendar.MILLISECOND, 0); - // At this point calSet would be a scheduling candidate. It's just the day the might not be right, yet. - - long milliSecondsInAWeek = 1000 * 60 * 60 * 24 * 7; - - for(int dayOfWeek : oneTrigger.getTimeFrame().getDayList()) - { - Calendar calSetWorkingCopy = (Calendar) calSet.clone(); - -// calSetWorkingCopy.set(Calendar.HOUR_OF_DAY, setTime.getHours()); -// calSetWorkingCopy.set(Calendar.MINUTE, setTime.getMinutes()); -// calSetWorkingCopy.set(Calendar.SECOND, 0); -// calSetWorkingCopy.set(Calendar.MILLISECOND, 0); - - int diff = dayOfWeek - calNow.get(Calendar.DAY_OF_WEEK); -// Log.i("AlarmManager", "Today: " + String.valueOf(calNow.get(Calendar.DAY_OF_WEEK)) + " / Sched.Day: " + String.valueOf(dayOfWeek) + " Difference to target day is: " + String.valueOf(diff)); - if(diff == 0) //if we're talking about the current day, is the time still in the future? - { - if(calSetWorkingCopy.getTime().getHours() < calNow.getTime().getHours()) - { -// Log.i("AlarmManager", "calSetWorkingCopy.getTime().getHours(" + String.valueOf(calSetWorkingCopy.getTime().getHours()) + ") < calNow.getTime().getHours(" + String.valueOf(calNow.getTime().getHours()) + ")"); - calSetWorkingCopy.add(Calendar.DAY_OF_MONTH, 7); //add a week - } - else if(calSetWorkingCopy.getTime().getHours() == calNow.getTime().getHours()) - { -// Log.i("AlarmManager", "calSetWorkingCopy.getTime().getHours() == calNow.getTime().getHours()"); - if(calSetWorkingCopy.getTime().getMinutes() <= calNow.getTime().getMinutes()) - { -// Log.i("AlarmManager", "calSetWorkingCopy.getTime().getMinutes() < calNow.getTime().getMinutes()"); - calSetWorkingCopy.add(Calendar.DAY_OF_MONTH, 7); //add a week - } - } - } - else if(diff < 0) - { -// Miscellaneous.logEvent("i", "AlarmManager", "Adding " + String.valueOf(diff+7) + " on top of " + String.valueOf(calSetWorkingCopy.get(Calendar.DAY_OF_WEEK))); - calSetWorkingCopy.add(Calendar.DAY_OF_WEEK, diff+7); // it's a past weekday, schedule for next week - } - else - { -// Miscellaneous.logEvent("i", "AlarmManager", "Adding " + String.valueOf(diff) + " on top of " + String.valueOf(calSetWorkingCopy.get(Calendar.DAY_OF_WEEK))); - calSetWorkingCopy.add(Calendar.DAY_OF_WEEK, diff); // it's a future weekday, schedule for that day - } - - i++; - i=(int)System.currentTimeMillis(); - String calSetWorkingCopyString = sdf.format(calSetWorkingCopy.getTime()) + " RequestCode: " + String.valueOf(i); -// Miscellaneous.logEvent("i", "AlarmManager", "Setting repeating alarm because of rule: " + oneRule.getName() + " beginning at " + calSetWorkingCopyString); - - alarmCandidates.add(calSetWorkingCopy.getTimeInMillis()); -// Intent alarmIntent = new Intent(automationServiceRef, AlarmListener.class); -// alarmIntent.setData(Uri.parse("myalarms://" + i)); -// PendingIntent alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, i, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); -// centralAlarmManagerInstance.setInexactRepeating(AlarmManager.RTC_WAKEUP, calSetWorkingCopy.getTimeInMillis(), milliSecondsInAWeek, alarmPendingIntent); -// requestCodeList.add(i); - } - } - } - } - -// // get a Calendar object with current time -// Calendar cal = Calendar.getInstance(); -// cal.add(Calendar.SECOND, 10); -// String calSetWorkingCopyString2 = sdf.format(cal.getTime()); -// Miscellaneous.logEvent("i", "AlarmManager", "Setting repeating alarm because of hardcoded test: beginning at " + calSetWorkingCopyString2); -// Intent alarmIntent2 = new Intent(automationServiceRef, AlarmListener.class); -// PendingIntent alarmPendingIntent2 = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent2, 0); -// centralAlarmManagerInstance.setInexactRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), 5000, alarmPendingIntent2); -// requestCodeList.add(0); - - scheduleNextAlarm(); - } - - private static void scheduleNextAlarm() - { - Long currentTime = System.currentTimeMillis(); - Long scheduleCandidate = null; - - if(alarmCandidates.size() == 0) - { - Miscellaneous.logEvent("i", "AlarmManager", "No alarms to be scheduled.", 3); - return; - } - else if(alarmCandidates.size() == 1) - { - // only one alarm, schedule that - scheduleCandidate = alarmCandidates.get(0); - } - else if(alarmCandidates.size() > 1) - { - scheduleCandidate = alarmCandidates.get(0); - - for(long alarmCandidate : alarmCandidates) - { - if(Math.abs(currentTime - alarmCandidate) < Math.abs(currentTime - scheduleCandidate)) - scheduleCandidate = alarmCandidate; - } - } - - Intent alarmIntent = new Intent(automationServiceRef, AlarmListener.class); - PendingIntent alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); - centralAlarmManagerInstance.set(AlarmManager.RTC_WAKEUP, scheduleCandidate, alarmPendingIntent); - - - SimpleDateFormat sdf = new SimpleDateFormat("E dd.MM.yyyy HH:mm"); - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(scheduleCandidate); - Miscellaneous.logEvent("i", "AlarmManager", "Chose " + sdf.format(calendar.getTime()) + " as next scheduled alarm.", 4); - - } - - public static void clearAlarms() - { - Miscellaneous.logEvent("i", "AlarmManager", "Clearing possibly standing alarms.", 4); - for(int requestCode : requestCodeList) - { - Intent alarmIntent = new Intent(automationServiceRef, AlarmListener.class); - PendingIntent alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, requestCode, alarmIntent, 0); -// Miscellaneous.logEvent("i", "AlarmManager", "Clearing alarm with request code: " + String.valueOf(requestCode)); - centralAlarmManagerInstance.cancel(alarmPendingIntent); - } - requestCodeList.clear(); - } - - private static void startAlarmListenerInternal(AutomationService givenAutomationServiceRef) - { - if(!alarmListenerActive) - { - Miscellaneous.logEvent("i", "AlarmListener", "Starting alarm listener.", 4); - AlarmListener.automationServiceRef = givenAutomationServiceRef; - centralAlarmManagerInstance = (AlarmManager)automationServiceRef.getSystemService(automationServiceRef.ALARM_SERVICE); -// alarmIntent = new Intent(automationServiceRef, AlarmListener.class); -// alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, 0); - alarmListenerActive = true; - Miscellaneous.logEvent("i", "AlarmListener", "Alarm listener started.", 4); - AlarmListener.setAlarms(); - -// // get a Calendar object with current time -// Calendar cal = Calendar.getInstance(); -// // add 5 minutes to the calendar object -// cal.add(Calendar.SECOND, 10); -// centralAlarmManagerInstance.setInexactRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), 5000, alarmPendingIntent); - } - else - Miscellaneous.logEvent("i", "AlarmListener", "Request to start AlarmListener. But it's already active.", 5); - } - - private static void stopAlarmListenerInternal() - { - if(alarmListenerActive) - { - Miscellaneous.logEvent("i", "AlarmListener", "Stopping alarm listener.", 4); - clearAlarms(); -// centralAlarmManagerInstance.cancel(alarmPendingIntent); - alarmListenerActive = false; - } - else - Miscellaneous.logEvent("i", "AlarmListener", "Request to stop AlarmListener. But it's not running.", 5); - } - public static void reloadAlarms() - { - AlarmListener.setAlarms(); - } - @Override - public void startListener(AutomationService automationService) - { - AlarmListener.startAlarmListener(automationService); - } - @Override - public void stopListener(AutomationService automationService) - { - AlarmListener.stopAlarmListener(automationService); - } - - public static boolean haveAllPermission() - { - return true; - } - - @Override - public boolean isListenerRunning() - { - return isAlarmListenerActive(); - } - @Override - public Trigger_Enum[] getMonitoredTrigger() - { - return new Trigger_Enum[] { Trigger_Enum.timeFrame }; - } - -} diff --git a/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java b/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java new file mode 100644 index 00000000..3c4cfc1e --- /dev/null +++ b/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java @@ -0,0 +1,431 @@ +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.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import com.jens.automation2.AutomationService; +import com.jens.automation2.Miscellaneous; +import com.jens.automation2.Rule; +import com.jens.automation2.TimeFrame; +import com.jens.automation2.Trigger; +import com.jens.automation2.Trigger.Trigger_Enum; + +import java.sql.Time; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; + +public class DateTimeListener extends BroadcastReceiver implements AutomationListenerInterface +{ + private static AutomationService automationServiceRef; + private static AlarmManager centralAlarmManagerInstance; +// private static Intent alarmIntent; +// private static PendingIntent alarmPendingIntent; + private static boolean alarmListenerActive=false; + private static ArrayList alarmCandidates = new ArrayList<>(); + + private static ArrayList requestCodeList = new ArrayList(); + + public static void startAlarmListener(final AutomationService automationServiceRef) + { + DateTimeListener.startAlarmListenerInternal(automationServiceRef); + } + public static void stopAlarmListener(Context context) + { + DateTimeListener.stopAlarmListenerInternal(); + } + + public static boolean isAlarmListenerActive() + { + return alarmListenerActive; + } + + @Override + public void onReceive(Context context, Intent intent) + { + Miscellaneous.logEvent("i", "AlarmListener", "Alarm received", 2); + Date now = new Date(); + String timeString = String.valueOf(now.getHours()) + ":" + String.valueOf(now.getMinutes()) + ":" + String.valueOf(now.getSeconds()); + Time passTime = Time.valueOf(timeString); + + ArrayList allRulesWithNowInTimeFrame = Rule.findRuleCandidatesByTime(passTime); + for(int i=0; i allRulesWithTimeFrames = new ArrayList(); + allRulesWithTimeFrames = Rule.findRuleCandidatesByTimeFrame(); + /* + * Take care of regular executions, no repetitions in between. + */ + Miscellaneous.logEvent("i", "DateTimeListener", "Checking rules for single run alarm candidates.", 5); + for(Rule oneRule : allRulesWithTimeFrames) + { + Miscellaneous.logEvent("i", "DateTimeListener","Checking rule " + oneRule.getName() + " for single run alarm candidates.", 5); + if(oneRule.isRuleActive()) + { + try + { + for(Trigger oneTrigger : oneRule.getTriggerSet()) + { + Miscellaneous.logEvent("i", "DateTimeListener","Checking trigger " + oneTrigger.toString() + " for single run alarm candidates.", 5); + + if(oneTrigger.getTriggerType().equals(Trigger_Enum.timeFrame)) + { + TimeFrame tf = new TimeFrame(oneTrigger.getTriggerParameter2()); + + Calendar calSet; + Time setTime; + + if(oneTrigger.getTriggerParameter()) + setTime = tf.getTriggerTimeStart(); + else + setTime = tf.getTriggerTimeStop(); + + calSet = (Calendar) calNow.clone(); + calSet.set(Calendar.HOUR_OF_DAY, setTime.getHours()); + calSet.set(Calendar.MINUTE, setTime.getMinutes()); + calSet.set(Calendar.SECOND, 0); + calSet.set(Calendar.MILLISECOND, 0); + // At this point calSet would be a scheduling candidate. It's just the day that might not be right, yet. + + for(int dayOfWeek : tf.getDayList()) + { + Calendar calSetWorkingCopy = (Calendar) calSet.clone(); + + int diff = dayOfWeek - calNow.get(Calendar.DAY_OF_WEEK); + if(diff == 0) // We're talking about the current weekday, but is the time still in the future? + { + if(calSetWorkingCopy.getTime().getHours() < calNow.getTime().getHours()) + { + calSetWorkingCopy.add(Calendar.DAY_OF_MONTH, 7); //add a week + } + else if(calSetWorkingCopy.getTime().getHours() == calNow.getTime().getHours()) + { + if(calSetWorkingCopy.getTime().getMinutes() <= calNow.getTime().getMinutes()) + { + calSetWorkingCopy.add(Calendar.DAY_OF_MONTH, 7); //add a week + } + } + } + else if(diff < 0) + { + calSetWorkingCopy.add(Calendar.DAY_OF_WEEK, diff+7); // it's a past weekday, schedule for next week + } + else + { + calSetWorkingCopy.add(Calendar.DAY_OF_WEEK, diff); // it's a future weekday, schedule for that day + } + + i++; + i=(int)System.currentTimeMillis(); + sdf.format(calSetWorkingCopy.getTime()); + String.valueOf(i); + + alarmCandidates.add(new ScheduleElement(calSetWorkingCopy, "Rule " + oneRule.getName() + ", trigger " + oneTrigger.toString())); + } + } + } + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "DateTimeListener","Error checking anything for rule " + oneRule.toString() + " needs to be added to candicates list: " + Log.getStackTraceString(e), 1); + } + } + } + + /* + * Only take care of repeated executions. + */ + Miscellaneous.logEvent("i", "DateTimeListener","Checking rules for repeated run alarm candidates.", 5); + for(Rule oneRule : allRulesWithTimeFrames) + { + Miscellaneous.logEvent("i", "DateTimeListener","Checking rule " + oneRule.getName() + " for repeated run alarm candidates.", 5); + if(oneRule.isRuleActive()) + { + try + { + Miscellaneous.logEvent("i", "DateTimeListener","Checking rule " + oneRule.toString() , 5); + + for(Trigger oneTrigger : oneRule.getTriggerSet()) + { + Miscellaneous.logEvent("i", "DateTimeListener","Checking trigger " + oneTrigger.toString() + " for repeated run alarm candidates.", 5); + if(oneTrigger.getTriggerType().equals(Trigger_Enum.timeFrame)) + { + Miscellaneous.logEvent("i", "DateTimeListener","Checking rule trigger " + oneTrigger.toString() , 5); + + /* + * Check for next repeated execution: + * + * Check if the rule currently applies.... + * + * If no -> do nothing + * If yes -> Take starting time and calculate the next repeated execution + * 1. Take starting time + * 2. Take current time + * 3. Calculate difference, but include check to see if we're after that time, + * be it start or end of the timeframe. + * 4. Take div result +1 and add this on top of starting time + * 5. Is this next possible execution still inside timeframe? Also consider timeframes spanning over midnight + */ + Calendar calSet; + Time setTime; + TimeFrame tf = new TimeFrame(oneTrigger.getTriggerParameter2()); + + if(tf.getRepetition() > 0) + { + if(oneTrigger.applies(calNow)) + { + Calendar calSchedule = getNextRepeatedExecutionAfter(oneTrigger, calNow); + + alarmCandidates.add(new ScheduleElement(calSchedule, "Rule " + oneRule.getName() + ", trigger " + oneTrigger.toString())); + } + } + } + } + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "DateTimeListener","Error checking anything for rule " + oneRule.toString() + " needs to be added to candicates list: " + Log.getStackTraceString(e), 1); + } + } + } + + scheduleNextAlarm(); + } + + private static void scheduleNextAlarm() + { + Long currentTime = System.currentTimeMillis(); + ScheduleElement scheduleCandidate = null; + + if(alarmCandidates.size() == 0) + { + Miscellaneous.logEvent("i", "AlarmManager", "No alarms to be scheduled.", 3); + return; + } + else if(alarmCandidates.size() == 1) + { + // only one alarm, schedule that + scheduleCandidate = alarmCandidates.get(0); + } + else if(alarmCandidates.size() > 1) + { + scheduleCandidate = alarmCandidates.get(0); + + for(ScheduleElement alarmCandidate : alarmCandidates) + { + if(Math.abs(currentTime - alarmCandidate.time.getTimeInMillis()) < Math.abs(currentTime - scheduleCandidate.time.getTimeInMillis())) + scheduleCandidate = alarmCandidate; + } + } + + Intent alarmIntent = new Intent(automationServiceRef, DateTimeListener.class); + PendingIntent alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + centralAlarmManagerInstance.set(AlarmManager.RTC_WAKEUP, scheduleCandidate.time.getTimeInMillis(), alarmPendingIntent); + + + SimpleDateFormat sdf = new SimpleDateFormat("E dd.MM.yyyy HH:mm"); + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(scheduleCandidate.time.getTimeInMillis()); + Miscellaneous.logEvent("i", "AlarmManager", "Chose " + sdf.format(calendar.getTime()) + " as next scheduled alarm.", 4); + } + + public static void clearAlarms() + { + Miscellaneous.logEvent("i", "AlarmManager", "Clearing possibly standing alarms.", 4); + for(int requestCode : requestCodeList) + { + Intent alarmIntent = new Intent(automationServiceRef, DateTimeListener.class); + PendingIntent alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, requestCode, alarmIntent, 0); +// Miscellaneous.logEvent("i", "AlarmManager", "Clearing alarm with request code: " + String.valueOf(requestCode)); + centralAlarmManagerInstance.cancel(alarmPendingIntent); + } + requestCodeList.clear(); + } + + private static void startAlarmListenerInternal(AutomationService givenAutomationServiceRef) + { + if(!alarmListenerActive) + { + Miscellaneous.logEvent("i", "AlarmListener", "Starting alarm listener.", 4); + DateTimeListener.automationServiceRef = givenAutomationServiceRef; + centralAlarmManagerInstance = (AlarmManager)automationServiceRef.getSystemService(automationServiceRef.ALARM_SERVICE); +// alarmIntent = new Intent(automationServiceRef, AlarmListener.class); +// alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, 0); + alarmListenerActive = true; + Miscellaneous.logEvent("i", "AlarmListener", "Alarm listener started.", 4); + DateTimeListener.setAlarms(); + +// // get a Calendar object with current time +// Calendar cal = Calendar.getInstance(); +// // add 5 minutes to the calendar object +// cal.add(Calendar.SECOND, 10); +// centralAlarmManagerInstance.setInexactRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), 5000, alarmPendingIntent); + } + else + Miscellaneous.logEvent("i", "AlarmListener", "Request to start AlarmListener. But it's already active.", 5); + } + + private static void stopAlarmListenerInternal() + { + if(alarmListenerActive) + { + Miscellaneous.logEvent("i", "AlarmListener", "Stopping alarm listener.", 4); + clearAlarms(); +// centralAlarmManagerInstance.cancel(alarmPendingIntent); + alarmListenerActive = false; + } + else + Miscellaneous.logEvent("i", "AlarmListener", "Request to stop AlarmListener. But it's not running.", 5); + } + public static void reloadAlarms() + { + DateTimeListener.setAlarms(); + } + @Override + public void startListener(AutomationService automationService) + { + DateTimeListener.startAlarmListener(automationService); + } + @Override + public void stopListener(AutomationService automationService) + { + DateTimeListener.stopAlarmListener(automationService); + } + + public static boolean haveAllPermission() + { + return true; + } + + @Override + public boolean isListenerRunning() + { + return isAlarmListenerActive(); + } + @Override + public Trigger_Enum[] getMonitoredTrigger() + { + return new Trigger_Enum[] { Trigger_Enum.timeFrame }; + } + + static class ScheduleElement implements Comparable + { + Calendar time; + String reason; + + public ScheduleElement(Calendar timestamp, String reason) + { + super(); + this.time = timestamp; + this.reason = reason; + } + + @Override + public int compareTo(ScheduleElement o) + { + if(time.getTimeInMillis() == o.time.getTimeInMillis()) + return 0; + if(time.getTimeInMillis() < o.time.getTimeInMillis()) + return -1; + else + return 1; + } + + @Override + public String toString() + { + return Miscellaneous.formatDate(time.getTime()) + ", reason : " + reason; + } + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static Calendar getNextRepeatedExecutionAfter(Trigger trigger, Calendar now) + { + Calendar calSet; + Time setTime; + TimeFrame tf = new TimeFrame(trigger.getTriggerParameter2()); + + if(tf.getRepetition() > 0) + { + if(trigger.getTriggerParameter()) + setTime = tf.getTriggerTimeStart(); + else + setTime = tf.getTriggerTimeStop(); + + calSet = (Calendar) now.clone(); + calSet.set(Calendar.HOUR_OF_DAY, setTime.getHours()); + calSet.set(Calendar.MINUTE, setTime.getMinutes()); + calSet.set(Calendar.SECOND, 0); + calSet.set(Calendar.MILLISECOND, 0); + +// if(this.applies(null)) +// { + // If the starting time is a day ahead remove 1 day. + if(calSet.getTimeInMillis() > now.getTimeInMillis()) + calSet.add(Calendar.DAY_OF_MONTH, -1); + + long differenceInSeconds = Math.abs(now.getTimeInMillis() - calSet.getTimeInMillis()) / 1000; + long nextExecutionMultiplier = Math.floorDiv(differenceInSeconds, tf.getRepetition()) + 1; + long nextScheduleTimestamp = (calSet.getTimeInMillis() / 1000) + (nextExecutionMultiplier * tf.getRepetition()); + Calendar calSchedule = Calendar.getInstance(); + calSchedule.setTimeInMillis(nextScheduleTimestamp * 1000); + + /* + * Das war mal aktiviert. Allerdings: Die ganze Funktion liefert zurück, wenn die Regel NOCH nicht + * zutrifft, aber wir z.B. gleich den zeitlichen Bereich betreten. + */ +// if(trigger.checkDateTime(calSchedule.getTime(), false)) +// { + return calSchedule; +// } +// } + } + else + Miscellaneous.logEvent("i", "DateTimeListener", "Trigger " + trigger.toString() + " is not executed repeatedly.", 5); + + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/receivers/TimeZoneListener.java b/app/src/main/java/com/jens/automation2/receivers/TimeZoneListener.java index c2856f2f..5961a98c 100644 --- a/app/src/main/java/com/jens/automation2/receivers/TimeZoneListener.java +++ b/app/src/main/java/com/jens/automation2/receivers/TimeZoneListener.java @@ -77,12 +77,12 @@ public class TimeZoneListener extends BroadcastReceiver implements AutomationLis if(action.equals(Intent.ACTION_TIMEZONE_CHANGED)) { Miscellaneous.logEvent("i", "TimeZoneListener", "Device timezone changed. Reloading alarms.", 3); - AlarmListener.reloadAlarms(); + DateTimeListener.reloadAlarms(); } else if(action.equals(Intent.ACTION_TIME_CHANGED)) { Miscellaneous.logEvent("i", "TimeZoneListener", "Device time changed. Reloading alarms.", 4); - AlarmListener.reloadAlarms(); + DateTimeListener.reloadAlarms(); } } @Override diff --git a/app/src/main/res/layout/activity_manage_trigger_timeframe.xml b/app/src/main/res/layout/activity_manage_trigger_timeframe.xml index a85659ac..0553e16f 100644 --- a/app/src/main/res/layout/activity_manage_trigger_timeframe.xml +++ b/app/src/main/res/layout/activity_manage_trigger_timeframe.xml @@ -119,6 +119,34 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/sunday" /> + + + + + + + + + +