diff --git a/app/src/apkFlavor/AndroidManifest.xml b/app/src/apkFlavor/AndroidManifest.xml index 2357f46..84b71d8 100644 --- a/app/src/apkFlavor/AndroidManifest.xml +++ b/app/src/apkFlavor/AndroidManifest.xml @@ -71,9 +71,13 @@ + + + + + + + + + + + + + + + - \ 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 718b8d9..b152f18 100644 --- a/app/src/main/java/com/jens/automation2/Action.java +++ b/app/src/main/java/com/jens/automation2/Action.java @@ -144,7 +144,7 @@ public class Action case takeScreenshot: return context.getResources().getString(R.string.takeScreenshot); case setLocationService: - return context.getResources().getString(R.string.setLocationService); + return context.getResources().getString(R.string.setLocationServiceCapital); default: return "Unknown"; } diff --git a/app/src/main/java/com/jens/automation2/ActivityManageRule.java b/app/src/main/java/com/jens/automation2/ActivityManageRule.java index f07971a..6111896 100644 --- a/app/src/main/java/com/jens/automation2/ActivityManageRule.java +++ b/app/src/main/java/com/jens/automation2/ActivityManageRule.java @@ -143,6 +143,8 @@ public class ActivityManageRule extends Activity 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; public static ActivityManageRule getInstance() { @@ -347,6 +349,12 @@ 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; default: break; } @@ -636,6 +644,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)); } @@ -644,15 +656,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); @@ -861,6 +873,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(); @@ -1994,6 +2013,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) @@ -2033,6 +2063,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) @@ -2162,6 +2205,8 @@ public class ActivityManageRule extends Activity 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 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..2539a9a --- /dev/null +++ b/app/src/main/java/com/jens/automation2/ActivityManageTriggerCalendar.java @@ -0,0 +1,349 @@ +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.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 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; + 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); + + 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(); + + 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)); + } + }); + + 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(chkCalendarAllDayEvent.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) + { + //TODO:try-catch + + 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 = all day event + 7 = availability list + 8 = 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]); + + chkCalendarAllDayEvent.setChecked(Boolean.parseBoolean(input[6])); + + String[] availabilities = null; + if(!StringUtils.isEmpty(input[7])) + availabilities = input[7].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[8])) + calendars = input[8].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))); + } + } + } + } + + @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/ActivityPermissions.java b/app/src/main/java/com/jens/automation2/ActivityPermissions.java index de627e3..221bca6 100644 --- a/app/src/main/java/com/jens/automation2/ActivityPermissions.java +++ b/app/src/main/java/com/jens/automation2/ActivityPermissions.java @@ -54,6 +54,7 @@ public class ActivityPermissions extends Activity 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"; @@ -558,6 +559,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); @@ -580,6 +583,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; } @@ -828,6 +836,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)); @@ -1023,6 +1037,10 @@ public class ActivityPermissions extends Activity 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; @@ -1091,6 +1109,10 @@ public class ActivityPermissions extends Activity 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); } } @@ -1218,6 +1240,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); @@ -1303,6 +1341,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(); diff --git a/app/src/main/java/com/jens/automation2/Miscellaneous.java b/app/src/main/java/com/jens/automation2/Miscellaneous.java index 4da9c91..cc035db 100644 --- a/app/src/main/java/com/jens/automation2/Miscellaneous.java +++ b/app/src/main/java/com/jens/automation2/Miscellaneous.java @@ -806,7 +806,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) { diff --git a/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java b/app/src/main/java/com/jens/automation2/ReceiverCoordinator.java index 93b0339..65b144d 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) { diff --git a/app/src/main/java/com/jens/automation2/Trigger.java b/app/src/main/java/com/jens/automation2/Trigger.java index 687356a..1db9c20 100644 --- a/app/src/main/java/com/jens/automation2/Trigger.java +++ b/app/src/main/java/com/jens/automation2/Trigger.java @@ -14,6 +14,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 +32,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 +65,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 +126,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 +252,14 @@ public class Trigger case subSystemState: if(!checkSubSystemState()) result = false; - break; case checkVariable: if(!checkVariable()) result = false; break; + case calendarEvent: + if(!checkCalendarEvent()) + result = false; + break; default: break; } @@ -609,6 +617,117 @@ public class Trigger return false; } + boolean checkCalendarEvent() + { + try + { + String[] conditions = this.getTriggerParameter2().split(Trigger.triggerParameter2Split); + List calendarEvents = CalendarReceiver.readCalendarEvents(AutomationService.getInstance(), false); + + /* + 0 = titleDirection + 1 = title; + 2 = descriptionDirection + 3 = description + 4 = eventLocationDirection + 5 = eventLocation + 6 = all day event + 7 = availabilityList + 8 = calendarList + */ + + for(CalendarReceiver.CalendarEvent event : calendarEvents) + { + boolean isActive = getTriggerParameter(); + if(isActive != event.isCurrentlyActive()) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Event has to be currently active: " + String.valueOf(triggerParameter) + ", but is required otherwise.", 5); + continue; + } + + if(!StringUtils.isEmpty(conditions[1])) + { + if (!Miscellaneous.compare(conditions[0], conditions[1], event.title)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Title does not match.", 5); + continue; + } + } + + if(!StringUtils.isEmpty(conditions[3])) + { + if (!Miscellaneous.compare(conditions[2], conditions[3], event.description)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Description does not match.", 5); + continue; + } + } + + if(!StringUtils.isEmpty(conditions[5])) + { + if (!Miscellaneous.compare(conditions[4], conditions[5], event.location)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Location does not match.", 5); + continue; + } + } + + if (Boolean.parseBoolean(conditions[6]) != event.allDay) + { + Miscellaneous.logEvent("i", "CalendarCheck", "All day setting does not match.", 5); + continue; + } + + if(!StringUtils.isEmpty(conditions[7])) + { + String[] availabilities = conditions[7].split(ActivityManageTriggerCalendar.separator); + if (availabilities.length > 0) + { + if (!Miscellaneous.arraySearch(availabilities, event.availability, false, true)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Availability does not match.", 5); + continue; + } + } + } + + if(!StringUtils.isEmpty(conditions[8])) + { + String[] calendars = conditions[8].split(ActivityManageTriggerCalendar.separator); + if (calendars.length > 0) + { + if (!Miscellaneous.arraySearch(calendars, String.valueOf(event.calendarId), false, true)) + { + Miscellaneous.logEvent("i", "CalendarCheck", "Calendar does not match.", 5); + continue; + } + } + } + + // No contradictions found + Miscellaneous.logEvent("i", "CalendarCheck", "Event " + event + " matches.", 4); + return true; + } + + // At this point none of the calendar items matches this trigger + + // If trigger demands no calendar event and there is absolutely no future event, we'll need to check for that. + if(calendarEvents.size() == 0) + { + if(getTriggerParameter() == false) + return true; + + // Further criteria don't matter if there are no events to check + } + } + catch(Exception e) + { + Miscellaneous.logEvent("e", "checkVariable()", Log.getStackTraceString(e), 1); + } + + return false; + } + boolean checkBluetooth() { Miscellaneous.logEvent("i", Miscellaneous.getAnyContext().getResources().getString(R.string.ruleCheckOf), String.format("Checking for bluetooth...", this.getParentRule().getName()), 4); @@ -1813,6 +1932,44 @@ 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])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.allDayEventTrue) + ", "); + else + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.allDayEventFalse) + ", "); + + if (!StringUtils.isEmpty(conditions[7])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.availabilities) + " " + conditions[7] + ", "); + + if (!StringUtils.isEmpty(conditions[8])) + returnString.append(Miscellaneous.getAnyContext().getResources().getString(R.string.calendars) + " " + conditions[8]); + + 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/receivers/CalendarReceiver.java b/app/src/main/java/com/jens/automation2/receivers/CalendarReceiver.java new file mode 100644 index 0000000..654de39 --- /dev/null +++ b/app/src/main/java/com/jens/automation2/receivers/CalendarReceiver.java @@ -0,0 +1,412 @@ +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.os.SystemClock; + +import androidx.annotation.NonNull; + +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.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 AutomationService automationServiceRef; + 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 Timer timer = null; + static TimerTask timerTask = null; + static Calendar nextWakeup = null; + static AlarmManager alarmManager = null; + static boolean alarmHasChanged = false; + + public static CalendarReceiver getInstance() + { + if(calendarReceiverInstance == null) + calendarReceiverInstance = new CalendarReceiver(); + + return calendarReceiverInstance; + } + + @Override + public void onReceive(Context context, Intent intent) + { + if(intent.getAction().equalsIgnoreCase(Intent.ACTION_PROVIDER_CHANGED)) + { + Miscellaneous.logEvent("i", "CalendarReceiver", "Received " + intent.getAction(), 5); + + calendarsCache = null; + calendarEventsCache = null; + + checkForRules(context); + armOrRearmTimer(); + } + else if(intent.getAction().equalsIgnoreCase(calendarAlarmAction)) + { + Miscellaneous.logEvent("i", "AlarmReceiver", "Received alarm for calendar receiver.", 5); + routineAtAlarm(); + } + } + + private 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 automationService) + { + if(!calendarReceiverActive) + { + if(calendarReceiverInstance == null) + calendarReceiverInstance = new CalendarReceiver(); + + if(calendarIntentFilter == null) + { + calendarIntentFilter = new IntentFilter(); + calendarIntentFilter.addAction(Intent.ACTION_PROVIDER_CHANGED); +// calendarIntentFilter.addDataScheme("content"); + } + + AutomationService.getInstance().registerReceiver(calendarReceiverInstance, calendarIntentFilter); + + calendarReceiverActive = true; + } + } + + @Override + public void stopListener(AutomationService automationService) + { + if(calendarReceiverActive) + { + if(calendarReceiverInstance != null) + { + AutomationService.getInstance().unregisterReceiver(calendarReceiverInstance); + calendarReceiverInstance = null; + } + + 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; + + public boolean isCurrentlyActive() + { + Calendar now = Calendar.getInstance(); + return now.getTimeInMillis() >= start.getTimeInMillis() && now.getTimeInMillis() < end.getTimeInMillis(); + } + + @NonNull + @Override + public String toString() + { + return title; + } + } + + 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[]{ "_id", "calendar_displayName", "ownerAccount", }, + null, null, null); + + cursor.moveToFirst(); + // fetching calendars name + String CNames[] = new String[cursor.getCount()]; + + List calendarlist = new ArrayList<>(); + + 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 includePastEvents) + { + if(calendarEventsCache == null) + { + calendarEventsCache = new ArrayList<>(); + + Cursor cursor; + + cursor = context.getContentResolver().query( + Uri.parse("content://com.android.calendar/events"), + new String[] { "calendar_id", "_id", "title", "description", "allDay", "dtstart", "dtend", "eventLocation", "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); + + if(includePastEvents || event.end.getTimeInMillis() > now.getTimeInMillis()) + 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(); + } + + private 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); + + 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(), false); + calculateNextWakeup(); + } + + // If it's now filled, go on + if(nextWakeup != null) + { + 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()", "Setting calendar alarm for " + nextWakeup.toString(), 5); + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextWakeup.getTimeInMillis(), pi); + + //alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextWakeup.getTimeInMillis(), pi); + } + } + else + timer.schedule(timerTask, nextWakeup.getTimeInMillis()); + } + } + + private static void calculateNextWakeup() + { + Calendar now = Calendar.getInstance(); + if (nextWakeup != null && nextWakeup.getTimeInMillis() < now.getTimeInMillis()) + nextWakeup = null; + + List events = readCalendarEvents(Miscellaneous.getAnyContext(), false); + if (events.size() > 0) + { + for (CalendarEvent event : events) + { + if (event.isCurrentlyActive()) + { + if (nextWakeup == null || event.end.getTimeInMillis() < nextWakeup.getTimeInMillis()) + { + nextWakeup = event.end; + Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Choosing end of event " + event.title + " as next wakeup.", 5); + if(!alarmHasChanged) + alarmHasChanged = true; + } + } + else + { + if (nextWakeup == null || event.start.getTimeInMillis() < nextWakeup.getTimeInMillis()) + { + nextWakeup = event.start; + Miscellaneous.logEvent("i", "calculateNextWakeupForCalendar()", "Choosing start of event " + event.title + " as next wakeup.", 5); + if(!alarmHasChanged) + alarmHasChanged = true; + } + } + } + } + //else + // we expect to be called byOnReceive() when new items exist + } + + public static void startCalendarReceiver(final AutomationService automationServiceRef) + { + if (!calendarReceiverActive) + { + CalendarReceiver.automationServiceRef = automationServiceRef; + + 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(); + } + } +} \ No newline at end of file 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/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_manage_trigger_calendar.xml b/app/src/main/res/layout/activity_manage_trigger_calendar.xml new file mode 100644 index 0000000..dac9a54 --- /dev/null +++ b/app/src/main/res/layout/activity_manage_trigger_calendar.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +