I have a problem with adding event to Google Calendar programmaticly.
Here is code:
private void addToGoogleCalendar(long beginTime) {
ContentValues event = new ContentValues();
Observable.from(dataKeeper.getCalendarIds().keySet()).filter(key -> dataKeeper.getCalendarIds().get(key).equals(defaultCalendarName.getText().toString())).forEach(key ->
event.put(CalendarContract.Reminders.CALENDAR_ID, Integer.parseInt(key))
);
event.put(CalendarContract.Reminders.TITLE, reminderName.getText().toString());
if (!TextUtils.isEmpty(commentEdittext.getText().toString()))
event.put(CalendarContract.Reminders.DESCRIPTION, commentEdittext.getText().toString());
event.put(CalendarContract.Reminders.EVENT_TIMEZONE, TimeZone.getDefault().getID());
event.put(CalendarContract.Reminders.DTSTART, beginTime);
event.put(CalendarContract.Reminders.DTEND, dataKeeper.getEnd());
event.put(CalendarContract.Reminders.EVENT_COLOR, getResources().getColor(R.color.primary));
// http://tools.ietf.org/html/rfc2445
if (dataKeeper.getPeriod() > 0 && dataKeeper.getPeriod() != 7)
event.put(CalendarContract.Reminders.RRULE, String.format("FREQ=DAILY;INTERVAL=%d;UNTIL=%s", dataKeeper.getPeriod() - 1,
new SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(dataKeeper.getEnd())));
else if (dataKeeper.getPeriod() == 7)
event.put(CalendarContract.Reminders.RRULE, String.format("FREQ=WEEKLY;INTERVAL=1;UNTIL=%s",
new SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(dataKeeper.getEnd())));
event.put(CalendarContract.Reminders.ALL_DAY, 1); // 0 for false, 1 for true
event.put(CalendarContract.Reminders.STATUS, CalendarContract.Events.STATUS_CONFIRMED);
String eventUriString = "content://com.android.calendar/events";
getContentResolver().insert(Uri.parse(eventUriString), event);
}
Inrerval = 1
So, when I add event, it adds as need, but in few seconds I see that event adds recursive. First day one time, next day two times and so on.
Method called only ones, so I have no idea WTF happens.
You've set the end of the master instance to the end of the recurrence rule.
The first instance has a duration of dataKeeper.getEnd() - beginTime. According to the RRULE this instance is recurs every day until dataKeeper.getEnd()
Now if dataKeeper.getEnd() is several days after beginTime it happens that although the fist instance is still ongoing on the second day (because it has a duration of multiple days), another instance starts on that day.
On the third day, the first two instances are still ongoing and the rule says another one should start.
So there is one instance added until the UNTIL date of the RRULE, after that the number of instances decreases by one each day.
To fix it make sure that the first instance is just a day long. Replace
event.put(CalendarContract.Reminders.DTEND, dataKeeper.getEnd());
by
event.put(CalendarContract.Events.DURATION, "P1D");
This sets the duration of each instance to 1 day. You should always use DURATION for recurring events anyway, see Events.
Btw, you should also use CalendarContract.Events instead of CalendarContract.Reminders. Although Reminders also implements EventsColumns it's meant for alarms. I guess it contains the event columns because it effectively reads from a join over both tables.
Related
I'm working on an Android app that has a functionality that is weekly basis, that is, every day of the week the user has to mark as done the day. This value is a boolean on my database, that is initialized with false, and is set to true when the user clicks on the checkbox. Everything is working fine.
But my problem is that I need to "reset" this boolean value to false on all the seven days of the week every time a new week begins. I don't need to have records of the past weeks. All that matters is the actual week (Sunday to Saturday).
It's a very simple task, I only need to do this:
for(WeekDay day: dao.getWeekDays()){
day.setDone(false);
dao.updateWeekDay(day); //update the value in database
}
So, I did some research (I'm new to android) and find out that Android has different schedule services like JobScheduler or AlarmManager. My app is designed to Android 10+ (API 29+).
What do you think is the best solution for my problem?
It's a very simple task (it won't take too much battery, internet,...) and I need to do this in a specific day (Sunday) every week. Also, this task needs to be done as soon as it possible, even if the phone is turned off on Sunday. It doesn't need to be a background service, but I need to guarantee that when the user opens the app and it's a new week, that method needs to be call before, but only if it had not been call in the actual week before.
Anyone has ideas?
Ok, I think I found a simple solution for my problem, based on other similar answers I read. I just need to run these function every time the app starts. I didn't need to use any background service, like WorkManager.
I only need to store in SharedPreferences the last date when the system did a reset in the values. Then, every time I open the app, it checks if today is in a different week from the last reset day. If it's true, then I run that "for cycle" in the question and update the last reset day to today in the SharedPreferences. If it's false, I do nothing.
The method inSameCalendarWeek checks if the day is in the same week from the same year of today (Locale.US guarantees that a week starts on Sunday. But I could change that to Locale.getDefault() to be more flexible). Also, for example, if December 31 is in the same week of January 1, even if they are in different years, the method will return true.
private void checkAndResetDoneDays() {
long lastResetDay = settings.getLong(LAST_DAY_RESET, 0);
LocalDate date = Instant.ofEpochMilli(lastResetDay).atZone(ZoneId.systemDefault()).toLocalDate();
if (!inSameCalendarWeek(date)) {
resetDoneDays();
settings.edit()
.putLong(LAST_DAY_RESET, LocalDate.now(ZoneId.systemDefault()).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli())
.commit();
}
}
public boolean inSameCalendarWeek(LocalDate firstDate) {
LocalDate secondDate = LocalDate.now(ZoneId.systemDefault());
// get a reference to the system of calendar weeks in US Locale (to guarantee that week starts on Sunday)
WeekFields weekFields = WeekFields.of(Locale.US);
// find out the calendar week for each of the dates
int firstDatesCalendarWeek = firstDate.get(weekFields.weekOfWeekBasedYear());
int secondDatesCalendarWeek = secondDate.get(weekFields.weekOfWeekBasedYear());
/*
* find out the week based year, too,
* two dates might be both in a calendar week number 1 for example,
* but in different years
*/
int firstWeekBasedYear = firstDate.get(weekFields.weekBasedYear());
int secondWeekBasedYear = secondDate.get(weekFields.weekBasedYear());
// return if they are equal or not
return firstDatesCalendarWeek == secondDatesCalendarWeek
&& firstWeekBasedYear == secondWeekBasedYear;
}
I want to delete SharedPreferences, if they exist, after one month automatically. I could not find any solutions. Is this possible to make?
Thanks a lot.
It depends.
The easiest way is to delete it when the user starts the app.
When the apps is created, you check the SharedPreferences for the last updated time.
If it's null (the first time), you save the current time in milliseconds as a long.
If it's not null, you read it and compare it against the current time. If it less than a month, you do nothing. If it's more than a month, you clear the shared preferences and, after clearing it, insert the new time.
Something like:
long lastUpdate = sharedPreferences.getLong(LAST_UPDATE, -1);
if(lastUpdate == -1) {
//First time
sharedPreferences.edit().putLong(LAST_UPDATE, System.currentTimeMillis()).apply();
} else {
boolean isMoreThanAMonth = //Here you should do the math. it depends, you want to consider a month like 30 days, or you want to know if it was in another month... somehthing like that
if(isMoreThanAMonth) {
sharedPreferences.edit().clear().apply()
}
}
Of course, if you want to clear the SharedPreferences even if the user does not open the app you should use a Service. It's more complex and expensive for the OS, so you should try to go for the first one if it fits your requirement.
long installed = context
.getPackageManager()
.getPackageInfo(context.getPackageName(), 0)
.firstInstallTime
;
public long firstInstallTime
The time at which the app was first installed. Units are as per System.currentTimeMillis().
Now you can compare two date and get months diffrent by using GregorianCalendar
after you get one month different do as you want..clear sharedPrefrence.edit().clear().commit()
One possible way,
1. get the calendar instance.
2. Get maximum day of month.
3. Store in a var1 string in format of dd/mm/yyyy.
4. Get the current date from some calendar object and store in same way from point 3 but in var2.
5. Compare two strings.
6. If match then it will be last day of month and call delete() on your files.
Done.
I include here the full problem description because I'm not exactly sure if the logic behind the solution is even correct, but I am pretty sure that it has to do with the way I'm setting the alarms themselves that's causing this inaccuracy, or just sometimes pure fault (alarms don't trigger at all).
A user can add a new medication from a list of medications.
Screen 1
When a certain medication is found, clicking on it will show this screen http://imgur.com/nLC9gTG
That screen contains the Medication's name and under the "Posology" title (the green bar) is where the reminders for that Medication can be added.
Forget the "Units" field.
The "Frequency" field accepts a number and the label to the right of the "Frequency" field is clickable, it causes a dropdown menu to appear, from which the user can select "times for day" or "times per week".
The "Days of week" label (the label is empty in the screenshot) is also clickable, it presents the user with a dropdown menu from which the user can select multiple days from the days of a week.
The "Treatment Duration" field accepts a number and the label to the right of the "Treatment Duration" field will reflect the user's choice of "Frequency" (if it's "times per week" then that label will say "weeks", if it's "times per month" then that label will say "months").
Screen 2
In this second screenshot http://imgur.com/AcUmlHH -- There is a Switch which allows the user to enable reminders for this Medication (item, instance, etc.) that he's attempting to add.
If the "Frequency" field above has a number greater than 0 (2, for example), then the reminders Switch will create a list of Reminder fields which it will show just underneath the "Get Notifications" green bar.
When the user finally presses on "Add Medication", a new Medication object will be created in the database, along with the "Frequency" (number of reminders) that the user has chosen to add for this Medication object.
Create a Medication table:
id
name
description
dosage
frequency
frequencyType
treatmentDuration
ForeignCollection<MedicationReminder>
ArrayList<DayChoice> (DayChoice is a class with "Day Name" and "Selected")
when
whenString
units
unitForm
remarks
remindersEnabled
Create a MedicationReminder table:
Medication (foreign key for the Medication table)
Calendar
int[] days_of_week
totalTimesToTrigger
Upon creating this new Medication object:
Medication medication = new Medication();
medication.setFrequency()
medication.setName().setDosage().setRemindersEnabled()....
assignForeignCollectionToParentObject(medication);
assignForeignCollectionToParentObject(Medication)
private void assignForeignCollectionToParentObject(Medication medicationObject) {
medicationDAO.assignEmptyForeignCollection(medicationObject, "medicationReminders");
MedicationRemindersRecyclerAdapter adapter =
(MedicationRemindersRecyclerAdapter) remindersRecyclerView.getAdapter();
//Clear previous reminders
medicationObject.getMedicationReminders().clear();
for (int i = 0; i < adapter.getItemCount(); i++) {
int realDaysSelected = 0;
MedicationReminder medReminder = adapter.getItem(i);
medReminder.setMedication(medicationObject);
medReminder.setDays_of_week(daysOfWeekArray);
//These days are populated when the user selected them from the "Days of Week" clickable label
for (int aDaysOfWeekArray : daysOfWeekArray) {
if (aDaysOfWeekArray != 0) realDaysSelected++;
}
medReminder.setTotalTimesToTrigger(
Integer.parseInt(treatmentDurationET.getText().toString()) * realDaysSelected);
medicationObject.getMedicationReminders().add(medReminder);
}
setupMedicationReminders(medicationObject.getMedicationReminders().iterator());
}
setupMedicationReminders()
public void setupMedicationReminders(Iterator<MedicationReminder> medicationRemindersIterator) {
PendingIntent pendingIntent;
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
while (medicationRemindersIterator.hasNext()) {
MedicationReminder medReminder = medicationRemindersIterator.next();
for (int i = 0; i < medReminder.getDays_of_week().length; i++) {
int dayChosen = medReminder.getDays_of_week()[i];
if (dayChosen != 0) {
medReminder.getAlarmTime().setTimeInMillis(System.currentTimeMillis());
medReminder.getAlarmTime().set(Calendar.DAY_OF_WEEK, dayChosen);
Intent intent = new Intent(AddExistingMedicationActivity.this, AlarmReceiver.class);
intent.putExtra(Constants.EXTRAS_ALARM_TYPE, "medications");
intent.putExtra(Constants.EXTRAS_MEDICATION_REMINDER_ITEM, (Parcelable) medReminder);
pendingIntent = PendingIntent.getBroadcast(this, medReminder.getId(), intent,
PendingIntent.FLAG_UPDATE_CURRENT);
int ALARM_TYPE = AlarmManager.ELAPSED_REALTIME_WAKEUP;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
am.setExactAndAllowWhileIdle(ALARM_TYPE, medReminder.getAlarmTime().getTimeInMillis(),
pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
am.setExact(ALARM_TYPE, medReminder.getAlarmTime().getTimeInMillis(), pendingIntent);
} else {
am.set(ALARM_TYPE, medReminder.getAlarmTime().getTimeInMillis(), pendingIntent);
}
}
}
}
}
The problem is when the medication reminders are added, they are always triggered shortly after being added, and all at the same time.
Say I select Frequency 2 for Saturday and Friday with a treatment duration of 1 week. This means that there will be a total of 4 reminders being added, 2 on Friday and 2 on Saturday.
When I do this, and it happens to be a Saturday, the alarms trigger at the same time together, for Saturday.
What's wrong?
When you do this:
medReminder.getAlarmTime().setTimeInMillis(System.currentTimeMillis());
medReminder.getAlarmTime().set(Calendar.DAY_OF_WEEK, dayChosen);
The results are unpredictable. If the current day is a Monday and you call set(Calendar.DAY_OF_WEEK) with Calendar.THURSDAY, should the date be changed to the previous Thursday? or the next Thursday? You don't know.
If your alarms are all going off immediately, this would indicate that changing the DAY_OF_WEEK is causing the calendar to go backwards instead of forwards. To verify that, after you set the DAY_OF_WEEK, call getTimeInMillis() and compare it against the current time. If it is smaller, then your calendar has gone back in time. To fix that, just add 7 days to the calendar.
Also, you are using this type of alarm: AlarmManager.ELAPSED_REALTIME_WAKEUP. This type takes a value that represents the amount of time that has passed since device boot.
However, you are using the RTC for the time value (ie: Calendar.getTimeInMillis()). These 2 values are incompatible. If you want to use the RTC, you need to use AlarmManager.RTC_WAKEUP.
The problem is when the medication reminders are added, they are always triggered shortly after being added, and all at the same time.
That's because that's what you asked for. You are always reading the current time and setting it to the reminder time:
medReminder.getAlarmTime().setTimeInMillis(System.currentTimeMillis());
You never read the time provided by the user in this screen, i.e. the Calendar field of your reminder is never set. Here is your code:
MedicationReminder medReminder = adapter.getItem(i);
medReminder.setMedication(medicationObject);
medReminder.setDays_of_week(daysOfWeekArray);
for (int aDaysOfWeekArray : daysOfWeekArray) {
if (aDaysOfWeekArray != 0) realDaysSelected++;
}
medReminder.setTotalTimesToTrigger(...);
You missed the line for actually setting the reminder time
This might not be answer to your question, but IMHO you should consider this.
I would argue that this is bad UX. User of this application could be confused about the times of reminders based only on time (without dates). So maybe it would be better to add full date next to each reminder that the user would be able to edit. When you generate them for the first time, you set them in right order.
Reminder 1: 2/2/2017 13:04
Reminder 2: 9/2/2017 13:04
Reminder 3: 16/2/2017 13:04
Reminder 4: 25/2/2017 13:04
This would also reflect real-world better. Lets consider this use case: user has his phone with him, but he forgot medication. He can't take it at right time, so he takes it some point later (maybe even tommorow). This will mess up the plan, but in this case he can edit the date to a date when he actually took his medication. Then you adjust the date and time on all reminders after the one he edited to follow the intervals based on times_per_week, times_per_month. He should not be able to change them to past date.
Reminder 1: 2/2/2017 13:04 // He took medication on time
Reminder 2: 9/2/2017 13:04 // He missed this one and changed to date bellow
10/2/2017 12:22 // This is when he actually took the medication
Reminder 3: 16/2/2017 13:04 // This is no longer valid
17/2/2017 12:22 // You adjust reminder to this date and time
Reminder 4: 25/2/2017 13:04 // This is no longer valid
26/2/2017 12:22 // You adjust reminder to this date and time
User must still be limited to a certain range of dates for each reminder. You can't let him pick just any date. This is based on his plan settings and the current date, but I don't want to get into that. It's a lot of work.
If you write these changes to your database there should be no data loss. You can later add a feature to your application: a report on how many times user was late vs how many times the user took the medication on time. It's a nice little feature for mobile application.
I am having problems figuring this out. This is my query:
SELECT * FROM events WHERE date('2012-9-4') >= start_date AND
date('2012-9-4') <= end_date
Now as you can see from above, I am only getting the rows that match that day in some way. In my case, any events that are on that day or came from previous days. The problem is, the case where, say, the previous day has an event from 10:00PM to 12:00AM (2012-9-3), shows up on the next day (2012-9-4). So I need to keep the case like that from happening.
This is where I am having trouble. I have no idea how to get it done correctly. I have tried different things, but they all fail. I need a way to make a statement in the where clause only run when the end date > start date, the end date == current day, and the end time == 00:00. Any advice would be greatly apprecited!!
Don't you just need to turn around the first parts of the where clauses
Where start_date >= date ('2012-9-4') and end_date <= date('2012-9-4')
Assuming your data always has end_date later than start_date (if not, then adding an OR with the reverse to pick them up to).
If, (confused here from the Q) you actually want intersecting dates (overlaps), then you need two intersecting clauses:
where (start_date >= date('2012-09-03 00:00:00') and start_date < date('2012-09-04 00:00:00')) or (end_date >= date('2012-09-03 00:00:00') and end_date < date('2012-09-04 00:00:00'))
I finally figured it out (pretty sure)!
SELECT * FROM events WHERE date('2012-9-4') >= start_date AND
date('2012-9-5') <= end_date AND datetime(end_date,end_time) != datetime(2012-9-5,'00:00')
After 2 days of thinking, I have no idea why using the time as well never occurred to me. I guess I need more practice with databases. In any case, the part I added filters the events listed by date and time. Example would be like my query dates, lets say 11:00PM the 4th to 12:00AM the 5th. The AND datetime(end_date,end_time) != datetime(2012-9-5,'00:00') essentially says "only output when its not the end date with a time of midnight." It will only filter out the 5th and not the 4th because midnight is technically another day. Thats the only case I ever want to filter out events anymore than just between the event periods. I appreciate Wolf5370 for the hints which helped.
For reoccurring events, I want to show the number of days left until the next occurrence in my Android calendar application.
Example:
Today: 2012-06-12
Reoccurring event: 19th June
=> 13 days left
In order to achieve this, I save the first occurrence in an object of data type Calendar:
private Calendar cal;
...
cal = new GregorianCalendar();
cal.set(Calendar.YEAR, USER_INPUT_YEAR);
cal.set(Calendar.MONTH, USER_INPUT_MONTH);
...
To calculate the days left I use this function:
public int getDaysLeft() {
Date next = this.getNextOccurrence();
if (next == null) {
return -1;
}
else {
long differenceInMilliseconds = next.getTime()-System.currentTimeMillis();
double differenceInDays = (double) differenceInMilliseconds/DateUtils.DAY_IN_MILLIS;
return (int) Math.ceil(differenceInDays);
}
}
Which uses this function:
public Date getNextOccurrence() {
if (this.cal == null) {
return null;
}
else {
Calendar today = new GregorianCalendar();
Calendar next = new GregorianCalendar();
next.setTime(this.cal.getTime());
next.set(Calendar.YEAR, today.get(Calendar.YEAR));
if ((today.get(Calendar.MONTH) > this.cal.get(Calendar.MONTH)) || ((today.get(Calendar.MONTH) == this.cal.get(Calendar.MONTH)) && (today.get(Calendar.DAY_OF_MONTH) > this.cal.get(Calendar.DAY_OF_MONTH)))) {
next.add(Calendar.YEAR, 1);
}
return next.getTime();
}
}
By the way, to get the initial date, I expect to find a YYYY-MM-DD value and parse it like this:
(new SimpleDateFormat("yyyy-MM-dd")).parse(INPUT_DATE_STRING)
This works fine in most cases, but some users report that they see numbers such as -1469913 as "days left". How can this happen?
I thought the date (cal) might be not set or invalid, but then it would show -1 or something like this, as there are null checks in all parts, right?
-1469913 means something like -4027 years ago! As it is a reoccurring event, I thought the "days left" information should always be between 0 and 366. What could cause this code to produce such a number? Does this mean that getNextOccurrence() returns a data that is 4027 years in the past? I can't explain this behaviour.
I hope you can help me. Thank you so much in advance!
Edit: As it may be helpful: The wrong dates' year is always output as 1 when using DateFormat.getDateInstance().format(), e.g. Jan 3, 1. Nevertheless, the result of getDaysLeft() is something like 4k years.
Edit #2: I found out a date like 1--22199-1 is one that produces the output of "4k years left". Nevertheless, it is successfully parsed by (new SimpleDateFormat("yyyy-MM-dd")).parse(). Similarly, -1-1-1-91- is correctly parsed as Jan 1, 2.
Edit #3: It turned out that a date as simple as "0000-01-03" was causing all the trouble. When I output the time in milliseconds it says -62167222800000. When I then output it to a GMT string it says 0001-01-03 - strange, isn't it? And when I set the year to 1900 the time in millis is suddenly -122095040400000. Why?
Working with dates it can be really difficult to figure out those obscure errors before they happen to a user in the wild. In many cases, it can be worth your time to make a little unit test that throws a few tens of million dates in the machinery and see if any extreme answers pop up.
Also this might be worth reading. You wont realize how bad the java date-class are before you have tried something that is way better. :)
EDIT: If the users give a very high input value, then there can be a number overflow when you throw your result to an integer in getDaysLeft(). Just keep it as a long. Or even better: Only accept sensible input value, warn the user if they input the year 20120 or something like that :)
EDIT2: I was wrong in my last edit, .ceil() protects against number overflows. To be honest I have no longer any idea how this bug can happen.
EDIT3: Responding to your third edit: Remember, Date and Calendar uses Unix time. That means that the time represented by a zero is 1970. Everything before 1970 will be represented by a negative value.
EDIT4: Remember that javas calendar-classes sucks. This code snippet demonstrates that the error is in fact in the Calendar-class:
Calendar next = new GregorianCalendar();
long date1 = -62167222800000L;
long date2 = -62135600400000L;
next.setTimeInMillis(date1);
next.set(Calendar.YEAR, 2012);
System.out.println(next.getTimeInMillis());
next.setTimeInMillis(date2);
next.set(Calendar.YEAR, 2012);
System.out.println(next.getTimeInMillis());
Output:
-125629491600000
1325545200000
It will however be very hard to track down the exact bug that causes this. The reason all those bugs remains are because fixing them might break legacy systems all over the world. My guess is that the bug originates from the inability to give negative years. This, for example, will give the output "2013":
Calendar next = new GregorianCalendar();
next.set(Calendar.YEAR, -2012);
System.out.println(next.get(Calendar.YEAR));
I would simply recommend you to not allow such extreme values in your input. Decide on an acceptable span and give an error message if the value is outside of those boundaries. If you would like to handle all possible dates in some futher application, just use joda time. You wont regret it :)
You got negative values in days that might be because user have entered the date of next occurance which is any previous date.
I think you should calculate your daysLeft like this way,
String inputDateString = "19/06/2012";
Calendar calCurr = Calendar.getInstance();//current date
Calendar calNext = Calendar.getInstance();// for next date
calNext.setTime(new Date(inputDateString)); // or do set Day,Month,Year like in your Question
if(calNext.after(calCurr)) // if the next date is after current
{
long timeDiff = calNext.getTimeInMillis() - calCurr.getTimeInMillis(); // time of next year if today is 15 june and some one enter 16 june for next occurance
int daysLeft = (int) (timeDiff/DateUtils.DAY_IN_MILLIS); // Days Left
}
else
{
long timeDiff = calCurr.getTimeInMillis() - calNext.getTimeInMillis();
timeDiff = DateUtils.YEAR_IN_MILLIS - timeDiff; // time of next year if today is 15 june and some one enter 14 june for next occurance
int daysLeft = (int) (timeDiff/DateUtils.DAY_IN_MILLIS); // Days Left
}