Polishing accessibility for date picker.

Bug: 8531032
Change-Id: Idff339300a8edcd353b4d8e37e8b218b7b99460b
diff --git a/res/layout/date_picker_header_view.xml b/res/layout/date_picker_header_view.xml
index 1314a1a..5fd73cc 100644
--- a/res/layout/date_picker_header_view.xml
+++ b/res/layout/date_picker_header_view.xml
@@ -22,4 +22,5 @@
     android:gravity="center"
     android:includeFontPadding="false"
     android:textColor="@color/white"
-    android:textSize="@dimen/date_picker_header_text_size" />
+    android:textSize="@dimen/date_picker_header_text_size"
+    android:importantForAccessibility="no" />
diff --git a/res/layout/date_picker_selected_date.xml b/res/layout/date_picker_selected_date.xml
index 22e5e92..2118ce1 100644
--- a/res/layout/date_picker_selected_date.xml
+++ b/res/layout/date_picker_selected_date.xml
@@ -24,7 +24,7 @@
     android:gravity="center"
     android:orientation="vertical" >
 
-    <LinearLayout
+    <com.android.datetimepicker.AccessibleLinearLayout
         android:id="@+id/date_picker_month_and_day"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -55,9 +55,9 @@
             android:includeFontPadding="false"
             android:textColor="@color/date_picker_selector"
             android:textSize="@dimen/selected_date_day_size" />
-    </LinearLayout>
+    </com.android.datetimepicker.AccessibleLinearLayout>
 
-    <TextView
+    <com.android.datetimepicker.AccessibleTextView
         android:id="@+id/date_picker_year"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/res/layout/date_picker_view_animator.xml b/res/layout/date_picker_view_animator.xml
index 1b02070..04501bd 100644
--- a/res/layout/date_picker_view_animator.xml
+++ b/res/layout/date_picker_view_animator.xml
@@ -13,7 +13,8 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<ViewAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.datetimepicker.date.AccessibleDateAnimator
+     xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@+id/animator"
      android:layout_width="@dimen/date_picker_component_width"
      android:layout_height="@dimen/date_picker_view_animator_height"
diff --git a/res/layout/time_header_label.xml b/res/layout/time_header_label.xml
index 6062678..cd4a834 100644
--- a/res/layout/time_header_label.xml
+++ b/res/layout/time_header_label.xml
@@ -46,7 +46,7 @@
             android:layout_marginLeft="@dimen/extra_time_label_margin"
             android:layout_marginRight="@dimen/extra_time_label_margin"
             android:layout_centerVertical="true" >
-            <com.android.datetimepicker.FakeButton
+            <com.android.datetimepicker.AccessibleTextView
                 android:id="@+id/hours"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
@@ -87,7 +87,7 @@
             android:layout_marginLeft="@dimen/extra_time_label_margin"
             android:layout_marginRight="@dimen/extra_time_label_margin"
             android:layout_centerVertical="true" >
-            <com.android.datetimepicker.FakeButton
+            <com.android.datetimepicker.AccessibleTextView
                 android:id="@+id/minutes"
                 style="@style/time_label"
                 android:layout_width="wrap_content"
@@ -96,8 +96,7 @@
                 android:text="@string/time_placeholder"
                 android:layout_gravity="center" />
         </FrameLayout>
-
-        <com.android.datetimepicker.FakeButton
+        <com.android.datetimepicker.AccessibleTextView
             android:id="@+id/ampm_hitspace"
             android:layout_width="@dimen/ampm_label_size"
             android:layout_height="wrap_content"
diff --git a/src/com/android/datetimepicker/FakeButton.java b/src/com/android/datetimepicker/AccessibleLinearLayout.java
similarity index 89%
copy from src/com/android/datetimepicker/FakeButton.java
copy to src/com/android/datetimepicker/AccessibleLinearLayout.java
index 6ab1b61..629f856 100644
--- a/src/com/android/datetimepicker/FakeButton.java
+++ b/src/com/android/datetimepicker/AccessibleLinearLayout.java
@@ -21,14 +21,14 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Button;
-import android.widget.TextView;
+import android.widget.LinearLayout;
 
 /**
  * Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility.
  */
