material-components / material-components-android

Modular and customizable Material Design UI components for Android
Apache License 2.0
16.01k stars 3.03k forks source link

[DatePicker] Incorrect TimeZones #882

Closed aarontwf closed 2 years ago

aarontwf commented 4 years ago

Description:

There seems to be some strange behavior with time zones with the new date picker I am in New Zealand so our time zone is currently UTC+13 and was testing around 9am

As shown below, it is displaying yesterday as today (I increased the valid date range to include yesterday as it is hidden if not in the range)

image

Also when setting the initially selected date via MaterialDatePicker.Builder.datePicker().setSelection(), if the millis for a specific UTC date/time is given, it is not accounting for the time zone offset and is selecting the day prior. Currently our workaround is to manually add the millisecond difference from UTC to local time.

Android API version:

Min 27, Target/compile 29

Material Library version:

1.1.0-rc01

Device:

Zebra TC51

thevoiceless commented 4 years ago

Pixel 3a emulator, seeing similar behavior in Denver when passing the current millisecond to setSelection():

AdamGrzybkowski commented 4 years ago

I had a similar(I guess) problem with time zone. Once the date was chosen I was displaying it to the user in MM/dd/yyyy format. I've noticed that on devices with UTC-X timezones it was subtracting one day I used this code to get the LocalDate

LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), clock.zone()).toLocalDate()

in my case the problem was fixed by hardcoding UTC zoneId instead of clock.zone()

mrea1 commented 4 years ago

any update here? I am seeing this issue on 1.2.0-alpha06

I am in timezone GMT-5 on May 6th at 9:35pm. The calendar "today" outline shows tomorrow, May 7th

image

aarontwf commented 4 years ago

@wcshi Does this also fix:

Also when setting the initially selected date via MaterialDatePicker.Builder.datePicker().setSelection(), if the millis for a specific UTC date/time is given, it is not accounting for the time zone offset and is selecting the day prior. Currently our workaround is to manually add the millisecond difference from UTC to local time.

Or is MaterialDatePicker.Builder.datePicker().setSelection() supposed to be in local time?

b95505017 commented 3 years ago

@dsn5ft I'm using 1.2.0-beta01 and still shows displaying yesterday as today

raajkumars commented 3 years ago

I was able to reproduce the issue by setting the test time to December 31, 2019 09:00:00 PM UTC+13:00 (New Zealand) time zone.

Hesamedin commented 3 years ago

I am using 1.3.0-alpha01 and have the same problem.

So, this is my workaround.

val picker = MaterialDatePicker.Builder.datePicker()
            .....
            .build()
picker.show(supportFragmentManager, picker.toString())
picker.addOnPositiveButtonClickListener {
            val utcTime = Date(it)
            val format = "yyy/MM/dd HH:mm:ss"
            val sdf = SimpleDateFormat(format, Locale.getDefault())
            sdf.timeZone = TimeZone.getTimeZone("UTC")
            val gmtTime = SimpleDateFormat(format, Locale.getDefault()).parse(sdf.format(utcTime))
            gmtTime?.let {  date ->
                vm.setNextDueDate(date.time)
            }
}
prianck commented 3 years ago

@ymarian any update to when this fix be rolled out

Manu-Jindal commented 3 years ago

Even I am facing this problem, I am in India Timezone (GMT+5:30), and the current date is showing to be yesterdays date. @ymarian @wcshi, any idea when the change will be rolled out? Kindly try to look into this with priority as it is a production app where we are facing this issue.

CodeIsmail commented 3 years ago

I'm also facing the same issue from Nigeria. GMT+1 TimeZone.

CodeIsmail commented 3 years ago

https://github.com/material-components/material-components-android/issues/1360#issuecomment-644790081 this solution worked for me.

paorauk commented 3 years ago

I'm using 1.3.0-alpha01 and still seeing yesterdays date being selected due to the timezone issue. When will the fix be availble in 1.3.0?

CsiszerTamas commented 3 years ago

I used the material:1.3.0-alpha02 and it seems it resolves the issue about highlighting yesterday as current day (instead of today).

AbhishekHirapara commented 3 years ago

Yes Issue is fixed for highlighting yesterday as current day. but still getting issue with setSelection() method of range selection. Can you please give some solution for this?

IgorGanapolsky commented 3 years ago

