fix: Off-by-one when selecting date fields #1695

This commit is contained in:
J-Jamet 2024-11-04 19:43:30 +01:00
parent 4ba77b76ec
commit 7c1c299282
30 changed files with 306 additions and 281 deletions

View file

@ -3,6 +3,7 @@ KeePassDX(4.1.0)
* Generate keyfile #1290
* Hide template group #1894
* Group count sum recursively #421
* Fix when selecting date fields #1695
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889
KeePassDX(4.0.8)

View file

@ -126,7 +126,7 @@ dependencies {
// Autofill
implementation "androidx.autofill:autofill:1.1.0"
// Time
implementation 'joda-time:joda-time:2.10.13'
implementation 'joda-time:joda-time:2.13.0'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
// Education

View file

@ -30,7 +30,6 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ProgressBar
import android.widget.Spinner
@ -73,6 +72,7 @@ import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
@ -87,6 +87,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.ToolbarAction
@ -301,7 +302,7 @@ class EntryEditActivity : DatabaseLockActivity(),
// Launch the time picker
MaterialTimePicker.Builder().build().apply {
addOnPositiveButtonClickListener {
mEntryEditViewModel.selectTime(this.hour, this.minute)
mEntryEditViewModel.selectTime(DataTime(this.hour, this.minute))
}
show(supportFragmentManager, "TimePickerFragment")
}
@ -309,7 +310,7 @@ class EntryEditActivity : DatabaseLockActivity(),
// Launch the date picker
MaterialDatePicker.Builder.datePicker().build().apply {
addOnPositiveButtonClickListener {
mEntryEditViewModel.selectDate(it)
mEntryEditViewModel.selectDate(datePickerToDataDate(it))
}
show(supportFragmentManager, "DatePickerFragment")
}

View file

@ -84,6 +84,7 @@ import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
@ -96,6 +97,7 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.KeyboardUtil.showKeyboard
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@ -440,7 +442,7 @@ class GroupActivity : DatabaseLockActivity(),
// Launch the time picker
MaterialTimePicker.Builder().build().apply {
addOnPositiveButtonClickListener {
mGroupEditViewModel.selectTime(this.hour, this.minute)
mGroupEditViewModel.selectTime(DataTime(this.hour, this.minute))
}
show(supportFragmentManager, "TimePickerFragment")
}
@ -448,7 +450,7 @@ class GroupActivity : DatabaseLockActivity(),
// Launch the date picker
MaterialDatePicker.Builder.datePicker().build().apply {
addOnPositiveButtonClickListener {
mGroupEditViewModel.selectDate(it)
mGroupEditViewModel.selectDate(datePickerToDataDate(it))
}
show(supportFragmentManager, "DatePickerFragment")
}

View file

@ -45,7 +45,6 @@ import com.kunzisoft.keepass.view.InheritedCompletionView
import com.kunzisoft.keepass.view.TagsCompletionView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter
import org.joda.time.DateTime
class GroupEditDialogFragment : DatabaseDialogFragment() {
@ -90,27 +89,21 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
}
mGroupEditViewModel.onDateSelected.observe(this) { dateMilliseconds ->
mGroupEditViewModel.onDateSelected.observe(this) { date ->
// Save the date
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withMillis(dateMilliseconds)
.toDate())
mGroupInfo.expiryTime.setDate(date.year, date.month, date.day)
expirationView.dateTime = mGroupInfo.expiryTime
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
// Trick to recall selection with time
mGroupEditViewModel.requestDateTimeSelection(instantTime)
mGroupEditViewModel.requestDateTimeSelection(
DateInstant(mGroupInfo.expiryTime.instant, DateInstant.Type.TIME)
)
}
}
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
// Save the time
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withHourOfDay(viewModelTime.hours)
.withMinuteOfHour(viewModelTime.minutes)
.toDate(), mGroupInfo.expiryTime.type)
mGroupInfo.expiryTime.setTime(viewModelTime.hour, viewModelTime.minute)
expirationView.dateTime = mGroupInfo.expiryTime
}

View file

@ -176,8 +176,8 @@ object AutofillHelper {
}
if (entryInfo.expires) {
val year = entryInfo.expiryTime.getYearInt()
val month = entryInfo.expiryTime.getMonthInt()
val year = entryInfo.expiryTime.getYear()
val month = entryInfo.expiryTime.getMonth()
val monthString = month.toString().padStart(2, '0')
val day = entryInfo.expiryTime.getDay()
val dayString = day.toString().padStart(2, '0')
@ -192,7 +192,7 @@ object AutofillHelper {
} else {
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forDate(entryInfo.expiryTime.date.time)
AutofillValue.forDate(entryInfo.expiryTime.toJavaMilliseconds())
)
}
}

View file

@ -0,0 +1,3 @@
package com.kunzisoft.keepass.model
data class DataDate(val year: Int, val month: Int, val day: Int)

View file

@ -0,0 +1,3 @@
package com.kunzisoft.keepass.model
data class DataTime(val hour: Int, val minute: Int)

View file