-public class FakeButton extends TextView {
+public class AccessibleLinearLayout extends LinearLayout {
 
-    public FakeButton(Context context, AttributeSet attrs) {
+    public AccessibleLinearLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
diff --git a/src/com/android/datetimepicker/FakeButton.java b/src/com/android/datetimepicker/AccessibleTextView.java
similarity index 92%
rename from src/com/android/datetimepicker/FakeButton.java
rename to src/com/android/datetimepicker/AccessibleTextView.java
index 6ab1b61..98fa744 100644
--- a/src/com/android/datetimepicker/FakeButton.java
+++ b/src/com/android/datetimepicker/AccessibleTextView.java
@@ -26,9 +26,9 @@
 /**
  * Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility.
  */
-public class FakeButton extends TextView {
+public class AccessibleTextView extends TextView {
 
-    public FakeButton(Context context, AttributeSet attrs) {
+    public AccessibleTextView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
diff --git a/src/com/android/datetimepicker/Utils.java b/src/com/android/datetimepicker/Utils.java
index 83590bf..ac603bc 100644
--- a/src/com/android/datetimepicker/Utils.java
+++ b/src/com/android/datetimepicker/Utils.java
@@ -19,6 +19,7 @@
 import android.animation.Keyframe;
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
+import android.annotation.SuppressLint;
 import android.os.Build;
 import android.text.format.Time;
 import android.view.View;
@@ -39,6 +40,17 @@
       return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
     }
 
+    /**
+     * Try to speak the specified text, for accessibility. Only available on JB or later.
+     * @param text Text to announce.
+     */
+    @SuppressLint("NewApi")
+    public static void tryAccessibilityAnnounce(View view, CharSequence text) {
+        if (isJellybeanOrLater() && view != null && text != null) {
+            view.announceForAccessibility(text);
+        }
+    }
+
     public static int getDaysInMonth(int month, int year) {
         switch (month) {
             case Calendar.JANUARY:
diff --git a/src/com/android/datetimepicker/date/AccessibleDateAnimator.java b/src/com/android/datetimepicker/date/AccessibleDateAnimator.java
new file mode 100644
index 0000000..fc022cd
--- /dev/null
+++ b/src/com/android/datetimepicker/date/AccessibleDateAnimator.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.datetimepicker.date;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ViewAnimator;
+
+public class AccessibleDateAnimator extends ViewAnimator {
+    private long mDateMillis;
+
+    public AccessibleDateAnimator(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setDateMillis(long dateMillis) {
+        mDateMillis = dateMillis;
+    }
+
+    /**
+     * Announce the currently-selected date when launched.
+     */
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+            // Clear the event's current text so that only the current date will be spoken.
+            event.getText().clear();
+            int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
+                    DateUtils.FORMAT_SHOW_WEEKDAY;
+
+            String dateString = DateUtils.formatDateTime(getContext(), mDateMillis, flags);
+            event.getText().add(dateString);
+            return true;
+        }
+        return super.dispatchPopulateAccessibilityEvent(event);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/datetimepicker/date/DatePickerDialog.java b/src/com/android/datetimepicker/date/DatePickerDialog.java
index 8954c35..84ef2f3 100644
--- a/src/com/android/datetimepicker/date/DatePickerDialog.java
+++ b/src/com/android/datetimepicker/date/DatePickerDialog.java
@@ -20,9 +20,14 @@
 import android.app.Activity;
 import android.app.DialogFragment;
 import android.content.Context;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.os.Vibrator;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -30,6 +35,7 @@
 import android.view.ViewGroup;
 import android.view.Window;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
 import android.widget.Button;
@@ -82,7 +88,7 @@
     private OnDateSetListener mCallBack;
     private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>();
 
-    private ViewAnimator mAnimator;
+    private AccessibleDateAnimator mAnimator;
 
     private TextView mDayOfWeekView;
     private LinearLayout mMonthAndDayView;
@@ -104,6 +110,12 @@
 
     private boolean mDelayAnimation = true;
 
+    // Accessibility strings.
+    private String mDayPickerDescription;
+    private String mSelectDay;
+    private String mYearPickerDescription;
+    private String mSelectYear;
+
     /**
      * The callback used to indicate the user is done filling in the date.
      */
@@ -219,9 +231,16 @@
         mDayPickerView = new DayPickerView(activity, this);
         mYearPickerView = new YearPickerView(activity, this);
 
-        mAnimator = (ViewAnimator) view.findViewById(R.id.animator);
+        Resources res = getResources();
+        mDayPickerDescription = res.getString(R.string.day_picker_description);
+        mSelectDay = res.getString(R.string.select_day);
+        mYearPickerDescription = res.getString(R.string.year_picker_description);
+        mSelectYear = res.getString(R.string.select_year);
+
+        mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator);
         mAnimator.addView(mDayPickerView);
         mAnimator.addView(mYearPickerView);
+        mAnimator.setDateMillis(mCalendar.getTimeInMillis());
         // TODO: Replace with animation decided upon by the design team.
         Animation animation = new AlphaAnimation(0.0f, 1.0f);
         animation.setDuration(ANIMATION_DURATION);
@@ -245,7 +264,7 @@
             }
         });
 
-        updateDisplay();
+        updateDisplay(false);
         setCurrentView(currentView);
 
         if (listPosition != -1) {
@@ -259,6 +278,8 @@
     }
 
     private void setCurrentView(final int viewIndex) {
+        long millis = mCalendar.getTimeInMillis();
+
         switch (viewIndex) {
             case MONTH_AND_DAY_VIEW:
                 ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f,
@@ -275,6 +296,11 @@
                     mCurrentView = viewIndex;
                 }
                 pulseAnimator.start();
+
+                int flags = DateUtils.FORMAT_SHOW_DATE;
+                String dayString = DateUtils.formatDateTime(getActivity(), millis, flags);
+                mAnimator.setContentDescription(mDayPickerDescription+": "+dayString);
+                Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay);
                 break;
             case YEAR_VIEW:
                 pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f);
@@ -290,19 +316,37 @@
                     mCurrentView = viewIndex;
                 }
                 pulseAnimator.start();
+
+                CharSequence yearString = YEAR_FORMAT.format(millis);
+                mAnimator.setContentDescription(mYearPickerDescription+": "+yearString);
+                Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear);
                 break;
         }
     }
 
