Setting MinDate on DatePicker moves CalendarView to 1964

16,843

Solution 1

TL;DR

It's not 1964 but 2100 formatted badly. CalendarView has bugs. formatDateRange() does not work past 2038.

Workaround #1

class DatePickerDialog1964 extends DatePickerDialog {
    DatePickerDialog1964(Context c) {
        super(c, null, 2013, 4, 21);

        @SuppressWarnings("deprecation")
        Date min = new Date(2013-1900, 4, 21);

        DatePicker p = getDatePicker();
        CalendarView cv = p.getCalendarView(); // should check for null
        long cur = cv.getDate();
        int d = cv.getFirstDayOfWeek();
        p.setMinDate(min.getTime());
        cv.setDate(cur + 1000L*60*60*24*40);
        cv.setFirstDayOfWeek((d + 1) % 7);
        cv.setDate(cur);
        cv.setFirstDayOfWeek(d);
    }
}

Workaround #2 I actually used

// Calendar view is a cascade of bugs.
// Work around that by explicitly disabling it.
datePicker.setCalendarViewShown(false);

Analysis

CalendarView uses android.text.format.DateUtils.formatDateRange() for producing the month/year name in the header. Calendar view only updates the header when the month number changes. For example, if the month number is the same but the year changes, the header is not updated.

Somewhere during layout phase, the underlying ListView invokes OnScrollListener on the calendar view as if the list was scrolled to the end. If the month number is changed, the header is updated with the test code above, the millis value passed to formatDateRange() is 4131043200000 which is somewhere in late 2100, and 2100 is the default maximum for DatePicker.

formatDateRange() in turn uses android.text.format.Time as its internal calendar representation, specifically setting the millis using its set() method. I did not check the underlying native code, but my educated guess is that the passed in milliseconds value gets truncated to 32-bit time_t seconds value with the inherent Year 2038 problem, wrapping to a value of a little more than 62 years. Time itself is using struct tm which represents years as an int since 1900, thus resulting in a Time in the 1960s which then gets formatted in the header.

This can be reproduced with plain DatePicker with CalendarView without min date set. Just use the spinners to move the year to 2038 or later, change the month (to trigger the header update) and you'll see the year in header wrap to 1902.

Now, the question remains why setting a minimum date makes the calendar view scroll to maximum date. Answer seems to be the calendar view's week ListView adapter. It indexes weeks from the minimum date, but when the minimum date changes, the adapter's notifyDataSetChanged() is not called. So the workaround #1 above works because:

  • Changing the date by more than a month makes the month header change.
  • Changing the first day of week makes the week listview notice its adapter's data has changed.

Solution 2

The above answers are right on about the sources of this bug. For my application, I'm using this workaround:

Every time the DatePicker's date or minDate is changed, call the following routine with the date that should be selected in the picker/calendar:

private void fixUpDatePickerCalendarView(Calendar date) {
    // Workaround for CalendarView bug relating to setMinDate():
    // https://code.google.com/p/android/issues/detail?id=42750
    // Set then reset the date on the calendar so that it properly
    // shows today's date. The choice of 24 months is arbitrary.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        final CalendarView cal = datePicker.getDatePicker().getCalendarView();
        if (cal != null) {
            date.add(Calendar.MONTH, 24);
            cal.setDate(date.getTimeInMillis(), false, true);
            date.add(Calendar.MONTH, -24);
            cal.setDate(date.getTimeInMillis(), false, true);
        }
    }
}

Solution 3

The solution that I ended up using:

private void fixBuggyCalendarview(CalendarView cv) {
    long current = cv.getDate();
    cv.setDate(cv.getMaxDate(), false, true);
    cv.setDate(current, false, true);
}

This also works when you have set min/max date for the CalendarView

Share:
16,843

Related videos on Youtube

laalto
Author by

laalto

I like questions and answers that promote learning and understanding. As a consequence, I value less questions that are just fix-this-for-me dumps or answers that are just code to be copied without learning anything in the process.

Updated on June 04, 2022

Comments

  • laalto
    laalto almost 2 years

    I'm debugging an issue where the CalendarView in a DatePicker moves to October 1964 if there is a non-default minimum date set. This reproduces at least on API17 Nexus7 emulator but there are reports about this issue on other devices where the platform style includes both spinners and calendar view in date picker.

    Here's a code snippet demonstrating the issue:

    class DatePickerDialog1964 extends DatePickerDialog {
        DatePickerDialog1964(Context c) {
            super(c, null, 2013, 4, 21);
    
            @SuppressWarnings("deprecation")
            Date min = new Date(2013-1900, 4, 21);
    
            DatePicker p = getDatePicker();
            p.setMinDate(min.getTime());
        }
    }
    

    ...

    new DatePickerDialog1964(context).show();
    

    Screen shot:

    October 1964 calendar view in date picker

    Of course, the expectation is that the spinners and calendar view would be showing the same date.

    I'll keep on debugging this but if any SO users have experience or other insight, please share.

    Related:

    • lilbyrdie
      lilbyrdie almost 11 years
      If you can live with a fixed minDate, setting it via XML actually works. Otherwise, more elegant fixes are available by pulling CalendarView.java out of AOSP.
  • Deepak Goel
    Deepak Goel over 10 years
    When i set January as a month it is returning February.
  • laalto
    laalto over 10 years
    @deepakgoel That's another issue. January is 0, February is 1 and so on. Zero-based indexing.
  • Deepak Goel
    Deepak Goel over 10 years
    So how we can fix this issue. I tried your code. In this case spinner view is giving january month and Calendar view is giving february month
  • Daniele B
    Daniele B over 10 years
    @deepakgoel, it's not a bug, that's the way it has been defined: developer.android.com/reference/android/widget/… The month value is in the range 0-11
  • Roel
    Roel about 8 years
    This makes my app crash on 29 februari 2016 because a year ago is than 27 februari and that is lower than the minimum date and it is also not the date set.
  • Jacky Lian
    Jacky Lian about 7 years
    In fact, the choice of months is not arbitrary. To prevent the 29 February problem, it should be 48 instead of 24.
  • Stephen McCormick
    Stephen McCormick over 6 years
    The getCalendarView() is deprecated, and on my Samsung tablet OS 7.0 it crashes. Any other solutions out there?
  • Stephen McCormick
    Stephen McCormick over 6 years
    The first solution does not work for Samsung 7.0 do to deprecation of the setCalendarView(). The 2nd solution did work on all devices I tried from 5.0 - 7.0