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    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
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
216        public void setAnimator(Animator animation) {
217            mAnimator = animation;
218        }
219
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
235        public FloatRef(int size) {
236            array = new float[size];
237        }
238
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     */
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.
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     */
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
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
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
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
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     */
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
536    public void setAnimateTodayAlpha(int alpha) {
537        mAnimateTodayAlpha = alpha;
538        invalidate();
539    }
540
541    @Override
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
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
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
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
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
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
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
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
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     */
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
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     */
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
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
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
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
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
1102    public void setClickedDay(float xLocation) {
1103        mClickedDayIndex = getDayIndexFromLocation(xLocation);
1104        invalidate();
1105    }
1106    public void clearClickedDay() {
1107        mClickedDayIndex = -1;
1108        invalidate();
1109    }
1110}
1111