-    private void updateDisplay() {
+    private void updateDisplay(boolean announce) {
         if (mDayOfWeekView != null) {
             mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
                     Locale.getDefault()).toUpperCase(Locale.getDefault()));
         }
+
         mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT,
                 Locale.getDefault()).toUpperCase(Locale.getDefault()));
         mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime()));
         mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime()));
+
+        // Accessibility.
+        long millis = mCalendar.getTimeInMillis();
+        mAnimator.setDateMillis(millis);
+        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
+        String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags);
+        mMonthAndDayView.setContentDescription(monthAndDayText);
+
+        if (announce) {
+            flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
+            String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags);
+            Utils.tryAccessibilityAnnounce(mAnimator, fullDateText);
+        }
     }
 
     public void setFirstDayOfWeek(int startOfWeek) {
@@ -359,7 +403,7 @@
         mCalendar.set(Calendar.YEAR, year);
         updatePickers();
         setCurrentView(MONTH_AND_DAY_VIEW);
-        updateDisplay();
+        updateDisplay(true);
     }
 
     @Override
@@ -368,7 +412,7 @@
         mCalendar.set(Calendar.MONTH, month);
         mCalendar.set(Calendar.DAY_OF_MONTH, day);
         updatePickers();
-        updateDisplay();
+        updateDisplay(true);
     }
 
     private void updatePickers() {
diff --git a/src/com/android/datetimepicker/date/DayPickerView.java b/src/com/android/datetimepicker/date/DayPickerView.java
index fcd1d8c..5a49e42 100644
--- a/src/com/android/datetimepicker/date/DayPickerView.java
+++ b/src/com/android/datetimepicker/date/DayPickerView.java
@@ -16,18 +16,27 @@
 
 package com.android.datetimepicker.date;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
+import android.os.Bundle;
 import android.os.Handler;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.AbsListView;
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.ListView;
 
+import com.android.datetimepicker.Utils;
 import com.android.datetimepicker.date.DatePickerDialog.OnDateChangedListener;
 import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay;
 
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+
 /**
  * This displays a list of months in a calendar format with selectable days.
  */
@@ -50,6 +59,7 @@
     protected int mNumWeeks = 6;
     protected boolean mShowWeekNumber = false;
     protected int mDaysPerWeek = 7;
+    private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault());
 
     // These affect the scroll speed and feel
     protected float mFriction = 1.0f;