@ -3,13 +3,17 @@ package com.kunzisoft.keepass.utils
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.model.DataDate
import java.text.DateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
object TimeUtil {
fun DateInstant.getDateTimeString(resources: Resources): String {
val locale = ConfigurationCompat.getLocales(resources.configuration)[0] ?: Locale.ROOT
val date = instant.toDate()
return when (type) {
DateInstant.Type.DATE -> DateFormat.getDateInstance(
DateFormat.MEDIUM,
@ -26,4 +30,22 @@ object TimeUtil {
.format(date)
}
}
// https://github.com/material-components/material-components-android/issues/882#issuecomment-1111374962
// To fix UTC time in date picker
fun datePickerToDataDate(millis: Long): DataDate {
val selectedUtc = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
selectedUtc.timeInMillis = millis
val selectedLocal = Calendar.getInstance()
selectedLocal.clear()
selectedLocal.set(
selectedUtc.get(Calendar.YEAR),
selectedUtc.get(Calendar.MONTH),
selectedUtc.get(Calendar.DAY_OF_MONTH))
return DataDate(
selectedLocal.get(Calendar.YEAR),
selectedLocal.get(Calendar.MONTH) + 1,
selectedLocal.get(Calendar.DAY_OF_MONTH),
)
}
}

View file

@ -1,3 +0,0 @@
package com.kunzisoft.keepass.view
data class DataTime(val hours: Int, val minutes: Int)

View file

@ -111,7 +111,7 @@ class DateTimeEditFieldView @JvmOverloads constructor(context: Context,
mDefault
}
set(value) {
mDateTime = DateInstant(value.date, mDateTime.type)
mDateTime = DateInstant(value.instant, mDateTime.type)
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
mDateTime.getDateTimeString(resources)
} else {

View file

@ -128,7 +128,7 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
mDefault
}
set(value) {
mDateTime = DateInstant(value.date, mDateTime.type)
mDateTime = DateInstant(value.instant, mDateTime.type)
assignExpiresDateText()
}

View file

@ -17,8 +17,9 @@ import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.otp.OtpEntryFields
import org.joda.time.DateTime
class TemplateEditView @JvmOverloads constructor(context: Context,
@ -211,35 +212,31 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
val dateTimeView = getFieldViewById(viewId)
if (dateTimeView is DateTimeEditFieldView) {
dateTimeView.dateTime = DateInstant(
action.invoke(dateTimeView.dateTime).date,
dateTimeView.dateTime.type)
action.invoke(dateTimeView.dateTime).instant,
dateTimeView.dateTime.type
)
}
}
}
fun setCurrentDateTimeValue(dateMilliseconds: Long) {
fun setCurrentDateTimeValue(date: DataDate) {
// Save the date
setCurrentDateTimeSelection { instant ->
val newDateInstant = DateInstant(
DateTime(instant.date)
.withMillis(dateMilliseconds)
.toDate(), instant.type)
if (instant.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(instant.date, DateInstant.Type.TIME)
setCurrentDateTimeSelection { dateInstant ->
dateInstant.setDate(date.year, date.month, date.day)
if (dateInstant.type == DateInstant.Type.DATE_TIME) {
// Trick to recall selection with time
mOnDateInstantClickListener?.invoke(instantTime)
mOnDateInstantClickListener?.invoke(
DateInstant(dateInstant.instant, DateInstant.Type.TIME)
)
}
newDateInstant
dateInstant
}
}
fun setCurrentTimeValue(time: DataTime) {
setCurrentDateTimeSelection { instant ->
DateInstant(
DateTime(instant.date)
.withHourOfDay(time.hours)
.withMinuteOfHour(time.minutes)
.toDate(), instant.type)
setCurrentDateTimeSelection { dateInstant ->
dateInstant.setTime(time.hour, time.minute)
dateInstant
}
}

View file

@ -4,7 +4,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.view.DataTime
import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime
abstract class NodeEditViewModel : ViewModel() {
@ -23,8 +24,8 @@ abstract class NodeEditViewModel : ViewModel() {
val requestDateTimeSelection : LiveData<DateInstant> get() = _requestDateTimeSelection
private val _requestDateTimeSelection = SingleLiveEvent<DateInstant>()
val onDateSelected : LiveData<Long> get() = _onDateSelected
private val _onDateSelected = SingleLiveEvent<Long>()
val onDateSelected : LiveData<DataDate> get() = _onDateSelected
private val _onDateSelected = SingleLiveEvent<DataDate>()
val onTimeSelected : LiveData<DataTime> get() = _onTimeSelected
private val _onTimeSelected = SingleLiveEvent<DataTime>()
@ -57,12 +58,12 @@ abstract class NodeEditViewModel : ViewModel() {
_requestDateTimeSelection.value = dateInstant
}
fun selectDate(dateMilliseconds: Long) {
_onDateSelected.value = dateMilliseconds
fun selectDate(date: DataDate) {
_onDateSelected.value = date
}
fun selectTime(hours: Int, minutes: Int) {
_onTimeSelected.value = DataTime(hours, minutes)
fun selectTime(dataTime: DataTime) {
_onTimeSelected.value = dataTime
}
private enum class ColorRequest {

View file

@ -36,7 +36,7 @@ android {
dependencies {
// Time
implementation 'joda-time:joda-time:2.10.13'
implementation 'joda-time:joda-time:2.13.0'
// Apache Commons
implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15'

View file

@ -21,27 +21,27 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.readSerializableCompat
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.readSerializableCompat
import com.kunzisoft.keepass.utils.writeEnum
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.Duration
import org.joda.time.Instant
import org.joda.time.LocalDate
import org.joda.time.LocalDateTime
import org.joda.time.LocalTime
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import org.joda.time.format.DateTimeFormat
import org.joda.time.format.DateTimeFormatter
class DateInstant : Parcelable {
private var jDate: Date = Date()
private var mInstant: Instant = Instant.now()
private var mType: Type = Type.DATE_TIME
val date: Date
get() = jDate
val instant: Instant
get() = mInstant
var type: Type
get() = mType
@ -50,42 +50,37 @@ class DateInstant : Parcelable {
}
constructor(source: DateInstant) {
this.jDate = Date(source.jDate.time)
this.mInstant = Instant(source.mInstant)
this.mType = source.mType
}
constructor(date: Date, type: Type = Type.DATE_TIME) {
jDate = Date(date.time)
constructor(instant: Instant, type: Type = Type.DATE_TIME) {
mInstant = Instant(instant)
mType = type
}
constructor(millis: Long, type: Type = Type.DATE_TIME) {
jDate = Date(millis)
mType = type
}
private fun parse(value: String, type: Type): Date {
private fun parse(value: String, type: Type): Instant {
return when (type) {
Type.DATE -> dateFormat.parse(value) ?: jDate
Type.TIME -> timeFormat.parse(value) ?: jDate
else -> dateTimeFormat.parse(value) ?: jDate
Type.DATE -> Instant(dateFormat.parseDateTime(value) ?: DateTime())
Type.TIME -> Instant(timeFormat.parseDateTime(value) ?: DateTime())
else -> Instant(dateTimeFormat.parseDateTime(value) ?: DateTime())
}
}
constructor(string: String, type: Type = Type.DATE_TIME) {
try {
jDate = parse(string, type)
mInstant = parse(string, type)
mType = type
} catch (e: Exception) {
// Retry with second format
try {
when (type) {
Type.TIME -> {
jDate = parse(string, Type.DATE)
mInstant = parse(string, Type.DATE)
mType = Type.DATE
}
else -> {
jDate = parse(string, Type.TIME)
mInstant = parse(string, Type.TIME)
mType = Type.TIME
}
}
@ -93,11 +88,11 @@ class DateInstant : Parcelable {
// Retry with third format
when (type) {
Type.DATE, Type.TIME -> {
jDate = parse(string, Type.DATE_TIME)
mInstant = parse(string, Type.DATE_TIME)
mType = Type.DATE_TIME
}
else -> {
jDate = parse(string, Type.DATE)
mInstant = parse(string, Type.DATE)
mType = Type.DATE
}
}
@ -110,11 +105,11 @@ class DateInstant : Parcelable {
}
constructor() {
jDate = Date()
mInstant = Instant.now()
}
constructor(parcel: Parcel) {
jDate = parcel.readSerializableCompat() ?: jDate
mInstant = parcel.readSerializableCompat() ?: mInstant
mType = parcel.readEnum<Type>() ?: mType
}
@ -123,47 +118,82 @@ class DateInstant : Parcelable {
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(jDate)
dest.writeSerializable(mInstant)
dest.writeEnum(mType)
}
fun getYearInt(): Int {
val dateFormat = SimpleDateFormat("yyyy", Locale.ENGLISH)
return dateFormat.format(date).toInt()
fun setDate(year: Int, month: Int, day: Int) {
mInstant = DateTime(mInstant)
.withYear(year)
.withMonthOfYear(month)
.withDayOfMonth(day)
.toInstant()
}
fun getMonthInt(): Int {
val dateFormat = SimpleDateFormat("MM", Locale.ENGLISH)
return dateFormat.format(date).toInt()
fun setTime(hour: Int, minute: Int) {
mInstant = DateTime(mInstant)
.withHourOfDay(hour)
.withMinuteOfHour(minute)
.toInstant()
}
fun getYear(): Int {
return mInstant.toDateTime().year
}
fun getMonth(): Int {
return mInstant.toDateTime().monthOfYear
}
fun getDay(): Int {
val dateFormat = SimpleDateFormat("dd", Locale.ENGLISH)
return dateFormat.format(date).toInt()
return mInstant.toDateTime().dayOfMonth
}
fun getHour(): Int {
return mInstant.toDateTime().hourOfDay
}
fun getMinute(): Int {
return mInstant.toDateTime().minuteOfHour
}
fun getSecond(): Int {
return mInstant.toDateTime().secondOfMinute
}
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
// it is not expires
fun isNeverExpires(): Boolean {
return LocalDateTime(jDate)
.isBefore(
LocalDateTime.fromDateFields(NEVER_EXPIRES.date)
.minusMonths(1))
return mInstant.isBefore(NEVER_EXPIRES.instant.minus(Duration.standardDays(30)))
}
fun isCurrentlyExpire(): Boolean {
return when (type) {
Type.DATE -> LocalDate.fromDateFields(jDate).isBefore(LocalDate.now())
Type.TIME -> LocalTime.fromDateFields(jDate).isBefore(LocalTime.now())
else -> LocalDateTime.fromDateFields(jDate).isBefore(LocalDateTime.now())
Type.DATE -> LocalDate.fromDateFields(mInstant.toDate()).isBefore(LocalDate.now())
Type.TIME -> LocalTime.fromDateFields(mInstant.toDate()).isBefore(LocalTime.now())
else -> LocalDateTime.fromDateFields(mInstant.toDate()).isBefore(LocalDateTime.now())
}
}
fun toDotNetSeconds(): Long {
val duration = Duration(JAVA_EPOCH_DATE_TIME, mInstant)
val seconds = duration.millis / 1000L
return seconds + EPOCH_OFFSET
}
fun toJavaMilliseconds(): Long {
return mInstant.millis
}
fun toDateTimeSecondsFormat(): String {
return dateTimeSecondsFormat.print(mInstant)
}
override fun toString(): String {
return when (type) {
Type.DATE -> dateFormat.format(jDate)
Type.TIME -> timeFormat.format(jDate)
else -> dateTimeFormat.format(jDate)
Type.DATE -> dateFormat.print(mInstant)
Type.TIME -> timeFormat.print(mInstant)
else -> dateTimeFormat.print(mInstant)
}
}
@ -171,47 +201,78 @@ class DateInstant : Parcelable {
if (this === other) return true
if (other !is DateInstant) return false
if (jDate != other.jDate) return false
if (mType != other.mType) return false
if (mType == Type.DATE || mType == Type.DATE_TIME) {
if (getYear() != other.getYear()) return false
if (getMonth() != other.getMonth()) return false
if (getDay() != other.getDay()) return false
if (getHour() != other.getHour()) return false
}
if (mType == Type.TIME || mType == Type.DATE_TIME) {
if (getMinute() != other.getMinute()) return false
if (getSecond() != other.getSecond()) return false
}
return true
}
override fun hashCode(): Int {
var result = jDate.hashCode()
var result = mInstant.hashCode()
result = 31 * result + mType.hashCode()
return result
}
fun isBefore(dateInstant: DateInstant): Boolean {
return this.mInstant.isBefore(dateInstant.mInstant)
}
fun isAfter(dateInstant: DateInstant): Boolean {
return this.mInstant.isAfter(dateInstant.mInstant)
}
fun compareTo(other: DateInstant?): Int {
return mInstant.compareTo(other?.mInstant)
}
enum class Type {
DATE_TIME, DATE, TIME
}
companion object {
val NEVER_EXPIRES = DateInstant(Calendar.getInstance().apply {
set(Calendar.YEAR, 2999)
set(Calendar.MONTH, 11)
set(Calendar.DAY_OF_MONTH, 28)
set(Calendar.HOUR, 23)
set(Calendar.MINUTE, 59)
set(Calendar.SECOND, 59)
}.time)
val IN_ONE_MONTH_DATE_TIME = DateInstant(
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE_TIME)
val IN_ONE_MONTH_DATE = DateInstant(
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE)
val IN_ONE_HOUR_TIME = DateInstant(
Instant.now().plus(Duration.standardHours(1)).toDate(), Type.TIME)
private val DOT_NET_EPOCH_DATE_TIME = DateTime(1, 1, 1, 0, 0, 0, DateTimeZone.UTC)
private val JAVA_EPOCH_DATE_TIME = DateTime(1970, 1, 1, 0, 0, 0, DateTimeZone.UTC)
private val EPOCH_OFFSET = (JAVA_EPOCH_DATE_TIME.millis - DOT_NET_EPOCH_DATE_TIME.millis) / 1000L
private val NEVER_EXPIRES_DATE_TIME = DateTime(2999, 11, 28, 23, 59, 59, DateTimeZone.UTC)
private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
val NEVER_EXPIRES = DateInstant(NEVER_EXPIRES_DATE_TIME.toInstant())
val IN_ONE_MONTH_DATE_TIME = DateInstant(
Instant.now().plus(Duration.standardDays(30)), Type.DATE_TIME)
val IN_ONE_MONTH_DATE = DateInstant(
Instant.now().plus(Duration.standardDays(30)), Type.DATE)
val IN_ONE_HOUR_TIME = DateInstant(
Instant.now().plus(Duration.standardHours(1)), Type.TIME)
val dateTimeSecondsFormat: DateTimeFormatter =
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
.withZoneUTC()
var dateTimeFormat: DateTimeFormatter =
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm'Z'")
.withZoneUTC()
var dateFormat: DateTimeFormatter =
DateTimeFormat.forPattern("yyyy-MM-dd'Z'")
.withZoneUTC()
var timeFormat: DateTimeFormatter =
DateTimeFormat.forPattern("HH:mm'Z'")
.withZoneUTC()
fun fromDotNetSeconds(seconds: Long): DateInstant {
val dt = DOT_NET_EPOCH_DATE_TIME.plus(seconds * 1000L)
// Switch corrupted dates to a more recent date that won't cause issues on the client
return DateInstant((if (dt.isBefore(JAVA_EPOCH_DATE_TIME)) { JAVA_EPOCH_DATE_TIME } else dt).toInstant())
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private val timeFormat = SimpleDateFormat("HH:mm'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
fun fromDateTimeSecondsFormat(value: String): DateInstant {
return DateInstant(dateTimeSecondsFormat.parseDateTime(value).toInstant())
}
@JvmField

View file

@ -23,7 +23,6 @@ package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import java.util.*
enum class SortNodeEnum {
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
@ -173,8 +172,8 @@ enum class SortNodeEnum {
) : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val creationCompare = object1.creationTime.date
.compareTo(object2.creationTime.date)
val creationCompare = object1.creationTime
.compareTo(object2.creationTime)
return if (creationCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2)
@ -192,8 +191,8 @@ enum class SortNodeEnum {
) : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastModificationCompare = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date)
val lastModificationCompare = object1.lastModificationTime
.compareTo(object2.lastModificationTime)
return if (lastModificationCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2)
@ -211,8 +210,8 @@ enum class SortNodeEnum {
) : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastAccessCompare = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date)
val lastAccessCompare = object1.lastAccessTime
.compareTo(object2.lastAccessTime)
return if (lastAccessCompare == 0)
NodeNaturalComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2)

View file

@ -36,13 +36,12 @@ import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.keepass.utils.readParcelableCompat
import com.kunzisoft.keepass.utils.readStringIntMap
import com.kunzisoft.keepass.utils.readStringParcelableMap
import com.kunzisoft.keepass.utils.writeStringIntMap
import com.kunzisoft.keepass.utils.writeStringParcelableMap
import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.Date
import java.util.UUID
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
@ -363,18 +362,16 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
}
fun removeOldestEntryFromHistory(): EntryKDBX? {
var min: Date? = null
var min: DateInstant? = null
var index = -1
for (i in history.indices) {
val entry = history[i]
val lastMod = entry.lastModificationTime.date
if (min == null || lastMod.before(min)) {
val lastModification = entry.lastModificationTime
if (min == null || lastModification.isBefore(min)) {
index = i
min = lastMod
min = lastModification
}
}
return if (index != -1) {
history.removeAt(index)
} else null

View file

@ -19,9 +19,6 @@
*/
package com.kunzisoft.keepass.database.file
import java.text.SimpleDateFormat
import java.util.*
object DatabaseKDBXXML {
const val ElemDocNode = "KeePassFile"
@ -128,8 +125,4 @@ object DatabaseKDBXXML {
const val ElemCustomData = "CustomData"
const val ElemStringDictExItem = "Item"
val DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}

View file

@ -1,46 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.file
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.Duration
import java.util.*
object DateKDBXUtil {
private val dotNetEpoch = DateTime(1, 1, 1, 0, 0, 0, DateTimeZone.UTC)
private val javaEpoch = DateTime(1970, 1, 1, 0, 0, 0, DateTimeZone.UTC)
private val epochOffset = (javaEpoch.millis - dotNetEpoch.millis) / 1000L
fun convertKDBX4Time(seconds: Long): Date {
val dt = dotNetEpoch.plus(seconds * 1000L)
// Switch corrupted dates to a more recent date that won't cause issues on the client
return if (dt.isBefore(javaEpoch)) {
javaEpoch.toDate()
} else dt.toDate()
}
fun convertDateToKDBX4Time(date: Date): Long {
val duration = Duration(javaEpoch, DateTime(date))
val seconds = duration.millis / 1000L
return seconds + epochOffset
}
}

View file

@ -165,7 +165,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
}
0x0003 -> {
newGroup?.let { group ->
group.creationTime = DateInstant(cipherInputStream.readBytes5ToDate())
group.creationTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
var iconId = cipherInputStream.readBytes4ToUInt().toKotlinInt()
@ -178,7 +178,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
}
0x0004 -> {
newGroup?.let { group ->
group.lastModificationTime = DateInstant(cipherInputStream.readBytes5ToDate())
group.lastModificationTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
entry.title = cipherInputStream.readBytesToString(fieldSize)
@ -186,7 +186,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
}
0x0005 -> {
newGroup?.let { group ->
group.lastAccessTime = DateInstant(cipherInputStream.readBytes5ToDate())
group.lastAccessTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
entry.url = cipherInputStream.readBytesToString(fieldSize)
@ -194,7 +194,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
}
0x0006 -> {
newGroup?.let { group ->
group.expiryTime = DateInstant(cipherInputStream.readBytes5ToDate())
group.expiryTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
entry.username = cipherInputStream.readBytesToString(fieldSize)
@ -221,22 +221,22 @@ class DatabaseInputKDB(database: DatabaseKDB)
group.groupFlags = cipherInputStream.readBytes4ToUInt().toKotlinInt()
} ?:
newEntry?.let { entry ->
entry.creationTime = DateInstant(cipherInputStream.readBytes5ToDate())
entry.creationTime = cipherInputStream.readBytes5ToDate()
}
}
0x000A -> {
newEntry?.let { entry ->
entry.lastModificationTime = DateInstant(cipherInputStream.readBytes5ToDate())
entry.lastModificationTime = cipherInputStream.readBytes5ToDate()
}
}
0x000B -> {
newEntry?.let { entry ->
entry.lastAccessTime = DateInstant(cipherInputStream.readBytes5ToDate())
entry.lastAccessTime = cipherInputStream.readBytes5ToDate()
}
}
0x000C -> {
newEntry?.let { entry ->
entry.expiryTime = DateInstant(cipherInputStream.readBytes5ToDate())
entry.expiryTime = cipherInputStream.readBytes5ToDate()
}
}
0x000D -> {

View file

@ -42,7 +42,6 @@ import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.HashedBlockInputStream
import com.kunzisoft.keepass.stream.HmacBlockInputStream
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@ -827,11 +826,10 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
@Throws(IOException::class, XmlPullParserException::class)
private fun readDateInstant(xpp: XmlPullParser): DateInstant {
val sDate = readString(xpp)
var utcDate: Date? = null
var utcDate = DateInstant()
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {
try {
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
utcDate = DateInstant.fromDateTimeSecondsFormat(sDate)
} catch (e: ParseException) {
// Catch with null test below
}
@ -842,12 +840,10 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
System.arraycopy(buf, 0, buf8, 0, min(buf.size, 8))
buf = buf8
}
val seconds = bytes64ToLong(buf)
utcDate = DateKDBXUtil.convertKDBX4Time(seconds)
utcDate = DateInstant.fromDotNetSeconds(seconds)
}
return DateInstant(utcDate ?: Date(0L))
return utcDate
}
@Throws(IOException::class, XmlPullParserException::class)

View file

@ -41,7 +41,6 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
import com.kunzisoft.keepass.stream.HmacBlockOutputStream
import com.kunzisoft.keepass.utils.*
@ -411,14 +410,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeDateInstant(name: String, value: DateInstant) {
val date = value.date
private fun writeDateInstant(name: String, date: DateInstant) {
if (header!!.version.isBefore(FILE_VERSION_40)) {
writeString(name, DatabaseKDBXXML.DateFormatter.format(date))
writeString(name, date.toDateTimeSecondsFormat())
} else {
val buf = longTo8Bytes(DateKDBXUtil.convertDateToKDBX4Time(date))
val b64 = String(Base64.encode(buf, BASE64_FLAG))
writeString(name, b64)
writeString(name, String(
Base64.encode(
longTo8Bytes(date.toDotNetSeconds()), BASE64_FLAG)
)
)
}
}

View file

@ -75,16 +75,16 @@ class EntryOutputKDB(private val mDatabase: DatabaseKDB,
writeStringToStream(mOutputStream, mEntry.notes)
// Create date
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime))
// Modification date
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime))
// Access date
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime))
// Expiration date
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime))
// Binary description
mOutputStream.write(BINARY_DESC_FIELD_TYPE)

View file

@ -48,22 +48,22 @@ class GroupOutputKDB(private val mGroup: GroupKDB,
// Create date
mOutputStream.write(CREATE_FIELD_TYPE)
mOutputStream.write(DATE_FIELD_SIZE)
mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date))
mOutputStream.write(dateTo5Bytes(mGroup.creationTime))
// Modification date
mOutputStream.write(MOD_FIELD_TYPE)
mOutputStream.write(DATE_FIELD_SIZE)
mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date))
mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime))
// Access date
mOutputStream.write(ACCESS_FIELD_TYPE)
mOutputStream.write(DATE_FIELD_SIZE)
mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date))
mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime))
// Expiration date
mOutputStream.write(EXPIRE_FIELD_TYPE)
mOutputStream.write(DATE_FIELD_SIZE)
mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date))
mOutputStream.write(dateTo5Bytes(mGroup.expiryTime))
// Image ID
mOutputStream.write(IMAGEID_FIELD_TYPE)

View file

@ -187,34 +187,34 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
fun merge(databaseToMerge: DatabaseKDBX) {
// Merge settings
if (database.nameChanged.date.before(databaseToMerge.nameChanged.date)) {
if (database.nameChanged.isBefore(databaseToMerge.nameChanged)) {
database.name = databaseToMerge.name
database.nameChanged = databaseToMerge.nameChanged
}
if (database.descriptionChanged.date.before(databaseToMerge.descriptionChanged.date)) {
if (database.descriptionChanged.isBefore(databaseToMerge.descriptionChanged)) {
database.description = databaseToMerge.description
database.descriptionChanged = databaseToMerge.descriptionChanged
}
if (database.defaultUserNameChanged.date.before(databaseToMerge.defaultUserNameChanged.date)) {
if (database.defaultUserNameChanged.isBefore(databaseToMerge.defaultUserNameChanged)) {
database.defaultUserName = databaseToMerge.defaultUserName
database.defaultUserNameChanged = databaseToMerge.defaultUserNameChanged
}
if (database.keyLastChanged.date.before(databaseToMerge.keyLastChanged.date)) {
if (database.keyLastChanged.isBefore(databaseToMerge.keyLastChanged)) {
database.keyChangeRecDays = databaseToMerge.keyChangeRecDays
database.keyChangeForceDays = databaseToMerge.keyChangeForceDays
database.isKeyChangeForceOnce = databaseToMerge.isKeyChangeForceOnce
database.keyLastChanged = databaseToMerge.keyLastChanged
}
if (database.recycleBinChanged.date.before(databaseToMerge.recycleBinChanged.date)) {
if (database.recycleBinChanged.isBefore(databaseToMerge.recycleBinChanged)) {
database.isRecycleBinEnabled = databaseToMerge.isRecycleBinEnabled
database.recycleBinUUID = databaseToMerge.recycleBinUUID
database.recycleBinChanged = databaseToMerge.recycleBinChanged
}
if (database.entryTemplatesGroupChanged.date.before(databaseToMerge.entryTemplatesGroupChanged.date)) {
if (database.entryTemplatesGroupChanged.isBefore(databaseToMerge.entryTemplatesGroupChanged)) {
database.entryTemplatesGroup = databaseToMerge.entryTemplatesGroup
database.entryTemplatesGroupChanged = databaseToMerge.entryTemplatesGroupChanged
}
if (database.settingsChanged.date.before(databaseToMerge.settingsChanged.date)) {
if (database.settingsChanged.isBefore(databaseToMerge.settingsChanged)) {
database.color = databaseToMerge.color
database.compressionAlgorithm = databaseToMerge.compressionAlgorithm
database.historyMaxItems = databaseToMerge.historyMaxItems
@ -245,8 +245,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
}
// Merge root group
if (rootGroup.lastModificationTime.date
.before(rootGroupToMerge.lastModificationTime.date)) {
if (rootGroup.lastModificationTime.isBefore(rootGroupToMerge.lastModificationTime)) {
rootGroup.updateWith(rootGroupToMerge, updateParents = false)
}
// Merge children
@ -293,7 +292,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
val customIconToMerge = databaseToMerge.iconsManager.getIcon(customIconUuid)
val customIconModificationToMerge = customIconToMerge?.lastModificationTime
if (customIconModification != null && customIconModificationToMerge != null) {
if (customIconModification.date.before(customIconModificationToMerge.date)) {
if (customIconModification.isBefore(customIconModificationToMerge)) {
customIcon.updateWith(customIconToMerge)
}
} else if (customIconModificationToMerge != null) {
@ -310,19 +309,17 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
val databaseIconModificationTime = databaseIcon?.lastModificationTime
if (databaseEntry != null
&& deletedObject.deletionTime.date
.after(databaseEntry.lastModificationTime.date)) {
&& deletedObject.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
}
if (databaseGroup != null
&& deletedObject.deletionTime.date
.after(databaseGroup.lastModificationTime.date)) {
&& deletedObject.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
}
if (databaseIcon != null
&& (
databaseIconModificationTime == null
|| (deletedObject.deletionTime.date.after(databaseIconModificationTime.date))
|| (deletedObject.deletionTime.isAfter(databaseIconModificationTime))
)
) {
database.removeCustomIcon(deletedObjectId)
@ -343,8 +340,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
val customDataItemModification = customDataItem.lastModificationTime
val customDataItemToMergeModification = customDataItemToMerge.lastModificationTime
if (customDataItemModification != null && customDataItemToMergeModification != null) {
if (customDataItemModification.date
.before(customDataItemToMergeModification.date)) {
if (customDataItemModification.isBefore(customDataItemToMergeModification)) {
customData.put(customDataItemToMerge)
}
} else {
@ -399,8 +395,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
// If it's a deleted object, but another instance was updated
// If entry parent to add exists and in current database
if ((deletedObject == null
|| deletedObject.deletionTime.date
.before(entryToMerge.lastModificationTime.date))
|| deletedObject.deletionTime.isBefore(entryToMerge.lastModificationTime))
&& parentEntryToMerge != null) {
database.addEntryTo(entryToMerge, parentEntryToMerge)
}
@ -408,8 +403,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
// Merge independently custom data
mergeCustomData(entry.customData, entryToMerge.customData)
// Merge by modification time
if (entry.lastModificationTime.date
.before(entryToMerge.lastModificationTime.date)
if (entry.lastModificationTime.isBefore(entryToMerge.lastModificationTime)
) {
addHistory(entry, entryToMerge)
if (parentEntryToMerge == entry.parent) {
@ -421,8 +415,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
database.addEntryTo(entryToMerge, parentEntryToMerge)
}
}
} else if (entry.lastModificationTime.date
.after(entryToMerge.lastModificationTime.date)
} else if (entry.lastModificationTime.isAfter(entryToMerge.lastModificationTime)
) {
addHistory(entryToMerge, entry)
}
@ -477,8 +470,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
if (group == null) {
// If group parent to add exists and in current database
if ((deletedObject == null
|| deletedObject.deletionTime.date
.before(groupToMerge.lastModificationTime.date))
|| deletedObject.deletionTime.isBefore(groupToMerge.lastModificationTime))
&& parentGroupToMerge != null) {
database.addGroupTo(groupToMerge, parentGroupToMerge)
}
@ -486,8 +478,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
// Merge independently custom data
mergeCustomData(group.customData, groupToMerge.customData)
// Merge by modification time
if (group.lastModificationTime.date
.before(groupToMerge.lastModificationTime.date)
if (group.lastModificationTime.isBefore(groupToMerge.lastModificationTime)
) {
if (parentGroupToMerge == group.parent) {
group.updateWith(groupToMerge, false)

View file

@ -232,7 +232,7 @@ class EntryInfo : NodeInfo {
}
creditCard?.expiration?.let {
expires = true
expiryTime = DateInstant(creditCard.expiration.millis)
expiryTime = DateInstant(creditCard.expiration.toInstant())
}
creditCard?.number?.let {
addUniqueField(Field(TemplateField.LABEL_NUMBER, ProtectedString(false, it)))

View file

@ -19,6 +19,9 @@
*/
package com.kunzisoft.keepass.utils
import com.kunzisoft.keepass.database.element.DateInstant
import org.joda.time.DateTime
import org.joda.time.Instant
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@ -93,7 +96,7 @@ fun InputStream.readBytes2ToUShort(): Int {
}
@Throws(IOException::class)
fun InputStream.readBytes5ToDate(): Date {
fun InputStream.readBytes5ToDate(): DateInstant {
return bytes5ToDate(readBytesLength(5))
}
@ -211,7 +214,7 @@ fun bytes16ToUuid(buf: ByteArray): UUID {
* Unpack date from 5 byte format. The five bytes at 'offset' are unpacked
* to a java.util.Date instance.
*/
fun bytes5ToDate(buf: ByteArray, calendar: Calendar = Calendar.getInstance()): Date {
fun bytes5ToDate(buf: ByteArray): DateInstant {
val dateSize = 5
val cDate = ByteArray(dateSize)
System.arraycopy(buf, 0, cDate, 0, dateSize)
@ -234,9 +237,14 @@ fun bytes5ToDate(buf: ByteArray, calendar: Calendar = Calendar.getInstance()): D
// File format is a 1 based month, java Calendar uses a zero based month
// File format is a 1 based day, java Calendar uses a 1 based day
calendar.set(year, month - 1, day, hour, minute, second)
return calendar.time
return DateInstant(Instant.ofEpochMilli(DateTime(
year,
month - 1,
day,
hour,
minute,
second
).millis))
}
@ -284,19 +292,17 @@ fun uuidTo16Bytes(uuid: UUID): ByteArray {
return buf
}
fun dateTo5Bytes(date: Date, calendar: Calendar = Calendar.getInstance()): ByteArray {
val buf = ByteArray(5)
calendar.time = date
val year = calendar.get(Calendar.YEAR)
fun dateTo5Bytes(dateInstant: DateInstant): ByteArray {
val year = dateInstant.getYear()
// File format is a 1 based month, java Calendar uses a zero based month
val month = calendar.get(Calendar.MONTH) + 1
val month = dateInstant.getMonth() + 1
// File format is a 1 based day, java Calendar uses a 1 based day
val day = calendar.get(Calendar.DAY_OF_MONTH)
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
val day = dateInstant.getDay()
val hour = dateInstant.getHour()
val minute = dateInstant.getMinute()
val second = dateInstant.getSecond()
val buf = ByteArray(5)
buf[0] = UnsignedInt(year shr 6 and 0x0000003F).toKotlinByte()
buf[1] = UnsignedInt(year and 0x0000003F shl 2 or (month shr 2 and 0x00000003)).toKotlinByte()
buf[2] = (month and 0x00000003 shl 6

View file

@ -22,6 +22,8 @@ package com.kunzisoft.keepass.tests.utils
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.utils.*
import junit.framework.TestCase
import org.joda.time.DateTime
import org.joda.time.Instant
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayOutputStream
import java.util.*
@ -136,24 +138,29 @@ class ValuesTest : TestCase() {
}
fun testDate() {
val cal = Calendar.getInstance()
val expected = DateInstant(
Instant.ofEpochMilli(
DateTime(
2008,
1,
2,
3,
4,
5
).millis))
val expected = Calendar.getInstance()
expected.set(2008, 1, 2, 3, 4, 5)
val actual = DateInstant(bytes5ToDate(dateTo5Bytes(expected)))
val actual = Calendar.getInstance()
actual.time = DateInstant(bytes5ToDate(dateTo5Bytes(expected.time, cal), cal)).date
val jDate = DateInstant(System.currentTimeMillis())
val jDate = DateInstant()
val intermediate = DateInstant(jDate)
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate.date)))
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate)))
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
assertEquals("Day mismatch: ", 2, actual.get(Calendar.DAY_OF_MONTH))
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
assertEquals("Year mismatch: ", 2008, actual.getYear())
assertEquals("Month mismatch: ", 1, actual.getMonth())
assertEquals("Day mismatch: ", 2, actual.getDay())
assertEquals("Hour mismatch: ", 3, actual.getHour())
assertEquals("Minute mismatch: ", 4, actual.getMinute())
assertEquals("Second mismatch: ", 5, actual.getSecond())
assertTrue("jDate and intermediate not equal", jDate == intermediate)
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
}

View file

@ -2,4 +2,5 @@
* Generate keyfile #1290
* Hide template group #1894
* Group count sum recursively #421
* Fix when selecting date fields #1695
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889