I have the same issue of date range off by one day. Using Material library 1.2.1. Any solution?

ikim24 commented 3 years ago

1.3.0 is currently in beta with the fix.

IgorGanapolsky commented 3 years ago

1.3.0 is currently in beta with the fix.

Have you tested the fix?

VincentJoshuaET commented 3 years ago

1.3.0 is currently in beta with the fix.

Not working for me. I used Instant.now().toEpochMilli() in setSelection() and the previous day gets selected.

ikim24 commented 3 years ago

@VincentJoshuaET please see the following from https://github.com/material-components/material-components-android/blob/00dc4c6b5af3939418f1c7d1e4c737dc3fb7fd67/docs/components/Picker.md#timezones:

The picker interprets all long values as milliseconds from the UTC Epoch. If you have access to Java 8 libraries, it is strongly recommended you use LocalDateTime and ZonedDateTime; otherwise, you will need to use Calendar.

sabrinanurhidayah commented 2 years ago

1.3.0 is currently in beta with the fix.

Not working for me. I used Instant.now().toEpochMilli() in setSelection() and the previous day gets selected.

This is worked for me

public Date dateFromUTC(Date date) {
    return new Date(date.getTime() + Calendar.getInstance().getTimeZone().getOffset(new Date().getTime()));
}

// call dateFromUTC in setSelection
MaterialDatePicker.Builder<Long> builder = MaterialDatePicker.Builder.datePicker();
builder.setSelection(dateFromUTC(YOUR_DATE).getTime());
danielandujar commented 2 years ago

Feb 2022, This issue is still happening in 1.5.0

sereden commented 2 years ago

Also still happens to me. Moreover, today is daylight saving time and I found the issue that we have to use the time zone of the picked date and not today.

As a workaround I implemented custom extensions:

fun <S> MaterialDatePicker<S>.addOnPositiveButtonClickListener(inliner: MaterialPickerInliner<S>, callback: (S) -> Unit): Boolean {
    return addOnPositiveButtonClickListener { time ->
        callback(inliner.inline(time, NegativeDirection()))
    }
}

fun <S> MaterialDatePicker.Builder<S>.setSelection(inliner: MaterialPickerInliner<S>, time: S): MaterialDatePicker.Builder<S> {
    setSelection(inliner.inline(time, PositiveDirection()))
    return this
}

fun CalendarConstraints.Builder.setOpenAt(inliner: DateMaterialPickerInliner, time: Long): CalendarConstraints.Builder {
    setOpenAt(inliner.inline(time, PositiveDirection()))
    return this
}

interface MaterialPickerInliner<S> {
    fun inline(s: S, inlineDirection: InlineDirection): S
}

abstract class InlineDirection(val direction:Int)

class PositiveDirection : InlineDirection(1)

class NegativeDirection : InlineDirection(-1)

class DateMaterialPickerInliner : MaterialPickerInliner<Long> {
    override fun inline(s: Long, inlineDirection: InlineDirection): Long {
        return inlineTimeWithTimeZone(s, inlineDirection)
    }
}

class DateRangeMaterialPickerInliner : MaterialPickerInliner<androidx.core.util.Pair<Long, Long>> {
    override fun inline(s: androidx.core.util.Pair<Long, Long>, inlineDirection: InlineDirection): androidx.core.util.Pair<Long, Long> {
        return androidx.core.util.Pair(inlineTimeWithTimeZone(s.first, inlineDirection), inlineTimeWithTimeZone(s.second, inlineDirection))
    }
}

/**
 * Inlines the time to the timezone with respect to daylight saving time.
 */
private fun inlineTimeWithTimeZone(time: Long, inlineDirection: InlineDirection): Long {
    val selectedTimeCalendar = Calendar.getInstance().apply { timeInMillis = time }
    return time + (inlineDirection.direction * selectedTimeCalendar.timeZone.getOffset(selectedTimeCalendar.timeInMillis))
}

And the usage

val calendarConstrains = CalendarConstraints.Builder()
            .setOpenAt(DateMaterialPickerInliner(), calendar.timeInMillis)

val materialDatePicker = MaterialDatePicker.create()
            .setCalendarConstraints(calendarConstrains.build())
            .setSelection(DateMaterialPickerInliner(), calendar.timeInMillis)
            .build()

