diff --git a/Habitica/build.gradle b/Habitica/build.gradle index 31d78c707..0d5a934f6 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -55,6 +55,8 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "androidx.preference:preference-ktx:1.2.0" + //Desugaring + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9' // Markdown implementation "io.noties.markwon:core:4.6.2" @@ -267,6 +269,9 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 + + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true } kotlinOptions { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt index 1537b85de..f8331b7c2 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt @@ -17,8 +17,14 @@ import com.habitrpg.android.habitica.receivers.TaskReceiver import com.habitrpg.shared.habitica.HLogger import com.habitrpg.shared.habitica.LogLevel import io.reactivex.rxjava3.core.Flowable -import java.util.Calendar -import java.util.Date +import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.TemporalAccessor +import java.util.* class TaskAlarmManager( private var context: Context, @@ -76,30 +82,42 @@ class TaskAlarmManager( private fun setTimeForDailyReminder(remindersItem: RemindersItem?, task: Task): RemindersItem? { val oldTime = remindersItem?.time - val newTime = task.getNextReminderOccurence(oldTime) ?: return null - val calendar = Calendar.getInstance() - calendar.time = newTime - @Suppress("DEPRECATION") - calendar.set( - calendar.get(Calendar.YEAR), - calendar.get(Calendar.MONTH), - calendar.get(Calendar.DATE), - oldTime?.hours ?: 0, - oldTime?.minutes ?: 0, - 0 - ) - remindersItem?.time = calendar.time + val newTime = (task.getNextReminderOccurence(oldTime) ?: return null) + + remindersItem?.time = newTime.withZoneSameLocal(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + return remindersItem } + fun formatter(): DateTimeFormatter = + DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendPattern("['T'][' ']") + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .appendPattern("[XX]") + .toFormatter() + + fun parse(dateTime: String?): ZonedDateTime? { + val parsed: TemporalAccessor = formatter().parseBest( + dateTime, + ZonedDateTime::from, LocalDateTime::from + ) + return if (parsed is ZonedDateTime) { + parsed + } else { + val defaultZone: ZoneId = ZoneId.of("UTC") + (parsed as LocalDateTime).atZone(defaultZone) + } + } + private fun setAlarmForRemindersItem(reminderItemTask: Task, remindersItem: RemindersItem?) { - val now = Date() - if (remindersItem == null || remindersItem.time?.before(now) == true) { + val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant() + if (remindersItem == null || parse(remindersItem.time)?.withZoneSameLocal(ZoneId.systemDefault())?.toInstant()?.isBefore(now) == true) { return } + val time = Date.from(parse(remindersItem.time)?.withZoneSameLocal(ZoneId.systemDefault())?.toInstant()) val cal = Calendar.getInstance() - cal.time = remindersItem.time + cal.time = time val intent = Intent(context, TaskReceiver::class.java) intent.action = remindersItem.id @@ -127,6 +145,7 @@ class TaskAlarmManager( ) setAlarm(context, cal.timeInMillis, sender) + } private fun removeAlarmForRemindersItem(remindersItem: RemindersItem) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/RemindersItem.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/RemindersItem.kt index 52f9253a8..e38f10509 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/RemindersItem.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/RemindersItem.kt @@ -4,13 +4,19 @@ import android.os.Parcel import android.os.Parcelable import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.TemporalAccessor import java.util.Date open class RemindersItem : RealmObject, Parcelable { @PrimaryKey var id: String? = null - var startDate: Date? = null - var time: Date? = null + var startDate: String? = null + var time: String? = null // Use to store task type before a task is created var type: String? = null @@ -21,8 +27,8 @@ open class RemindersItem : RealmObject, Parcelable { override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeString(id) - dest.writeLong(this.startDate?.time ?: -1) - dest.writeLong(this.time?.time ?: -1) + dest.writeString(startDate) + dest.writeString(time) } companion object CREATOR : Parcelable.Creator { @@ -33,8 +39,8 @@ open class RemindersItem : RealmObject, Parcelable { constructor(source: Parcel) { id = source.readString() - startDate = Date(source.readLong()) - time = Date(source.readLong()) + startDate = source.readString() + time = source.readString() } constructor() @@ -48,4 +54,24 @@ open class RemindersItem : RealmObject, Parcelable { override fun hashCode(): Int { return id?.hashCode() ?: 0 } + + fun formatter(): DateTimeFormatter = + DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendPattern("['T'][' ']") + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .appendPattern("[XX]") + .toFormatter() + + fun parse(dateTime: String): ZonedDateTime? { + val parsed: TemporalAccessor = formatter().parseBest( + dateTime, + ZonedDateTime::from, LocalDateTime::from + ) + return if (parsed is ZonedDateTime) { + parsed + } else { + val defaultZone: ZoneId = ZoneId.of("UTC") + (parsed as LocalDateTime).atZone(defaultZone) + } + } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index be7804d8f..4fb914ce7 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -18,6 +18,12 @@ import java.util.Date import java.util.GregorianCalendar import org.json.JSONArray import org.json.JSONException +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.TemporalAccessor open class Task : RealmObject, BaseMainObject, Parcelable { @@ -181,32 +187,46 @@ open class Task : RealmObject, BaseMainObject, Parcelable { fun checkIfDue(): Boolean = isDue == true - fun getNextReminderOccurence(oldTime: Date?): Date? { + fun getNextReminderOccurence(oldTime: String?): ZonedDateTime? { if (oldTime == null) { return null } - val today = Calendar.getInstance() - - val newTime = GregorianCalendar() - newTime.time = oldTime - newTime.set(today.get(Calendar.YEAR), today.get(Calendar.MONTH), today.get(Calendar.DAY_OF_MONTH)) - if (today.before(newTime)) { - today.add(Calendar.DAY_OF_MONTH, -1) - } - val nextDate = nextDue?.firstOrNull() + return if (nextDate != null && !isDisplayedActive) { val nextDueCalendar = GregorianCalendar() nextDueCalendar.time = nextDate - newTime.set(nextDueCalendar.get(Calendar.YEAR), nextDueCalendar.get(Calendar.MONTH), nextDueCalendar.get(Calendar.DAY_OF_MONTH)) - newTime.time + parse(oldTime) + ?.withYear(nextDueCalendar.get(Calendar.YEAR)) + ?.withMonth(nextDueCalendar.get(Calendar.MONTH)) + ?.withDayOfMonth(nextDueCalendar.get(Calendar.DAY_OF_MONTH)) } else if (isDisplayedActive) { - newTime.time + parse(oldTime) } else { null } } + fun formatter(): DateTimeFormatter = + DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendPattern("['T'][' ']") + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .appendPattern("[XX]") + .toFormatter() + + fun parse(dateTime: String): ZonedDateTime? { + val parsed: TemporalAccessor = formatter().parseBest( + dateTime, + ZonedDateTime::from, LocalDateTime::from + ) + return if (parsed is ZonedDateTime) { + parsed + } else { + val defaultZone: ZoneId = ZoneId.of("UTC") + (parsed as LocalDateTime).atZone(defaultZone) + } + } + fun parseMarkdown() { parsedText = MarkdownParser.parseMarkdown(text) parsedNotes = MarkdownParser.parseMarkdown(notes) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/DailyViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/DailyViewHolder.kt index b6ed64248..1f0f3c5cb 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/DailyViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/DailyViewHolder.kt @@ -5,6 +5,12 @@ import com.habitrpg.android.habitica.models.responses.TaskDirection import com.habitrpg.android.habitica.models.tasks.ChecklistItem import com.habitrpg.android.habitica.models.tasks.Task import java.text.DateFormat +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.TemporalAccessor import java.util.Calendar import java.util.Date @@ -37,13 +43,19 @@ class DailyViewHolder( val calendar = Calendar.getInstance() val nextReminder = data.reminders?.firstOrNull { calendar.time = now - calendar.set(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DATE), it.time?.hours ?: 0, it.time?.minutes ?: 0, 0) + calendar.set(calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DATE), + parse(it.time)?.hour ?: 0, + parse(it.time)?.minute ?: 0, + 0) now < calendar.time } ?: data.reminders?.first() var reminderString = "" if (nextReminder?.time != null) { - reminderString += formatter.format(nextReminder.time) + val time = Date.from(parse(nextReminder.time)?.withZoneSameLocal(ZoneId.systemDefault())?.toInstant()) + reminderString += formatter.format(time) } if ((data.reminders?.size ?: 0) > 1) { reminderString = "$reminderString (+${(data.reminders?.size ?: 0) - 1})" @@ -54,6 +66,26 @@ class DailyViewHolder( super.bind(data, position, displayMode) } + fun formatter(): DateTimeFormatter = + DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendPattern("['T'][' ']") + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .appendPattern("[XX]") + .toFormatter() + + fun parse(dateTime: String?): ZonedDateTime? { + val parsed: TemporalAccessor = formatter().parseBest( + dateTime, + ZonedDateTime::from, LocalDateTime::from + ) + return if (parsed is ZonedDateTime) { + parsed + } else { + val defaultZone: ZoneId = ZoneId.of("UTC") + (parsed as LocalDateTime).atZone(defaultZone) + } + } + override fun shouldDisplayAsActive(newTask: Task?): Boolean { return newTask?.isDisplayedActive ?: false } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderItemFormView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderItemFormView.kt index df4e73afa..d7ccbf63e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderItemFormView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderItemFormView.kt @@ -22,6 +22,13 @@ import com.habitrpg.android.habitica.extensions.layoutInflater import com.habitrpg.android.habitica.models.tasks.RemindersItem import com.habitrpg.android.habitica.models.tasks.TaskType import java.text.DateFormat +import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.TemporalAccessor import java.util.Calendar import java.util.Date import java.util.Locale @@ -33,13 +40,14 @@ class ReminderItemFormView @JvmOverloads constructor( ) : LinearLayout(context, attrs, defStyleAttr), TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener { private val formattedTime: CharSequence? get() { - val time = item.time - return if (time != null) { + return if (item.time != null) { + val time = Date.from(parse(item.time)?.withZoneSameLocal(ZoneId.systemDefault())?.toInstant()) formatter.format(time) } else { "" } } + private val binding = TaskFormReminderItemBinding.inflate(context.layoutInflater, this) private val formatter: DateFormat @@ -103,22 +111,20 @@ class ReminderItemFormView @JvmOverloads constructor( binding.button.drawable.mutate().setTint(tintColor) binding.textView.setOnClickListener { - val calendar = Calendar.getInstance() - item.time?.let { calendar.time = it } if (taskType == TaskType.DAILY) { val timePickerDialog = TimePickerDialog( context, this, - calendar.get(Calendar.HOUR_OF_DAY), - calendar.get(Calendar.MINUTE), + ZonedDateTime.now().hour, + ZonedDateTime.now().minute, android.text.format.DateFormat.is24HourFormat(context) ) timePickerDialog.show() } else { val timePickerDialog = DatePickerDialog( context, this, - calendar.get(Calendar.YEAR), - calendar.get(Calendar.MONTH), - calendar.get(Calendar.DAY_OF_MONTH) + ZonedDateTime.now().year, + ZonedDateTime.now().monthValue, + ZonedDateTime.now().dayOfMonth ) if ((firstDayOfWeek ?: -1) >= 0) { timePickerDialog.datePicker.firstDayOfWeek = firstDayOfWeek ?: 0 @@ -130,34 +136,53 @@ class ReminderItemFormView @JvmOverloads constructor( } override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) { valueChangedListener?.let { - val calendar = Calendar.getInstance() - item.time?.let { calendar.time = it } - calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) - calendar.set(Calendar.MINUTE, minute) - item.time = calendar.time + val calendar = ZonedDateTime.now() + .withHour(hourOfDay) + .withMinute(minute) + item.time = calendar.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) binding.textView.text = formattedTime - item.time?.let { date -> it(date) } + item.time?.let { date -> it(Date.from(parse(date)?.withZoneSameLocal(ZoneId.systemDefault())?.toInstant())) } } } override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) { valueChangedListener?.let { - val calendar = Calendar.getInstance() - item.time?.let { calendar.time = it } - calendar.set(Calendar.YEAR, year) - calendar.set(Calendar.MONTH, month) - calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) - item.time = calendar.time +// val calendar = Calendar.getInstance() + val calendar = ZonedDateTime.now() + .withYear(year) + .withMonth(month) + .withDayOfMonth(dayOfMonth) + item.time = calendar.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) binding.textView.text = formattedTime - item.time?.let { date -> it(date) } + item.time?.let { date -> Date.from(parse(date)?.withZoneSameLocal(ZoneId.systemDefault())?.toInstant()) } val timePickerDialog = TimePickerDialog( context, this, - calendar.get(Calendar.HOUR_OF_DAY), - calendar.get(Calendar.MINUTE), + ZonedDateTime.now().hour, + ZonedDateTime.now().minute, android.text.format.DateFormat.is24HourFormat(context) ) timePickerDialog.show() } } + + fun formatter(): DateTimeFormatter = + DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendPattern("['T'][' ']") + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .appendPattern("[XX]") + .toFormatter() + + fun parse(dateTime: String?): ZonedDateTime? { + val parsed: TemporalAccessor = formatter().parseBest( + dateTime, + ZonedDateTime::from, LocalDateTime::from + ) + return if (parsed is ZonedDateTime) { + parsed + } else { + val defaultZone: ZoneId = ZoneId.of("UTC") + (parsed as LocalDateTime).atZone(defaultZone) + } + } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/utils/TaskSerializer.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/utils/TaskSerializer.kt index ab2650a59..9131f24a3 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/utils/TaskSerializer.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/utils/TaskSerializer.kt @@ -104,8 +104,8 @@ class TaskSerializer : JsonSerializer, JsonDeserializer { val remindersObject = reminderElement.asJsonObject val reminder = RemindersItem() reminder.id = remindersObject.getAsString("id") - reminder.startDate = context.deserialize(remindersObject.get("startDate"), Date::class.java) - reminder.time = context.deserialize(remindersObject.get("time"), Date::class.java) + reminder.startDate = remindersObject.getAsString("startDate") + reminder.time = remindersObject.getAsString("time") task.reminders?.add(reminder) } } @@ -207,9 +207,9 @@ class TaskSerializer : JsonSerializer, JsonDeserializer { val jsonObject = JsonObject() jsonObject.addProperty("id", item.id) if (item.startDate != null) { - jsonObject.addProperty("startDate", item.startDate?.time) + jsonObject.addProperty("startDate", item.startDate) } - jsonObject.addProperty("time", item.time?.time ?: Date().time) + jsonObject.addProperty("time", item.time) jsonArray.add(jsonObject) } return jsonArray diff --git a/build.gradle b/build.gradle index 3f6744988..1d36d355a 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.0.2' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 42c46ff72..f1c2836a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Oct 27 17:32:09 CET 2020 +#Sun Apr 17 16:10:31 EDT 2022 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +zipStoreBase=GRADLE_USER_HOME