@@ -78,6 +88,7 @@
     protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;
 
     private final DatePickerController mController;
+    private boolean mPerformingScroll;
 
     public DayPickerView(Context context, DatePickerController controller) {
         super(context);
@@ -162,7 +173,6 @@
         mTempDay.set(day);
         final int position = (day.year - mController.getMinYear())
                 * SimpleMonthAdapter.MONTHS_IN_YEAR + day.month;
-        Log.d(TAG, "Year: " + day.year);
 
         View child;
         int i = 0;
@@ -400,6 +410,84 @@
     protected void layoutChildren() {
         final CalendarDay focusedDay = findAccessibilityFocus();
         super.layoutChildren();
-        restoreAccessibilityFocus(focusedDay);
+        if (mPerformingScroll) {
+            mPerformingScroll = false;
+        } else {
+            restoreAccessibilityFocus(focusedDay);
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        event.setItemCount(-1);
+   }
+
+    private String getMonthAndYearString(CalendarDay day) {
+        Calendar cal = Calendar.getInstance();
+        cal.set(day.year, day.month, day.day);
+
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append(cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()));
+        sbuf.append(" ");
+        sbuf.append(YEAR_FORMAT.format(cal.getTime()));
+        return sbuf.toString();
+    }
+
+    /**
+     * Necessary for accessibility, to ensure we support "scrolling" forward and backward
+     * in the month list.
+     */
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+      super.onInitializeAccessibilityNodeInfo(info);
+      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+    }
+
+    /**
+     * When scroll forward/backward events are received, announce the newly scrolled-to month.
+     */
+    @SuppressLint("NewApi")
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD &&
+                action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
+            return super.performAccessibilityAction(action, arguments);
+        }
+
+        // Figure out what month is showing.
+        int firstVisiblePosition = getFirstVisiblePosition();
+        int month = firstVisiblePosition % 12;
+        int year = firstVisiblePosition / 12 + mController.getMinYear();
+        CalendarDay day = new CalendarDay(year, month, 1);
+
+        // Scroll either forward or backward one month.
+        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
+            day.month++;
+            if (day.month == 12) {
+                day.month = 0;
+                day.year++;
+            }
+        } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
+            View firstVisibleView = getChildAt(0);
+            // If the view is fully visible, jump one month back. Otherwise, we'll just jump
+            // to the first day of first visible month.
+            if (firstVisibleView != null && firstVisibleView.getTop() >= -1) {
+                // There's an off-by-one somewhere, so the top of the first visible item will
+                // actually be -1 when it's at the exact top.
+                day.month--;
+                if (day.month == -1) {
+                    day.month = 11;
+                    day.year--;
+                }
+            }
+        }
+
+        // Go to that month.
+        Utils.tryAccessibilityAnnounce(this, getMonthAndYearString(day));
+        goTo(day, true, false, true);
+        mPerformingScroll = true;
+        return true;
     }
 }
