1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.calendar.month;
18 
19 import com.android.calendar.Event;
20 import com.android.calendar.R;
21 import com.android.calendar.Utils;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.ObjectAnimator;
26 import android.app.Service;
27 import android.content.Context;
28 import android.content.res.Configuration;
29 import android.content.res.Resources;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.Paint;
33 import android.graphics.Paint.Align;
34 import android.graphics.Paint.Style;
35 import android.graphics.Typeface;
36 import android.graphics.drawable.Drawable;
37 import android.provider.CalendarContract.Attendees;
38 import android.text.TextPaint;
39 import android.text.TextUtils;
40 import android.text.format.DateFormat;
41 import android.text.format.DateUtils;
42 import android.text.format.Time;
43 import android.util.Log;
44 import android.view.MotionEvent;
45 import android.view.accessibility.AccessibilityEvent;
46 import android.view.accessibility.AccessibilityManager;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Formatter;
51 import java.util.HashMap;
52 import java.util.Iterator;
53 import java.util.List;
54 import java.util.Locale;
55 
56 public class MonthWeekEventsView extends SimpleWeekView {
57 
58     private static final String TAG = "MonthView";
59 
60     private static final boolean DEBUG_LAYOUT = false;
61 
62     public static final String VIEW_PARAMS_ORIENTATION = "orientation";
63     public static final String VIEW_PARAMS_ANIMATE_TODAY = "animate_today";
64 
65     /* NOTE: these are not constants, and may be multiplied by a scale factor */
66     private static int TEXT_SIZE_MONTH_NUMBER = 32;
67     private static int TEXT_SIZE_EVENT = 12;
68     private static int TEXT_SIZE_EVENT_TITLE = 14;
69     private static int TEXT_SIZE_MORE_EVENTS = 12;
70     private static int TEXT_SIZE_MONTH_NAME = 14;
71     private static int TEXT_SIZE_WEEK_NUM = 12;
72 
73     private static int DNA_MARGIN = 4;
74     private static int DNA_ALL_DAY_HEIGHT = 4;
75     private static int DNA_MIN_SEGMENT_HEIGHT = 4;
76     private static int DNA_WIDTH = 8;
77     private static int DNA_ALL_DAY_WIDTH = 32;
78     private static int DNA_SIDE_PADDING = 6;
79     private static int CONFLICT_COLOR = Color.BLACK;
80     private static int EVENT_TEXT_COLOR = Color.WHITE;
81 
82     private static int DEFAULT_EDGE_SPACING = 0;
83     private static int SIDE_PADDING_MONTH_NUMBER = 4;
84     private static int TOP_PADDING_MONTH_NUMBER = 4;
85     private static int TOP_PADDING_WEEK_NUMBER = 4;
86     private static int SIDE_PADDING_WEEK_NUMBER = 20;
87     private static int DAY_SEPARATOR_OUTER_WIDTH = 0;
88     private static int DAY_SEPARATOR_INNER_WIDTH = 1;
89     private static int DAY_SEPARATOR_VERTICAL_LENGTH = 53;
90     private static int DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT = 64;
91     private static int MIN_WEEK_WIDTH = 50;
92 
93     private static int EVENT_X_OFFSET_LANDSCAPE = 38;
94     private static int EVENT_Y_OFFSET_LANDSCAPE = 8;
95     private static int EVENT_Y_OFFSET_PORTRAIT = 7;
96     private static int EVENT_SQUARE_WIDTH = 10;
97     private static int EVENT_SQUARE_BORDER = 2;
98     private static int EVENT_LINE_PADDING = 2;
99     private static int EVENT_RIGHT_PADDING = 4;
100     private static int EVENT_BOTTOM_PADDING = 3;
101 
102     private static int TODAY_HIGHLIGHT_WIDTH = 2;
103 
104     private static int SPACING_WEEK_NUMBER = 24;
105     private static boolean mInitialized = false;
106     private static boolean mShowDetailsInMonth;
107 
108     protected Time mToday = new Time();
109     protected boolean mHasToday = false;
110     protected int mTodayIndex = -1;
111     protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE;
112     protected List<ArrayList<Event>> mEvents = null;
113     protected ArrayList<Event> mUnsortedEvents = null;
114     HashMap<Integer, Utils.DNAStrand> mDna = null;
115     // This is for drawing the outlines around event chips and supports up to 10
116     // events being drawn on each day. The code will expand this if necessary.
117     protected FloatRef mEventOutlines = new FloatRef(10 * 4 * 4 * 7);
118 
119 
120 
121     protected static StringBuilder mStringBuilder = new StringBuilder(50);
122     // TODO recreate formatter when locale changes
123     protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
124 
125     protected Paint mMonthNamePaint;
126     protected TextPaint mEventPaint;
127     protected TextPaint mSolidBackgroundEventPaint;
128     protected TextPaint mFramedEventPaint;
129     protected TextPaint mDeclinedEventPaint;
130     protected TextPaint mEventExtrasPaint;
131     protected TextPaint mEventDeclinedExtrasPaint;
132     protected Paint mWeekNumPaint;
133     protected Paint mDNAAllDayPaint;
134     protected Paint mDNATimePaint;
135     protected Paint mEventSquarePaint;
136 
137 
138     protected Drawable mTodayDrawable;
139 
140     protected int mMonthNumHeight;
141     protected int mMonthNumAscentHeight;
142     protected int mEventHeight;
143     protected int mEventAscentHeight;
144     protected int mExtrasHeight;
145     protected int mExtrasAscentHeight;
146     protected int mExtrasDescent;
147     protected int mWeekNumAscentHeight;
148 
149     protected int mMonthBGColor;
150     protected int mMonthBGOtherColor;
151     protected int mMonthBGTodayColor;
152     protected int mMonthNumColor;
153     protected int mMonthNumOtherColor;
154     protected int mMonthNumTodayColor;
155     protected int mMonthNameColor;
156     protected int mMonthNameOtherColor;
157     protected int mMonthEventColor;
158     protected int mMonthDeclinedEventColor;
159     protected int mMonthDeclinedExtrasColor;
160     protected int mMonthEventExtraColor;
161     protected int mMonthEventOtherColor;
162     protected int mMonthEventExtraOtherColor;
163     protected int mMonthWeekNumColor;
164     protected int mMonthBusyBitsBgColor;
165     protected int mMonthBusyBitsBusyTimeColor;
166     protected int mMonthBusyBitsConflictTimeColor;
167     private int mClickedDayIndex = -1;
168     private int mClickedDayColor;
169     private static final int mClickedAlpha = 128;
170 
171     protected int mEventChipOutlineColor = 0xFFFFFFFF;
172     protected int mDaySeparatorInnerColor;
173     protected int mTodayAnimateColor;
174 
175     private boolean mAnimateToday;
176     private int mAnimateTodayAlpha = 0;
177     private ObjectAnimator mTodayAnimator = null;
178 
179     private final TodayAnimatorListener mAnimatorListener = new TodayAnimatorListener();
180 
181     class TodayAnimatorListener extends AnimatorListenerAdapter {
182         private volatile Animator mAnimator = null;
183         private volatile boolean mFadingIn = false;
184 
185         @Override
onAnimationEnd(Animator animation)186         public void onAnimationEnd(Animator animation) {
187             synchronized (this) {
188                 if (mAnimator != animation) {
189                     animation.removeAllListeners();
190                     animation.cancel();
191                     return;
192                 }
193                 if (mFadingIn) {
194                     if (mTodayAnimator != null) {
195                         mTodayAnimator.removeAllListeners();
196                         mTodayAnimator.cancel();
197                     }
198                     mTodayAnimator = ObjectAnimator.ofInt(MonthWeekEventsView.this,
199                             "animateTodayAlpha", 255, 0);
200                     mAnimator = mTodayAnimator;
201                     mFadingIn = false;
202                     mTodayAnimator.addListener(this);
203                     mTodayAnimator.setDuration(600);
204                     mTodayAnimator.start();
205                 } else {
206                     mAnimateToday = false;
207                     mAnimateTodayAlpha = 0;
208                     mAnimator.removeAllListeners();
209                     mAnimator = null;
210                     mTodayAnimator = null;
211                     invalidate();
212                 }
213             }
214         }
215 
setAnimator(Animator animation)216         public void setAnimator(Animator animation) {
217             mAnimator = animation;
218         }
219 
setFadingIn(boolean fadingIn)220         public void setFadingIn(boolean fadingIn) {
221             mFadingIn = fadingIn;
222         }
223 
224     }
225 
226     private int[] mDayXs;
227 
228     /**
229      * This provides a reference to a float array which allows for easy size
230      * checking and reallocation. Used for drawing lines.
231      */
232     private class FloatRef {
233         float[] array;
234 
FloatRef(int size)235         public FloatRef(int size) {
236             array = new float[size];
237         }
238 
ensureSize(int newSize)239         public void ensureSize(int newSize) {
240             if (newSize >= array.length) {
241                 // Add enough space for 7 more boxes to be drawn
242                 array = Arrays.copyOf(array, newSize + 16 * 7);
243             }
244         }
245     }
246 
247     /**
248      * Shows up as an error if we don't include this.
249      */
MonthWeekEventsView(Context context)250     public MonthWeekEventsView(Context context) {
251         super(context);
252     }
253 
254     // Sets the list of events for this week. Takes a sorted list of arrays
255     // divided up by day for generating the large month version and the full
256     // arraylist sorted by start time to generate the dna version.
setEvents(List<ArrayList<Event>> sortedEvents, ArrayList<Event> unsortedEvents)257     public void setEvents(List<ArrayList<Event>> sortedEvents, ArrayList<Event> unsortedEvents) {
258         setEvents(sortedEvents);
259         // The MIN_WEEK_WIDTH is a hack to prevent the view from trying to
260         // generate dna bits before its width has been fixed.
261         createDna(unsortedEvents);
262     }
263 
264     /**
265      * Sets up the dna bits for the view. This will return early if the view
266      * isn't in a state that will create a valid set of dna yet (such as the
267      * views width not being set correctly yet).
268      */
createDna(ArrayList<Event> unsortedEvents)269     public void createDna(ArrayList<Event> unsortedEvents) {
270         if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) {
271             // Stash the list of events for use when this view is ready, or
272             // just clear it if a null set has been passed to this view
273             mUnsortedEvents = unsortedEvents;
274             mDna = null;
275             return;
276         } else {
277             // clear the cached set of events since we're ready to build it now
278             mUnsortedEvents = null;
279         }
280         // Create the drawing coordinates for dna
281         if (!mShowDetailsInMonth) {
282             int numDays = mEvents.size();
283             int effectiveWidth = mWidth - mPadding * 2;
284             if (mShowWeekNum) {
285                 effectiveWidth -= SPACING_WEEK_NUMBER;
286             }
287             DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING;
288             mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH);
289             mDayXs = new int[numDays];
290             for (int day = 0; day < numDays; day++) {
291                 mDayXs[day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING;
292 
293             }
294 
295             int top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1;
296             int bottom = mHeight - DNA_MARGIN;
297             mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom,
298                     DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext());
299         }
300     }
301 
setEvents(List<ArrayList<Event>> sortedEvents)302     public void setEvents(List<ArrayList<Event>> sortedEvents) {
303         mEvents = sortedEvents;
304         if (sortedEvents == null) {
305             return;
306         }
307         if (sortedEvents.size() != mNumDays) {
308             if (Log.isLoggable(TAG, Log.ERROR)) {
309                 Log.wtf(TAG, "Events size must be same as days displayed: size="
310                         + sortedEvents.size() + " days=" + mNumDays);
311             }
312             mEvents = null;
313             return;
314         }
315     }
316 
loadColors(Context context)317     protected void loadColors(Context context) {
318         Resources res = context.getResources();
319         mMonthWeekNumColor = res.getColor(R.color.month_week_num_color);
320         mMonthNumColor = res.getColor(R.color.month_day_number);
321         mMonthNumOtherColor = res.getColor(R.color.month_day_number_other);
322         mMonthNumTodayColor = res.getColor(R.color.month_today_number);
323         mMonthNameColor = mMonthNumColor;
324         mMonthNameOtherColor = mMonthNumOtherColor;
325         mMonthEventColor = res.getColor(R.color.month_event_color);
326         mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color);
327         mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color);
328         mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color);
329         mMonthEventOtherColor = res.getColor(R.color.month_event_other_color);
330         mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color);
331         mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor);
332         mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor);
333         mMonthBGColor = res.getColor(R.color.month_bgcolor);
334         mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines);
335         mTodayAnimateColor = res.getColor(R.color.today_highlight_color);
336         mClickedDayColor = res.getColor(R.color.day_clicked_background_color);
337         mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light);
338     }
339 
340     /**
341      * Sets up the text and style properties for painting. Override this if you
342      * want to use a different paint.
343      */
344     @Override
initView()345     protected void initView() {
346         super.initView();
347 
348         if (!mInitialized) {
349             Resources resources = getContext().getResources();
350             mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month);
351             TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title);
352             TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number);
353             SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin);
354             CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color);
355             EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color);
356             if (mScale != 1) {
357                 TOP_PADDING_MONTH_NUMBER *= mScale;
358                 TOP_PADDING_WEEK_NUMBER *= mScale;
359                 SIDE_PADDING_MONTH_NUMBER *= mScale;
360                 SIDE_PADDING_WEEK_NUMBER *= mScale;
361                 SPACING_WEEK_NUMBER *= mScale;
362                 TEXT_SIZE_MONTH_NUMBER *= mScale;
363                 TEXT_SIZE_EVENT *= mScale;
364                 TEXT_SIZE_EVENT_TITLE *= mScale;
365                 TEXT_SIZE_MORE_EVENTS *= mScale;
366                 TEXT_SIZE_MONTH_NAME *= mScale;
367                 TEXT_SIZE_WEEK_NUM *= mScale;
368                 DAY_SEPARATOR_OUTER_WIDTH *= mScale;
369                 DAY_SEPARATOR_INNER_WIDTH *= mScale;
370                 DAY_SEPARATOR_VERTICAL_LENGTH *= mScale;
371                 DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT *= mScale;
372                 EVENT_X_OFFSET_LANDSCAPE *= mScale;
373                 EVENT_Y_OFFSET_LANDSCAPE *= mScale;
374                 EVENT_Y_OFFSET_PORTRAIT *= mScale;
375                 EVENT_SQUARE_WIDTH *= mScale;
376                 EVENT_SQUARE_BORDER *= mScale;
377                 EVENT_LINE_PADDING *= mScale;
378                 EVENT_BOTTOM_PADDING *= mScale;
379                 EVENT_RIGHT_PADDING *= mScale;
380                 DNA_MARGIN *= mScale;
381                 DNA_WIDTH *= mScale;
382                 DNA_ALL_DAY_HEIGHT *= mScale;
383                 DNA_MIN_SEGMENT_HEIGHT *= mScale;
384                 DNA_SIDE_PADDING *= mScale;
385                 DEFAULT_EDGE_SPACING *= mScale;
386                 DNA_ALL_DAY_WIDTH *= mScale;
387                 TODAY_HIGHLIGHT_WIDTH *= mScale;
388             }
389             if (!mShowDetailsInMonth) {
390                 TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN;
391             }
392             mInitialized = true;
393         }
394         mPadding = DEFAULT_EDGE_SPACING;
395         loadColors(getContext());
396         // TODO modify paint properties depending on isMini
397 
398         mMonthNumPaint = new Paint();
399         mMonthNumPaint.setFakeBoldText(false);
400         mMonthNumPaint.setAntiAlias(true);
401         mMonthNumPaint.setTextSize(TEXT_SIZE_MONTH_NUMBER);
402         mMonthNumPaint.setColor(mMonthNumColor);
403         mMonthNumPaint.setStyle(Style.FILL);
404         mMonthNumPaint.setTextAlign(Align.RIGHT);
405         mMonthNumPaint.setTypeface(Typeface.DEFAULT);
406 
407         mMonthNumAscentHeight = (int) (-mMonthNumPaint.ascent() + 0.5f);
408         mMonthNumHeight = (int) (mMonthNumPaint.descent() - mMonthNumPaint.ascent() + 0.5f);
409 
410         mEventPaint = new TextPaint();
411         mEventPaint.setFakeBoldText(true);
412         mEventPaint.setAntiAlias(true);
413         mEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE);
414         mEventPaint.setColor(mMonthEventColor);
415 
416         mSolidBackgroundEventPaint = new TextPaint(mEventPaint);
417         mSolidBackgroundEventPaint.setColor(EVENT_TEXT_COLOR);
418         mFramedEventPaint = new TextPaint(mSolidBackgroundEventPaint);
419 
420         mDeclinedEventPaint = new TextPaint();
421         mDeclinedEventPaint.setFakeBoldText(true);
422         mDeclinedEventPaint.setAntiAlias(true);
423         mDeclinedEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE);
424         mDeclinedEventPaint.setColor(mMonthDeclinedEventColor);
425 
426         mEventAscentHeight = (int) (-mEventPaint.ascent() + 0.5f);
427         mEventHeight = (int) (mEventPaint.descent() - mEventPaint.ascent() + 0.5f);
428 
429         mEventExtrasPaint = new TextPaint();
430         mEventExtrasPaint.setFakeBoldText(false);
431         mEventExtrasPaint.setAntiAlias(true);
432         mEventExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER);
433         mEventExtrasPaint.setTextSize(TEXT_SIZE_EVENT);
434         mEventExtrasPaint.setColor(mMonthEventExtraColor);
435         mEventExtrasPaint.setStyle(Style.FILL);
436         mEventExtrasPaint.setTextAlign(Align.LEFT);
437         mExtrasHeight = (int)(mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f);
438         mExtrasAscentHeight = (int)(-mEventExtrasPaint.ascent() + 0.5f);
439         mExtrasDescent = (int)(mEventExtrasPaint.descent() + 0.5f);
440 
441         mEventDeclinedExtrasPaint = new TextPaint();
442         mEventDeclinedExtrasPaint.setFakeBoldText(false);
443         mEventDeclinedExtrasPaint.setAntiAlias(true);
444         mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER);
445         mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT);
446         mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor);
447         mEventDeclinedExtrasPaint.setStyle(Style.FILL);
448         mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT);
449 
450         mWeekNumPaint = new Paint();
451         mWeekNumPaint.setFakeBoldText(false);
452         mWeekNumPaint.setAntiAlias(true);
453         mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM);
454         mWeekNumPaint.setColor(mWeekNumColor);
455         mWeekNumPaint.setStyle(Style.FILL);
456         mWeekNumPaint.setTextAlign(Align.RIGHT);
457 
458         mWeekNumAscentHeight = (int) (-mWeekNumPaint.ascent() + 0.5f);
459 
460         mDNAAllDayPaint = new Paint();
461         mDNATimePaint = new Paint();
462         mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor);
463         mDNATimePaint.setStyle(Style.FILL_AND_STROKE);
464         mDNATimePaint.setStrokeWidth(DNA_WIDTH);
465         mDNATimePaint.setAntiAlias(false);
466         mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor);
467         mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE);
468         mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH);
469         mDNAAllDayPaint.setAntiAlias(false);
470 
471         mEventSquarePaint = new Paint();
472         mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER);
473         mEventSquarePaint.setAntiAlias(false);
474 
475         if (DEBUG_LAYOUT) {
476             Log.d("EXTRA", "mScale=" + mScale);
477             Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint.ascent()
478                     + " descent=" + mMonthNumPaint.descent() + " int height=" + mMonthNumHeight);
479             Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint.ascent()
480                     + " descent=" + mEventPaint.descent() + " int height=" + mEventHeight
481                     + " int ascent=" + mEventAscentHeight);
482             Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent()
483                     + " descent=" + mEventExtrasPaint.descent() + " int height=" + mExtrasHeight);
484             Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent()
485                     + " descent=" + mWeekNumPaint.descent());
486         }
487     }
488 
489     @Override
setWeekParams(HashMap<String, Integer> params, String tz)490     public void setWeekParams(HashMap<String, Integer> params, String tz) {
491         super.setWeekParams(params, tz);
492 
493         if (params.containsKey(VIEW_PARAMS_ORIENTATION)) {
494             mOrientation = params.get(VIEW_PARAMS_ORIENTATION);
495         }
496 
497         updateToday(tz);
498         mNumCells = mNumDays + 1;
499 
500         if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) {
501             synchronized (mAnimatorListener) {
502                 if (mTodayAnimator != null) {
503                     mTodayAnimator.removeAllListeners();
504                     mTodayAnimator.cancel();
505                 }
506                 mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha",
507                         Math.max(mAnimateTodayAlpha, 80), 255);
508                 mTodayAnimator.setDuration(150);
509                 mAnimatorListener.setAnimator(mTodayAnimator);
510                 mAnimatorListener.setFadingIn(true);
511                 mTodayAnimator.addListener(mAnimatorListener);
512                 mAnimateToday = true;
513                 mTodayAnimator.start();
514             }
515         }
516     }
517 
518     /**
519      * @param tz
520      */
updateToday(String tz)521     public boolean updateToday(String tz) {
522         mToday.timezone = tz;
523         mToday.setToNow();
524         mToday.normalize(true);
525         int julianToday = Time.getJulianDay(mToday.toMillis(false), mToday.gmtoff);
526         if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) {
527             mHasToday = true;
528             mTodayIndex = julianToday - mFirstJulianDay;
529         } else {
530             mHasToday = false;
531             mTodayIndex = -1;
532         }
533         return mHasToday;
534     }
535 
setAnimateTodayAlpha(int alpha)536     public void setAnimateTodayAlpha(int alpha) {
537         mAnimateTodayAlpha = alpha;
538         invalidate();
539     }
540 
541     @Override
onDraw(Canvas canvas)542     protected void onDraw(Canvas canvas) {
543         drawBackground(canvas);
544         drawWeekNums(canvas);
545         drawDaySeparators(canvas);
546         if (mHasToday && mAnimateToday) {
547             drawToday(canvas);
548         }
549         if (mShowDetailsInMonth) {
550             drawEvents(canvas);
551         } else {
552             if (mDna == null && mUnsortedEvents != null) {
553                 createDna(mUnsortedEvents);
554             }
555             drawDNA(canvas);
556         }
557         drawClick(canvas);
558     }
559 
drawToday(Canvas canvas)560     protected void drawToday(Canvas canvas) {
561         r.top = DAY_SEPARATOR_INNER_WIDTH + (TODAY_HIGHLIGHT_WIDTH / 2);
562         r.bottom = mHeight - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f);
563         p.setStyle(Style.STROKE);
564         p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH);
565         r.left = computeDayLeftPosition(mTodayIndex) + (TODAY_HIGHLIGHT_WIDTH / 2);
566         r.right = computeDayLeftPosition(mTodayIndex + 1)
567                 - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f);
568         p.setColor(mTodayAnimateColor | (mAnimateTodayAlpha << 24));
569         canvas.drawRect(r, p);
570         p.setStyle(Style.FILL);
571     }
572 
573     // TODO move into SimpleWeekView
574     // Computes the x position for the left side of the given day
computeDayLeftPosition(int day)575     private int computeDayLeftPosition(int day) {
576         int effectiveWidth = mWidth;
577         int x = 0;
578         int xOffset = 0;
579         if (mShowWeekNum) {
580             xOffset = SPACING_WEEK_NUMBER + mPadding;
581             effectiveWidth -= xOffset;
582         }
583         x = day * effectiveWidth / mNumDays + xOffset;
584         return x;
585     }
586 
587     @Override
drawDaySeparators(Canvas canvas)588     protected void drawDaySeparators(Canvas canvas) {
589         float lines[] = new float[8 * 4];
590         int count = 6 * 4;
591         int wkNumOffset = 0;
592         int i = 0;
593         if (mShowWeekNum) {
594             // This adds the first line separating the week number
595             int xOffset = SPACING_WEEK_NUMBER + mPadding;
596             count += 4;
597             lines[i++] = xOffset;
598             lines[i++] = 0;
599             lines[i++] = xOffset;
600             lines[i++] = mHeight;
601             wkNumOffset++;
602         }
603         count += 4;
604         lines[i++] = 0;
605         lines[i++] = 0;
606         lines[i++] = mWidth;
607         lines[i++] = 0;
608         int y0 = 0;
609         int y1 = mHeight;
610 
611         while (i < count) {
612             int x = computeDayLeftPosition(i / 4 - wkNumOffset);
613             lines[i++] = x;
614             lines[i++] = y0;
615             lines[i++] = x;
616             lines[i++] = y1;
617         }
618         p.setColor(mDaySeparatorInnerColor);
619         p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH);
620         canvas.drawLines(lines, 0, count, p);
621     }
622 
623     @Override
drawBackground(Canvas canvas)624     protected void drawBackground(Canvas canvas) {
625         int i = 0;
626         int offset = 0;
627         r.top = DAY_SEPARATOR_INNER_WIDTH;
628         r.bottom = mHeight;
629         if (mShowWeekNum) {
630             i++;
631             offset++;
632         }
633         if (!mOddMonth[i]) {
634             while (++i < mOddMonth.length && !mOddMonth[i])
635                 ;
636             r.right = computeDayLeftPosition(i - offset);
637             r.left = 0;
638             p.setColor(mMonthBGOtherColor);
639             canvas.drawRect(r, p);
640             // compute left edge for i, set up r, draw
641         } else if (!mOddMonth[(i = mOddMonth.length - 1)]) {
642             while (--i >= offset && !mOddMonth[i])
643                 ;
644             i++;
645             // compute left edge for i, set up r, draw
646             r.right = mWidth;
647             r.left = computeDayLeftPosition(i - offset);
648             p.setColor(mMonthBGOtherColor);
649             canvas.drawRect(r, p);
650         }
651         if (mHasToday) {
652             p.setColor(mMonthBGTodayColor);
653             r.left = computeDayLeftPosition(mTodayIndex);
654             r.right = computeDayLeftPosition(mTodayIndex + 1);
655             canvas.drawRect(r, p);
656         }
657     }
658 
659     // Draw the "clicked" color on the tapped day
drawClick(Canvas canvas)660     private void drawClick(Canvas canvas) {
661         if (mClickedDayIndex != -1) {
662             int alpha = p.getAlpha();
663             p.setColor(mClickedDayColor);
664             p.setAlpha(mClickedAlpha);
665             r.left = computeDayLeftPosition(mClickedDayIndex);
666             r.right = computeDayLeftPosition(mClickedDayIndex + 1);
667             r.top = DAY_SEPARATOR_INNER_WIDTH;
668             r.bottom = mHeight;
669             canvas.drawRect(r, p);
670             p.setAlpha(alpha);
671         }
672     }
673 
674     @Override
drawWeekNums(Canvas canvas)675     protected void drawWeekNums(Canvas canvas) {
676         int y;
677 
678         int i = 0;
679         int offset = -1;
680         int todayIndex = mTodayIndex;
681         int x = 0;
682         int numCount = mNumDays;
683         if (mShowWeekNum) {
684             x = SIDE_PADDING_WEEK_NUMBER + mPadding;
685             y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER;
686             canvas.drawText(mDayNumbers[0], x, y, mWeekNumPaint);
687             numCount++;
688             i++;
689             todayIndex++;
690             offset++;
691 
692         }
693 
694         y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER;
695 
696         boolean isFocusMonth = mFocusDay[i];
697         boolean isBold = false;
698         mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor);
699         for (; i < numCount; i++) {
700             if (mHasToday && todayIndex == i) {
701                 mMonthNumPaint.setColor(mMonthNumTodayColor);
702                 mMonthNumPaint.setFakeBoldText(isBold = true);
703                 if (i + 1 < numCount) {
704                     // Make sure the color will be set back on the next
705                     // iteration
706                     isFocusMonth = !mFocusDay[i + 1];
707                 }
708             } else if (mFocusDay[i] != isFocusMonth) {
709                 isFocusMonth = mFocusDay[i];
710                 mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor);
711             }
712             x = computeDayLeftPosition(i - offset) - (SIDE_PADDING_MONTH_NUMBER);
713             canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint);
714             if (isBold) {
715                 mMonthNumPaint.setFakeBoldText(isBold = false);
716             }
717         }
718     }
719 
drawEvents(Canvas canvas)720     protected void drawEvents(Canvas canvas) {
721         if (mEvents == null) {
722             return;
723         }
724 
725         int day = -1;
726         for (ArrayList<Event> eventDay : mEvents) {
727             day++;
728             if (eventDay == null || eventDay.size() == 0) {
729                 continue;
730             }
731             int ySquare;
732             int xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1;
733             int rightEdge = computeDayLeftPosition(day + 1);
734 
735             if (mOrientation == Configuration.ORIENTATION_PORTRAIT) {
736                 ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER;
737                 rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1;
738             } else {
739                 ySquare = EVENT_Y_OFFSET_LANDSCAPE;
740                 rightEdge -= EVENT_X_OFFSET_LANDSCAPE;
741             }
742 
743             // Determine if everything will fit when time ranges are shown.
744             boolean showTimes = true;
745             Iterator<Event> iter = eventDay.iterator();
746             int yTest = ySquare;
747             while (iter.hasNext()) {
748                 Event event = iter.next();
749                 int newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(),
750                         showTimes, /*doDraw*/ false);
751                 if (newY == yTest) {
752                     showTimes = false;
753                     break;
754                 }
755                 yTest = newY;
756             }
757 
758             int eventCount = 0;
759             iter = eventDay.iterator();
760             while (iter.hasNext()) {
761                 Event event = iter.next();
762                 int newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(),
763                         showTimes, /*doDraw*/ true);
764                 if (newY == ySquare) {
765                     break;
766                 }
767                 eventCount++;
768                 ySquare = newY;
769             }
770 
771             int remaining = eventDay.size() - eventCount;
772             if (remaining > 0) {
773                 drawMoreEvents(canvas, remaining, xSquare);
774             }
775         }
776     }
777 
addChipOutline(FloatRef lines, int count, int x, int y)778     protected int addChipOutline(FloatRef lines, int count, int x, int y) {
779         lines.ensureSize(count + 16);
780         // top of box
781         lines.array[count++] = x;
782         lines.array[count++] = y;
783         lines.array[count++] = x + EVENT_SQUARE_WIDTH;
784         lines.array[count++] = y;
785         // right side of box
786         lines.array[count++] = x + EVENT_SQUARE_WIDTH;
787         lines.array[count++] = y;
788         lines.array[count++] = x + EVENT_SQUARE_WIDTH;
789         lines.array[count++] = y + EVENT_SQUARE_WIDTH;
790         // left side of box
791         lines.array[count++] = x;
792         lines.array[count++] = y;
793         lines.array[count++] = x;
794         lines.array[count++] = y + EVENT_SQUARE_WIDTH + 1;
795         // bottom of box
796         lines.array[count++] = x;
797         lines.array[count++] = y + EVENT_SQUARE_WIDTH;
798         lines.array[count++] = x + EVENT_SQUARE_WIDTH + 1;
799         lines.array[count++] = y + EVENT_SQUARE_WIDTH;
800 
801         return count;
802     }
803 
804     /**
805      * Attempts to draw the given event. Returns the y for the next event or the
806      * original y if the event will not fit. An event is considered to not fit
807      * if the event and its extras won't fit or if there are more events and the
808      * more events line would not fit after drawing this event.
809      *
810      * @param canvas the canvas to draw on
811      * @param event the event to draw
812      * @param x the top left corner for this event's color chip
813      * @param y the top left corner for this event's color chip
814      * @param rightEdge the rightmost point we're allowed to draw on (exclusive)
815      * @param moreEvents indicates whether additional events will follow this one
816      * @param showTimes if set, a second line with a time range will be displayed for non-all-day
817      *   events
818      * @param doDraw if set, do the actual drawing; otherwise this just computes the height
819      *   and returns
820      * @return the y for the next event or the original y if it won't fit
821      */
drawEvent(Canvas canvas, Event event, int x, int y, int rightEdge, boolean moreEvents, boolean showTimes, boolean doDraw)822     protected int drawEvent(Canvas canvas, Event event, int x, int y, int rightEdge,
823             boolean moreEvents, boolean showTimes, boolean doDraw) {
824         /*
825          * Vertical layout:
826          *   (top of box)
827          * a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent
828          * b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event
829          * c. [optional] Time range (mExtrasHeight)
830          * d. EVENT_LINE_PADDING
831          *
832          * Repeat (b,c,d) as needed and space allows.  If we have more events than fit, we need
833          * to leave room for something like "+2" at the bottom:
834          *
835          * e. "+ more" line (mExtrasHeight)
836          *
837          * f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING)
838          *   (bottom of box)
839          */
840         final int BORDER_SPACE = EVENT_SQUARE_BORDER + 1;       // want a 1-pixel gap inside border
841         final int STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2;   // adjust bounds for stroke width
842         boolean allDay = event.allDay;
843         int eventRequiredSpace = mEventHeight;
844         if (allDay) {
845             // Add a few pixels for the box we draw around all-day events.
846             eventRequiredSpace += BORDER_SPACE * 2;
847         } else if (showTimes) {
848             // Need room for the "1pm - 2pm" line.
849             eventRequiredSpace += mExtrasHeight;
850         }
851         int reservedSpace = EVENT_BOTTOM_PADDING;   // leave a bit of room at the bottom
852         if (moreEvents) {
853             // More events follow.  Leave a bit of space between events.
854             eventRequiredSpace += EVENT_LINE_PADDING;
855 
856             // Make sure we have room for the "+ more" line.  (The "+ more" line is expected
857             // to be <= the height of an event line, so we won't show "+1" when we could be
858             // showing the event.)
859             reservedSpace += mExtrasHeight;
860         }
861 
862         if (y + eventRequiredSpace + reservedSpace > mHeight) {
863             // Not enough space, return original y
864             return y;
865         } else if (!doDraw) {
866             return y + eventRequiredSpace;
867         }
868 
869         boolean isDeclined = event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED;
870         int color = event.color;
871         if (isDeclined) {
872             color = Utils.getDeclinedColorFromColor(color);
873         }
874 
875         int textX, textY, textRightEdge;
876 
877         if (allDay) {
878             // We shift the render offset "inward", because drawRect with a stroke width greater
879             // than 1 draws outside the specified bounds.  (We don't adjust the left edge, since
880             // we want to match the existing appearance of the "event square".)
881             r.left = x;
882             r.right = rightEdge - STROKE_WIDTH_ADJ;
883             r.top = y + STROKE_WIDTH_ADJ;
884             r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ;
885             textX = x + BORDER_SPACE;
886             textY = y + mEventAscentHeight + BORDER_SPACE;
887             textRightEdge = rightEdge - BORDER_SPACE;
888         } else {
889             r.left = x;
890             r.right = x + EVENT_SQUARE_WIDTH;
891             r.bottom = y + mEventAscentHeight;
892             r.top = r.bottom - EVENT_SQUARE_WIDTH;
893             textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING;
894             textY = y + mEventAscentHeight;
895             textRightEdge = rightEdge;
896         }
897 
898         Style boxStyle = Style.STROKE;
899         boolean solidBackground = false;
900         if (event.selfAttendeeStatus != Attendees.ATTENDEE_STATUS_INVITED) {
901             boxStyle = Style.FILL_AND_STROKE;
902             if (allDay) {
903                 solidBackground = true;
904             }
905         }
906         mEventSquarePaint.setStyle(boxStyle);
907         mEventSquarePaint.setColor(color);
908         canvas.drawRect(r, mEventSquarePaint);
909 
910         float avail = textRightEdge - textX;
911         CharSequence text = TextUtils.ellipsize(
912                 event.title, mEventPaint, avail, TextUtils.TruncateAt.END);
913         Paint textPaint;
914         if (solidBackground) {
915             // Text color needs to contrast with solid background.
916             textPaint = mSolidBackgroundEventPaint;
917         } else if (isDeclined) {
918             // Use "declined event" color.
919             textPaint = mDeclinedEventPaint;
920         } else if (allDay) {
921             // Text inside frame is same color as frame.
922             mFramedEventPaint.setColor(color);
923             textPaint = mFramedEventPaint;
924         } else {
925             // Use generic event text color.
926             textPaint = mEventPaint;
927         }
928         canvas.drawText(text.toString(), textX, textY, textPaint);
929         y += mEventHeight;
930         if (allDay) {
931             y += BORDER_SPACE * 2;
932         }
933 
934         if (showTimes && !allDay) {
935             // show start/end time, e.g. "1pm - 2pm"
936             textY = y + mExtrasAscentHeight;
937             mStringBuilder.setLength(0);
938             text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis,
939                     event.endMillis, DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL,
940                     Utils.getTimeZone(getContext(), null)).toString();
941             text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END);
942             canvas.drawText(text.toString(), textX, textY, isDeclined ? mEventDeclinedExtrasPaint
943                     : mEventExtrasPaint);
944             y += mExtrasHeight;
945         }
946 
947         y += EVENT_LINE_PADDING;
948 
949         return y;
950     }
951 
drawMoreEvents(Canvas canvas, int remainingEvents, int x)952     protected void drawMoreEvents(Canvas canvas, int remainingEvents, int x) {
953         int y = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING);
954         String text = getContext().getResources().getQuantityString(
955                 R.plurals.month_more_events, remainingEvents);
956         mEventExtrasPaint.setAntiAlias(true);
957         mEventExtrasPaint.setFakeBoldText(true);
958         canvas.drawText(String.format(text, remainingEvents), x, y, mEventExtrasPaint);
959         mEventExtrasPaint.setFakeBoldText(false);
960     }
961 
962     /**
963      * Draws a line showing busy times in each day of week The method draws
964      * non-conflicting times in the event color and times with conflicting
965      * events in the dna conflict color defined in colors.
966      *
967      * @param canvas
968      */
drawDNA(Canvas canvas)969     protected void drawDNA(Canvas canvas) {
970         // Draw event and conflict times
971         if (mDna != null) {
972             for (Utils.DNAStrand strand : mDna.values()) {
973                 if (strand.color == CONFLICT_COLOR || strand.points == null
974                         || strand.points.length == 0) {
975                     continue;
976                 }
977                 mDNATimePaint.setColor(strand.color);
978                 canvas.drawLines(strand.points, mDNATimePaint);
979             }
980             // Draw black last to make sure it's on top
981             Utils.DNAStrand strand = mDna.get(CONFLICT_COLOR);
982             if (strand != null && strand.points != null && strand.points.length != 0) {
983                 mDNATimePaint.setColor(strand.color);
984                 canvas.drawLines(strand.points, mDNATimePaint);
985             }
986             if (mDayXs == null) {
987                 return;
988             }
989             int numDays = mDayXs.length;
990             int xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2;
991             if (strand != null && strand.allDays != null && strand.allDays.length == numDays) {
992                 for (int i = 0; i < numDays; i++) {
993                     // this adds at most 7 draws. We could sort it by color and
994                     // build an array instead but this is easier.
995                     if (strand.allDays[i] != 0) {
996                         mDNAAllDayPaint.setColor(strand.allDays[i]);
997                         canvas.drawLine(mDayXs[i] + xOffset, DNA_MARGIN, mDayXs[i] + xOffset,
998                                 DNA_MARGIN + DNA_ALL_DAY_HEIGHT, mDNAAllDayPaint);
999                     }
1000                 }
1001             }
1002         }
1003     }
1004 
1005     @Override
updateSelectionPositions()1006     protected void updateSelectionPositions() {
1007         if (mHasSelectedDay) {
1008             int selectedPosition = mSelectedDay - mWeekStart;
1009             if (selectedPosition < 0) {
1010                 selectedPosition += 7;
1011             }
1012             int effectiveWidth = mWidth - mPadding * 2;
1013             effectiveWidth -= SPACING_WEEK_NUMBER;
1014             mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding;
1015             mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding;
1016             mSelectedLeft += SPACING_WEEK_NUMBER;
1017             mSelectedRight += SPACING_WEEK_NUMBER;
1018         }
1019     }
1020 
getDayIndexFromLocation(float x)1021     public int getDayIndexFromLocation(float x) {
1022         int dayStart = mShowWeekNum ? SPACING_WEEK_NUMBER + mPadding : mPadding;
1023         if (x < dayStart || x > mWidth - mPadding) {
1024             return -1;
1025         }
1026         // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
1027         return ((int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)));
1028     }
1029 
1030     @Override
getDayFromLocation(float x)1031     public Time getDayFromLocation(float x) {
1032         int dayPosition = getDayIndexFromLocation(x);
1033         if (dayPosition == -1) {
1034             return null;
1035         }
1036         int day = mFirstJulianDay + dayPosition;
1037 
1038         Time time = new Time(mTimeZone);
1039         if (mWeek == 0) {
1040             // This week is weird...
1041             if (day < Time.EPOCH_JULIAN_DAY) {
1042                 day++;
1043             } else if (day == Time.EPOCH_JULIAN_DAY) {
1044                 time.set(1, 0, 1970);
1045                 time.normalize(true);
1046                 return time;
1047             }
1048         }
1049 
1050         time.setJulianDay(day);
1051         return time;
1052     }
1053 
1054     @Override
onHoverEvent(MotionEvent event)1055     public boolean onHoverEvent(MotionEvent event) {
1056         Context context = getContext();
1057         // only send accessibility events if accessibility and exploration are
1058         // on.
1059         AccessibilityManager am = (AccessibilityManager) context
1060                 .getSystemService(Service.ACCESSIBILITY_SERVICE);
1061         if (!am.isEnabled() || !am.isTouchExplorationEnabled()) {
1062             return super.onHoverEvent(event);
1063         }
1064         if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
1065             Time hover = getDayFromLocation(event.getX());
1066             if (hover != null
1067                     && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) {
1068                 Long millis = hover.toMillis(true);
1069                 String date = Utils.formatDateRange(context, millis, millis,
1070                         DateUtils.FORMAT_SHOW_DATE);
1071                 AccessibilityEvent accessEvent = AccessibilityEvent
1072                         .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
1073                 accessEvent.getText().add(date);
1074                 if (mShowDetailsInMonth && mEvents != null) {
1075                     int dayStart = SPACING_WEEK_NUMBER + mPadding;
1076                     int dayPosition = (int) ((event.getX() - dayStart) * mNumDays / (mWidth
1077                             - dayStart - mPadding));
1078                     ArrayList<Event> events = mEvents.get(dayPosition);
1079                     List<CharSequence> text = accessEvent.getText();
1080                     for (Event e : events) {
1081                         text.add(e.getTitleAndLocation() + ". ");
1082                         int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
1083                         if (!e.allDay) {
1084                             flags |= DateUtils.FORMAT_SHOW_TIME;
1085                             if (DateFormat.is24HourFormat(context)) {
1086                                 flags |= DateUtils.FORMAT_24HOUR;
1087                             }
1088                         } else {
1089                             flags |= DateUtils.FORMAT_UTC;
1090                         }
1091                         text.add(Utils.formatDateRange(context, e.startMillis, e.endMillis,
1092                                 flags) + ". ");
1093                     }
1094                 }
1095                 sendAccessibilityEventUnchecked(accessEvent);
1096                 mLastHoverTime = hover;
1097             }
1098         }
1099         return true;
1100     }
1101 
setClickedDay(float xLocation)1102     public void setClickedDay(float xLocation) {
1103         mClickedDayIndex = getDayIndexFromLocation(xLocation);
1104         invalidate();
1105     }
clearClickedDay()1106     public void clearClickedDay() {
1107         mClickedDayIndex = -1;
1108         invalidate();
1109     }
1110 }
1111