materialDatePicker.addOnPositiveButtonClickListener(DateMaterialPickerInliner()) { time ->
            // TODO
        }
drchen commented 2 years ago

Hey Raaj, can you take a look and see if it's still reproducible?

raajkumars commented 2 years ago

I could not reproduce this issue using the latest Catalog demo application

danielandujar commented 2 years ago

The issue is easily reproducible, I can create a demo app to show it

raajkumars commented 2 years ago

@danielandujar That would be very helpful and much appreciated!

danielandujar commented 2 years ago

Ok, so here's my demo: @raajkumars https://github.com/danielandujar/DatePickerErrorDemo

Let me add an image as explanation: Screenshot_20220405-220008

raajkumars commented 2 years ago

Thanks for putting together a demo app. Based on this app and the information you provided, here is what I see:

Current Host Date/time: Fri, Apr 1 2022 10 PM Current Date/time in UTC: Sat, Apr 2, 2022 2:00 AM Selected Date/time in UTC: Sat, Apr 2, 2022 12:00 APM

So yes, the difference would be 2 hours ago. I think that the confusion stems from the fact that date picker receives and returns date values in UTC. Whereas the app is expecting to receive the selected date in local timezone. To convert the selected date in UTC to local date without changing the values you can try something like this:

var selectedUtc = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
selectedUtc.setTimeInMillis(millis)
var selectedLocal = Calendar.getInstance()
selectedLocal.clear()
selectedLocal.set(selectedUtc.get(Calendar.YEAR), selectedUtc.get(Calendar.MONTH), selectedUtc.get(Calendar.DATE))
raajkumars commented 2 years ago

After a very thorough investigation, I could not reproduce the issue which causes wrong day to be displayed as today. The other issue related to getting the current selection is a confusion that stems from the fact the datepicker uses UTC to represent the days in the picker. The selected date returned by the picker will be in UTC. To covert this value to local timezone without a timezone conversion, please use the code snippet similar to the one posted above.

danielandujar commented 2 years ago

So @raajkumars, this is not what we would expect from the datepicker itself, having to do workarounds for something obvious as dates. The DatePicker should AT BARE MINIMUN say that in the docs and implementation samples. But ideally do Both of these: a) Be defaulted to the device's default TimeZone b) Have the ability to change the timezone in code.

and by the way, This it is the same reason the ticket was open in the first place

raajkumars commented 2 years ago

@danielandujar If my memory serves right the first implementation of date picker class used local timezone. However in order to avoid a number of bug the implementation was changed later to use UTC. That said you points are valid. It will be inconvenient for developers to change the timezone of the selected date(s) everywhere in the code. Perhaps we could add the following methods to the picker:

public final S getSelectionInTimezone(@NotNull Timezone timezone);
public final S getSelectionInLocalTimezone();
Zhuinden commented 1 year ago

The secret for me was to set setTextInputFormat's SimpleDateFormat's timezone to be UTC

                    setTextInputFormat(SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()).apply {
                        timeZone = TimeZone.getTimeZone("UTC")
                    })

(along with https://github.com/material-components/material-components-android/issues/882#issuecomment-1111374962 )

Gizcerbes commented 9 months ago

Solution for me.

val d = Calendar.getInstance().apply {
   timeInMillis = viewModel.endTime.value + TimeZone.getDefault().rawOffset
}
MaterialDatePicker.Builder.datePicker()
   .setTitleText("Select date")
   .setSelection(d.timeInMillis)
   .build()
   .apply {
      addOnPositiveButtonClickListener { viewModel.setDate(it) }
   }
NonCoderF commented 1 week ago

val finalDate = (SpecificDate?.time ?: 0L) val timeOffset = TimeZone.getDefault().getOffset(finalDate).toLong()

val datePicker = datePicker { this.date = finalDate + if (timeOffset > 0) timeOffset else - timeOffset this.validTo = Date().time + timeOffset this.positiveButton(context.getString(R.string.common_globalokay)) { , y -> val offset = TimeZone.getDefault().getOffset(y).toLong() val selectedDate = y + if (offset > 0) offset else - offset } } datePicker.safeShow((context as AppCompatActivity))

DatePicker is a wrapper DSL over the MaterialDialog......... The offset is added and subtracted accordingly. You guys can figure out the rest.....CHILLLLLL