diff --git a/src/com/android/datetimepicker/date/SimpleMonthAdapter.java b/src/com/android/datetimepicker/date/SimpleMonthAdapter.java
index 84be210..10a815a 100644
--- a/src/com/android/datetimepicker/date/SimpleMonthAdapter.java
+++ b/src/com/android/datetimepicker/date/SimpleMonthAdapter.java
@@ -16,6 +16,7 @@
 
 package com.android.datetimepicker.date;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.util.Log;
 import android.view.View;
@@ -24,6 +25,7 @@
 import android.widget.BaseAdapter;
 
 import com.android.datetimepicker.date.SimpleMonthView.OnDayClickListener;
+import com.android.datetimepicker.R;
 
 import java.util.Calendar;
 import java.util.HashMap;
@@ -104,7 +106,7 @@
     /**
      * Updates the selected day and related parameters.
      *
-     * @param selectedTime The time to highlight
+     * @param day The day to highlight
      */
     public void setSelectedDay(CalendarDay day) {
         mSelectedDay = day;
@@ -137,6 +139,7 @@
         return position;
     }
 
+    @SuppressLint("NewApi")
     @SuppressWarnings("unchecked")
     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
@@ -186,6 +189,7 @@
         return mSelectedDay.year == year && mSelectedDay.month == month;
     }
 
