diff --git a/app/build.gradle b/app/build.gradle index ccf6dbc..f0cee5e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,11 +8,11 @@ android { defaultConfig { applicationId "com.jens.automation2" minSdkVersion 16 - compileSdkVersion 31 + compileSdkVersion 33 buildToolsVersion '29.0.2' useLibrary 'org.apache.http.legacy' versionCode 138 - versionName "1.7.21" + versionName "1.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/apkFlavor/AndroidManifest.xml b/app/src/apkFlavor/AndroidManifest.xml index 0188eca..083cd1e 100644 --- a/app/src/apkFlavor/AndroidManifest.xml +++ b/app/src/apkFlavor/AndroidManifest.xml @@ -1,5 +1,7 @@ - + - @@ -70,6 +71,14 @@ + + + + + @@ -138,9 +148,11 @@ + @@ -179,9 +191,12 @@ + + @@ -227,9 +242,11 @@ + @@ -261,6 +278,17 @@ android:exported="true" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/apkFlavor/java/com/jens/automation2/MyGoogleApiClient.java b/app/src/apkFlavor/java/com/jens/automation2/MyGoogleApiClient.java index 5b5f3e5..90cbd91 100644 --- a/app/src/apkFlavor/java/com/jens/automation2/MyGoogleApiClient.java +++ b/app/src/apkFlavor/java/com/jens/automation2/MyGoogleApiClient.java @@ -18,7 +18,7 @@ public class MyGoogleApiClient public com.google.android.gms.appindexing.Action getIndexApiAction() { Thing object = new Thing.Builder() - .setName("ActivityMainScreen Page") // TODO: Define a title for the content shown. + .setName("ActivityMainScreen Page") // TODO: Make sure this auto-generated URL is correct. .setUrl(Uri.parse("http://[ENTER-YOUR-URL-HERE]")) .build(); diff --git a/app/src/apkFlavor/java/com/jens/automation2/Rule.java b/app/src/apkFlavor/java/com/jens/automation2/Rule.java index cfb6204..31840a0 100644 --- a/app/src/apkFlavor/java/com/jens/automation2/Rule.java +++ b/app/src/apkFlavor/java/com/jens/automation2/Rule.java @@ -10,9 +10,12 @@ 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 java.util.ArrayList; import java.util.Calendar; @@ -373,23 +376,28 @@ public class Rule implements Comparable { if(hasNotAppliedSinceLastExecution()) { - Miscellaneous.logEvent("i", "getsGreenLight()", "Rule " + getName() + " applies and has flipped since its last execution.", 4); + Miscellaneous.logEvent("i", "getsGreenLight()", "Rule \"" + getName() + "\" applies and has flipped since its last execution.", 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); + 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); + 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); + Miscellaneous.logEvent("i", "RuleCheck", "Automation service not running. Rule \"" + getName() + "\" cannot apply.", 3); return false; } @@ -401,7 +409,7 @@ public class Rule implements Comparable 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); + 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; } @@ -507,7 +515,7 @@ public class Rule implements Comparable { boolean isActuallyToggleable = isActuallyToggable(); - boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); +// boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); boolean doToggle = ruleToggle && isActuallyToggleable; String message; @@ -521,6 +529,29 @@ public class Rule implements Comparable 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 @@ -780,4 +811,71 @@ public class Rule implements Comparable 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; + } + + public static int getAmountOfActivatedRules() + { + int amount = 0; + + for(Rule r : Rule.getRuleCollection()) + { + if(r.isRuleActive()) + amount++; + } + + return amount; + } } \ No newline at end of file diff --git a/app/src/fdroidFlavor/AndroidManifest.xml b/app/src/fdroidFlavor/AndroidManifest.xml index 1a7d25e..f85fd9f 100644 --- a/app/src/fdroidFlavor/AndroidManifest.xml +++ b/app/src/fdroidFlavor/AndroidManifest.xml @@ -1,5 +1,7 @@ - + - @@ -68,6 +69,13 @@ + + + + @@ -136,9 +145,11 @@ + @@ -177,8 +188,12 @@ + + + @@ -224,9 +239,11 @@ + @@ -246,6 +263,17 @@ android:exported="true" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java b/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java index c28d366..80e9a86 100644 --- a/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java +++ b/app/src/fdroidFlavor/java/com/jens/automation2/Rule.java @@ -10,7 +10,10 @@ import android.os.Looper; import android.util.Log; import android.widget.Toast; +import androidx.annotation.Nullable; import com.jens.automation2.receivers.BroadcastListener; +import com.jens.automation2.receivers.CalendarReceiver; + import java.util.ArrayList; import java.util.Calendar; import java.util.Date; @@ -344,7 +347,16 @@ public class Rule implements Comparable if(oneTrigger.getTriggerType().equals(Trigger.Trigger_Enum.timeFrame)) { if(oneTrigger.getTimeFrame().repetition > 0) - return true; + { + 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)) { @@ -361,23 +373,28 @@ public class Rule implements Comparable { if(hasNotAppliedSinceLastExecution()) { - Miscellaneous.logEvent("i", "getsGreenLight()", "Rule " + getName() + " applies and has flipped since its last execution.", 4); + Miscellaneous.logEvent("i", "getsGreenLight()", "Rule \"" + getName() + "\" applies and has flipped since its last execution.", 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); + 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); + 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); + Miscellaneous.logEvent("i", "RuleCheck", "Automation service not running. Rule \"" + getName() + "\" cannot apply.", 3); return false; } @@ -389,7 +406,7 @@ public class Rule implements Comparable 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); + 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; } @@ -471,7 +488,7 @@ public class Rule implements Comparable { boolean isActuallyToggleable = isActuallyToggable(); - boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); +// boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); boolean doToggle = ruleToggle && isActuallyToggleable; String message; @@ -485,6 +502,29 @@ public class Rule implements Comparable 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 @@ -744,4 +784,71 @@ public class Rule implements Comparable 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; + } + + public static int getAmountOfActivatedRules() + { + int amount = 0; + + for(Rule r : Rule.getRuleCollection()) + { + if(r.isRuleActive()) + amount++; + } + + return amount; + } } diff --git a/app/src/googlePlayFlavor/AndroidManifest.xml b/app/src/googlePlayFlavor/AndroidManifest.xml index b4426bc..2aec771 100644 --- a/app/src/googlePlayFlavor/AndroidManifest.xml +++ b/app/src/googlePlayFlavor/AndroidManifest.xml @@ -1,5 +1,7 @@ - + - @@ -65,6 +66,14 @@ + + + + + - + @@ -121,6 +132,7 @@ + + + + - @@ -210,6 +224,7 @@ + - - @@ -245,6 +258,18 @@ android:exported="true" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java b/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java index 5ae17ff..904aa28 100644 --- a/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java +++ b/app/src/googlePlayFlavor/java/com/jens/automation2/Rule.java @@ -10,9 +10,12 @@ 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 java.util.ArrayList; import java.util.Calendar; @@ -347,7 +350,16 @@ public class Rule implements Comparable if(oneTrigger.getTriggerType().equals(Trigger.Trigger_Enum.timeFrame)) { if(oneTrigger.getTimeFrame().repetition > 0) - return true; + { + 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)) { @@ -364,23 +376,28 @@ public class Rule implements Comparable { if(hasNotAppliedSinceLastExecution()) { - Miscellaneous.logEvent("i", "getsGreenLight()", "Rule " + getName() + " applies and has flipped since its last execution.", 4); + Miscellaneous.logEvent("i", "getsGreenLight()", "Rule \"" + getName() + "\" applies and has flipped since its last execution.", 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); + 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); + 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); + Miscellaneous.logEvent("i", "RuleCheck", "Automation service not running. Rule \"" + getName() + "\" cannot apply.", 3); return false; } @@ -392,7 +409,7 @@ public class Rule implements Comparable 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); + 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; } @@ -498,7 +515,7 @@ public class Rule implements Comparable { boolean isActuallyToggleable = isActuallyToggable(); - boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); +// boolean notLastActive = getLastActivatedRule() == null || !getLastActivatedRule().equals(Rule.this); boolean doToggle = ruleToggle && isActuallyToggleable; String message; @@ -512,6 +529,29 @@ public class Rule implements Comparable 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 @@ -771,4 +811,71 @@ public class Rule implements Comparable 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; + } + + public static int getAmountOfActivatedRules() + { + int amount = 0; + + for(Rule r : Rule.getRuleCollection()) + { + if(r.isRuleActive()) + amount++; + } + + return amount; + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5918e6..74b7379 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,4 +1,3 @@ - \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/Action.java b/app/src/main/java/com/jens/automation2/Action.java index 0983fa9..4800146 100644 --- a/app/src/main/java/com/jens/automation2/Action.java +++ b/app/src/main/java/com/jens/automation2/Action.java @@ -7,10 +7,11 @@ import android.util.Log; import android.widget.Toast; import org.apache.commons.lang3.StringUtils; -import org.apache.http.client.methods.HttpGet; import java.util.ArrayList; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; public class Action @@ -19,7 +20,10 @@ public class Action public static final String actionParameter2Split = "ap2split"; public static final String intentPairSeparator = "intPairSplit"; + public static final String actionParameters2SeparatorInner = "a2splitInner"; + public static final String actionParameters2SeparatorOuter = "a2splitOuter"; public static final String vibrateSeparator = ","; + public static final String httpErrorDefaultText = "HTTP_ERROR"; public enum Action_Enum { @@ -56,6 +60,8 @@ public class Action startPhoneCall, stopPhoneCall, copyToClipboard, + takeScreenshot, + setLocationService, sendTextMessage; public String getFullName(Context context) @@ -140,6 +146,10 @@ public class Action return context.getResources().getString(R.string.endPhoneCall); case copyToClipboard: return context.getResources().getString(R.string.copyTextToClipboard); + case takeScreenshot: + return context.getResources().getString(R.string.takeScreenshot); + case setLocationService: + return context.getResources().getString(R.string.setLocationServiceCapital); default: return "Unknown"; } @@ -307,22 +317,61 @@ public class Action break; case copyToClipboard: returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.copyTextToClipboard)); + break; + case takeScreenshot: + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.takeScreenshot)); + break; + case setLocationService: + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.setLocationService) + ": " ); + switch(Integer.parseInt(getParameter2())) + { + case android.provider.Settings.Secure.LOCATION_MODE_OFF: + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.off)); + break; + case android.provider.Settings.Secure.LOCATION_MODE_SENSORS_ONLY: + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.LOCATION_MODE_SENSOR_ONLY)); + break; + case android.provider.Settings.Secure.LOCATION_MODE_BATTERY_SAVING: + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.LOCATION_MODE_BATTERY_SAVING)); + break; + case android.provider.Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.LOCATION_MODE_HIGH_ACCURACY)); + break; + } + break; default: returnString.append(action.toString()); } if (this.getAction().equals(Action_Enum.triggerUrl)) { - String[] components = parameter2.split(";"); + String[] components; + if(parameter2.contains(Action.actionParameter2Split)) + components = parameter2.split(Action.actionParameter2Split); + else + components = parameter2.split(";"); + if (components.length >= 3) { + returnString.append(" ("); + if(components.length >= 4) + returnString.append(components[3]); + else + returnString.append(ActivityManageActionTriggerUrl.methodGet); + returnString.append(")"); + returnString.append(": " + components[2]); if (parameter1) returnString.append(" " + Miscellaneous.getAnyContext().getResources().getString(R.string.usingAuthentication) + "."); } else + { + returnString.append(" ("); + returnString.append(ActivityManageActionTriggerUrl.methodGet);; + returnString.append(")"); returnString.append(": " + components[0]); + } } else if (this.getAction().equals(Action_Enum.startOtherActivity)) { @@ -414,7 +463,7 @@ public class Action { returnString.append(": " + parameter2.replace(Action.actionParameter2Split, "; ").replace(Action.intentPairSeparator, "/")); } - else if(this.getAction().equals(Action_Enum.setVariable) || this.getAction().equals(Action_Enum.copyToClipboard)) + else if(this.getAction().equals(Action_Enum.setVariable) || this.getAction().equals(Action_Enum.copyToClipboard) || this.getAction().equals(Action_Enum.setLocationService)) ; // it's completed further above already else if (parameter2 != null && parameter2.length() > 0) returnString.append(": " + parameter2.replace(Action.actionParameter2Split, "; ")); @@ -632,6 +681,12 @@ public class Action case copyToClipboard: Actions.copyToClipboard(context, Miscellaneous.replaceVariablesInText(this.getParameter2(), context)); break; + case takeScreenshot: + Actions.takeScreenshot(); + break; + case setLocationService: + Actions.setLocationService(Integer.parseInt(this.getParameter2()), AutomationService.getInstance()); + break; default: Miscellaneous.logEvent("w", "Action", context.getResources().getString(R.string.unknownActionSpecified), 3); break; @@ -645,18 +700,33 @@ public class Action } private void triggerUrl(Context context) - { + { + //TODO: Check if data needs to be escaped String username = null; String password = null; + String method = ActivityManageActionTriggerUrl.methodGet; String url; + String params = null; - String[] components = getParameter2().split(";"); + String[] components; + if(getParameter2().contains(Action.actionParameter2Split)) + components = getParameter2().split(Action.actionParameter2Split, -1); + else + components = getParameter2().split(";", -1); if(components.length >= 3) { username = components[0]; password = components[1]; url = components[2]; + + if(components.length >= 4) + method = components[3]; + + if(components.length >= 5) + { + params = components[4]; + } } else // compatibility for very old versions which haven't upgraded, yet. url = components[0]; @@ -664,15 +734,21 @@ public class Action try { url = Miscellaneous.replaceVariablesInText(url, context); + if(!StringUtils.isEmpty(params)) + params = Miscellaneous.replaceVariablesInText(params, context); Actions myAction = new Actions(); Miscellaneous.logEvent("i", "HTTP", "Attempting download of " + url, 4); //getResources().getString("attemptingDownloadOf"); - + + /* + Theoretically credentials could be saved, but authentication has been turned off afterwards. + The following if clause is there to force username and password to be null. + */ if(this.getParameter1()) // use authentication - new DownloadTask().execute(url, username, password); + new DownloadTask().execute(url, username, password, method, params); else - new DownloadTask().execute(url, null, null); + new DownloadTask().execute(url, null, null, method, params); } catch(Exception e) { @@ -687,32 +763,49 @@ public class Action { Thread.setDefaultUncaughtExceptionHandler(Miscellaneous.uncaughtExceptionHandler); - int attempts=1; + int attempts = 1; String urlString=parameters[0]; String urlUsername = null; String urlPassword = null; + String method = ActivityManageActionTriggerUrl.methodGet; + Map httpParams = new HashMap<>(); + if(parameters.length >= 3) { - urlUsername=parameters[1]; - urlPassword=parameters[2]; + urlUsername = parameters[1]; + urlPassword = parameters[2]; + + if(parameters.length >= 4) + method = parameters[3]; + + if(parameters.length >= 5 && parameters[4] != null) + { + // has params + String[] paramPairs = parameters[4].split(Action.actionParameters2SeparatorOuter); + for(String pair : paramPairs) + { + String[] pieces = pair.split(Action.actionParameters2SeparatorInner); + httpParams.put(pieces[0], pieces[1]); + } + } + } - String response = "httpError"; - HttpGet post; - - if(Settings.httpAttempts < 1) + String response = httpErrorDefaultText; + + if(Settings.httpAttempts < 1) Miscellaneous.logEvent("w", "HTTP Request", Miscellaneous.getAnyContext().getResources().getString(R.string.cantDownloadTooFewRequestsInSettings), 3); - while(attempts <= Settings.httpAttempts && response.equals("httpError")) + while(attempts <= Settings.httpAttempts && response.equals(httpErrorDefaultText)) { Miscellaneous.logEvent("i", "HTTP Request", "Attempt " + String.valueOf(attempts++) + " of " + String.valueOf(Settings.httpAttempts), 3); // Either thorough checking or no encryption if(!Settings.httpAcceptAllCertificates || !urlString.toLowerCase(Locale.getDefault()).contains("https")) - response = Miscellaneous.downloadURL(urlString, urlUsername, urlPassword); + response = Miscellaneous.downloadURL(urlString, urlUsername, urlPassword, method, httpParams); else - response = Miscellaneous.downloadURLwithoutCertificateChecking(urlString, urlUsername, urlPassword); + response = Miscellaneous.downloadUrlWithoutCertificateChecking(urlString, urlUsername, urlPassword, method, httpParams); try { diff --git a/app/src/main/java/com/jens/automation2/Actions.java b/app/src/main/java/com/jens/automation2/Actions.java index bc830e2..1e6a294 100644 --- a/app/src/main/java/com/jens/automation2/Actions.java +++ b/app/src/main/java/com/jens/automation2/Actions.java @@ -1,6 +1,7 @@ package com.jens.automation2; import android.Manifest; +import android.accessibilityservice.AccessibilityService; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.NotificationManager; @@ -225,7 +226,16 @@ public class Actions Map map = AutomationService.getInstance().getVariableMap(); if(parts.length > 1) - map.put(parts[0], parts[1]); + { + try + { + map.put(parts[0], Miscellaneous.replaceVariablesInText(parts[1], Miscellaneous.getAnyContext())); + } + catch (Exception e) + { + map.put(parts[0], parts[1]); + } + } else map.remove(parts[0]); } @@ -725,7 +735,6 @@ public class Actions if (method == null) throw new NoSuchMethodException(); - /* * For some reason this doesn't work, throws NoSuchMethodExpection even if the method is present. */ @@ -985,6 +994,7 @@ public class Actions public void useDownloadedWebpage(String result) { // Toast.makeText(context, "Result: " + result, Toast.LENGTH_LONG).show(); + Actions.setVariable("last_triggerurl_result" + Action.actionParameter2Split + result); } public static HttpClient getInsecureSslClient(HttpClient client) @@ -1043,7 +1053,12 @@ public class Actions { Miscellaneous.logEvent("i", "StartOtherActivity", "Starting other Activity...", 4); - String params[] = param.split(";"); + String params[]; + + if(param.contains(Action.actionParameter2Split)) + params = param.split(Action.actionParameter2Split); + else + params = param.split(";"); try { @@ -1977,7 +1992,6 @@ public class Actions boolean suAvailable = false; String suVersion = null; String suVersionInternal = null; -// List suResult = null; int suResult; boolean success = false; @@ -1991,12 +2005,15 @@ public class Actions suVersionInternal = Shell.SU.version(true); Miscellaneous.logEvent("i", "executeCommandViaSu()", "suVersion: " + suVersion + ", suVersionInternal: " + suVersionInternal, 5); + Miscellaneous.logEvent("i", "executeCommandViaSu()", "calling method: " + Miscellaneous.getCallingMethodName(), 5); -// suResult = Shell.SU.run(commands); suResult = Shell.Pool.SU.run(commands); -// if (suResult != null) -// success = true; + if(Miscellaneous.getCallingMethodName().equals("runExecutable")) + { + Actions.setVariable("last_run_executable_exit_code" + Action.actionParameter2Split + String.valueOf(suResult)); +// Actions.setVariable("last_run_executable_output" + Action.actionParameter2Split + (String) result[1]); + } Miscellaneous.logEvent("i", "executeCommandViaSu()", "RC=" + String.valueOf(suResult), 3); @@ -2060,7 +2077,10 @@ public class Actions else result = runExternalApplication(path, 0, workingDir, null); - boolean execResult = (boolean) result[0]; + boolean execResult = ((int) result[0] == 0); + + Actions.setVariable("last_run_executable_exit_code" + Action.actionParameter2Split + String.valueOf((int) result[0])); + Actions.setVariable("last_run_executable_output" + Action.actionParameter2Split + (String) result[1]); return execResult; } @@ -2108,19 +2128,6 @@ public class Actions stderr = process.getErrorStream (); stdout = process.getInputStream (); - // "write" the parms into stdin - /*line = "param1" + "\n"; - stdin.write(line.getBytes() ); - stdin.flush(); - - line = "param2" + "\n"; - stdin.write(line.getBytes() ); - stdin.flush(); - - line = "param3" + "\n"; - stdin.write(line.getBytes() ); - stdin.flush();*/ - stdin.close(); // clean up if any output in stdout @@ -2185,10 +2192,6 @@ public class Actions Miscellaneous.logEvent("i", "Running executable", "Error running external application.", 1); -// if(slotMap != null) -// for(String key : slotMap.keySet()) -// System.clearProperty(key); - return null; } @@ -2281,7 +2284,18 @@ public class Actions public static void startPhoneCall(Context context, String phoneNumber) { - Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + Uri.encode(phoneNumber))); + Intent intent; + + /* + Bug in Android 14 makes it necessary to add double quotes around MMI code. + More precisely it's required for codes containing the # character. + */ + + if(Build.VERSION.SDK_INT >= 34) + intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + Uri.encode("\"" + phoneNumber + "\""))); + else + intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + Uri.encode(phoneNumber))); + // intent.setClassName("com.android.phone","com.android.phone.OutgoingCallBroadcaster"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_FROM_BACKGROUND); @@ -2334,4 +2348,26 @@ public class Actions clipboard.setPrimaryClip(clip); } } + + public static void takeScreenshot() + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + { + MyAccessibilityService.getInstance().performGlobalAction(AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT); + } + } + + public static void setLocationService(int desiredState, Context context) + { +// if(desiredState) +// { +// android.provider.Settings.Secure.putString(context.getContentResolver(), android.provider.Settings.Secure.LOCATION_MODE, new Integer(android.provider.Settings.Secure.LOCATION_MODE_HIGH_ACCURACY).toString()); +// } +// else +// { +// android.provider.Settings.Secure.putString(context.getContentResolver(), android.provider.Settings.Secure.LOCATION_MODE, new Integer(android.provider.Settings.Secure.LOCATION_MODE_OFF).toString()); +// } + android.provider.Settings.Secure.putString(context.getContentResolver(), android.provider.Settings.Secure.LOCATION_MODE, String.valueOf(desiredState)); + } + } \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/ActivityMainScreen.java b/app/src/main/java/com/jens/automation2/ActivityMainScreen.java index 16711e3..258f6b7 100644 --- a/app/src/main/java/com/jens/automation2/ActivityMainScreen.java +++ b/app/src/main/java/com/jens/automation2/ActivityMainScreen.java @@ -526,15 +526,23 @@ public class ActivityMainScreen extends ActivityGeneric { if (Rule.getRuleCollection().size() > 0) { + if(Rule.getAmountOfActivatedRules() == 0) + { + Toast.makeText(context, context.getResources().getString(R.string.serviceWontStartNoActivatedRules), Toast.LENGTH_LONG).show(); + activityMainScreenInstance.toggleService.setChecked(false); + return; + } + if (!AutomationService.isMyServiceRunning(context)) { -// if(myServiceIntent == null) //do we need that line????? myServiceIntent = new Intent(context, AutomationService.class); myServiceIntent.putExtra("startAtBoot", startAtBoot); context.startService(myServiceIntent); - } else + } + else Miscellaneous.logEvent("w", "Service", context.getResources().getString(R.string.logServiceAlreadyRunning), 3); - } else + } + else { Toast.makeText(context, context.getResources().getString(R.string.serviceWontStart), Toast.LENGTH_LONG).show(); activityMainScreenInstance.toggleService.setChecked(false); diff --git a/app/src/main/java/com/jens/automation2/ActivityManageActionLocationService.java b/app/src/main/java/com/jens/automation2/ActivityManageActionLocationService.java new file mode 100644 index 0000000..1105ce8 --- /dev/null +++ b/app/src/main/java/com/jens/automation2/ActivityManageActionLocationService.java @@ -0,0 +1,75 @@ +package com.jens.automation2; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.view.View; +import android.widget.Button; +import android.widget.RadioButton; + +import androidx.annotation.Nullable; + +public class ActivityManageActionLocationService extends Activity +{ + RadioButton rbActionLocationServiceOff, rbActionLocationServiceSensorsOnly, rbActionLocationServiceBatterySaving, rbActionLocationServiceHighAccuracy; + Button bActionSetLocationServiceSave; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + Miscellaneous.setDisplayLanguage(this); + setContentView(R.layout.activity_manage_action_location_service); + + rbActionLocationServiceOff = (RadioButton) findViewById(R.id.rbActionLocationServiceOff); + rbActionLocationServiceSensorsOnly = (RadioButton)findViewById(R.id.rbActionLocationServiceSensorsOnly); + rbActionLocationServiceBatterySaving = (RadioButton)findViewById(R.id.rbActionLocationServiceBatterySaving); + rbActionLocationServiceHighAccuracy = (RadioButton)findViewById(R.id.rbActionLocationServiceHighAccuracy); + bActionSetLocationServiceSave = (Button) findViewById(R.id.bActionSetLocationServiceSave); + + Intent input = getIntent(); + + if(input.hasExtra(ActivityManageRule.intentNameActionParameter2)) + { + String[] params = input.getStringExtra(ActivityManageRule.intentNameActionParameter2).split(Action.actionParameter2Split); + int desiredState = Integer.parseInt(params[0]); + + switch(desiredState) + { + case Settings.Secure.LOCATION_MODE_OFF: + rbActionLocationServiceOff.setChecked(true); + break; + case Settings.Secure.LOCATION_MODE_SENSORS_ONLY: + rbActionLocationServiceSensorsOnly.setChecked(true); + break; + case Settings.Secure.LOCATION_MODE_BATTERY_SAVING: + rbActionLocationServiceBatterySaving.setChecked(true); + break; + case Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: + rbActionLocationServiceHighAccuracy.setChecked(true); + break; + } + } + + bActionSetLocationServiceSave.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View view) + { + Intent response = new Intent(); + if(rbActionLocationServiceOff.isChecked()) + response.putExtra(ActivityManageRule.intentNameActionParameter2, String.valueOf(Settings.Secure.LOCATION_MODE_OFF)); + else if(rbActionLocationServiceSensorsOnly.isChecked()) + response.putExtra(ActivityManageRule.intentNameActionParameter2, String.valueOf(Settings.Secure.LOCATION_MODE_SENSORS_ONLY)); + else if(rbActionLocationServiceBatterySaving.isChecked()) + response.putExtra(ActivityManageRule.intentNameActionParameter2, String.valueOf(Settings.Secure.LOCATION_MODE_BATTERY_SAVING)); + else + response.putExtra(ActivityManageRule.intentNameActionParameter2, String.valueOf(Settings.Secure.LOCATION_MODE_HIGH_ACCURACY)); + + setResult(RESULT_OK, response); + finish(); + } + }); + } +} diff --git a/app/src/main/java/com/jens/automation2/ActivityManageActionSendBroadcast.java b/app/src/main/java/com/jens/automation2/ActivityManageActionSendBroadcast.java index fe4409d..fa60b10 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManageActionSendBroadcast.java +++ b/app/src/main/java/com/jens/automation2/ActivityManageActionSendBroadcast.java @@ -236,8 +236,6 @@ public class ActivityManageActionSendBroadcast extends Activity @Override public void onNothingSelected(AdapterView arg0) { - // TODO Auto-generated method stub - } }); } diff --git a/app/src/main/java/com/jens/automation2/ActivityManageActionSetVariable.java b/app/src/main/java/com/jens/automation2/ActivityManageActionSetVariable.java index fcff468..89a6813 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManageActionSetVariable.java +++ b/app/src/main/java/com/jens/automation2/ActivityManageActionSetVariable.java @@ -1,6 +1,6 @@ package com.jens.automation2; -import static com.jens.automation2.ActivityManageActionTriggerUrl.edit; +//import static com.jens.automation2.ActivityManageActionTriggerUrl.edit; import android.app.Activity; import android.content.Intent; diff --git a/app/src/main/java/com/jens/automation2/ActivityManageActionStartActivity.java b/app/src/main/java/com/jens/automation2/ActivityManageActionStartActivity.java index 0807268..d712308 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManageActionStartActivity.java +++ b/app/src/main/java/com/jens/automation2/ActivityManageActionStartActivity.java @@ -58,10 +58,10 @@ public class ActivityManageActionStartActivity extends Activity RadioButton rbStartAppSelectByActivity, rbStartAppSelectByAction, rbStartAppByActivity, rbStartAppByBroadcast, rbStartAppByService, rbStartAppByForegroundService; final String urlShowExamples = "https://server47.de/automation/examples_startProgram.html"; - final static String startByActivityString = "0"; - final static String startByBroadcastString = "1"; - final static String startByServiceString = "2"; - final static String startByForegroundServiceString = "3"; + public final static String startByActivityString = "0"; + public final static String startByBroadcastString = "1"; + public final static String startByServiceString = "2"; + public final static String startByForegroundServiceString = "3"; final static int requestCodeForRequestQueryAllPackagesPermission = 4711; @@ -234,29 +234,28 @@ public class ActivityManageActionStartActivity extends Activity String parameter2 = ""; if (rbStartAppSelectByActivity.isChecked()) - parameter2 += etPackageName.getText().toString() + ";" + etActivityOrActionPath.getText().toString(); + parameter2 += etPackageName.getText().toString() + Action.actionParameter2Split + etActivityOrActionPath.getText().toString(); else { if (etPackageName.getText().toString() != null && etPackageName.getText().toString().length() > 0) - parameter2 += etPackageName.getText().toString() + ";" + etActivityOrActionPath.getText().toString(); + parameter2 += etPackageName.getText().toString() + Action.actionParameter2Split + etActivityOrActionPath.getText().toString(); else - parameter2 += Actions.dummyPackageString + ";" + etActivityOrActionPath.getText().toString(); + parameter2 += Actions.dummyPackageString + Action.actionParameter2Split + etActivityOrActionPath.getText().toString(); -// if(etClassName.getText().toString().length() > 0) - parameter2 += ";" + etClassName.getText().toString(); + parameter2 += Action.actionParameter2Split + etClassName.getText().toString(); } if (rbStartAppByActivity.isChecked()) - parameter2 += ";" + startByActivityString; + parameter2 += Action.actionParameter2Split + startByActivityString; else if(rbStartAppByService.isChecked()) - parameter2 += ";" + startByServiceString; + parameter2 += Action.actionParameter2Split + startByServiceString; else if(rbStartAppByForegroundService.isChecked()) - parameter2 += ";" + startByForegroundServiceString; + parameter2 += Action.actionParameter2Split + startByForegroundServiceString; else - parameter2 += ";" + startByBroadcastString; + parameter2 += Action.actionParameter2Split + startByBroadcastString; for (String s : intentPairList) - parameter2 += ";" + s; + parameter2 += Action.actionParameter2Split + s; returnData.putExtra(ActivityManageRule.intentNameActionParameter2, parameter2); @@ -292,8 +291,6 @@ public class ActivityManageActionStartActivity extends Activity @Override public void onNothingSelected(AdapterView arg0) { - // TODO Auto-generated method stub - } }); @@ -628,7 +625,13 @@ public class ActivityManageActionStartActivity extends Activity rbStartAppSelectByActivity.setChecked(!selectionByAction); rbStartAppSelectByAction.setChecked(selectionByAction); - String[] params = input.getStringExtra(ActivityManageRule.intentNameActionParameter2).split(";"); + String[] params; + String partsString = input.getStringExtra(ActivityManageRule.intentNameActionParameter2); + + if(partsString.contains(Action.actionParameter2Split)) + params = partsString.split(Action.actionParameter2Split); + else + params = partsString.split(";"); if(Miscellaneous.isNumeric(params[2])) // old configuration file { diff --git a/app/src/main/java/com/jens/automation2/ActivityManageActionTriggerUrl.java b/app/src/main/java/com/jens/automation2/ActivityManageActionTriggerUrl.java index 4f1dc64..5ed0b13 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManageActionTriggerUrl.java +++ b/app/src/main/java/com/jens/automation2/ActivityManageActionTriggerUrl.java @@ -1,6 +1,10 @@ package com.jens.automation2; import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; @@ -13,27 +17,37 @@ import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.widget.ListView; +import android.widget.RadioButton; import android.widget.TableLayout; import android.widget.Toast; +import androidx.annotation.NonNull; + import com.jens.automation2.Action.Action_Enum; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; import java.util.Map; public class ActivityManageActionTriggerUrl extends Activity { - Button bSaveTriggerUrl; - EditText etTriggerUrl, etTriggerUrlUsername, etTriggerUrlPassword; + Button bSaveTriggerUrl, bAddHttpParam; + EditText etTriggerUrl, etTriggerUrlUsername, etTriggerUrlPassword, etParameterName, etParameterValue; ListView lvTriggerUrlPostParameters; CheckBox chkTriggerUrlUseAuthentication; + RadioButton rbTriggerUrlMethodGet, rbTriggerUrlMethodPost; TableLayout tlTriggerUrlAuthentication; - + ArrayAdapter httpParametersAdapter; + + private ArrayList httpParamsList = new ArrayList<>(); ArrayAdapter> lvTriggerUrlPostParametersAdapter; + + public static final String methodGet = "GET"; + public static final String methodPost = "POST"; -// private String existingUrl = ""; - - public static boolean edit = false; - public static Action resultingAction = null; +// public static boolean edit = false; +// public static Action resultingAction = null; @Override protected void onCreate(Bundle savedInstanceState) @@ -49,6 +63,32 @@ public class ActivityManageActionTriggerUrl extends Activity lvTriggerUrlPostParameters = (ListView)findViewById(R.id.lvTriggerUrlPostParameters); tlTriggerUrlAuthentication = (TableLayout)findViewById(R.id.tlTriggerUrlAuthentication); bSaveTriggerUrl = (Button)findViewById(R.id.bSaveSpeakText); + rbTriggerUrlMethodGet = (RadioButton) findViewById(R.id.rbTriggerUrlMethodGet); + rbTriggerUrlMethodPost = (RadioButton) findViewById(R.id.rbTriggerUrlMethodPost); + etTriggerUrl = (EditText) findViewById(R.id.etTriggerUrl); + etParameterName = (EditText) findViewById(R.id.etParameterName); + etParameterValue = (EditText)findViewById(R.id.etParameterValue); + bAddHttpParam = (Button)findViewById(R.id.bAddHttpParam); + + etParameterName.setEnabled(false); + etParameterValue.setEnabled(false); + bAddHttpParam.setEnabled(false); + + rbTriggerUrlMethodPost.setOnCheckedChangeListener(new OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) + { + etParameterName.setEnabled(checked); + etParameterValue.setEnabled(checked); + bAddHttpParam.setEnabled(checked); + if(checked) + lvTriggerUrlPostParameters.setVisibility(View.VISIBLE); + } + }); + + httpParametersAdapter = new ArrayAdapter(this, R.layout.text_view_for_poi_listview_mediumtextsize, httpParamsList); + bSaveTriggerUrl.setOnClickListener(new OnClickListener() { @Override @@ -56,44 +96,64 @@ public class ActivityManageActionTriggerUrl extends Activity { if(etTriggerUrl.getText().toString().length() > 0) { - if(resultingAction == null) - { - resultingAction = new Action(); - resultingAction.setAction(Action_Enum.triggerUrl); - resultingAction.setParameter1(chkTriggerUrlUseAuthentication.isChecked()); - - String username = etTriggerUrlUsername.getText().toString(); - String password = etTriggerUrlPassword.getText().toString(); - - if(username == null) - username = ""; - - if(password == null) - password = ""; - - ActivityManageActionTriggerUrl.resultingAction.setParameter2( - username + ";" + - password + ";" + - etTriggerUrl.getText().toString().trim() - ); - } - backToRuleManager(); + Intent returnIntent = new Intent(); + + returnIntent.putExtra(ActivityManageRule.intentNameActionParameter1, chkTriggerUrlUseAuthentication.isChecked()); + + String username = etTriggerUrlUsername.getText().toString(); + String password = etTriggerUrlPassword.getText().toString(); + + if(username == null) + username = ""; + + if(password == null) + password = ""; + + String method = methodGet; + if(rbTriggerUrlMethodPost.isChecked()) + method = methodPost; + + String httpParams = ""; + for (String s : httpParamsList) + httpParams += Action.actionParameters2SeparatorOuter + s; + if(httpParams.length() > 0) + httpParams = httpParams.substring(Action.actionParameters2SeparatorOuter.length()); + + returnIntent.putExtra(ActivityManageRule.intentNameActionParameter2, + username + Action.actionParameter2Split + + password + Action.actionParameter2Split + + etTriggerUrl.getText().toString().trim() + Action.actionParameter2Split + + method + Action.actionParameter2Split + + httpParams + ); + + setResult(RESULT_OK, returnIntent); + finish(); } else Toast.makeText(getBaseContext(), getResources().getString(R.string.urlTooShort), Toast.LENGTH_LONG).show(); } }); - chkTriggerUrlUseAuthentication.setOnCheckedChangeListener(new OnCheckedChangeListener() - { + { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if(isChecked) + { tlTriggerUrlAuthentication.setVisibility(View.VISIBLE); + rbTriggerUrlMethodGet.setChecked(false); + rbTriggerUrlMethodPost.setChecked(true); + rbTriggerUrlMethodGet.setEnabled(false); + rbTriggerUrlMethodPost.setEnabled(false); + } else + { tlTriggerUrlAuthentication.setVisibility(View.GONE); + rbTriggerUrlMethodGet.setEnabled(true); + rbTriggerUrlMethodPost.setEnabled(true); + } etTriggerUrlUsername.setEnabled(isChecked); etTriggerUrlPassword.setEnabled(isChecked); @@ -110,52 +170,86 @@ public class ActivityManageActionTriggerUrl extends Activity }); updateListView(); - - ActivityManageActionTriggerUrl.edit = getIntent().getBooleanExtra("edit", false); - if(edit) + if(getIntent().hasExtra(ActivityManageRule.intentNameActionParameter2)) { - // username,password,URL - String[] components = ActivityManageActionTriggerUrl.resultingAction.getParameter2().split(";"); + // username,password,URL,etc. + String[] components; + + if(getIntent().getStringExtra(ActivityManageRule.intentNameActionParameter2).contains(Action.actionParameter2Split)) + components = getIntent().getStringExtra(ActivityManageRule.intentNameActionParameter2).split(Action.actionParameter2Split, -1); + else + components = getIntent().getStringExtra(ActivityManageRule.intentNameActionParameter2).split(";", -1); if(components.length >= 3) { - etTriggerUrl.setText(components[2]); - chkTriggerUrlUseAuthentication.setChecked(ActivityManageActionTriggerUrl.resultingAction.getParameter1()); + etTriggerUrl.setText(components[2]); + chkTriggerUrlUseAuthentication.setChecked(getIntent().getBooleanExtra(ActivityManageRule.intentNameActionParameter1, false)); etTriggerUrlUsername.setText(components[0]); etTriggerUrlPassword.setText(components[1]); + + if(components.length >= 4) + { + switch(components[3]) + { + case methodPost: + rbTriggerUrlMethodPost.setChecked(true); + break; + case methodGet: + default: + rbTriggerUrlMethodGet.setChecked(true); + break; + } + } + + if(components.length >= 5) + { + if(!StringUtils.isEmpty(components[4]) && components[4].contains(Action.actionParameters2SeparatorInner)) + { + String httpParams[] = components[4].split(Action.actionParameters2SeparatorOuter); + for (String paramPair : httpParams) + httpParamsList.add(paramPair); + + updateHttpParamsList(); + } + } } else etTriggerUrl.setText(components[0]); } - } - - private void backToRuleManager() - { - if(edit && resultingAction != null) + + bAddHttpParam.setOnClickListener(new OnClickListener() { - String username = etTriggerUrlUsername.getText().toString(); - String password = etTriggerUrlPassword.getText().toString(); - - if(username == null) - username = ""; - - if(password == null) - password = ""; - - ActivityManageActionTriggerUrl.resultingAction.setParameter1(chkTriggerUrlUseAuthentication.isChecked()); - - ActivityManageActionTriggerUrl.resultingAction.setParameter2( - username + ";" + - password + ";" + - etTriggerUrl.getText().toString() - ); - } - - setResult(RESULT_OK); - - this.finish(); + @Override + public void onClick(View view) + { + if(StringUtils.isEmpty(etParameterName.getText()) || StringUtils.isEmpty(etParameterValue.getText())) + { + Toast.makeText(ActivityManageActionTriggerUrl.this, getResources().getString(R.string.enterValidDataIntoParametersFields), Toast.LENGTH_SHORT).show(); + return; + } + + httpParamsList.add(etParameterName.getText() + Action.actionParameters2SeparatorInner + etParameterValue.getText()); + + updateHttpParamsList(); + etParameterName.setText(""); + etParameterValue.setText(""); + + if(lvTriggerUrlPostParameters.getVisibility() != View.VISIBLE) + lvTriggerUrlPostParameters.setVisibility(View.VISIBLE); + } + }); + + lvTriggerUrlPostParameters.setOnItemLongClickListener(new OnItemLongClickListener() + { + @Override + public boolean onItemLongClick(AdapterView arg0, View arg1, int arg2, long arg3) + { + getHttpParamsDialog(arg2).show(); + return false; + } + }); } - + private void updateListView() { Miscellaneous.logEvent("i", "ListView", "Attempting to update lvTriggerUrlPostParameters", 4); @@ -163,10 +257,36 @@ public class ActivityManageActionTriggerUrl extends Activity { if(lvTriggerUrlPostParameters.getAdapter() == null) lvTriggerUrlPostParameters.setAdapter(lvTriggerUrlPostParametersAdapter); - + lvTriggerUrlPostParametersAdapter.notifyDataSetChanged(); } catch(NullPointerException e) {} } + + private void updateHttpParamsList() + { + if(lvTriggerUrlPostParameters.getAdapter() == null) + lvTriggerUrlPostParameters.setAdapter(httpParametersAdapter); + + httpParametersAdapter.notifyDataSetChanged(); + } + + private AlertDialog getHttpParamsDialog(final int itemPosition) + { + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(ActivityManageActionTriggerUrl.this); + alertDialogBuilder.setTitle(getResources().getString(R.string.whatToDoWithIntentPair)); + alertDialogBuilder.setItems(new String[]{getResources().getString(R.string.delete)}, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + // Only 1 choice at the moment, no need to check + ActivityManageActionTriggerUrl.this.httpParamsList.remove(itemPosition); + updateHttpParamsList(); + } + }); + AlertDialog alertDialog = alertDialogBuilder.create(); + return alertDialog; + } } diff --git a/app/src/main/java/com/jens/automation2/ActivityManagePoi.java b/app/src/main/java/com/jens/automation2/ActivityManagePoi.java index b6826f2..1035e56 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManagePoi.java +++ b/app/src/main/java/com/jens/automation2/ActivityManagePoi.java @@ -411,22 +411,16 @@ public class ActivityManagePoi extends Activity @Override public void onProviderDisabled(String provider) { - // TODO Auto-generated method stub - } @Override public void onProviderEnabled(String provider) { - // TODO Auto-generated method stub - } @Override public void onStatusChanged(String provider, int status, Bundle extras) { - // TODO Auto-generated method stub - } } @@ -454,22 +448,16 @@ public class ActivityManagePoi extends Activity @Override public void onProviderDisabled(String provider) { - // TODO Auto-generated method stub - } @Override public void onProviderEnabled(String provider) { - // TODO Auto-generated method stub - } @Override public void onStatusChanged(String provider, int status, Bundle extras) { - // TODO Auto-generated method stub - } } diff --git a/app/src/main/java/com/jens/automation2/ActivityManageRule.java b/app/src/main/java/com/jens/automation2/ActivityManageRule.java index 32fda94..3958eeb 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManageRule.java +++ b/app/src/main/java/com/jens/automation2/ActivityManageRule.java @@ -141,6 +141,12 @@ public class ActivityManageRule extends Activity final static int requestCodeTriggerCheckVariableEdit = 828; final static int requestCodeActionCopyTextToClipboardAdd = 829; final static int requestCodeActionCopyTextToClipboardEdit = 830; + final static int requestCodeActionSetLocationServiceAdd = 831; + final static int requestCodeActionSetLocationServiceEdit = 832; + final static int requestCodeTriggerCalendarEventAdd = 833; + final static int requestCodeTriggerCalendarEventEdit = 834; + final static int requestCodeTriggerChargingAdd = 835; + final static int requestCodeTriggerChargingEdit = 836; public static ActivityManageRule getInstance() { @@ -345,6 +351,18 @@ public class ActivityManageRule extends Activity variableStateEditor.putExtra(intentNameTriggerParameter2, selectedTrigger.getTriggerParameter2()); startActivityForResult(variableStateEditor, requestCodeTriggerCheckVariableEdit); break; + case calendarEvent: + Intent calendarStateEditor = new Intent(ActivityManageRule.this, ActivityManageTriggerCalendar.class); + calendarStateEditor.putExtra(intentNameTriggerParameter1, selectedTrigger.getTriggerParameter()); + calendarStateEditor.putExtra(intentNameTriggerParameter2, selectedTrigger.getTriggerParameter2()); + startActivityForResult(calendarStateEditor, requestCodeTriggerCalendarEventEdit); + break; + case charging: + Intent chargingStateEditor = new Intent(ActivityManageRule.this, ActivityManageTriggerCharging.class); + chargingStateEditor.putExtra(intentNameTriggerParameter1, selectedTrigger.getTriggerParameter()); + chargingStateEditor.putExtra(intentNameTriggerParameter2, selectedTrigger.getTriggerParameter2()); + startActivityForResult(chargingStateEditor, requestCodeTriggerChargingEdit); + break; default: break; } @@ -386,9 +404,8 @@ public class ActivityManageRule extends Activity break; case triggerUrl: Intent activityEditTriggerUrlIntent = new Intent(ActivityManageRule.this, ActivityManageActionTriggerUrl.class); - ActivityManageActionTriggerUrl.resultingAction = a; - ActivityManageActionTriggerUrl.resultingAction.setParentRule(ruleToEdit); - activityEditTriggerUrlIntent.putExtra("edit", true); + activityEditTriggerUrlIntent.putExtra(intentNameActionParameter1, a.getParameter1()); + activityEditTriggerUrlIntent.putExtra(intentNameActionParameter2, a.getParameter2()); startActivityForResult(activityEditTriggerUrlIntent, requestCodeActionTriggerUrlEdit); break; case speakText: @@ -478,6 +495,12 @@ public class ActivityManageRule extends Activity actionCopyToClipboardIntent.putExtra(intentNameActionParameter2, a.getParameter2()); startActivityForResult(actionCopyToClipboardIntent, requestCodeActionCopyTextToClipboardEdit); break; + case setLocationService: + Intent actionSetLocationServiceIntent = new Intent(context, ActivityManageActionLocationService.class); +// actionSetLocationServiceIntent.putExtra(intentNameActionParameter1, a.getParameter1()); + actionSetLocationServiceIntent.putExtra(intentNameActionParameter2, a.getParameter2()); + startActivityForResult(actionSetLocationServiceIntent, requestCodeActionSetLocationServiceEdit); + break; default: Miscellaneous.logEvent("w", "Edit action", "Editing of action type " + a.getAction().toString() + " not implemented, yet.", 4); break; @@ -628,6 +651,10 @@ public class ActivityManageRule extends Activity items.add(new Item(typesLong[i].toString(), R.drawable.router)); else if(types[i].toString().equals(Trigger_Enum.subSystemState.toString())) items.add(new Item(typesLong[i].toString(), R.drawable.subsystemstate)); + else if(types[i].toString().equals(Trigger_Enum.checkVariable.toString())) + items.add(new Item(typesLong[i].toString(), R.drawable.variable)); + else if(types[i].toString().equals(Trigger_Enum.calendarEvent.toString())) + items.add(new Item(typesLong[i].toString(), R.drawable.calendar)); else items.add(new Item(typesLong[i].toString(), R.drawable.placeholder)); } @@ -636,15 +663,15 @@ public class ActivityManageRule extends Activity { public View getView(int position, View convertView, ViewGroup parent) { - //User super class to create the View + // User super class to create the View View v = super.getView(position, convertView, parent); TextView tv = (TextView)v.findViewById(android.R.id.text1); - //Put the image on the TextView + // Put the image on the TextView tv.setCompoundDrawablesWithIntrinsicBounds(items.get(position).icon, 0, 0, 0); - //Add margin between image and text (support various screen densities) + // Add margin between image and text (support various screen densities) int dp5 = (int) (5 * getResources().getDisplayMetrics().density + 0.5f); tv.setCompoundDrawablePadding(dp5); @@ -688,7 +715,14 @@ public class ActivityManageRule extends Activity startActivityForResult(timeFrameEditor, requestCodeTriggerTimeframeAdd); return; } - else if(triggerType == Trigger_Enum.charging || triggerType == Trigger_Enum.musicPlaying) + else if(triggerType == Trigger_Enum.charging) + { + newTrigger.setTriggerType(Trigger_Enum.charging); + Intent triggerChargingIntent = new Intent(myContext, ActivityManageTriggerCharging.class); + startActivityForResult(triggerChargingIntent, requestCodeTriggerChargingAdd); + return; + } + else if(triggerType == Trigger_Enum.musicPlaying) booleanChoices = new String[]{getResources().getString(R.string.started), getResources().getString(R.string.stopped)}; else if(triggerType == Trigger_Enum.usb_host_connection) booleanChoices = new String[]{getResources().getString(R.string.connected), getResources().getString(R.string.disconnected)}; @@ -853,6 +887,13 @@ public class ActivityManageRule extends Activity startActivityForResult(variableTriggerEditor, requestCodeTriggerCheckVariableAdd); return; } + else if(triggerType == Trigger_Enum.calendarEvent) + { + newTrigger.setTriggerType(Trigger_Enum.calendarEvent); + Intent calendarTriggerEditor = new Intent(myContext, ActivityManageTriggerCalendar.class); + startActivityForResult(calendarTriggerEditor, requestCodeTriggerCalendarEventAdd); + return; + } else getTriggerParameterDialog(context, booleanChoices).show(); @@ -1355,9 +1396,11 @@ public class ActivityManageRule extends Activity { if(resultCode == RESULT_OK) { - //add TriggerUrl - ActivityManageActionTriggerUrl.resultingAction.setParentRule(ruleToEdit); - ruleToEdit.getActionSet().add(ActivityManageActionTriggerUrl.resultingAction); + newAction.setParentRule(ruleToEdit); + newAction.setAction(Action_Enum.triggerUrl); + newAction.setParameter1(data.getBooleanExtra(intentNameActionParameter1, true)); + newAction.setParameter2(data.getStringExtra(intentNameActionParameter2)); + ruleToEdit.getActionSet().add(newAction); this.refreshActionList(); } } @@ -1365,7 +1408,14 @@ public class ActivityManageRule extends Activity { if(resultCode == RESULT_OK) { - //edit TriggerUrl + ruleToEdit.getActionSet().get(editIndex).setParentRule(ruleToEdit); + + if(data.hasExtra(intentNameActionParameter1)) + ruleToEdit.getActionSet().get(editIndex).setParameter1(data.getBooleanExtra(intentNameActionParameter1, true)); + + if(data.hasExtra(intentNameActionParameter2)) + ruleToEdit.getActionSet().get(editIndex).setParameter2(data.getStringExtra(intentNameActionParameter2)); + this.refreshActionList(); } } @@ -1422,6 +1472,30 @@ public class ActivityManageRule extends Activity this.refreshTriggerList(); } } + else if(requestCode == requestCodeTriggerChargingAdd) + { + if(resultCode == RESULT_OK) + { + newTrigger.setTriggerParameter(data.getBooleanExtra(ActivityManageRule.intentNameTriggerParameter1, false)); + newTrigger.setTriggerParameter2(data.getStringExtra(ActivityManageRule.intentNameTriggerParameter2)); + newTrigger.setParentRule(ruleToEdit); + ruleToEdit.getTriggerSet().add(newTrigger); + this.refreshTriggerList(); + } + } + else if(requestCode == requestCodeTriggerChargingEdit) + { + if(resultCode == RESULT_OK) + { + Trigger responseTimeFrame = new Trigger(); + responseTimeFrame.setTriggerType(Trigger_Enum.charging); + responseTimeFrame.setTriggerParameter(data.getBooleanExtra(intentNameTriggerParameter1, true)); + responseTimeFrame.setTriggerParameter2(data.getStringExtra(intentNameTriggerParameter2)); + responseTimeFrame.setParentRule(ruleToEdit); + ruleToEdit.getTriggerSet().set(editIndex, responseTimeFrame); + this.refreshTriggerList(); + } + } else if(requestCode == requestCodeActionStartActivityAdd) { // manage start of other activity @@ -1986,6 +2060,17 @@ public class ActivityManageRule extends Activity this.refreshTriggerList(); } } + else if(requestCode == requestCodeTriggerCalendarEventAdd) + { + if(resultCode == RESULT_OK) + { + newTrigger.setTriggerParameter(data.getBooleanExtra(intentNameTriggerParameter1, true)); + newTrigger.setTriggerParameter2(data.getStringExtra(intentNameTriggerParameter2)); + newTrigger.setParentRule(ruleToEdit); + ruleToEdit.getTriggerSet().add(newTrigger); + this.refreshTriggerList(); + } + } else if(requestCode == requestCodeTriggerTetheringEdit) { if(resultCode == RESULT_OK) @@ -2025,6 +2110,19 @@ public class ActivityManageRule extends Activity this.refreshTriggerList(); } } + else if(requestCode == requestCodeTriggerCalendarEventEdit) + { + if(resultCode == RESULT_OK) + { + Trigger editedTrigger = new Trigger(); + editedTrigger.setTriggerType(Trigger_Enum.calendarEvent); + editedTrigger.setTriggerParameter(data.getBooleanExtra(intentNameTriggerParameter1, true)); + editedTrigger.setTriggerParameter2(data.getStringExtra(intentNameTriggerParameter2)); + editedTrigger.setParentRule(ruleToEdit); + ruleToEdit.getTriggerSet().set(editIndex, editedTrigger); + this.refreshTriggerList(); + } + } else if(requestCode == requestCodeActionCopyTextToClipboardAdd) { if(resultCode == RESULT_OK) @@ -2047,6 +2145,32 @@ public class ActivityManageRule extends Activity ruleToEdit.getActionSet().get(editIndex).setParameter2(data.getStringExtra(intentNameActionParameter2)); } + this.refreshActionList(); + } + } + else if(requestCode == requestCodeActionSetLocationServiceAdd) + { + if(resultCode == RESULT_OK) + { + newAction.setParentRule(ruleToEdit); +// newAction.setParameter1(data.getBooleanExtra(intentNameActionParameter1, false)); + newAction.setParameter2(data.getStringExtra(intentNameActionParameter2)); + ruleToEdit.getActionSet().add(newAction); + this.refreshActionList(); + } + } + else if(requestCode == requestCodeActionSetLocationServiceEdit) + { + if(resultCode == RESULT_OK) + { + ruleToEdit.getActionSet().get(editIndex).setParentRule(ruleToEdit); +// ruleToEdit.getActionSet().get(editIndex).setParameter1(data.getBooleanExtra(intentNameActionParameter1, false)); + + if(data.hasExtra(intentNameActionParameter2)) + { + ruleToEdit.getActionSet().get(editIndex).setParameter2(data.getStringExtra(intentNameActionParameter2)); + } + this.refreshActionList(); } } @@ -2126,6 +2250,12 @@ public class ActivityManageRule extends Activity } else if(types[i].toString().equals(Action_Enum.copyToClipboard.toString())) items.add(new Item(typesLong[i].toString(), R.drawable.clipboard)); + else if(types[i].toString().equals(Action_Enum.takeScreenshot.toString())) + items.add(new Item(typesLong[i].toString(), R.drawable.copier)); + else if(types[i].toString().equals(Action_Enum.setVariable.toString())) + items.add(new Item(typesLong[i].toString(), R.drawable.variable)); + else if(types[i].toString().equals(Action_Enum.setLocationService.toString())) + items.add(new Item(typesLong[i].toString(), R.drawable.compass_small)); else items.add(new Item(typesLong[i].toString(), R.drawable.placeholder)); } @@ -2162,7 +2292,7 @@ public class ActivityManageRule extends Activity { //launch other activity to enter a url and parameters; newAction.setAction(Action_Enum.triggerUrl); - ActivityManageActionTriggerUrl.resultingAction = null; +// ActivityManageActionTriggerUrl.resultingAction = null; Intent editTriggerIntent = new Intent(context, ActivityManageActionTriggerUrl.class); startActivityForResult(editTriggerIntent, requestCodeActionTriggerUrlAdd); } @@ -2352,6 +2482,18 @@ public class ActivityManageRule extends Activity Intent intent = new Intent(ActivityManageRule.this, ActivityManageActionCopyToClipboard.class); startActivityForResult(intent, requestCodeActionCopyTextToClipboardAdd); } + else if(Action.getActionTypesAsArray()[which].toString().equals(Action_Enum.takeScreenshot.toString())) + { + newAction.setAction(Action_Enum.takeScreenshot); + ruleToEdit.getActionSet().add(newAction); + refreshActionList(); + } + else if(Action.getActionTypesAsArray()[which].toString().equals(Action_Enum.setLocationService.toString())) + { + newAction.setAction(Action_Enum.setLocationService); + Intent intent = new Intent(ActivityManageRule.this, ActivityManageActionLocationService.class); + startActivityForResult(intent, requestCodeActionSetLocationServiceAdd); + } } }); diff --git a/app/src/main/java/com/jens/automation2/ActivityManageTriggerCalendar.java b/app/src/main/java/com/jens/automation2/ActivityManageTriggerCalendar.java new file mode 100644 index 0000000..6c2280e --- /dev/null +++ b/app/src/main/java/com/jens/automation2/ActivityManageTriggerCalendar.java @@ -0,0 +1,404 @@ +package com.jens.automation2; + +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.util.Log; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.jens.automation2.receivers.CalendarReceiver; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +public class ActivityManageTriggerCalendar extends Activity +{ + CheckBox chkCalendarEventActive, chkCalendarAvailabilityBusy, chkCalendarAvailabilityFree, chkCalendarAvailabilityTentative, chkCalendarAvailabilityOutOfOffice, chkCalendarAvailabilityWorkingElsewhere, chkCalendarAllDayEvent, chkCalendarEvaluateAllDayEvent, chkCalendarEvaluateReoccurring, chkCalendarReoccurring; + Spinner spinnerCalendarTitleDirection, spinnerCalendarLocationDirection, spinnerCalendarDescriptionDirection; + EditText etCalendarTitle, etCalendarLocation, etCalendarDescription; + LinearLayout llCalendarSelection; + Button bSaveTriggerCalendar; + List checkboxesCalendars = new ArrayList<>(); + final static String separator = ","; + TextView tvMissingCalendarHint; + + private static String[] directions; + ArrayAdapter directionSpinnerAdapter; + public static int requestCodePermissionReadCalendar = 815; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + Miscellaneous.setDisplayLanguage(this); + setContentView(R.layout.activity_manage_trigger_calendar); + + chkCalendarEventActive = (CheckBox) findViewById(R.id.chkCalendarEventActive); + spinnerCalendarTitleDirection = (Spinner)findViewById(R.id.spinnerCalendarTitleDirection); + spinnerCalendarLocationDirection = (Spinner)findViewById(R.id.spinnerCalendarLocationDirection); + spinnerCalendarDescriptionDirection = (Spinner)findViewById(R.id.spinnerCalendarDescriptionDirection); + chkCalendarAllDayEvent = (CheckBox)findViewById(R.id.chkCalendarAllDayEvent); + chkCalendarAvailabilityBusy = (CheckBox)findViewById(R.id.chkCalendarAvailabilityBusy); + chkCalendarAvailabilityFree = (CheckBox)findViewById(R.id.chkCalendarAvailabilityFree); + chkCalendarAvailabilityTentative = (CheckBox)findViewById(R.id.chkCalendarAvailabilityTentative); + chkCalendarAvailabilityOutOfOffice = (CheckBox)findViewById(R.id.chkCalendarAvailabilityOutOfOffice); + chkCalendarAvailabilityWorkingElsewhere = (CheckBox)findViewById(R.id.chkCalendarAvailabilityWorkingElsewhere); + chkCalendarEvaluateAllDayEvent = (CheckBox)findViewById(R.id.chkCalendarEvaluateAllDayEvent); + chkCalendarEvaluateReoccurring = (CheckBox)findViewById(R.id.chkCalendarEvaluateReoccurring); + chkCalendarReoccurring = (CheckBox)findViewById(R.id.chkCalendarReoccurring); + + tvMissingCalendarHint = (TextView) findViewById(R.id.tvMissingCalendarHint); + + llCalendarSelection = (LinearLayout)findViewById(R.id.llCalendarSelection); + + etCalendarTitle = (EditText)findViewById(R.id.etCalendarTitle); + etCalendarLocation = (EditText)findViewById(R.id.etCalendarLocation); + etCalendarDescription = (EditText)findViewById(R.id.etCalendarDescription); + + bSaveTriggerCalendar = (Button)findViewById(R.id.bSaveTriggerCalendar); + + directions = new String[] { + getResources().getString(R.string.directionStringEquals), + getResources().getString(R.string.directionStringContains), + getResources().getString(R.string.directionStringDoesNotContain), + getResources().getString(R.string.directionStringStartsWith), + getResources().getString(R.string.directionStringEndsWith), + getResources().getString(R.string.directionStringNotEquals) + }; + directionSpinnerAdapter = new ArrayAdapter<>(this, R.layout.text_view_for_poi_listview_mediumtextsize, ActivityManageTriggerCalendar.directions); + spinnerCalendarTitleDirection.setAdapter(directionSpinnerAdapter); + spinnerCalendarLocationDirection.setAdapter(directionSpinnerAdapter); + spinnerCalendarDescriptionDirection.setAdapter(directionSpinnerAdapter); + directionSpinnerAdapter.notifyDataSetChanged(); + + chkCalendarEvaluateAllDayEvent.setChecked(false); + chkCalendarAllDayEvent.setEnabled(false); + chkCalendarEvaluateAllDayEvent.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) + { + chkCalendarAllDayEvent.setEnabled(checked); + } + }); + + chkCalendarEvaluateReoccurring.setChecked(false); + chkCalendarReoccurring.setEnabled(false); + chkCalendarEvaluateReoccurring.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) + { + chkCalendarReoccurring.setEnabled(checked); + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + { + if(ActivityPermissions.havePermission(Manifest.permission.READ_CALENDAR, ActivityManageTriggerCalendar.this) || ActivityPermissions.havePermission(Manifest.permission.WRITE_CALENDAR, ActivityManageTriggerCalendar.this)) + populateCalenderCheckboxes(); + else + { + AlertDialog.Builder builder = new AlertDialog.Builder(ActivityManageTriggerCalendar.this); + builder.setTitle(getResources().getString(R.string.info)); + builder.setMessage(getResources().getString(R.string.permissionCalendarRequired)); + builder.setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialogInterface, int i) + { + ActivityManageTriggerCalendar.this.finish(); + } + }); + builder.setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialogInterface, int i) + { + requestPermissions(new String[]{ Manifest.permission.READ_CALENDAR } , requestCodePermissionReadCalendar); + } + }); + builder.show(); + } + } + else + populateCalenderCheckboxes(); + + chkCalendarEventActive.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) + { + if(checked) + chkCalendarEventActive.setText(R.string.eventIsCurrentlyHappening); + else + chkCalendarEventActive.setText(R.string.eventIsCurrentlyNotHappening); + } + }); + + chkCalendarAllDayEvent.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) + { + if(checked) + chkCalendarAllDayEvent.setText(getResources().getString(R.string.allDayEventTrue)); + else + chkCalendarAllDayEvent.setText(getResources().getString(R.string.allDayEventFalse)); + } + }); + + chkCalendarReoccurring.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) + { + if(checked) + chkCalendarReoccurring.setText(R.string.reoccurringTrue); + else + chkCalendarReoccurring.setText(R.string.reoccurringFalse); + } + }); + + bSaveTriggerCalendar.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View view) + { + String titleDir = Trigger.getMatchCode(spinnerCalendarTitleDirection.getSelectedItem().toString()); + String title = etCalendarTitle.getText().toString(); + String descriptionDir = Trigger.getMatchCode(spinnerCalendarDescriptionDirection.getSelectedItem().toString()); + String description = etCalendarDescription.getText().toString(); + String locationDir = Trigger.getMatchCode(spinnerCalendarLocationDirection.getSelectedItem().toString()); + String location = etCalendarLocation.getText().toString(); + + List availabilityList = new ArrayList<>(); + if(chkCalendarAvailabilityBusy.isChecked()) + availabilityList.add(String.valueOf(CalendarContract.Events.AVAILABILITY_BUSY)); + + if(chkCalendarAvailabilityFree.isChecked()) + availabilityList.add(String.valueOf(CalendarContract.Events.AVAILABILITY_FREE)); + + if(chkCalendarAvailabilityTentative.isChecked()) + availabilityList.add(String.valueOf(CalendarContract.Events.AVAILABILITY_TENTATIVE)); + + if(chkCalendarAvailabilityOutOfOffice.isChecked()) + availabilityList.add(String.valueOf(CalendarReceiver.AVAILABILITY_OUT_OF_OFFICE)); + + if(chkCalendarAvailabilityWorkingElsewhere.isChecked()) + availabilityList.add(String.valueOf(CalendarReceiver.AVAILABILITY_WORKING_ELSEWHERE)); + + List selectedCalendarsList = new ArrayList<>(); + for(CheckBox calCheckbox : checkboxesCalendars) + { + if(calCheckbox.isChecked()) + selectedCalendarsList.add((CalendarReceiver.AndroidCalendar) calCheckbox.getTag()); + } + List selectedCalendarsIdArray = new ArrayList<>(); + for(CalendarReceiver.AndroidCalendar cal : selectedCalendarsList) + selectedCalendarsIdArray.add(String.valueOf(cal.calendarId)); + + String returnString = + titleDir + Trigger.triggerParameter2Split + title + Trigger.triggerParameter2Split + + descriptionDir + Trigger.triggerParameter2Split + description + Trigger.triggerParameter2Split + + locationDir + Trigger.triggerParameter2Split + location + Trigger.triggerParameter2Split + + String.valueOf(chkCalendarEvaluateAllDayEvent.isChecked()) + Trigger.triggerParameter2Split + + String.valueOf(chkCalendarAllDayEvent.isChecked()) + Trigger.triggerParameter2Split + + String.valueOf(chkCalendarEvaluateReoccurring.isChecked()) + Trigger.triggerParameter2Split + + String.valueOf(chkCalendarReoccurring.isChecked()) + Trigger.triggerParameter2Split + + Miscellaneous.explode(separator, availabilityList.toArray(new String[availabilityList.size()])) + Trigger.triggerParameter2Split + + Miscellaneous.explode(separator, selectedCalendarsIdArray.toArray(new String[selectedCalendarsIdArray.size()])); + + Intent data = new Intent(); + data.putExtra(ActivityManageRule.intentNameTriggerParameter1, chkCalendarEventActive.isChecked()); + data.putExtra(ActivityManageRule.intentNameTriggerParameter2, returnString); + ActivityManageTriggerCalendar.this.setResult(RESULT_OK, data); + + finish(); + } + }); + + Intent inputIntent = getIntent(); + if(inputIntent.hasExtra(ActivityManageRule.intentNameTriggerParameter1)) + loadValuesIntoGui(inputIntent); + } + + private void populateCalenderCheckboxes() + { + List calList = CalendarReceiver.readCalendars(ActivityManageTriggerCalendar.this); + + if(calList != null) + { + if(calList.size() > 0) + { + for (CalendarReceiver.AndroidCalendar cal : calList) + { + CheckBox oneCalCheckbox = new CheckBox(ActivityManageTriggerCalendar.this); + oneCalCheckbox.setText(cal.toString()); + oneCalCheckbox.setTag(cal); + llCalendarSelection.addView(oneCalCheckbox); + checkboxesCalendars.add(oneCalCheckbox); + } + } + else + Miscellaneous.messageBox(getResources().getString(R.string.warning), getResources().getString(R.string.noCalendarsOnYourDevice), ActivityManageTriggerCalendar.this).show(); + } + else + Miscellaneous.messageBox(getResources().getString(R.string.warning), getResources().getString(R.string.errorReadingCalendars), ActivityManageTriggerCalendar.this).show(); + } + + void loadValuesIntoGui(Intent data) + { + try + { + if (data.hasExtra(ActivityManageRule.intentNameTriggerParameter1)) + chkCalendarEventActive.setChecked(data.getBooleanExtra(ActivityManageRule.intentNameTriggerParameter1, true)); + + if (data.hasExtra(ActivityManageRule.intentNameTriggerParameter2)) + { + String input[] = data.getStringExtra(ActivityManageRule.intentNameTriggerParameter2).split(Trigger.triggerParameter2Split, -1); + /* + 0 = titleDir + 1 = title + 2 = descriptionDir + 3 = description + 4 = locationDir + 5 = location + 6 = evaluate all day event + 7 = all day event + 8 = evaluate reoccurring + 9 = reoccurring + 10 = availability list + 11 = calendars list + */ + + for (int i = 0; i < directions.length; i++) + { + if (Trigger.getMatchCode(directions[i]).equalsIgnoreCase(input[0])) + spinnerCalendarTitleDirection.setSelection(i); + + if (Trigger.getMatchCode(directions[i]).equalsIgnoreCase(input[2])) + spinnerCalendarDescriptionDirection.setSelection(i); + + if (Trigger.getMatchCode(directions[i]).equalsIgnoreCase(input[4])) + spinnerCalendarLocationDirection.setSelection(i); + } + + etCalendarTitle.setText(input[1]); + etCalendarDescription.setText(input[3]); + etCalendarLocation.setText(input[5]); + + chkCalendarEvaluateAllDayEvent.setChecked(Boolean.parseBoolean(input[6])); + chkCalendarAllDayEvent.setChecked(Boolean.parseBoolean(input[7])); + + chkCalendarEvaluateReoccurring.setChecked(Boolean.parseBoolean(input[8])); + chkCalendarReoccurring.setChecked(Boolean.parseBoolean(input[9])); + + String[] availabilities = null; + if (!StringUtils.isEmpty(input[10])) + availabilities = input[10].split(separator); + + if (availabilities != null) + { + for (String avail : availabilities) + { + if (Integer.parseInt(avail) == CalendarContract.Events.AVAILABILITY_BUSY) + chkCalendarAvailabilityBusy.setChecked(true); + else if (Integer.parseInt(avail) == CalendarContract.Events.AVAILABILITY_FREE) + chkCalendarAvailabilityFree.setChecked(true); + else if (Integer.parseInt(avail) == CalendarContract.Events.AVAILABILITY_TENTATIVE) + chkCalendarAvailabilityTentative.setChecked(true); + else if (Integer.parseInt(avail) == CalendarReceiver.AVAILABILITY_OUT_OF_OFFICE) + chkCalendarAvailabilityOutOfOffice.setChecked(true); + else if (Integer.parseInt(avail) == CalendarReceiver.AVAILABILITY_WORKING_ELSEWHERE) + chkCalendarAvailabilityWorkingElsewhere.setChecked(true); + } + } + + String[] calendars = null; + if (!StringUtils.isEmpty(input[11])) + calendars = input[11].split(separator); + + if (calendars != null) + { + List usedCalendarIDs = new ArrayList<>(); + List unusedCalendarIDs = new ArrayList<>(); + for (CheckBox checkbox : checkboxesCalendars) + { + int id = ((CalendarReceiver.AndroidCalendar) checkbox.getTag()).calendarId; + for (String calId : calendars) + { + if (calId.equals(String.valueOf(id))) + { + usedCalendarIDs.add(String.valueOf(id)); + checkbox.setChecked(true); + break; + } + } + } + for (String calId : calendars) + { + if (!Miscellaneous.arraySearch((ArrayList) usedCalendarIDs, calId, false, true)) + unusedCalendarIDs.add(calId); + } + if (unusedCalendarIDs.size() > 0) + { + /* + A calendar has been configured that has been deleted since. We cannot resolve it. + It will be removed with the next save, but we should inform this user + of these circumstances. + */ + + tvMissingCalendarHint.setText(String.format(getResources().getString(R.string.calendarsMissingHint), Miscellaneous.explode(", ", (ArrayList) unusedCalendarIDs))); + } + } + } + } + catch (Exception e) + { + Miscellaneous.logEvent("e", "ActivityManagerTriggerCalender", "Error loading values into GUI: " + Log.getStackTraceString(e), 1); + Toast.makeText(ActivityManageTriggerCalendar.this, getResources().getString(R.string.errorLoadingValues), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) + { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if(requestCode == requestCodePermissionReadCalendar) + { + if( + permissions[0].equals(Manifest.permission.READ_CALENDAR) + || + permissions[0].equals(Manifest.permission.WRITE_CALENDAR) + ) + { + if(grantResults[0] == PackageManager.PERMISSION_GRANTED) + populateCalenderCheckboxes(); + else + finish(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/ActivityManageTriggerCharging.java b/app/src/main/java/com/jens/automation2/ActivityManageTriggerCharging.java new file mode 100644 index 0000000..8a87c7e --- /dev/null +++ b/app/src/main/java/com/jens/automation2/ActivityManageTriggerCharging.java @@ -0,0 +1,87 @@ +package com.jens.automation2; + +import android.app.Activity; +import android.content.Intent; +import android.os.BatteryManager; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.RadioButton; + +import androidx.annotation.Nullable; + +import com.jens.automation2.ActivityManageRule; +import com.jens.automation2.Miscellaneous; +import com.jens.automation2.R; +import com.jens.automation2.Trigger; + +import org.apache.commons.lang3.StringUtils; + +public class ActivityManageTriggerCharging extends Activity +{ + RadioButton rbChargingOn, rbChargingOff, rbChargingTypeAny, rbChargingTypeAc, rbChargingTypeUsb, rbChargingTypeWireless; + Button bTriggerChargingSave; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + Miscellaneous.setDisplayLanguage(this); + setContentView(R.layout.activity_manage_trigger_charging); + + rbChargingOn = (RadioButton) findViewById(R.id.rbChargingOn); + rbChargingOff = (RadioButton) findViewById(R.id.rbChargingOff); + rbChargingTypeAny = (RadioButton) findViewById(R.id.rbChargingTypeAny); + rbChargingTypeAc = (RadioButton) findViewById(R.id.rbChargingTypeAc); + rbChargingTypeUsb = (RadioButton) findViewById(R.id.rbChargingTypeUsb); + rbChargingTypeWireless = (RadioButton) findViewById(R.id.rbChargingTypeWireless); + + bTriggerChargingSave = (Button) findViewById(R.id.bTriggerChargingSave); + + Intent input = getIntent(); + if(input.hasExtra(ActivityManageRule.intentNameTriggerParameter1)) + { + + rbChargingOn.setChecked(input.getBooleanExtra(ActivityManageRule.intentNameTriggerParameter1, true)); + rbChargingOff.setChecked(!input.getBooleanExtra(ActivityManageRule.intentNameTriggerParameter1, false)); + + if(input.hasExtra(ActivityManageRule.intentNameTriggerParameter2)) + { + + String[] params2 = input.getStringExtra(ActivityManageRule.intentNameTriggerParameter2).split(Trigger.triggerParameter2Split); + int chargingType = Integer.parseInt(params2[0]); + + rbChargingTypeAny.setChecked(chargingType == 0); + rbChargingTypeAc.setChecked(chargingType == BatteryManager.BATTERY_PLUGGED_AC); + rbChargingTypeUsb.setChecked(chargingType == BatteryManager.BATTERY_PLUGGED_USB); + rbChargingTypeWireless.setChecked(chargingType == BatteryManager.BATTERY_PLUGGED_WIRELESS); + } + } + + bTriggerChargingSave.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View view) + { + Intent response = new Intent(); + response.putExtra(ActivityManageRule.intentNameTriggerParameter1, rbChargingOn.isChecked()); + + String param2 = ""; + + if(rbChargingTypeAny.isChecked()) + param2 = "0"; + else if(rbChargingTypeAc.isChecked()) + param2 = String.valueOf(BatteryManager.BATTERY_PLUGGED_AC); + else if(rbChargingTypeUsb.isChecked()) + param2 = String.valueOf(BatteryManager.BATTERY_PLUGGED_USB); + else if(rbChargingTypeWireless.isChecked()) + param2 = String.valueOf(BatteryManager.BATTERY_PLUGGED_WIRELESS); + + response.putExtra(ActivityManageRule.intentNameTriggerParameter2, param2); + + setResult(RESULT_OK, response); + finish(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/ActivityPermissions.java b/app/src/main/java/com/jens/automation2/ActivityPermissions.java index 6bf0f00..f637f6f 100644 --- a/app/src/main/java/com/jens/automation2/ActivityPermissions.java +++ b/app/src/main/java/com/jens/automation2/ActivityPermissions.java @@ -18,6 +18,8 @@ import android.os.Bundle; import android.os.PowerManager; import android.provider.Settings; import android.text.Html; +import android.text.TextUtils; +import android.text.util.Linkify; import android.util.Log; import android.view.View; import android.widget.Button; @@ -51,12 +53,14 @@ public class ActivityPermissions extends Activity private static final int requestCodeForPermissionsBatteryOptimization = 12048; private static final int requestCodeForPermissionNotificationAccessAndroid13 = 12049; private static final int requestCodeForPermissionsManageOverlay = 12050; + private static final int requestCodeForPermissionsAccessibility = 12051; + private static final int requestCodeForPermissionsScheduleExactAlarms = 12052; protected String[] specificPermissionsToRequest = null; public static String intentExtraName = "permissionsToBeRequested"; Button bCancelPermissions, bRequestPermissions; - TextView tvPermissionsExplanation, tvPermissionsExplanationSystemSettings, tvPermissionsExplanationLong; + TextView tvPermissionsExplanation, tvPermissionsExplanationSystemSettings, tvPermissionsExplanationLong, tvRestrictionPermissionsNotice; static ActivityPermissions instance = null; public final static String permissionNameWireguard = "com.wireguard.android.permission.CONTROL_TUNNELS"; @@ -87,6 +91,7 @@ public class ActivityPermissions extends Activity tvPermissionsExplanation = (TextView)findViewById(R.id.tvPermissionsExplanation); tvPermissionsExplanationSystemSettings = (TextView)findViewById(R.id.tvPermissionsExplanationSystemSettings); tvPermissionsExplanationLong = (TextView)findViewById(R.id.tvPermissionsExplanationLong); + tvRestrictionPermissionsNotice = (TextView)findViewById(R.id.tvRestrictionPermissionsNotice); bCancelPermissions.setOnClickListener(new View.OnClickListener() { @@ -161,7 +166,7 @@ public class ActivityPermissions extends Activity /* Filter location permission and only name it once */ - if(s.equals(Manifest.permission.ACCESS_COARSE_LOCATION) | s.equals(Manifest.permission.ACCESS_FINE_LOCATION)) + if(s.equals(Manifest.permission.ACCESS_COARSE_LOCATION) || s.equals(Manifest.permission.ACCESS_FINE_LOCATION)) { if(!locationPermissionExplained) { @@ -305,6 +310,10 @@ public class ActivityPermissions extends Activity { return android.provider.Settings.canDrawOverlays(Miscellaneous.getAnyContext()); } + else if(s.equals(Manifest.permission.BIND_ACCESSIBILITY_SERVICE)) + { + return haveAccessibilityAccess(Miscellaneous.getAnyContext()); + } else { int res = context.checkCallingOrSelfPermission(s); @@ -323,11 +332,59 @@ public class ActivityPermissions extends Activity return active; } + public static boolean haveAccessibilityAccess(Context mContext) + { + int accessibilityEnabled = 0; + + final String service = mContext.getPackageName() + "/" + BuildConfig.APPLICATION_ID + ".MyAccessibilityService"; + + boolean accessibilityFound = false; + try + { + accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED); +// Log.v(TAG, "accessibilityEnabled = " + accessibilityEnabled); + } + catch (Settings.SettingNotFoundException e) + { +// Log.e(TAG, "Error finding setting, default accessibility to not found: " + e.getMessage()); + } + TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':'); + + if (accessibilityEnabled == 1) + { + String settingValue = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + if (settingValue != null) + { + TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + splitter.setString(settingValue); + while (splitter.hasNext()) + { + String accessibilityService = splitter.next(); + + if (accessibilityService.equalsIgnoreCase(service)) + { + return true; + } + } + } + } + + return accessibilityFound; + } + public static void requestOverlay() { Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION); ActivityPermissions.getInstance().startActivityForResult(intent, requestCodeForPermissionsManageOverlay); } + + public static void requestBindAccessibilityService() + { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ActivityPermissions.getInstance().startActivityForResult(intent, requestCodeForPermissionsAccessibility); + } + public static void requestDeviceAdmin() { if(!haveDeviceAdmin()) @@ -370,10 +427,19 @@ public class ActivityPermissions extends Activity if(!havePermission(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, workingContext)) addToArrayListUnique(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, requiredPermissions); - for(Profile p : Profile.getProfileCollection()) + if(Build.VERSION.SDK_INT >= 33 && BuildConfig.FLAVOR.equals(AutomationService.flavor_name_googleplay)) { - if(p.changeIncomingCallsRingtone || p.changeNotificationRingtone) - addToArrayListUnique(Manifest.permission.READ_EXTERNAL_STORAGE, requiredPermissions); + if (!havePermission(android.Manifest.permission.POST_NOTIFICATIONS, workingContext)) + addToArrayListUnique(android.Manifest.permission.POST_NOTIFICATIONS, requiredPermissions); + } + + if(!havePermission(Manifest.permission.READ_EXTERNAL_STORAGE, workingContext)) + { + for (Profile p : Profile.getProfileCollection()) + { + if (p.changeIncomingCallsRingtone || p.changeNotificationRingtone) + addToArrayListUnique(Manifest.permission.READ_EXTERNAL_STORAGE, requiredPermissions); + } } if (!onlyGeneral) @@ -407,27 +473,6 @@ public class ActivityPermissions extends Activity } } } - - /* - Not all permissions need to be asked for. - */ - - /*if(shouldShowRequestPermissionRationale("android.permission.RECORD_AUDIO")) - Toast.makeText(ActivityMainScreen.this, "shouldShowRequestPermissionRationale", Toast.LENGTH_LONG).show(); - else - Toast.makeText(ActivityMainScreen.this, "not shouldShowRequestPermissionRationale", Toast.LENGTH_LONG).show();*/ - -// addToArrayListUnique("Manifest.permission.RECORD_AUDIO", requiredPermissions); - /*int hasPermission = checkSelfPermission(Manifest.permission.RECORD_AUDIO); - if (hasPermission == PackageManager.PERMISSION_DENIED) - { - Toast.makeText(ActivityMainScreen.this, "Don't have record_audio. Requesting...", Toast.LENGTH_LONG).show(); -// requestPermissions(new String[]{"Manifest.permission.CAMERA"}, requestCodeForPermissions); - ActivityCompat.requestPermissions(ActivityMainScreen.this, new String[]{"Manifest.permission.CAMERA"}, requestCodeForPermissions); - } - else - Toast.makeText(ActivityMainScreen.this, "Have record_audio.", Toast.LENGTH_LONG).show();*/ - } return requiredPermissions.toArray(new String[requiredPermissions.size()]); @@ -520,6 +565,8 @@ public class ActivityPermissions extends Activity addToArrayListUnique(Manifest.permission.INTERNET, requiredPermissions); break; case timeFrame: + if(Build.VERSION.SDK_INT >= 31 && Miscellaneous.getTargetSDK(Miscellaneous.getAnyContext()) >= 31) + addToArrayListUnique(Manifest.permission.SCHEDULE_EXACT_ALARM, requiredPermissions); break; case usb_host_connection: addToArrayListUnique(Manifest.permission.READ_PHONE_STATE, requiredPermissions); @@ -542,6 +589,11 @@ public class ActivityPermissions extends Activity case notification: addToArrayListUnique(Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE, requiredPermissions); break; + case calendarEvent: + addToArrayListUnique(Manifest.permission.READ_CALENDAR, requiredPermissions); + if(Build.VERSION.SDK_INT >= 31 && Miscellaneous.getTargetSDK(Miscellaneous.getAnyContext()) >= 31) + addToArrayListUnique(Manifest.permission.SCHEDULE_EXACT_ALARM, requiredPermissions); + break; default: break; } @@ -650,7 +702,18 @@ public class ActivityPermissions extends Activity // ) // addToArrayListUnique("net.kollnig.missioncontrol.permission.ADMIN", requiredPermissions); if(Build.VERSION.SDK_INT >= 29) - addToArrayListUnique(Manifest.permission.SYSTEM_ALERT_WINDOW, requiredPermissions); + { + String parts[]; + if(action.getParameter2().contains(Action.actionParameter2Split)) + parts = action.getParameter2().split(Action.actionParameter2Split); + else + parts = action.getParameter2().split(";"); + + // Permission only required for starts of activity, not broadcasts or services + + if(parts[2].equals(ActivityManageActionStartActivity.startByActivityString)) + addToArrayListUnique(Manifest.permission.SYSTEM_ALERT_WINDOW, requiredPermissions); + } break; case triggerUrl: addToArrayListUnique(Manifest.permission.INTERNET, requiredPermissions); @@ -711,6 +774,12 @@ public class ActivityPermissions extends Activity case stopPhoneCall: addToArrayListUnique(Manifest.permission.ANSWER_PHONE_CALLS, requiredPermissions); break; + case takeScreenshot: + addToArrayListUnique(Manifest.permission.BIND_ACCESSIBILITY_SERVICE, requiredPermissions); + break; + case setLocationService: + addToArrayListUnique(Manifest.permission.WRITE_SECURE_SETTINGS, requiredPermissions); + break; default: break; } @@ -773,6 +842,12 @@ public class ActivityPermissions extends Activity case Manifest.permission.WRITE_EXTERNAL_STORAGE: usingElements.add(getResources().getString(R.string.storeSettings)); break; + case Manifest.permission.SCHEDULE_EXACT_ALARM: + for(String ruleName : getRulesUsing(Trigger.Trigger_Enum.timeFrame)) + usingElements.add(String.format(getResources().getString(R.string.ruleXrequiresThis), ruleName)); + for(String ruleName : getRulesUsing(Trigger.Trigger_Enum.calendarEvent)) + usingElements.add(String.format(getResources().getString(R.string.ruleXrequiresThis), ruleName)); + break; case Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE: for(String ruleName : getRulesUsing(Trigger.Trigger_Enum.notification)) usingElements.add(String.format(getResources().getString(R.string.ruleXrequiresThis), ruleName)); @@ -960,6 +1035,18 @@ public class ActivityPermissions extends Activity case Manifest.permission.QUERY_ALL_PACKAGES: usingElements.add(getResources().getString(R.string.queryAllPackages)); break; + case Manifest.permission.BIND_ACCESSIBILITY_SERVICE: + for(String ruleName : getRulesUsing(Action.Action_Enum.takeScreenshot)) + usingElements.add(String.format(getResources().getString(R.string.ruleXrequiresThis), ruleName)); + break; + case Manifest.permission.WRITE_SECURE_SETTINGS: + for(String ruleName : getRulesUsing(Action.Action_Enum.setLocationService)) + usingElements.add(String.format(getResources().getString(R.string.ruleXrequiresThis), ruleName)); + break; + case Manifest.permission.READ_CALENDAR: + for(String ruleName : getRulesUsing(Trigger.Trigger_Enum.calendarEvent)) + usingElements.add(String.format(getResources().getString(R.string.ruleXrequiresThis), ruleName)); + break; } return usingElements; @@ -968,6 +1055,15 @@ public class ActivityPermissions extends Activity @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + /* + All of the following permissions need to be "manually" activated by the user in some + buried system menu. + In my opinion by mistake the function will be called when the user has just landed + on one of those screens, not when he exits it again. To compensate for that onResume() + is overridden. This enables the permission screen to automatically close after all + required permissions have been granted. + */ + super.onActivityResult(requestCode, resultCode, data); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) @@ -1015,6 +1111,14 @@ public class ActivityPermissions extends Activity if (requestCode == requestCodeForPermissionsManageOverlay) if(havePermission(Manifest.permission.SYSTEM_ALERT_WINDOW, ActivityPermissions.this)) requestPermissions(cachedPermissionsToRequest, true); + + if (requestCode == requestCodeForPermissionsAccessibility) + if(havePermission(Manifest.permission.BIND_ACCESSIBILITY_SERVICE, ActivityPermissions.this)) + requestPermissions(cachedPermissionsToRequest, true); + + if (requestCode == requestCodeForPermissionsScheduleExactAlarms) + if(havePermission(Manifest.permission.SCHEDULE_EXACT_ALARM, ActivityPermissions.this)) + requestPermissions(cachedPermissionsToRequest, true); } } @@ -1074,10 +1178,14 @@ public class ActivityPermissions extends Activity } else if (s.equalsIgnoreCase(Manifest.permission.ACCESS_NOTIFICATION_POLICY)) { + if(BuildConfig.FLAVOR.equals(AutomationService.flavor_name_apk)) + Miscellaneous.messageBox(getResources().getString(R.string.info), getResources().getString(R.string.noticeRestrictedPermissions), ActivityPermissions.this).show(); + requiredPermissions.remove(s); cachedPermissionsToRequest = requiredPermissions; Intent intent = new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS); startActivityForResult(intent, requestCodeForPermissionsNotificationPolicy); + return; } else if (s.equalsIgnoreCase(Manifest.permission.SYSTEM_ALERT_WINDOW)) @@ -1096,6 +1204,22 @@ public class ActivityPermissions extends Activity diag.show(); return; } + else if (s.equalsIgnoreCase(Manifest.permission.BIND_ACCESSIBILITY_SERVICE)) + { + AlertDialog diag = Miscellaneous.messageBox(getResources().getString(R.string.info), getResources().getString(R.string.accessibilityApiPermissionHint), ActivityPermissions.this); + diag.setOnDismissListener(new DialogInterface.OnDismissListener() + { + @Override + public void onDismiss(DialogInterface dialogInterface) + { + requiredPermissions.remove(s); + cachedPermissionsToRequest = requiredPermissions; + requestBindAccessibilityService(); + } + }); + diag.show(); + return; + } else if (s.equalsIgnoreCase(Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE)) { if(Build.VERSION.SDK_INT >= 33) @@ -1122,6 +1246,22 @@ public class ActivityPermissions extends Activity return; } + else if (s.equalsIgnoreCase(Manifest.permission.SCHEDULE_EXACT_ALARM)) + { + AlertDialog diag = Miscellaneous.messageBox(getResources().getString(R.string.info), getResources().getString(R.string.alarmsPermissionHint), ActivityPermissions.this); + diag.setOnDismissListener(new DialogInterface.OnDismissListener() + { + @Override + public void onDismiss(DialogInterface dialogInterface) + { + requiredPermissions.remove(s); + cachedPermissionsToRequest = requiredPermissions; + requestScheduleExactAlarms(); + } + }); + diag.show(); + return; + } else if(s.equals(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)) { requiredPermissions.remove(s); @@ -1151,6 +1291,13 @@ public class ActivityPermissions extends Activity return; } + else if(s.equalsIgnoreCase(Manifest.permission.WRITE_SECURE_SETTINGS)) + { + AlertDialog diaglog = Miscellaneous.messageBox(getResources().getString(R.string.info), getResources().getString(R.string.writeSecureSettingsNotice), ActivityPermissions.this); + diaglog.show(); + Linkify.addLinks((TextView) diaglog.findViewById(android.R.id.message), Linkify.ALL); +// return; + } } } @@ -1200,6 +1347,14 @@ public class ActivityPermissions extends Activity startActivityForResult(intent, requestCodeForPermissionsNotifications); } + + void requestScheduleExactAlarms() + { + Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); + startActivityForResult(intent, requestCodeForPermissionsScheduleExactAlarms); + } + + protected void applyChanges() { AutomationService service = AutomationService.getInstance(); @@ -1578,4 +1733,47 @@ public class ActivityPermissions extends Activity return false; } } + + @Override + protected void onResume() + { + super.onResume(); + + if(Build.VERSION.SDK_INT >= 33 && BuildConfig.FLAVOR.equals(AutomationService.flavor_name_apk)) + { + for (String p : getRequiredPermissions(false)) + { + if (p.equals(Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE) || p.equals(Manifest.permission.BIND_ACCESSIBILITY_SERVICE)) + { + tvRestrictionPermissionsNotice.setText(getResources().getString(R.string.noticeRestrictedPermissions)); + + /* + Opening the app's settings directly does not work because the + mentioned 3 dots are only displayed when you went there the hard way. + */ + /* + tvRestrictionPermissionsNotice.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View view) + { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + } + })*/; + break; + } + } + } + + for(String p : getRequiredPermissions(false)) + { + if(!havePermission(p, this)) + return; + } + + // have all + setHaveAllPermissions(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/ActivityVolumeTest.java b/app/src/main/java/com/jens/automation2/ActivityVolumeTest.java index 4fce038..91cdc0b 100644 --- a/app/src/main/java/com/jens/automation2/ActivityVolumeTest.java +++ b/app/src/main/java/com/jens/automation2/ActivityVolumeTest.java @@ -49,20 +49,15 @@ public class ActivityVolumeTest extends Activity @Override public void onStopTrackingTouch(SeekBar seekBar) { - // TODO Auto-generated method stub - } @Override public void onStartTrackingTouch(SeekBar seekBar) { - // TODO Auto-generated method stub - } @Override - public void onProgressChanged(SeekBar seekBar, int progress, - boolean fromUser) + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { etReferenceValue.setText(String.valueOf(sbReferenceValue.getProgress())); } diff --git a/app/src/main/java/com/jens/automation2/AsyncTasks.java b/app/src/main/java/com/jens/automation2/AsyncTasks.java index 793e2f4..4aefcb7 100644 --- a/app/src/main/java/com/jens/automation2/AsyncTasks.java +++ b/app/src/main/java/com/jens/automation2/AsyncTasks.java @@ -22,7 +22,7 @@ public class AsyncTasks try { - String result = Miscellaneous.downloadURL("https://server47.de/automation/?action=getLatestVersionCode", null, null).trim(); + String result = Miscellaneous.downloadURL("https://server47.de/automation/?action=getLatestVersionCode", null, null, ActivityManageActionTriggerUrl.methodGet, null).trim(); int latestVersion = Integer.parseInt(result); // At this point the update check itself has already been successful. diff --git a/app/src/main/java/com/jens/automation2/AutomationService.java b/app/src/main/java/com/jens/automation2/AutomationService.java index 51c7120..cae6c32 100644 --- a/app/src/main/java/com/jens/automation2/AutomationService.java +++ b/app/src/main/java/com/jens/automation2/AutomationService.java @@ -28,6 +28,7 @@ import androidx.core.app.NotificationManagerCompat; import com.jens.automation2.Trigger.Trigger_Enum; import com.jens.automation2.location.LocationProvider; +import com.jens.automation2.receivers.CalendarReceiver; import com.jens.automation2.receivers.DateTimeListener; import com.jens.automation2.receivers.PackageReplacedReceiver; import com.jens.automation2.receivers.PhoneStatusListener; @@ -127,7 +128,18 @@ public class AutomationService extends Service implements OnInitListener // Store a reference to myself. Other classes often need a context or something, this can provide that. centralInstance = this; - Miscellaneous.setDisplayLanguage(AutomationService.this); + /* + This has been reported to throw a NullPointerException under + rare circumstances. The root cause remains unknown. + */ + try + { + Miscellaneous.setDisplayLanguage(AutomationService.this); + } + catch(NullPointerException e) + { + Miscellaneous.logEvent("e", "setDisplayLanguage()", Log.getStackTraceString(e), 3); + } } public boolean checkStartupRequirements(Context context, boolean startAtBoot) @@ -309,6 +321,7 @@ public class AutomationService extends Service implements OnInitListener ReceiverCoordinator.applySettingsAndRules(); DateTimeListener.reloadAlarms(); + CalendarReceiver.armOrRearmTimer(); } @Override @@ -679,8 +692,6 @@ public class AutomationService extends Service implements OnInitListener @Override public void onInit(int status) { - // TODO Auto-generated method stub - } /** diff --git a/app/src/main/java/com/jens/automation2/Miscellaneous.java b/app/src/main/java/com/jens/automation2/Miscellaneous.java index 4275f26..106be2c 100644 --- a/app/src/main/java/com/jens/automation2/Miscellaneous.java +++ b/app/src/main/java/com/jens/automation2/Miscellaneous.java @@ -37,16 +37,22 @@ import android.util.Log; import android.widget.Toast; import com.jens.automation2.location.LocationProvider; +import com.jens.automation2.receivers.CalendarReceiver; import com.jens.automation2.receivers.NotificationListener; import com.jens.automation2.receivers.PhoneStatusListener; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; +import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; @@ -69,6 +75,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.lang.Thread.UncaughtExceptionHandler; @@ -94,6 +101,7 @@ import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Scanner; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -113,6 +121,8 @@ import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; import androidx.documentfile.provider.DocumentFile; +import eu.chainfire.libsuperuser.Shell; + public class Miscellaneous extends Service { protected static String writeableFolderStringCache = null; @@ -120,7 +130,7 @@ public class Miscellaneous extends Service public static final String lineSeparator = System.getProperty("line.separator"); - public static String downloadURL(String url, String username, String password) + public static String downloadURL(String url, String username, String password, String method, Map httpParams) { HttpClient httpclient = new DefaultHttpClient(); StringBuilder responseBody = new StringBuilder(); @@ -148,7 +158,27 @@ public class Miscellaneous extends Service connection.setDoOutput(true); connection.setRequestProperty ("Authorization", "Basic " + encodedCredentials); } - + else if(method.equals(ActivityManageActionTriggerUrl.methodPost)) + connection.setRequestMethod("POST"); + + if(httpParams.size() > 0) + { + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + + List paramPairs = new ArrayList(); + + for(String key : httpParams.keySet()) + paramPairs.add(new BasicNameValuePair(key, httpParams.get(key))); + + OutputStream os = connection.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); + writer.write(getQuery(paramPairs)); + writer.flush(); + writer.close(); + } + InputStream content = (InputStream)connection.getInputStream(); BufferedReader in = new BufferedReader (new InputStreamReader (content)); String line; @@ -173,34 +203,67 @@ public class Miscellaneous extends Service return responseBody.toString(); } } - - public static String downloadURLwithoutCertificateChecking(String url, String username, String password) - { -// HttpClient httpclient = new DefaultHttpClient(); -// StringBuilder responseBody = new StringBuilder(); - boolean errorFound = false; - try + private static String getQuery(List params) throws UnsupportedEncodingException + { + StringBuilder result = new StringBuilder(); + boolean first = true; + + for (NameValuePair pair : params) + { + if (first) + first = false; + else + result.append("&"); + + result.append(URLEncoder.encode(pair.getName(), "UTF-8")); + result.append("="); + result.append(URLEncoder.encode(pair.getValue(), "UTF-8")); + } + + return result.toString(); + } + + public static String downloadUrlWithoutCertificateChecking(String url, String username, String password, String method, Map httpParams) + { + try { HttpParams params = new BasicHttpParams(); params.setParameter(HttpProtocolParams.USE_EXPECT_CONTINUE, false); HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); HttpClient httpclient = new DefaultHttpClient(params); httpclient = Actions.getInsecureSslClient(httpclient); - - HttpPost httppost = new HttpPost(url); + + HttpRequestBase httpRequest; + if( + method.equals(ActivityManageActionTriggerUrl.methodPost) + || + (username != null && password != null) + || + httpParams.size() > 0 + ) + httpRequest = new HttpPost(url); + else + httpRequest = new HttpGet(url); // Add http simple authentication if specified if(username != null && password != null) { String encodedCredentials = Base64.encodeToString(new String(username + ":" + password).getBytes(), Base64.DEFAULT); -// List nameValuePairs = new ArrayList(1); - httppost.addHeader("Authorization", "Basic " + encodedCredentials); -// nameValuePairs.add(new BasicNameValuePair("Authorization", "Basic " + encodedCredentials)); -// httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs, "UTF-8")); + httpRequest.addHeader("Authorization", "Basic " + encodedCredentials); } + + if(httpParams.size() > 0) + { + List paramPairs = new ArrayList(); + + for(String key : httpParams.keySet()) + paramPairs.add(new BasicNameValuePair(key, httpParams.get(key))); + + ((HttpPost)httpRequest).setEntity(new UrlEncodedFormEntity(paramPairs, "UTF-8")); + } - HttpResponse response = httpclient.execute(httppost); + HttpResponse response = httpclient.execute(httpRequest); HttpEntity entity = response.getEntity(); if (entity != null) { @@ -211,8 +274,7 @@ public class Miscellaneous extends Service catch(Exception e) { Miscellaneous.logEvent("e", "HTTP error", Log.getStackTraceString(e), 3); - errorFound = true; - return "httpError"; + return "httpError"; } // finally // { @@ -237,25 +299,9 @@ public class Miscellaneous extends Service @Override public IBinder onBind(Intent arg0) { - // TODO Auto-generated method stub return null; } -// public static void logEvent(String type, String header, String description) -// { -// if(type.equals("e")) -// Log.e(header, description); -// -// if(type.equals("w")) -// Log.w(header, description); -// -// if(type.equals("i")) -// Log.i(header, description); -// -// if(Settings.writeLogFile) -// writeToLogFile(type, header, description); -// } - public static void logEvent(String type, String header, String description, int logLevel) { try @@ -292,7 +338,6 @@ public class Miscellaneous extends Service { logCleanerRunning = true; - long maxSizeInBytes = (long)Settings.logFileMaxSize * 1024 * 1024; if(logFile.exists() && logFile.length() > (maxSizeInBytes)) @@ -746,6 +791,78 @@ public class Miscellaneous extends Service } } + if(source.contains("[last_trigger_url_result]")) + { + try + { + source = source.replace("[last_trigger_url_result]", AutomationService.getInstance().getVariableMap().get("last_trigger_url_result")); + } + catch (Exception e) + { + Miscellaneous.logEvent("w", "Variable replacement", "Error replacing variable last_trigger_url_result.", 3); + } + } + + if(source.contains("[last_run_executable_exit_code]")) + { + try + { + source = source.replace("[last_run_executable_exit_code]", AutomationService.getInstance().getVariableMap().get("last_run_executable_exit_code")); + } + catch (Exception e) + { + Miscellaneous.logEvent("w", "Variable replacement", "Error replacing variable last_run_executable_exit_code.", 3); + } + } + + if(source.contains("[last_run_executable_output]")) + { + try + { + source = source.replace("[last_run_executable_output]", AutomationService.getInstance().getVariableMap().get("last_run_executable_output")); + } + catch (Exception e) + { + Miscellaneous.logEvent("w", "Variable replacement", "Error replacing variable last_run_executable_output.", 3); + } + } + + if(source.contains("[last_calendar_title]")) + { + try + { + source = source.replace("[last_calendar_title]", CalendarReceiver.getLastTriggeringEvent().title); + } + catch (Exception e) + { + Miscellaneous.logEvent("w", "Variable replacement", "Error replacing variable last_calendar_title.", 3); + } + } + + if(source.contains("[last_calendar_description]")) + { + try + { + source = source.replace("[last_calendar_description]", CalendarReceiver.getLastTriggeringEvent().description); + } + catch (Exception e) + { + Miscellaneous.logEvent("w", "Variable replacement", "Error replacing variable last_calendar_description.", 3); + } + } + + if(source.contains("[last_calendar_location]")) + { + try + { + source = source.replace("[last_calendar_location]", CalendarReceiver.getLastTriggeringEvent().location); + } + catch (Exception e) + { + Miscellaneous.logEvent("w", "Variable replacement", "Error replacing variable last_calendar_location.", 3); + } + } + while(source.contains("[variable-")) { int pos1 = source.indexOf("[variable-"); @@ -792,7 +909,7 @@ public class Miscellaneous extends Service alertDialog.setTitle(title); alertDialog.setMessage(message); - alertDialog.setPositiveButton("Ok", new DialogInterface.OnClickListener() + alertDialog.setPositiveButton(context.getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { @@ -828,36 +945,40 @@ public class Miscellaneous extends Service */ public static boolean isPhoneRooted() { -// if(true) -// return true; - - // get from build info - String buildTags = Build.TAGS; - if (buildTags != null && buildTags.contains("test-keys")) - { - return true; - } - - // check if /system/app/Superuser.apk is present try { - File file = new File("/system/app/Superuser.apk"); - if (file.exists()) + return Shell.SU.available(); + } + catch(Exception e) + { + // get from build info + String buildTags = Build.TAGS; + if (buildTags != null && buildTags.contains("test-keys")) { return true; } - } - catch (Exception e1) - { - // ignore - } - // try executing commands - return canExecuteCommand("/system/xbin/which su") - || - canExecuteCommand("/system/bin/which su") - || - canExecuteCommand("which su"); + // check if /system/app/Superuser.apk is present + try + { + File file = new File("/system/app/Superuser.apk"); + if (file.exists()) + { + return true; + } + } + catch (Exception e1) + { + // ignore + } + + // try executing commands + return canExecuteCommand("/system/xbin/which su") + || + canExecuteCommand("/system/bin/which su") + || + canExecuteCommand("which su"); + } } // executes a command on the system @@ -905,51 +1026,45 @@ public class Miscellaneous extends Service private static void disableSSLCertificateChecking() { try - { - SSLSocketFactory ssf = null; - - try - { - SSLContext ctx = SSLContext.getInstance("TLS"); - - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(null, null); - ssf = new MySSLSocketFactoryInsecure(trustStore); - ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); - ctx.init(null, null, null); + { + SSLSocketFactory ssf = null; + + try + { + SSLContext ctx = SSLContext.getInstance("TLS"); + + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + ssf = new MySSLSocketFactoryInsecure(trustStore); + ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + ctx.init(null, null, null); // return new DefaultHttpClient(ccm, client.getParams()); - } - catch (Exception ex) - { - ex.printStackTrace(); + } + catch (Exception ex) + { + ex.printStackTrace(); // return null; - } - - // Install the all-trusting trust manager - SSLContext sc = SSLContext.getInstance("TLS"); - sc.init(null, getInsecureTrustManager(), new java.security.SecureRandom()); + } + + // Install the all-trusting trust manager + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, getInsecureTrustManager(), new java.security.SecureRandom()); // HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); // HttpsURLConnection.setDefaultSSLSocketFactory(ssf); - - // Install the all-trusting host verifier - HttpsURLConnection.setDefaultHostnameVerifier(getInsecureHostnameVerifier()); - HttpsURLConnection.setDefaultHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); - } - catch (KeyManagementException e) - { - // TODO Auto-generated catch block - e.printStackTrace(); - } - catch (NoSuchAlgorithmException e) - { - // TODO Auto-generated catch block - e.printStackTrace(); - } - finally - { - - } + + // Install the all-trusting host verifier + HttpsURLConnection.setDefaultHostnameVerifier(getInsecureHostnameVerifier()); + HttpsURLConnection.setDefaultHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + } + catch (KeyManagementException e) + { + Miscellaneous.logEvent("e", "SSL", Log.getStackTraceString(e), 4); + } + catch (NoSuchAlgorithmException e) + { + Miscellaneous.logEvent("e", "SSL", Log.getStackTraceString(e), 4); + } } public static TrustManager[] getInsecureTrustManager() @@ -2066,4 +2181,10 @@ public class Miscellaneous extends Service return output; } + + public static String getCallingMethodName() + { + StackTraceElement callingFrame = Thread.currentThread().getStackTrace()[4]; + return callingFrame.getMethodName(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/MyAccessibilityService.java b/app/src/main/java/com/jens/automation2/MyAccessibilityService.java new file mode 100644 index 0000000..79edbd1 --- /dev/null +++ b/app/src/main/java/com/jens/automation2/MyAccessibilityService.java @@ -0,0 +1,51 @@ +package com.jens.automation2; + +import android.accessibilityservice.AccessibilityService; +import android.os.Build; +import android.util.Log; +import android.view.Display; +import android.view.accessibility.AccessibilityEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +public class MyAccessibilityService extends AccessibilityService +{ + static MyAccessibilityService instance; + + public static MyAccessibilityService getInstance() + { + if(instance == null) + { + instance = new MyAccessibilityService(); + } + + return instance; + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) + { + + } + + @Override + public void onInterrupt() + { + + } + + @Override + public void onCreate() + { + super.onCreate(); + instance = this; + } + + @Override + protected void onServiceConnected() + { + super.onServiceConnected(); + Miscellaneous.logEvent("i", "Accessibility service", "Service started.", 4); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/News.java b/app/src/main/java/com/jens/automation2/News.java index 040b8de..9fc3b81 100644 --- a/app/src/main/java/com/jens/automation2/News.java +++ b/app/src/main/java/com/jens/automation2/News.java @@ -79,7 +79,7 @@ public class News if (!(new File(filePath)).exists() || Settings.lastNewsPolltime == Settings.default_lastNewsPolltime || now.getTimeInMillis() >= Settings.lastNewsPolltime + (long)(Settings.newsDisplayForXDays * 24 * 60 * 60 * 1000)) { String newsUrl = "https://server47.de/automation/appNews.php"; - newsContent = Miscellaneous.downloadURL(newsUrl, null, null); + newsContent = Miscellaneous.downloadURL(newsUrl, null, null, ActivityManageActionTriggerUrl.methodGet, null); // Cache content to local storage if(Miscellaneous.writeStringToFile(filePath, newsContent)) diff --git a/app/src/main/java/com/jens/automation2/PointOfInterest.java b/app/src/main/java/com/jens/automation2/PointOfInterest.java index 0f799a0..78f48bc 100644 --- a/app/src/main/java/com/jens/automation2/PointOfInterest.java +++ b/app/src/main/java/com/jens/automation2/PointOfInterest.java @@ -694,22 +694,16 @@ public class PointOfInterest implements Comparable @Override public void onProviderDisabled(String provider) { - // TODO Auto-generated method stub - } @Override public void onProviderEnabled(String provider) { - // TODO Auto-generated method stub - } @Override public void onStatusChanged(String provider, int status, Bundle extras) { - // TODO Auto-generated method stub - } } diff --git a/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java b/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java index 93b0339..914b56d 100644 --- a/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java +++ b/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java @@ -6,6 +6,7 @@ import android.util.Log; import com.jens.automation2.location.CellLocationChangedReceiver; import com.jens.automation2.location.WifiBroadcastReceiver; import com.jens.automation2.receivers.BroadcastListener; +import com.jens.automation2.receivers.CalendarReceiver; import com.jens.automation2.receivers.DateTimeListener; import com.jens.automation2.receivers.AutomationListenerInterface; import com.jens.automation2.receivers.BatteryReceiver; @@ -210,6 +211,9 @@ public class ReceiverCoordinator if(Rule.isAnyRuleUsing(Trigger.Trigger_Enum.screenState)) ScreenStateReceiver.startScreenStateReceiver(AutomationService.getInstance()); + + if(Rule.isAnyRuleUsing(Trigger.Trigger_Enum.calendarEvent)) + CalendarReceiver.startCalendarReceiver(AutomationService.getInstance()); } public static void stopAllReceivers() @@ -243,6 +247,7 @@ public class ReceiverCoordinator BluetoothReceiver.stopBluetoothReceiver(); HeadphoneJackListener.getInstance().stopListener(AutomationService.getInstance()); DeviceOrientationListener.getInstance().stopListener(AutomationService.getInstance()); + CalendarReceiver.getInstance().stopListener(AutomationService.getInstance()); } catch(Exception e) { @@ -464,6 +469,19 @@ public class ReceiverCoordinator } } + if(Rule.isAnyRuleUsing(Trigger.Trigger_Enum.calendarEvent)) + { + if(!CalendarReceiver.getInstance().isListenerRunning()) + CalendarReceiver.getInstance().startListener(AutomationService.getInstance()); + else + CalendarReceiver.armOrRearmTimer(); + } + else + { + if(CalendarReceiver.getInstance().isListenerRunning()) + CalendarReceiver.getInstance().stopListener(AutomationService.getInstance()); + } + AutomationService.updateNotification(); } } diff --git a/app/src/main/java/com/jens/automation2/Settings.java b/app/src/main/java/com/jens/automation2/Settings.java index 8717d38..99c44ae 100644 --- a/app/src/main/java/com/jens/automation2/Settings.java +++ b/app/src/main/java/com/jens/automation2/Settings.java @@ -149,56 +149,45 @@ public class Settings implements SharedPreferences @Override public Editor edit() { - // TODO Auto-generated method stub return null; } @Override public Map getAll() { - // TODO Auto-generated method stub return null; } @Override public boolean getBoolean(String arg0, boolean arg1) { - // TODO Auto-generated method stub return false; } @Override public float getFloat(String arg0, float arg1) { - // TODO Auto-generated method stub return 0; } @Override public int getInt(String arg0, int arg1) { - // TODO Auto-generated method stub return 0; } @Override public long getLong(String arg0, long arg1) { - // TODO Auto-generated method stub return 0; } @Override public String getString(String arg0, String arg1) { - // TODO Auto-generated method stub return null; } @Override public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener arg0) { - // TODO Auto-generated method stub - } @Override public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener arg0) { - // TODO Auto-generated method stub - } public static void readFromPersistentStorage(Context context) @@ -619,7 +608,6 @@ public class Settings implements SharedPreferences @Override public Set getStringSet(String arg0, Set arg1) { - // TODO Auto-generated method stub return null; } } \ 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 687356a..5277c56 100644 --- a/app/src/main/java/com/jens/automation2/Trigger.java +++ b/app/src/main/java/com/jens/automation2/Trigger.java @@ -2,6 +2,7 @@ package com.jens.automation2; import android.bluetooth.BluetoothDevice; import android.content.Context; +import android.os.BatteryManager; import android.os.Build; import android.service.notification.StatusBarNotification; import android.telephony.TelephonyManager; @@ -14,6 +15,7 @@ import com.jens.automation2.location.WifiBroadcastReceiver; import com.jens.automation2.receivers.BatteryReceiver; import com.jens.automation2.receivers.BluetoothReceiver; import com.jens.automation2.receivers.BroadcastListener; +import com.jens.automation2.receivers.CalendarReceiver; import com.jens.automation2.receivers.ConnectivityReceiver; import com.jens.automation2.receivers.DeviceOrientationListener; import com.jens.automation2.receivers.HeadphoneJackListener; @@ -31,6 +33,7 @@ import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.List; import java.util.Map; public class Trigger @@ -63,6 +66,7 @@ public class Trigger tethering, subSystemState, checkVariable, + calendarEvent, phoneCall; //phoneCall always needs to be at the very end because of Google's shitty so called privacy public String getFullName(Context context) @@ -123,6 +127,8 @@ public class Trigger return context.getResources().getString(R.string.subSystemState); case checkVariable: return context.getResources().getString(R.string.checkVariable); + case calendarEvent: + return context.getResources().getString(R.string.calendarEventCapital); default: return "Unknown"; } @@ -247,11 +253,14 @@ public class Trigger case subSystemState: if(!checkSubSystemState()) result = false; - break; case checkVariable: if(!checkVariable()) result = false; break; + case calendarEvent: + if(!checkCalendarEvent(false)) + result = false; + break; default: break; } @@ -358,6 +367,14 @@ public class Trigger else Miscellaneous.logEvent("i", "NotificationCheck", "A required text for a notification trigger was not specified.", 5); + /* + We can only get here through the startup routine of the main service. + Because the notification did not come in at runtime, but was there + before we started, w need to take a record of it. + */ + if(NotificationListener.getLastNotification() == null) + NotificationListener.setLastNotification(sn); + foundMatch = true; break; } @@ -609,6 +626,141 @@ public class Trigger return false; } + public boolean checkCalendarEvent(boolean ignoreActive) + { + try + { + List calendarEvents = CalendarReceiver.readCalendarEvents(AutomationService.getInstance(), true,false); + + for(CalendarReceiver.CalendarEvent event : calendarEvents) + { + if(!checkCalendarEvent(event, ignoreActive)) + continue; + + return true; + } + + // At this point none of the calendar items match this trigger + + /* + If trigger demands no calendar event and there is absolutely no future event, + further criteria don't matter if there are no events to check. + */ + if(calendarEvents.size() == 0 && getTriggerParameter() == false) + return true; + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "checkCalendarEvent()", Log.getStackTraceString(e), 1); + } + + return false; + } + + public boolean checkCalendarEvent(CalendarReceiver.CalendarEvent event, boolean ignoreActive) + { + String[] conditions = this.getTriggerParameter2().split(Trigger.triggerParameter2Split); + List calendarEvents = CalendarReceiver.readCalendarEvents(AutomationService.getInstance(), true,false); + + /* + 0 = titleDir + 1 = title + 2 = descriptionDir + 3 = description + 4 = locationDir + 5 = location + 6 = evaluate all day event + 7 = all day event + 8 = evaluate reoccurring + 9 = reoccurring + 10 = availability list + 11 = calendars list + */ + + boolean isActive = getTriggerParameter(); + if (!ignoreActive && isActive != event.isCurrentlyActive()) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Event " + event.title + " has to be currently active: " + String.valueOf(triggerParameter) + ", but is required otherwise.", 5); + return false; + } + + if (!StringUtils.isEmpty(conditions[1])) + { + if (!Miscellaneous.compare(conditions[0], conditions[1], event.title)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Title of event " + event.title + " does not match.", 5); + return false; + } + } + + if (!StringUtils.isEmpty(conditions[3])) + { + if (!Miscellaneous.compare(conditions[2], conditions[3], event.description)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Description " + event.title + " does not match.", 5); + return false; + } + } + + if (!StringUtils.isEmpty(conditions[5])) + { + if (!Miscellaneous.compare(conditions[4], conditions[5], event.location)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Location " + event.title + " does not match.", 5); + return false; + } + } + + if (Boolean.parseBoolean(conditions[6])) + { + if (Boolean.parseBoolean(conditions[7]) != event.allDay) + { + Miscellaneous.logEvent("i", "CalendarCheck", "All day setting " + event.title + " does not match.", 5); + return false; + } + } + + if (Boolean.parseBoolean(conditions[8])) + { + if (Boolean.parseBoolean(conditions[9]) != event.reoccurring) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Reoccurring setting " + event.title + " does not match.", 5); + return false; + } + } + + if (!StringUtils.isEmpty(conditions[10])) + { + String[] availabilities = conditions[10].split(ActivityManageTriggerCalendar.separator); + if (availabilities.length > 0) + { + if (!Miscellaneous.arraySearch(availabilities, event.availability, false, true)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Availability of event " + event.title + " does not match.", 5); + return false; + } + } + } + + if (!StringUtils.isEmpty(conditions[11])) + { + String[] calendars = conditions[11].split(ActivityManageTriggerCalendar.separator); + if (calendars.length > 0) + { + if (!Miscellaneous.arraySearch(calendars, String.valueOf(event.calendarId), false, true)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Calendar of event " + event.title + " does not match.", 5); + return false; + } + } + } + + // No contradictions found + Miscellaneous.logEvent("i", "CalendarCheck", "Event " + event + " matches.", 4); + + return true; + } + boolean checkBluetooth() { Miscellaneous.logEvent("i", Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), String.format("Checking for bluetooth...", this.getParentRule().getName()), 4); @@ -948,13 +1100,13 @@ public class Trigger { if(!this.getTriggerParameter()) { - Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getParentRule().getName()), String.format("Rule %1$s doesn't apply. We're entering POI: " + this.getPointOfInterest().getName() + ", not leaving it.", getParentRule().getName()), 4); + Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getParentRule().getName()), String.format("Rule \"%1$s\" doesn't apply. We're entering POI: " + this.getPointOfInterest().getName() + ", not leaving it.", getParentRule().getName()), 4); return false; } } else { - Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getParentRule().getName()), String.format("Rule %1$s doesn't apply. This is " + activePoi.getName() + ", not " + this.getPointOfInterest().getName() + ".", getParentRule().getName()), 4); + Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getParentRule().getName()), String.format("Rule \"%1$s\" doesn't apply. This is " + activePoi.getName() + ", not " + this.getPointOfInterest().getName() + ".", getParentRule().getName()), 4); return false; } } @@ -980,7 +1132,7 @@ public class Trigger } else { - Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getParentRule().getName()), String.format("Rule %1$s doesn't apply. We're not at POI \"" + this.getPointOfInterest().getName() + "\".", getParentRule().getName()), 3); + Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getParentRule().getName()), String.format("Rule \"%1$s\" doesn't apply. We're not at POI \"" + this.getPointOfInterest().getName() + "\".", getParentRule().getName()), 3); return false; } // } @@ -989,7 +1141,7 @@ public class Trigger { if(!this.getTriggerParameter()) { - Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getParentRule().getName()), String.format("Rule %1$s doesn't apply. We're at no POI. Rule specifies to be at anyone.", getParentRule().getName()), 5); + Miscellaneous.logEvent("i", String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), this.getParentRule().getName()), String.format("Rule \"%1$s\" doesn't apply. We're at no POI. Rule specifies to be at anyone.", getParentRule().getName()), 5); return false; } } @@ -1020,22 +1172,34 @@ public class Trigger boolean checkCharging() { - if(BatteryReceiver.isDeviceCharging(Miscellaneous.getAnyContext()) == 0) + if(BatteryReceiver.isDeviceCharging(Miscellaneous.getAnyContext()) == 0) // unknown state { return false; // unknown charging state, can't activate rule under these conditions } - else if(BatteryReceiver.isDeviceCharging(Miscellaneous.getAnyContext()) == 1) + else if(BatteryReceiver.isDeviceCharging(Miscellaneous.getAnyContext()) == 1) // we are discharging { - if(this.getTriggerParameter()) //rule says when charging, but we're currently discharging - return false; + if(!this.getTriggerParameter()) // rule says when charging, but we're currently discharging + return true; } - else if(BatteryReceiver.isDeviceCharging(Miscellaneous.getAnyContext()) == 2) + else if(BatteryReceiver.isDeviceCharging(Miscellaneous.getAnyContext()) == 2) // we are charging { - if(!this.getTriggerParameter()) //rule says when discharging, but we're currently charging - return false; + if(this.getTriggerParameter()) // rule says when discharging, but we're currently charging + { + // check charging type + if(StringUtils.isEmpty(getTriggerParameter2())) + return true; + else + { + int desiredType; + String[] typeParams = getTriggerParameter2().split(triggerParameter2Split, -1); + desiredType = Integer.parseInt(typeParams[0]); + if(desiredType == BatteryReceiver.getCurrentChargingType()) + return true; + } + } } - return true; + return false; } boolean checkTetheringActive() @@ -1474,6 +1638,20 @@ public class Trigger else returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.stopping) + " "); returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.triggerCharging)); + returnString.append(" ("); + if(!StringUtils.isEmpty(getTriggerParameter2())) + { + String[] pieces = getTriggerParameter2().split(triggerParameter2Split, -1); + if(pieces[0].equals("0")) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.any)); + else if(pieces[0].equals(String.valueOf(BatteryManager.BATTERY_PLUGGED_AC))) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.charging_AC)); + else if(pieces[0].equals(String.valueOf(BatteryManager.BATTERY_PLUGGED_USB))) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.usb)); + else if(pieces[0].equals(String.valueOf(BatteryManager.BATTERY_PLUGGED_WIRELESS))) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.charging_wireless)); + } + returnString.append(")"); break; case batteryLevel: returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.batteryLevel)); @@ -1813,6 +1991,55 @@ public class Trigger else returnString.append(String.format(Miscellaneous.getAnyContext().getResources().getString(R.string.variableCheckStringDeleted), triggerParameter2)); break; + case calendarEvent: + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.calendarEvent)); + + returnString.append(" ("); + + if(triggerParameter) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.eventIsCurrentlyHappening)); + else + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.eventIsCurrentlyNotHappening)); + + returnString.append( ", "); + + String[] conditions = triggerParameter2.split(triggerParameter2Split, -1); + + if (!StringUtils.isEmpty(conditions[1])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.title) + " " + conditions[0] + " " + conditions[1] + ", "); + if (!StringUtils.isEmpty(conditions[3])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.location) + " " + conditions[2] + " " + conditions[3] + ", "); + if (!StringUtils.isEmpty(conditions[5])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.calendarDescription) + " " + conditions[4] + " " + conditions[5] + ", "); + + if(Boolean.parseBoolean(conditions[6])) + { + if (Boolean.parseBoolean(conditions[7])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.allDayEventTrue) + ", "); + else + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.allDayEventFalse) + ", "); + } + + if(Boolean.parseBoolean(conditions[8])) + { + if (Boolean.parseBoolean(conditions[9])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.reoccurringTrue) + ", "); + else + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.reoccurringFalse) + ", "); + } + + if (!StringUtils.isEmpty(conditions[10])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.availabilities) + " " + conditions[10] + ", "); + + if (!StringUtils.isEmpty(conditions[11])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.calendars) + " " + conditions[11]); + + if (returnString.toString().endsWith(", ")) + returnString.delete(returnString.length() - 2, returnString.length()); + + returnString.append(")"); + + break; default: returnString.append("error"); break; diff --git a/app/src/main/java/com/jens/automation2/XmlFileInterface.java b/app/src/main/java/com/jens/automation2/XmlFileInterface.java index ca192c9..4d48ee9 100644 --- a/app/src/main/java/com/jens/automation2/XmlFileInterface.java +++ b/app/src/main/java/com/jens/automation2/XmlFileInterface.java @@ -217,7 +217,7 @@ public class XmlFileInterface } serializer.endTag(null, "ProfileCollection"); - + serializer.startTag(null, "RuleCollection"); for(int i=0; i= 3) { if(newTagPieces[2].contains(Action.intentPairSeparator)) - newTag = newTagPieces[0] + ";" + newTagPieces[1] + ";" + ActivityManageActionStartActivity.startByActivityString + ";" + newTagPieces[2]; + newTag = newTagPieces[0] + Action.actionParameter2Split + newTagPieces[1] + Action.actionParameter2Split + ActivityManageActionStartActivity.startByActivityString + Action.actionParameter2Split + newTagPieces[2]; } newAction.setParameter2(newTag); diff --git a/app/src/main/java/com/jens/automation2/location/CellLocationChangedReceiver.java b/app/src/main/java/com/jens/automation2/location/CellLocationChangedReceiver.java index e3b7015..0e61555 100644 --- a/app/src/main/java/com/jens/automation2/location/CellLocationChangedReceiver.java +++ b/app/src/main/java/com/jens/automation2/location/CellLocationChangedReceiver.java @@ -272,29 +272,23 @@ public class CellLocationChangedReceiver extends PhoneStateListener locationListenerArmed = false; Miscellaneous.logEvent("i", "LocationListener", "Disarmed location listener, accuracy reached", 4); } - -// Miscellaneous.logEvent("i", "LocationListener", "Giving update to POI class"); -// PointOfInterest.positionUpdate(up2DateLocation, parentLocationProvider.parentService); } @Override public void onProviderDisabled(String provider) { - // TODO Auto-generated method stub } @Override public void onProviderEnabled(String provider) { - // TODO Auto-generated method stub } @Override public void onStatusChanged(String provider, int status, Bundle extras) { - // TODO Auto-generated method stub } } diff --git a/app/src/main/java/com/jens/automation2/location/WifiBroadcastReceiver.java b/app/src/main/java/com/jens/automation2/location/WifiBroadcastReceiver.java index 55f5862..1b7782b 100644 --- a/app/src/main/java/com/jens/automation2/location/WifiBroadcastReceiver.java +++ b/app/src/main/java/com/jens/automation2/location/WifiBroadcastReceiver.java @@ -1,6 +1,5 @@ package com.jens.automation2.location; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -18,6 +17,8 @@ import com.jens.automation2.Rule; import com.jens.automation2.Settings; import com.jens.automation2.Trigger; +import org.apache.commons.lang3.StringUtils; + import java.util.ArrayList; public class WifiBroadcastReceiver extends BroadcastReceiver @@ -30,7 +31,7 @@ public class WifiBroadcastReceiver extends BroadcastReceiver protected static boolean mayCellLocationChangedReceiverBeActivatedFromWifiPointOfView = true; protected static WifiBroadcastReceiver wifiBrInstance; protected static IntentFilter wifiListenerIntentFilter; - protected static boolean wifiListenerActive=false; + protected static boolean wifiListenerActive = false; final static String unknownSsidName = ""; @@ -46,13 +47,16 @@ public class WifiBroadcastReceiver extends BroadcastReceiver public static void setLastWifiSsid(String newWifiSsid) { + // Remove double quotes that sometimes come if(newWifiSsid.startsWith("\"") && newWifiSsid.endsWith("\"")) newWifiSsid = newWifiSsid.substring(1, newWifiSsid.length()-1); + // If it's a real name, not an empty string, it's stored as the last ssid if(newWifiSsid.length() > 0) { - if(newWifiSsid.equals(unknownSsidName)) - WifiBroadcastReceiver.lastWifiSsidReal = lastWifiSsid; + if(!newWifiSsid.equals(unknownSsidName)) + WifiBroadcastReceiver.lastWifiSsidReal = lastWifiSsid; + WifiBroadcastReceiver.lastWifiSsid = newWifiSsid; } } @@ -72,24 +76,20 @@ public class WifiBroadcastReceiver extends BroadcastReceiver { try { - // int state = -1; + if(!StringUtils.isEmpty(intent.getAction())) + Miscellaneous.logEvent("i", "WifiReceiver", "Received signal with action \""+ intent.getAction() + "\".", 4); + else + Miscellaneous.logEvent("i", "WifiReceiver", "Received signal with empty action.", 4); + NetworkInfo myWifi = null; if(intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) // fired upon disconnection { - // state = intent.getIntExtra(WifiManager.NETWORK_STATE_CHANGED_ACTION, -1); - // Miscellaneous.logEvent("i", "WifiReceiver", "NETWORK_STATE_CHANGED_ACTION: " + String.valueOf(state)); myWifi = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); } WifiManager myWifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE); - // ConnectivityManager connManager = (ConnectivityManager)context.getSystemService(context.CONNECTIVITY_SERVICE); - // myWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - // myWifi = state - // WifiInfo wifiInfo = myWifiManager.getConnectionInfo(); - - // SupplicantState supState = wifiInfo.getSupplicantState(); - + if(intent.getAction().equals(WifiManager.RSSI_CHANGED_ACTION)) // fired upon connection { String ssid = myWifiManager.getConnectionInfo().getSSID(); @@ -105,8 +105,8 @@ public class WifiBroadcastReceiver extends BroadcastReceiver CellLocationChangedReceiver.stopCellLocationChangedReceiver(); /* - TODO: Every time the screen is turned on, we receiver a "wifi has been connected"-event. - This is technically wrong and not really any changed to when the screen was off. It has + TODO: Every time the screen is turned on, we receive a "wifi has been connected"-event. + This is technically wrong and not really any change to when the screen was off. It has to be filtered. */ } @@ -132,12 +132,12 @@ public class WifiBroadcastReceiver extends BroadcastReceiver } else if(!myWifi.isConnectedOrConnecting()) // really disconnected? because sometimes also fires on connect { - if(wasConnected) // wir könnten einfach noch nicht daheim sein + if(wasConnected) // we could simply not be home yet { try { wasConnected = false; - Miscellaneous.logEvent("i", "WifiReceiver", String.format(context.getResources().getString(R.string.disconnectedFromWifi), getLastWifiSsid()) + " Switching to CellLocationChangedReceiver.", 3); + Miscellaneous.logEvent("i", "WifiReceiver", "Disconnected from wifi \"" + getLastWifiSsid() + "\". Switching to CellLocationChangedReceiver.", 3); mayCellLocationChangedReceiverBeActivatedFromWifiPointOfView = true; CellLocationChangedReceiver.startCellLocationChangedReceiver(); lastConnectedState = false; @@ -226,17 +226,16 @@ public class WifiBroadcastReceiver extends BroadcastReceiver { try { - if(wifiListenerActive) + if (wifiListenerActive) { Miscellaneous.logEvent("i", "Wifi Listener", "Stopping wifiListener", 4); wifiListenerActive = false; parentLocationProvider.getParentService().unregisterReceiver(wifiBrInstance); } } - catch(Exception ex) + catch (Exception ex) { Miscellaneous.logEvent("e", "Wifi Listener", "Error stopping wifiListener: " + Log.getStackTraceString(ex), 3); } } - } \ No newline at end of file diff --git a/app/src/main/java/com/jens/automation2/receivers/BatteryReceiver.java b/app/src/main/java/com/jens/automation2/receivers/BatteryReceiver.java index d9064e1..85247d0 100644 --- a/app/src/main/java/com/jens/automation2/receivers/BatteryReceiver.java +++ b/app/src/main/java/com/jens/automation2/receivers/BatteryReceiver.java @@ -7,7 +7,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.BatteryManager; import android.util.Log; -import android.widget.Toast; import com.jens.automation2.ActivityPermissions; import com.jens.automation2.AutomationService; @@ -25,6 +24,9 @@ public class BatteryReceiver extends BroadcastReceiver implements AutomationList static boolean batteryReceiverActive = false; static IntentFilter batteryIntentFilter = null; static Intent batteryStatus = null; + + private static int currentChargingState = 0; //0=unknown, 1=no, 2=yes + private static int currentChargingType = 0; //AC, wireless, USB static BroadcastReceiver batteryInfoReceiverInstance = null; public static void startBatteryReceiver(final AutomationService automationServiceRef) @@ -41,8 +43,6 @@ public class BatteryReceiver extends BroadcastReceiver implements AutomationList batteryIntentFilter = new IntentFilter(); batteryIntentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); batteryIntentFilter.addAction(Intent.ACTION_BATTERY_LOW); - // batteryIntentFilter.addAction(Intent.ACTION_POWER_CONNECTED); - // batteryIntentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED); } batteryStatus = automationServiceRef.registerReceiver(batteryInfoReceiverInstance, batteryIntentFilter); @@ -79,8 +79,6 @@ public class BatteryReceiver extends BroadcastReceiver implements AutomationList return batteryLevel; } - private static int currentChargingState = 0; //0=unknown, 1=no, 2=yes - public static int getCurrentChargingState() { return currentChargingState; @@ -123,7 +121,12 @@ public class BatteryReceiver extends BroadcastReceiver implements AutomationList case BatteryManager.BATTERY_PLUGGED_AC: // Toast.makeText(context, "Regular charging", Toast.LENGTH_LONG).show(); Miscellaneous.logEvent("i", "BatteryReceiver", "Regular charging.", 5); - this.actionCharging(context); + this.actionCharging(context, statusPlugged); + break; + case BatteryManager.BATTERY_PLUGGED_WIRELESS: + // Toast.makeText(context, "Regular charging", Toast.LENGTH_LONG).show(); + Miscellaneous.logEvent("i", "BatteryReceiver", "Wireless charging.", 5); + this.actionCharging(context, statusPlugged); break; case BatteryManager.BATTERY_PLUGGED_USB: this.actionUsbConnected(context); @@ -134,8 +137,8 @@ public class BatteryReceiver extends BroadcastReceiver implements AutomationList { case BatteryManager.BATTERY_STATUS_CHARGING: case BatteryManager.BATTERY_STATUS_FULL: - Miscellaneous.logEvent("i", "BatteryReceiver", "Device has been fully charged.", 5); - this.actionCharging(context); +// Miscellaneous.logEvent("i", "BatteryReceiver", "Device has been fully charged.", 5); + this.actionCharging(context, statusPlugged); break; case BatteryManager.BATTERY_STATUS_DISCHARGING: case BatteryManager.BATTERY_STATUS_NOT_CHARGING: @@ -155,28 +158,33 @@ public class BatteryReceiver extends BroadcastReceiver implements AutomationList switch(currentChargingState) { case 0: - Miscellaneous.logEvent("w", "ChargingInfo", "Status of device charging was requested. Information isn't available, yet.", 4); + Miscellaneous.logEvent("w", "ChargingInfo", "Information isn't available, yet.", 4); break; case 1: - Miscellaneous.logEvent("i", "ChargingInfo", "Status of device charging was requested. Device is discharging.", 3); + Miscellaneous.logEvent("i", "ChargingInfo", "Device is discharging.", 3); break; case BatteryManager.BATTERY_STATUS_CHARGING: - Miscellaneous.logEvent("i", "ChargingInfo", "Status of device charging was requested. Device is charging.", 3); + Miscellaneous.logEvent("i", "ChargingInfo", "Device is charging.", 3); break; } return currentChargingState; } - private void actionCharging(Context context) + public static int getCurrentChargingType() + { + return currentChargingType; + } + + private void actionCharging(Context context, int statusPlugged) { if(currentChargingState != BatteryManager.BATTERY_STATUS_CHARGING) // Avoid flooding the log. This event will occur on a regular basis even though charging state wasn't changed. { Miscellaneous.logEvent("i", "BatteryReceiver", "Battery is charging or full.", 3); currentChargingState = BatteryManager.BATTERY_STATUS_CHARGING; - //activate rule(s) + currentChargingType = statusPlugged; + ArrayList ruleCandidates = Rule.findRuleCandidates(Trigger_Enum.charging); -// ArrayList ruleCandidates = Rule.findRuleCandidatesByCharging(true); for(int i=0; i ruleCandidates = Rule.findRuleCandidates(Trigger_Enum.usb_host_connection); -// ArrayList ruleCandidates = Rule.findRuleCandidatesByUsbHost(true); for(Rule oneRule : ruleCandidates) { if(oneRule.getsGreenLight(context)) oneRule.activate(automationServiceRef, false); } - this.actionCharging(context); + this.actionCharging(context, BatteryManager.BATTERY_PLUGGED_USB); } } diff --git a/app/src/main/java/com/jens/automation2/receivers/BroadcastListener.java b/app/src/main/java/com/jens/automation2/receivers/BroadcastListener.java index 204a3b5..b994a3e 100644 --- a/app/src/main/java/com/jens/automation2/receivers/BroadcastListener.java +++ b/app/src/main/java/com/jens/automation2/receivers/BroadcastListener.java @@ -52,9 +52,13 @@ public class BroadcastListener extends android.content.BroadcastReceiver impleme { broadcastsCollection.add(new EventOccurrence(Calendar.getInstance(), intent.getAction())); - for(String key : intent.getExtras().keySet()) + Miscellaneous.logEvent("i", "Broadcast received", "Broadcast " + intent.getAction() + " received.", 4); + if(intent.getExtras() != null && intent.getExtras().size() > 0) { - Miscellaneous.logEvent("i", "Broadcast extra", "Broadcast " + intent.getAction() + " has extra " + key + " and type " + intent.getExtras().get(key).getClass().getName(), 4); + for (String key : intent.getExtras().keySet()) + { + Miscellaneous.logEvent("i", "Broadcast extra", "Broadcast " + intent.getAction() + " has extra " + key + " and type " + intent.getExtras().get(key).getClass().getName(), 4); + } } ArrayList ruleCandidates = Rule.findRuleCandidates(Trigger.Trigger_Enum.broadcastReceived); diff --git a/app/src/main/java/com/jens/automation2/receivers/CalendarReceiver.java b/app/src/main/java/com/jens/automation2/receivers/CalendarReceiver.java new file mode 100644 index 0000000..c7b808e --- /dev/null +++ b/app/src/main/java/com/jens/automation2/receivers/CalendarReceiver.java @@ -0,0 +1,659 @@ +package com.jens.automation2.receivers; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.CalendarContract; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.jens.automation2.AutomationService; +import com.jens.automation2.Miscellaneous; +import com.jens.automation2.Rule; +import com.jens.automation2.Trigger; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +public class CalendarReceiver extends BroadcastReceiver implements AutomationListenerInterface +{ + static CalendarReceiver calendarReceiverInstance = null; + static boolean calendarReceiverActive = false; + static IntentFilter calendarIntentFilter = null; + private static Intent calendarIntent = null; + + public static final int AVAILABILITY_OUT_OF_OFFICE = 4; + public static final int AVAILABILITY_WORKING_ELSEWHERE = 5; + public static final String calendarAlarmAction = "ALARM_FOR_CALENDAR"; + + static List calendarsCache = null; + static List calendarEventsCache = null; + static List calendarEventsReoccurringCache = null; + + // To determine for which events which rules have been executed + static List calendarEventsUsed = new ArrayList<>(); + + static Timer timer = null; + static TimerTask timerTask = null; + static Calendar nextWakeup = null; + static AlarmManager alarmManager = null; + static boolean wakeupNeedsToBeScheduledOrRescheduled = false; + + public static CalendarEvent getLastTriggeringEvent() + { + if(calendarEventsUsed.size() > 0) + { + return calendarEventsUsed.get(calendarEventsUsed.size() -1).event; + } + + return null; + } + + public static class RuleEventPair + { + Rule rule; + CalendarEvent event; + + public RuleEventPair(Rule rule, CalendarEvent event) + { + this.rule = rule; + this.event = event; + } + } + + public static void addUsedPair(RuleEventPair pair) + { + // Add pair only if it's not in the list already. + for(RuleEventPair usedPair : calendarEventsUsed) + { + if(usedPair.rule.equals(pair.rule) && usedPair.event.equals(pair.event)) + return; + } + + calendarEventsUsed.add(pair); + } + + public static CalendarReceiver getInstance() + { + if(calendarReceiverInstance == null) + calendarReceiverInstance = new CalendarReceiver(); + + return calendarReceiverInstance; + } + + @Override + public void onReceive(Context context, Intent intent) + { + Miscellaneous.logEvent("i", "CalendarReceiver", "Received " + intent.getAction(), 4); + + if(intent.getAction().equalsIgnoreCase(Intent.ACTION_PROVIDER_CHANGED)) + { + Miscellaneous.logEvent("i", "CalendarReceiver", "Clearing calendar caches.", 4); + + clearCaches(); + + routineAtAlarm(); + } + else if(intent.getAction().equalsIgnoreCase(calendarAlarmAction)) + { + routineAtAlarm(); + } + } + + static void checkForRules(Context context) + { + ArrayList ruleCandidates = Rule.findRuleCandidates(Trigger.Trigger_Enum.calendarEvent); + for (int i = 0; i < ruleCandidates.size(); i++) + { + if (ruleCandidates.get(i).getsGreenLight(context)) + ruleCandidates.get(i).activate(AutomationService.getInstance(), false); + } + } + + @Override + public void startListener(AutomationService automationServiceRef) + { + startCalendarReceiver(automationServiceRef); + } + + static void clearCaches() + { + calendarsCache = null; + calendarEventsCache = null; + calendarEventsReoccurringCache = null; + } + + @Override + public void stopListener(AutomationService automationService) + { + if(calendarReceiverActive) + { + if(calendarReceiverInstance != null) + { + AutomationService.getInstance().unregisterReceiver(calendarReceiverInstance); + calendarReceiverInstance = null; + } + + clearCaches(); + calendarEventsUsed.clear(); + + calendarReceiverActive = false; + } + } + + @Override + public boolean isListenerRunning() + { + return calendarReceiverActive; + } + + @Override + public Trigger.Trigger_Enum[] getMonitoredTrigger() + { + return new Trigger.Trigger_Enum[]{Trigger.Trigger_Enum.calendarEvent}; + } + + public static class AndroidCalendar + { + public int calendarId; + public String displayName; + public String accountString; + + @NonNull + @Override + public String toString() + { + return displayName + " (" + accountString + ")"; + } + } + + public static class CalendarEvent + { + public AndroidCalendar calendar; + public int calendarId; + public String eventId; + public String title; + public String description; + public String location; + public String availability; + public Calendar start, end; + public boolean allDay, reoccurring; + + public boolean isCurrentlyActive() + { + Calendar now = Calendar.getInstance(); + return now.getTimeInMillis() >= start.getTimeInMillis() && now.getTimeInMillis() < end.getTimeInMillis(); + } + + @NonNull + @Override + public String toString() + { + return "Title: " + title + ", location: " + location + ", description: " + description + ", start: " + Miscellaneous.formatDate(start.getTime()) + ", end: " + Miscellaneous.formatDate(end.getTime()) + ", is currently active: " + String.valueOf(isCurrentlyActive()) + ", all day: " + String.valueOf(allDay) + ", availability: " + availability; + } + + @Override + public boolean equals(@Nullable Object obj) + { + try + { + CalendarEvent compareEvent = (CalendarEvent) obj; + return calendarId == compareEvent.calendarId + && + eventId.equals(compareEvent.eventId) + && + title.equals(compareEvent.title) + && + description.equals(compareEvent.description) + && + location.equals(compareEvent.location) + && + availability.equals(compareEvent.availability) + && + start.getTimeInMillis() == compareEvent.start.getTimeInMillis() + && + end.getTimeInMillis() == compareEvent.end.getTimeInMillis() + && + allDay == compareEvent.allDay; + } + catch (Exception e) + { + Miscellaneous.logEvent("e", "CalendarReceiver compare()", Log.getStackTraceString(e), 5); + return false; + } + } + } + + public static List readCalendars(Context context) + { + if(calendarsCache == null) + { + calendarsCache = new ArrayList<>(); + + Cursor cursor; + + cursor = context.getContentResolver().query( + Uri.parse("content://com.android.calendar/calendars"), + + new String[]{ CalendarContract.Calendars._ID, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, CalendarContract.Calendars.OWNER_ACCOUNT, }, + null, null, null); + + cursor.moveToFirst(); + // fetching calendars name + String CNames[] = new String[cursor.getCount()]; + + for (int i = 0; i < CNames.length; i++) + { + try + { + AndroidCalendar calendar = new AndroidCalendar(); + calendar.calendarId = Integer.parseInt(cursor.getString(0)); + calendar.displayName = cursor.getString(1); + calendar.accountString = cursor.getString(2); + + calendarsCache.add(calendar); + } + catch (Exception e) + { + } + cursor.moveToNext(); + } + + if (cursor != null) + cursor.close(); + } + + return calendarsCache; + } + + public static List readCalendarEvents(Context context, boolean includeReoccurring, boolean includePastEvents) + { + if(calendarEventsCache == null) + { + calendarEventsCache = new ArrayList<>(); + + Cursor cursor; + + cursor = context.getContentResolver().query( + Uri.parse("content://com.android.calendar/events"), + new String[] { + CalendarContract.Events.CALENDAR_ID, + CalendarContract.Events._ID, + CalendarContract.Events.TITLE, + CalendarContract.Events.DESCRIPTION, + CalendarContract.Events.ALL_DAY, + CalendarContract.Events.DTSTART, + CalendarContract.Events.DTEND, + CalendarContract.Events.EVENT_LOCATION, + CalendarContract.Events.AVAILABILITY + }, + null, null, null); + + cursor.moveToFirst(); + // fetching calendars name + String CNames[] = new String[cursor.getCount()]; + + Calendar now = Calendar.getInstance(); + + for (int i = 0; i < CNames.length; i++) + { + try + { + CalendarEvent event = new CalendarEvent(); + event.calendarId = Integer.parseInt(cursor.getString(0)); + + for(AndroidCalendar cal : readCalendars(context)) + { + if(cal.calendarId == event.calendarId) + { + event.calendar = cal; + break; + } + } + + event.eventId = cursor.getString(1); + event.title = cursor.getString(2); + event.description = cursor.getString(3); + event.allDay = cursor.getString(4).equals("1"); + event.start = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(5))); + event.end = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(6))); + event.location = cursor.getString(7); + event.availability = cursor.getString(8); + event.reoccurring = false; + + if(includePastEvents || event.end.getTimeInMillis() > now.getTimeInMillis()) + calendarEventsCache.add(event); + } + catch (Exception e) + {} + cursor.moveToNext(); + } + + if(cursor != null) + cursor.close(); + + } + + if(includeReoccurring && calendarEventsReoccurringCache == null) + { + calendarEventsReoccurringCache = new ArrayList<>(); + + Cursor cursor; + + Calendar queryStart, queryEnd; + if(includePastEvents) + queryStart = Miscellaneous.calendarFromLong(0); + else + queryStart = Calendar.getInstance(); + + queryEnd = Calendar.getInstance(); + queryEnd.add(Calendar.YEAR, 1); + + cursor = context.getContentResolver().query( + Uri.parse("content://com.android.calendar/instances/when/" + queryStart.getTimeInMillis() + "/" + queryEnd.getTimeInMillis()), + new String[] { + CalendarContract.Instances.CALENDAR_ID, + CalendarContract.Instances._ID, + CalendarContract.Instances.TITLE, + CalendarContract.Instances.DESCRIPTION, + CalendarContract.Instances.ALL_DAY, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.EVENT_LOCATION, + CalendarContract.Instances.AVAILABILITY + }, + null, null, null); + + cursor.moveToFirst(); + String CNames[] = new String[cursor.getCount()]; + + Calendar now = Calendar.getInstance(); + + for (int i = 0; i < CNames.length; i++) + { + try + { + CalendarEvent event = new CalendarEvent(); + event.calendarId = Integer.parseInt(cursor.getString(0)); + + for(AndroidCalendar cal : readCalendars(context)) + { + if(cal.calendarId == event.calendarId) + { + event.calendar = cal; + break; + } + } + + event.eventId = cursor.getString(1); + event.title = cursor.getString(2); + event.description = cursor.getString(3); + event.allDay = cursor.getString(4).equals("1"); + event.start = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(5))); + event.end = Miscellaneous.calendarFromLong(Long.parseLong(cursor.getString(6))); + event.location = cursor.getString(7); + event.availability = cursor.getString(8); + event.reoccurring = true; + + if(includePastEvents || event.end.getTimeInMillis() > now.getTimeInMillis()) + { + // For the moment keeping separate records to some extent + calendarEventsReoccurringCache.add(event); + calendarEventsCache.add(event); + } + } + catch (Exception e) + {} + cursor.moveToNext(); + } + + if(cursor != null) + cursor.close(); + + } + + return calendarEventsCache; + } + + protected static void routineAtAlarm() + { + checkForRules(Miscellaneous.getAnyContext()); + + // Set next timer + calculateNextWakeup(); + armOrRearmTimer(); + } + + public static void armOrRearmTimer() + { + PendingIntent pi = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + { + if (alarmManager == null) + { + alarmManager = (AlarmManager) Miscellaneous.getAnyContext().getSystemService(Context.ALARM_SERVICE); + } + + if(pi == null) + { + Intent intent = new Intent(Miscellaneous.getAnyContext(), CalendarReceiver.class); + intent.setAction(calendarAlarmAction); + pi = PendingIntent.getBroadcast(AutomationService.getInstance(), 0, intent, 0); + } + } + else + { + timerTask = new TimerTask() + { + @Override + public void run() + { + routineAtAlarm(); + } + }; + + if(timer != null) + { + timer.cancel(); + timer.purge(); + } + timer = new Timer(); + } + + if(nextWakeup == null) + { + readCalendarEvents(Miscellaneous.getAnyContext(), true,false); + calculateNextWakeup(); + } + + // If it's now filled, go on + if(nextWakeup != null && wakeupNeedsToBeScheduledOrRescheduled) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && alarmManager.canScheduleExactAlarms())) + { + try + { + alarmManager.cancel(pi); + } + catch (Exception e) + { + + } + Miscellaneous.logEvent("i", "armOrRearmTimer()", "Scheduling wakeup for calendar at " + Miscellaneous.formatDate(nextWakeup.getTime()), 4); + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextWakeup.getTimeInMillis(), pi); + wakeupNeedsToBeScheduledOrRescheduled = false; + } + } + else + timer.schedule(timerTask, nextWakeup.getTimeInMillis()); + } + } + + private static void calculateNextWakeup() + { + Calendar now = Calendar.getInstance(); + List events = readCalendarEvents(Miscellaneous.getAnyContext(), true, false); + + if (events.size() == 0) + { + Miscellaneous.logEvent("i", "calculateNextWakeup()", "No future events, nothing to schedule.", 4); + } + else + { + List ruleCandidates = Rule.findRuleCandidates(Trigger.Trigger_Enum.calendarEvent); + List wakeUpCandidatesList = new ArrayList<>(); + + for (CalendarEvent event : events) + { + for (Rule r : ruleCandidates) + { + for (Trigger t : r.getTriggerSet()) + { + if (t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent) && t.checkCalendarEvent(event, true)) + { + /* + Device needs to wakeup at start AND end of events, no matter what is specified in triggers. + This is because we also need to know when a trigger doesn't apply anymore to make it + count for hasStateNotAppliedSinceLastRuleExecution(). + Otherwise the same rule would not get executed again even after calendar events have come and gone. + */ + + if(event.start.getTimeInMillis() > now.getTimeInMillis()) + wakeUpCandidatesList.add(event.start.getTimeInMillis()); + + if(event.end.getTimeInMillis() > now.getTimeInMillis()) + wakeUpCandidatesList.add(event.end.getTimeInMillis()); + } + } + } + } + + Collections.sort(wakeUpCandidatesList); + + if(wakeUpCandidatesList.size() == 0) + Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Not scheduling any calendar related wakeup as there are no future events that might match a configured trigger.", 4); + else + { + if (nextWakeup == null || nextWakeup.getTimeInMillis() != wakeUpCandidatesList.get(0)) + { + Calendar newAlarm = Miscellaneous.calendarFromLong(wakeUpCandidatesList.get(0)); + if (nextWakeup == null) + Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Chose " + Miscellaneous.formatDate(newAlarm.getTime()) + " as next wakeup for calendar triggers. Old was null.", 4); + else + Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Chose " + Miscellaneous.formatDate(newAlarm.getTime()) + " as next wakeup for calendar triggers. Old was " + Miscellaneous.formatDate(nextWakeup.getTime()), 4); + nextWakeup = newAlarm; + if (!wakeupNeedsToBeScheduledOrRescheduled) + wakeupNeedsToBeScheduledOrRescheduled = true; + } + else + Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Alarm " + Miscellaneous.formatDate(nextWakeup.getTime()) + " has been selected as next wakeup, but not rescheduling since this was not a change.", 4); + } + } + } + + public static void startCalendarReceiver(final AutomationService automationServiceRef) + { + if (!calendarReceiverActive) + { + if (calendarReceiverInstance == null) + calendarReceiverInstance = new CalendarReceiver(); + + if (calendarIntentFilter == null) + { + calendarIntentFilter = new IntentFilter(); + calendarIntentFilter.addAction(Intent.ACTION_PROVIDER_CHANGED); + calendarIntentFilter.addDataScheme("content"); + calendarIntentFilter.addDataAuthority("com.android.calendar", null); + } + + calendarIntent = automationServiceRef.registerReceiver(calendarReceiverInstance, calendarIntentFilter); + + calendarReceiverActive = true; + + armOrRearmTimer(); + } + } + + public static boolean mayRuleStillBeActivatedForPendingCalendarEvents(Rule rule) + { + for(RuleEventPair pair : calendarEventsUsed) + Miscellaneous.logEvent("i", "mayRuleStillBeActivatedForPendingCalendarEvents()", "Existing pair of " + pair.rule.getName() + " and " + pair.event, 5); + + for(CalendarEvent event : readCalendarEvents(Miscellaneous.getAnyContext(), true,false)) + { + for(Trigger t : rule.getTriggerSet()) + { + if(t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent) && t.checkCalendarEvent(event, false)) + { + if (!hasEventBeenUsedInRule(rule, event)) + { + /* + If there are multiple parallel calendar events and a rule has multiple + triggers of type calendar event, we don't want the rule to fire only once. + */ + if(rule.getAmountOfTriggersForType(Trigger.Trigger_Enum.calendarEvent) == 1) + { + Miscellaneous.logEvent("i", "mayRuleStillBeActivatedForPendingCalendarEvents()", "Rule " + rule.getName() + " has not been used in conjunction with event " + event, 4); + return true; + } + } + } + } + } + + return false; + } + + public static boolean hasEventBeenUsedInRule(Rule rule, CalendarEvent event) + { + for (RuleEventPair executedPair : calendarEventsUsed) + { + if (executedPair.rule.equals(rule) && executedPair.event.equals(event)) + return true; + } + + return false; + } + + public static List getApplyingCalendarEvents(Rule rule) + { + List returnList = new ArrayList<>(); + + try + { + List calendarEvents = CalendarReceiver.readCalendarEvents(AutomationService.getInstance(), true,false); + + for(Trigger t : rule.getTriggerSet()) + { + if(t.getTriggerType().equals(Trigger.Trigger_Enum.calendarEvent)) + { + for (CalendarReceiver.CalendarEvent event : calendarEvents) + { + if (t.checkCalendarEvent(event, false)) + returnList.add(event); + } + } + } + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "getApplyingCalendarEvents()", Log.getStackTraceString(e), 1); + } + + return returnList; + } +} \ No newline at end of file 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 75f49e7..f78ec62 100644 --- a/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java +++ b/app/src/main/java/com/jens/automation2/receivers/DateTimeListener.java @@ -23,13 +23,14 @@ import java.sql.Time; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; public class DateTimeListener extends BroadcastReceiver implements AutomationListenerInterface { private static AutomationService automationServiceRef; private static AlarmManager centralAlarmManagerInstance; - private static boolean alarmListenerActive=false; + private static boolean alarmListenerActive = false; private static ArrayList alarmCandidates = new ArrayList<>(); private static ArrayList requestCodeList = new ArrayList(); static PendingIntent alarmPendingIntent = null; @@ -201,7 +202,7 @@ public class DateTimeListener extends BroadcastReceiver implements AutomationLis { Calendar calSchedule = getNextRepeatedExecutionAfter(oneTrigger, calNow); - alarmCandidates.add(new ScheduleElement(calSchedule, "Rule " + oneRule.getName() + ", trigger " + oneTrigger.toString())); + alarmCandidates.add(new ScheduleElement(calSchedule, "Rule " + oneRule.getName() + ", repetition in trigger " + oneTrigger.toString())); } } } @@ -219,43 +220,28 @@ public class DateTimeListener extends BroadcastReceiver implements AutomationLis 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); - - if(Miscellaneous.getAnyContext().getApplicationContext().getApplicationInfo().targetSdkVersion >= 31) - alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); else - alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + { + Collections.sort(alarmCandidates); - centralAlarmManagerInstance.set(AlarmManager.RTC_WAKEUP, scheduleCandidate.time.getTimeInMillis(), alarmPendingIntent); + Miscellaneous.logEvent("i", "AlarmManager", "Chose this as next scheduled alarm: " + alarmCandidates.get(0), 4); - SimpleDateFormat sdf = new SimpleDateFormat("E dd.MM.yyyy HH:mm:ss"); - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(scheduleCandidate.time.getTimeInMillis()); - Miscellaneous.logEvent("i", "AlarmManager", "Chose " + sdf.format(calendar.getTime()) + " as next scheduled alarm.", 4); + Intent alarmIntent = new Intent(automationServiceRef, DateTimeListener.class); + + if(Miscellaneous.getAnyContext().getApplicationContext().getApplicationInfo().targetSdkVersion >= 31) + alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + else + alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + centralAlarmManagerInstance.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmCandidates.get(0).time.getTimeInMillis(), alarmPendingIntent); + else + centralAlarmManagerInstance.set(AlarmManager.RTC_WAKEUP, alarmCandidates.get(0).time.getTimeInMillis(), alarmPendingIntent); + } } public static void clearAlarms() @@ -266,7 +252,7 @@ public class DateTimeListener extends BroadcastReceiver implements AutomationLis Intent alarmIntent = new Intent(automationServiceRef, DateTimeListener.class); if(alarmPendingIntent == null) alarmPendingIntent = PendingIntent.getBroadcast(automationServiceRef, requestCode, alarmIntent, 0); -// Miscellaneous.logEvent("i", "AlarmManager", "Clearing alarm with request code: " + String.valueOf(requestCode)); + centralAlarmManagerInstance.cancel(alarmPendingIntent); } requestCodeList.clear(); @@ -279,17 +265,9 @@ public class DateTimeListener extends BroadcastReceiver implements AutomationLis 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); @@ -301,7 +279,6 @@ public class DateTimeListener extends BroadcastReceiver implements AutomationLis { Miscellaneous.logEvent("i", "AlarmListener", "Stopping alarm listener.", 4); clearAlarms(); - centralAlarmManagerInstance.cancel(alarmPendingIntent); alarmListenerActive = false; } else @@ -398,11 +375,6 @@ public class DateTimeListener extends BroadcastReceiver implements AutomationLis 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. - */ - return calSchedule; } else diff --git a/app/src/main/java/com/jens/automation2/receivers/NotificationListener.java b/app/src/main/java/com/jens/automation2/receivers/NotificationListener.java index b81adf9..21d87b1 100644 --- a/app/src/main/java/com/jens/automation2/receivers/NotificationListener.java +++ b/app/src/main/java/com/jens/automation2/receivers/NotificationListener.java @@ -3,13 +3,13 @@ package com.jens.automation2.receivers; import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; -import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.Log; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.jens.automation2.AutomationService; @@ -19,6 +19,7 @@ import com.jens.automation2.Trigger; import java.util.ArrayList; import java.util.Calendar; +import java.util.List; // See here for reference: http://gmariotti.blogspot.com/2013/11/notificationlistenerservice-and-kitkat.html @@ -26,8 +27,6 @@ import java.util.Calendar; @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) public class NotificationListener extends NotificationListenerService// implements AutomationListenerInterface { - static Calendar lastResponseToNotification = null; - static boolean listenerRunning = false; static NotificationListener instance; static SimpleNotification lastNotification = null; @@ -43,13 +42,30 @@ public class NotificationListener extends NotificationListenerService// implemen // a bitmap to be used instead of the small icon when showing the notification payload public static final String EXTRA_LARGE_ICON = "android.largeIcon"; - protected static IntentFilter notificationReceiverIntentFilter = null; - + public static void setLastNotification(SimpleNotification notification) + { + lastNotification = notification; + } public static SimpleNotification getLastNotification() { return lastNotification; } + // To determine for which notifications which rules have been executed + static List notificationUsed = new ArrayList<>(); + + public static class RuleNotificationPair + { + Rule rule; + SimpleNotification notification; + + public RuleNotificationPair(Rule rule, SimpleNotification sn) + { + this.rule = rule; + this.notification = sn; + } + } + @Override public void onCreate() { @@ -84,6 +100,7 @@ public class NotificationListener extends NotificationListenerService// implemen synchronized boolean checkNotification(boolean created, StatusBarNotification sbn) { + //TODO: Merge with functino in Trigger class if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { lastNotification = convertNotificationToSimpleNotification(created, sbn); @@ -215,6 +232,19 @@ public class NotificationListener extends NotificationListenerService// implemen ", text='" + text + '\'' + '}'; } + + @Override + public boolean equals(@Nullable Object obj) + { + return + this.publishTime.getTimeInMillis() == ((SimpleNotification)obj).publishTime.getTimeInMillis() + && + this.app.equals(((SimpleNotification)obj).app) + && + this.title.equals(((SimpleNotification)obj).title) + && + this.text.equals(((SimpleNotification)obj).text); + } } @Override @@ -235,7 +265,6 @@ public class NotificationListener extends NotificationListenerService// implemen cancelNotification(sbn.getPackageName(), sbn.getTag(), sbn.getId()); else cancelNotification(sbn.getKey()); - } @RequiresApi(api = Build.VERSION_CODES.KITKAT) diff --git a/app/src/main/res/drawable-hdpi/calendar.png b/app/src/main/res/drawable-hdpi/calendar.png new file mode 100644 index 0000000..f529d02 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/calendar.png differ diff --git a/app/src/main/res/drawable-hdpi/copier.png b/app/src/main/res/drawable-hdpi/copier.png new file mode 100644 index 0000000..81257cb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/copier.png differ diff --git a/app/src/main/res/drawable-hdpi/ear.png b/app/src/main/res/drawable-hdpi/ear.png index 916b101..36cd243 100644 Binary files a/app/src/main/res/drawable-hdpi/ear.png and b/app/src/main/res/drawable-hdpi/ear.png differ diff --git a/app/src/main/res/drawable-hdpi/variable.png b/app/src/main/res/drawable-hdpi/variable.png new file mode 100644 index 0000000..31b83bb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/variable.png differ diff --git a/app/src/main/res/layout/activity_help_text.xml b/app/src/main/res/layout/activity_help_text.xml index ca35757..434ae67 100644 --- a/app/src/main/res/layout/activity_help_text.xml +++ b/app/src/main/res/layout/activity_help_text.xml @@ -88,21 +88,54 @@ android:textAppearance="?android:attr/textAppearanceLarge" /> - + - + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_margin="10dp" > + - + android:id="@+id/tvRuleHelpText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/helpTextRules" /> + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +