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