+
     @Override
     public void onDayClick(SimpleMonthView view, CalendarDay day) {
         if (day != null) {
diff --git a/src/com/android/datetimepicker/date/SimpleMonthView.java b/src/com/android/datetimepicker/date/SimpleMonthView.java
index bcb592f..832bdfb 100644
--- a/src/com/android/datetimepicker/date/SimpleMonthView.java
+++ b/src/com/android/datetimepicker/date/SimpleMonthView.java
@@ -27,6 +27,7 @@
 import android.os.Bundle;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.text.format.DateFormat;
 import android.text.format.DateUtils;
 import android.text.format.Time;
 import android.util.SparseArray;
@@ -52,6 +53,7 @@
  * within the specified month.
  */
 public class SimpleMonthView extends View {
+    private static final String TAG = "SimpleMonthView";
 
     /**
      * These params can be passed into the view to control how it appears.
@@ -404,17 +406,19 @@
         mNodeProvider.invalidateParent();
     }
 
+    private String getMonthAndYearString() {
+        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
+                | DateUtils.FORMAT_NO_MONTH_DAY;
+        mStringBuilder.setLength(0);
+        long millis = mCalendar.getTimeInMillis();
+        return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
+                Time.getCurrentTimezone()).toString();
+    }
+
     private void drawMonthTitle(Canvas canvas) {
         int x = (mWidth + 2 * mPadding) / 2;
         int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
-        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
-                | DateUtils.FORMAT_NO_MONTH_DAY;
-
-        mStringBuilder.setLength(0);
-        long millis = mCalendar.getTimeInMillis();
-        String title = DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
-                Time.getCurrentTimezone()).toString();
-        canvas.drawText(title, x, y, mMonthTitlePaint);
+        canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
     }
 
     private void drawMonthDayLabels(Canvas canvas) {
@@ -455,7 +459,6 @@
                 mMonthNumPaint.setColor(mDayTextColor);
             }
             canvas.drawText(String.format("%d", dayNumber), x, y, mMonthNumPaint);
-
             j++;
             if (j == mNumDays) {
                 j = 0;
@@ -550,6 +553,8 @@
         private final SparseArray<CalendarDay> mCachedItems = new SparseArray<CalendarDay>();
         private final Rect mTempRect = new Rect();
 
+        Calendar recycle;
+
         public MonthViewNodeProvider(Context context, View parent) {
             super(context, parent);
         }
@@ -659,19 +664,17 @@
          * @return A description of the time object
          */
         private CharSequence getItemDescription(CalendarDay item) {
-            final StringBuffer sbuf = new StringBuffer();
-            sbuf.append(String.format("%d", item.day));
-            sbuf.append(" ");
-            sbuf.append(mCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG,
-                    Locale.getDefault()));
-            sbuf.append(" ");
-            sbuf.append(String.format("%d", mYear));
+            if (recycle == null) {
+                recycle = Calendar.getInstance();
+            }
+            recycle.set(item.year, item.month, item.day);
+            CharSequence date = DateFormat.format("dd MMMM yyyy", recycle.getTimeInMillis());
 
             if (item.day == mSelectedDay) {
-                return getContext().getString(R.string.item_is_selected, sbuf);
+                return getContext().getString(R.string.item_is_selected, date);
             }
 
-            return sbuf;
+            return date;
         }
     }
 
diff --git a/src/com/android/datetimepicker/date/TextViewWithCircularIndicator.java b/src/com/android/datetimepicker/date/TextViewWithCircularIndicator.java
index 66140f5..09a0a4b 100644
--- a/src/com/android/datetimepicker/date/TextViewWithCircularIndicator.java
+++ b/src/com/android/datetimepicker/date/TextViewWithCircularIndicator.java
@@ -22,7 +22,9 @@
 import android.graphics.Paint;
 import android.graphics.Paint.Align;
 import android.graphics.Paint.Style;
+import android.text.format.DateUtils;
 import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.TextView;
 
 import com.android.datetimepicker.R;
@@ -38,6 +40,8 @@
 
     private final int mRadius;
     private final int mCircleColor;
+    private final String mItemIsSelectedText;
+
     private boolean mDrawCircle;
 
     public TextViewWithCircularIndicator(Context context, AttributeSet attrs) {
@@ -45,6 +49,8 @@
         Resources res = context.getResources();
         mCircleColor = res.getColor(R.color.blue);
         mRadius = res.getDimensionPixelOffset(R.dimen.month_select_circle_radius);
+        mItemIsSelectedText = context.getResources().getString(R.string.item_is_selected);
+
         init();
     }
 
@@ -71,4 +77,14 @@
             canvas.drawCircle(width / 2, height / 2, radius, mCirclePaint);
         }
     }
+
+    @Override
+    public CharSequence getContentDescription() {
+        CharSequence itemText = getText();
+        if (mDrawCircle) {
+            return String.format(mItemIsSelectedText, itemText);
+        } else {
+            return itemText;
+        }
+    }
 }
diff --git a/src/com/android/datetimepicker/date/YearPickerView.java b/src/com/android/datetimepicker/date/YearPickerView.java
index 411c574..70d2522 100644
--- a/src/com/android/datetimepicker/date/YearPickerView.java
+++ b/src/com/android/datetimepicker/date/YearPickerView.java
@@ -19,8 +19,10 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.drawable.StateListDrawable;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
@@ -28,6 +30,7 @@
 import android.widget.TextView;
 
 import com.android.datetimepicker.R;
+import com.android.datetimepicker.Utils;
 import com.android.datetimepicker.date.DatePickerDialog.OnDateChangedListener;
 
 import java.util.ArrayList;
@@ -37,6 +40,7 @@
  * Displays a selectable list of years.
  */
 public class YearPickerView extends ListView implements OnItemClickListener, OnDateChangedListener {
+    private static final String TAG = "YearPickerView";
 
     private final DatePickerController mController;
     private YearAdapter mAdapter;
@@ -147,4 +151,13 @@
         mAdapter.notifyDataSetChanged();
         postSetSelectionCentered(mController.getSelectedDay().year - mController.getMinYear());
     }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+            event.setFromIndex(0);
+            event.setToIndex(0);
+        }
+    }
 }
diff --git a/src/com/android/datetimepicker/time/TimePickerDialog.java b/src/com/android/datetimepicker/time/TimePickerDialog.java
index 27742a2..2a385f7 100644
--- a/src/com/android/datetimepicker/time/TimePickerDialog.java
+++ b/src/com/android/datetimepicker/time/TimePickerDialog.java
@@ -17,7 +17,6 @@
 package com.android.datetimepicker.time;
 
 import android.animation.ObjectAnimator;
-import android.annotation.SuppressLint;
 import android.app.ActionBar.LayoutParams;
 import android.app.DialogFragment;
 import android.content.Context;
@@ -293,11 +292,11 @@
     private void updateAmPmDisplay(int amOrPm) {
         if (amOrPm == AM) {
             mAmPmTextView.setText(mAmText);
-            tryAccessibilityAnnounce(mAmText);
+            Utils.tryAccessibilityAnnounce(mTimePicker, mAmText);
             mAmPmHitspace.setContentDescription(mAmText);
         } else if (amOrPm == PM){
             mAmPmTextView.setText(mPmText);
-            tryAccessibilityAnnounce(mPmText);
+            Utils.tryAccessibilityAnnounce(mTimePicker, mPmText);
             mAmPmHitspace.setContentDescription(mPmText);
         } else {
             mAmPmTextView.setText(mDoublePlaceholderText);
@@ -330,7 +329,7 @@
                 setCurrentItemShowing(MINUTE_INDEX, true, true, false);
                 announcement += ". " + mSelectMinutes;
             }
-            tryAccessibilityAnnounce(announcement);
+            Utils.tryAccessibilityAnnounce(mTimePicker, announcement);
         } else if (pickerIndex == MINUTE_INDEX){
             setMinute(newValue);
         } else if (pickerIndex == AMPM_INDEX) {
@@ -359,7 +358,7 @@
         mHourView.setText(text);
         mHourSpaceView.setText(text);
         if (announce) {
-            tryAccessibilityAnnounce(text);
+            Utils.tryAccessibilityAnnounce(mTimePicker, text);
         }
     }
 
@@ -368,7 +367,7 @@
             value = 0;
         }
         CharSequence text = String.format(Locale.getDefault(), "%02d", value);
-        tryAccessibilityAnnounce(text);
+        Utils.tryAccessibilityAnnounce(mTimePicker, text);
         mMinuteView.setText(text);
         mMinuteSpaceView.setText(text);
     }
@@ -386,14 +385,14 @@
             }
             mTimePicker.setContentDescription(mHourPickerDescription+": "+hours);
             if (announce) {
-                tryAccessibilityAnnounce(mSelectHours);
+                Utils.tryAccessibilityAnnounce(mTimePicker, mSelectHours);
             }
             labelToAnimate = mHourView;
         } else {
             int minutes = mTimePicker.getMinutes();
             mTimePicker.setContentDescription(mMinutePickerDescription+": "+minutes);
             if (announce) {
-                tryAccessibilityAnnounce(mSelectMinutes);
+                Utils.tryAccessibilityAnnounce(mTimePicker, mSelectMinutes);
             }
             labelToAnimate = mMinuteView;
         }
@@ -411,17 +410,6 @@
     }
 
     /**
-     * Try to speak the specified text, for accessibility. Only available on JB or later.
-     * @param text
-     */
-    @SuppressLint("NewApi")
-    private void tryAccessibilityAnnounce(CharSequence text) {
-        if (Utils.isJellybeanOrLater() && mTimePicker != null && text != null) {
-            mTimePicker.announceForAccessibility(text);
-        }
-    }
-
-    /**
      * For keyboard mode, processes key events.
      * @param keyCode the pressed key.
      * @return true if the key was successfully processed, false otherwise.