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.R;
20import com.android.calendar.Utils;
21
22import android.app.Service;
23import android.content.Context;
24import android.content.res.Resources;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Paint.Align;
28import android.graphics.Paint.Style;
29import android.graphics.Rect;
30import android.graphics.drawable.Drawable;
31import android.text.format.DateUtils;
32import android.text.format.Time;
33import android.view.MotionEvent;
34import android.view.View;
35import android.view.accessibility.AccessibilityEvent;
36import android.view.accessibility.AccessibilityManager;
37
38import java.security.InvalidParameterException;
39import java.util.HashMap;
40
41/**
42 * <p>
43 * This is a dynamic view for drawing a single week. It can be configured to
44 * display the week number, start the week on a given day, or show a reduced
45 * number of days. It is intended for use as a single view within a ListView.
46 * See {@link SimpleWeeksAdapter} for usage.
47 * </p>
48 */
49public class SimpleWeekView extends View {
50    private static final String TAG = "MonthView";
51
52    /**
53     * These params can be passed into the view to control how it appears.
54     * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default
55     * values are unlikely to fit most layouts correctly.
56     */
57    /**
58     * This sets the height of this week in pixels
59     */
60    public static final String VIEW_PARAMS_HEIGHT = "height";
61    /**
62     * This specifies the position (or weeks since the epoch) of this week,
63     * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
64     */
65    public static final String VIEW_PARAMS_WEEK = "week";
66    /**
67     * This sets one of the days in this view as selected {@link Time#SUNDAY}
68     * through {@link Time#SATURDAY}.
69     */
70    public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day";
71    /**
72     * Which day the week should start on. {@link Time#SUNDAY} through
73     * {@link Time#SATURDAY}.
74     */
75    public static final String VIEW_PARAMS_WEEK_START = "week_start";
76    /**
77     * How many days to display at a time. Days will be displayed starting with
78     * {@link #mWeekStart}.
79     */
80    public static final String VIEW_PARAMS_NUM_DAYS = "num_days";
81    /**
82     * Which month is currently in focus, as defined by {@link Time#month}
83     * [0-11].
84     */
85    public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month";
86    /**
87     * If this month should display week numbers. false if 0, true otherwise.
88     */
89    public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num";
90
91    protected static int DEFAULT_HEIGHT = 32;
92    protected static int MIN_HEIGHT = 10;
93    protected static final int DEFAULT_SELECTED_DAY = -1;
94    protected static final int DEFAULT_WEEK_START = Time.SUNDAY;
95    protected static final int DEFAULT_NUM_DAYS = 7;
96    protected static final int DEFAULT_SHOW_WK_NUM = 0;
97    protected static final int DEFAULT_FOCUS_MONTH = -1;
98
99    protected static int DAY_SEPARATOR_WIDTH = 1;
100
101    protected static int MINI_DAY_NUMBER_TEXT_SIZE = 14;
102    protected static int MINI_WK_NUMBER_TEXT_SIZE = 12;
103    protected static int MINI_TODAY_NUMBER_TEXT_SIZE = 18;
104    protected static int MINI_TODAY_OUTLINE_WIDTH = 2;
105    protected static int WEEK_NUM_MARGIN_BOTTOM = 4;
106
107    // used for scaling to the device density
108    protected static float mScale = 0;
109
110    // affects the padding on the sides of this view
111    protected int mPadding = 0;
112
113    protected Rect r = new Rect();
114    protected Paint p = new Paint();
115    protected Paint mMonthNumPaint;
116    protected Drawable mSelectedDayLine;
117
118    // Cache the number strings so we don't have to recompute them each time
119    protected String[] mDayNumbers;
120    // Quick lookup for checking which days are in the focus month
121    protected boolean[] mFocusDay;
122    // Quick lookup for checking which days are in an odd month (to set a different background)
123    protected boolean[] mOddMonth;
124    // The Julian day of the first day displayed by this item
125    protected int mFirstJulianDay = -1;
126    // The month of the first day in this week
127    protected int mFirstMonth = -1;
128    // The month of the last day in this week
129    protected int mLastMonth = -1;
130    // The position of this week, equivalent to weeks since the week of Jan 1st,
131    // 1970
132    protected int mWeek = -1;
133    // Quick reference to the width of this view, matches parent
134    protected int mWidth;
135    // The height this view should draw at in pixels, set by height param
136    protected int mHeight = DEFAULT_HEIGHT;
137    // Whether the week number should be shown
138    protected boolean mShowWeekNum = false;
139    // If this view contains the selected day
140    protected boolean mHasSelectedDay = false;
141    // If this view contains the today
142    protected boolean mHasToday = false;
143    // Which day is selected [0-6] or -1 if no day is selected
144    protected int mSelectedDay = DEFAULT_SELECTED_DAY;
145    // Which day is today [0-6] or -1 if no day is today
146    protected int mToday = DEFAULT_SELECTED_DAY;
147    // Which day of the week to start on [0-6]
148    protected int mWeekStart = DEFAULT_WEEK_START;
149    // How many days to display
150    protected int mNumDays = DEFAULT_NUM_DAYS;
151    // The number of days + a spot for week number if it is displayed
152    protected int mNumCells = mNumDays;
153    // The left edge of the selected day
154    protected int mSelectedLeft = -1;
155    // The right edge of the selected day
156    protected int mSelectedRight = -1;
157    // The timezone to display times/dates in (used for determining when Today
158    // is)
159    protected String mTimeZone = Time.getCurrentTimezone();
160
161    protected int mBGColor;
162    protected int mSelectedWeekBGColor;
163    protected int mFocusMonthColor;
164    protected int mOtherMonthColor;
165    protected int mDaySeparatorColor;
166    protected int mTodayOutlineColor;
167    protected int mWeekNumColor;
168
169    public SimpleWeekView(Context context) {
170        super(context);
171
172        Resources res = context.getResources();
173
174        mBGColor = res.getColor(R.color.month_bgcolor);
175        mSelectedWeekBGColor = res.getColor(R.color.month_selected_week_bgcolor);
176        mFocusMonthColor = res.getColor(R.color.month_mini_day_number);
177        mOtherMonthColor = res.getColor(R.color.month_other_month_day_number);
178        mDaySeparatorColor = res.getColor(R.color.month_grid_lines);
179        mTodayOutlineColor = res.getColor(R.color.mini_month_today_outline_color);
180        mWeekNumColor = res.getColor(R.color.month_week_num_color);
181        mSelectedDayLine = res.getDrawable(R.drawable.dayline_minical_holo_light);
182
183        if (mScale == 0) {
184            mScale = context.getResources().getDisplayMetrics().density;
185            if (mScale != 1) {
186                DEFAULT_HEIGHT *= mScale;
187                MIN_HEIGHT *= mScale;
188                MINI_DAY_NUMBER_TEXT_SIZE *= mScale;
189                MINI_TODAY_NUMBER_TEXT_SIZE *= mScale;
190                MINI_TODAY_OUTLINE_WIDTH *= mScale;
191                WEEK_NUM_MARGIN_BOTTOM *= mScale;
192                DAY_SEPARATOR_WIDTH *= mScale;
193                MINI_WK_NUMBER_TEXT_SIZE *= mScale;
194            }
195        }
196
197        // Sets up any standard paints that will be used
198        initView();
199    }
200
201    /**
202     * Sets all the parameters for displaying this week. The only required
203     * parameter is the week number. Other parameters have a default value and
204     * will only update if a new value is included, except for focus month,
205     * which will always default to no focus month if no value is passed in. See
206     * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
207     *
208     * @param params A map of the new parameters, see
209     *            {@link #VIEW_PARAMS_HEIGHT}
210     * @param tz The time zone this view should reference times in
211     */
212    public void setWeekParams(HashMap<String, Integer> params, String tz) {
213        if (!params.containsKey(VIEW_PARAMS_WEEK)) {
214            throw new InvalidParameterException("You must specify the week number for this view");
215        }
216        setTag(params);
217        mTimeZone = tz;
218        // We keep the current value for any params not present
219        if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
220            mHeight = params.get(VIEW_PARAMS_HEIGHT);
221            if (mHeight < MIN_HEIGHT) {
222                mHeight = MIN_HEIGHT;
223            }
224        }
225        if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
226            mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
227        }
228        mHasSelectedDay = mSelectedDay != -1;
229        if (params.containsKey(VIEW_PARAMS_NUM_DAYS)) {
230            mNumDays = params.get(VIEW_PARAMS_NUM_DAYS);
231        }
232        if (params.containsKey(VIEW_PARAMS_SHOW_WK_NUM)) {
233            if (params.get(VIEW_PARAMS_SHOW_WK_NUM) != 0) {
234                mShowWeekNum = true;
235            } else {
236                mShowWeekNum = false;
237            }
238        }
239        mNumCells = mShowWeekNum ? mNumDays + 1 : mNumDays;
240
241        // Allocate space for caching the day numbers and focus values
242        mDayNumbers = new String[mNumCells];
243        mFocusDay = new boolean[mNumCells];
244        mOddMonth = new boolean[mNumCells];
245        mWeek = params.get(VIEW_PARAMS_WEEK);
246        int julianMonday = Utils.getJulianMondayFromWeeksSinceEpoch(mWeek);
247        Time time = new Time(tz);
248        time.setJulianDay(julianMonday);
249
250        // If we're showing the week number calculate it based on Monday
251        int i = 0;
252        if (mShowWeekNum) {
253            mDayNumbers[0] = Integer.toString(time.getWeekNumber());
254            i++;
255        }
256
257        if (params.containsKey(VIEW_PARAMS_WEEK_START)) {
258            mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
259        }
260
261        // Now adjust our starting day based on the start day of the week
262        // If the week is set to start on a Saturday the first week will be
263        // Dec 27th 1969 -Jan 2nd, 1970
264        if (time.weekDay != mWeekStart) {
265            int diff = time.weekDay - mWeekStart;
266            if (diff < 0) {
267                diff += 7;
268            }
269            time.monthDay -= diff;
270            time.normalize(true);
271        }
272
273        mFirstJulianDay = Time.getJulianDay(time.toMillis(true), time.gmtoff);
274        mFirstMonth = time.month;
275
276        // Figure out what day today is
277        Time today = new Time(tz);
278        today.setToNow();
279        mHasToday = false;
280        mToday = -1;
281
282        int focusMonth = params.containsKey(VIEW_PARAMS_FOCUS_MONTH) ? params.get(
283                VIEW_PARAMS_FOCUS_MONTH)
284                : DEFAULT_FOCUS_MONTH;
285
286        for (; i < mNumCells; i++) {
287            if (time.monthDay == 1) {
288                mFirstMonth = time.month;
289            }
290            mOddMonth [i] = (time.month %2) == 1;
291            if (time.month == focusMonth) {
292                mFocusDay[i] = true;
293            } else {
294                mFocusDay[i] = false;
295            }
296            if (time.year == today.year && time.yearDay == today.yearDay) {
297                mHasToday = true;
298                mToday = i;
299            }
300            mDayNumbers[i] = Integer.toString(time.monthDay++);
301            time.normalize(true);
302        }
303        // We do one extra add at the end of the loop, if that pushed us to a
304        // new month undo it
305        if (time.monthDay == 1) {
306            time.monthDay--;
307            time.normalize(true);
308        }
309        mLastMonth = time.month;
310
311        updateSelectionPositions();
312    }
313
314    /**
315     * Sets up the text and style properties for painting. Override this if you
316     * want to use a different paint.
317     */
318    protected void initView() {
319        p.setFakeBoldText(false);
320        p.setAntiAlias(true);
321        p.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
322        p.setStyle(Style.FILL);
323
324        mMonthNumPaint = new Paint();
325        mMonthNumPaint.setFakeBoldText(true);
326        mMonthNumPaint.setAntiAlias(true);
327        mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
328        mMonthNumPaint.setColor(mFocusMonthColor);
329        mMonthNumPaint.setStyle(Style.FILL);
330        mMonthNumPaint.setTextAlign(Align.CENTER);
331    }
332
333    /**
334     * Returns the month of the first day in this week
335     *
336     * @return The month the first day of this view is in
337     */
338    public int getFirstMonth() {
339        return mFirstMonth;
340    }
341
342    /**
343     * Returns the month of the last day in this week
344     *
345     * @return The month the last day of this view is in
346     */
347    public int getLastMonth() {
348        return mLastMonth;
349    }
350
351    /**
352     * Returns the julian day of the first day in this view.
353     *
354     * @return The julian day of the first day in the view.
355     */
356    public int getFirstJulianDay() {
357        return mFirstJulianDay;
358    }
359
360    /**
361     * Calculates the day that the given x position is in, accounting for week
362     * number. Returns a Time referencing that day or null if
363     *
364     * @param x The x position of the touch event
365     * @return A time object for the tapped day or null if the position wasn't
366     *         in a day
367     */
368    public Time getDayFromLocation(float x) {
369        int dayStart = mShowWeekNum ? (mWidth - mPadding * 2) / mNumCells + mPadding : mPadding;
370        if (x < dayStart || x > mWidth - mPadding) {
371            return null;
372        }
373        // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
374        int dayPosition = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
375        int day = mFirstJulianDay + dayPosition;
376
377        Time time = new Time(mTimeZone);
378        if (mWeek == 0) {
379            // This week is weird...
380            if (day < Time.EPOCH_JULIAN_DAY) {
381                day++;
382            } else if (day == Time.EPOCH_JULIAN_DAY) {
383                time.set(1, 0, 1970);
384                time.normalize(true);
385                return time;
386            }
387        }
388
389        time.setJulianDay(day);
390        return time;
391    }
392
393    @Override
394    protected void onDraw(Canvas canvas) {
395        drawBackground(canvas);
396        drawWeekNums(canvas);
397        drawDaySeparators(canvas);
398    }
399
400    /**
401     * This draws the selection highlight if a day is selected in this week.
402     * Override this method if you wish to have a different background drawn.
403     *
404     * @param canvas The canvas to draw on
405     */
406    protected void drawBackground(Canvas canvas) {
407        if (mHasSelectedDay) {
408            p.setColor(mSelectedWeekBGColor);
409            p.setStyle(Style.FILL);
410        } else {
411            return;
412        }
413        r.top = 1;
414        r.bottom = mHeight - 1;
415        r.left = mPadding;
416        r.right = mSelectedLeft;
417        canvas.drawRect(r, p);
418        r.left = mSelectedRight;
419        r.right = mWidth - mPadding;
420        canvas.drawRect(r, p);
421    }
422
423    /**
424     * Draws the week and month day numbers for this week. Override this method
425     * if you need different placement.
426     *
427     * @param canvas The canvas to draw on
428     */
429    protected void drawWeekNums(Canvas canvas) {
430        int y = ((mHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH;
431        int nDays = mNumCells;
432
433        int i = 0;
434        int divisor = 2 * nDays;
435        if (mShowWeekNum) {
436            p.setTextSize(MINI_WK_NUMBER_TEXT_SIZE);
437            p.setStyle(Style.FILL);
438            p.setTextAlign(Align.CENTER);
439            p.setAntiAlias(true);
440            p.setColor(mWeekNumColor);
441            int x = (mWidth - mPadding * 2) / divisor + mPadding;
442            canvas.drawText(mDayNumbers[0], x, y, p);
443            i++;
444        }
445
446        boolean isFocusMonth = mFocusDay[i];
447        mMonthNumPaint.setColor(isFocusMonth ? mFocusMonthColor : mOtherMonthColor);
448        mMonthNumPaint.setFakeBoldText(false);
449        for (; i < nDays; i++) {
450            if (mFocusDay[i] != isFocusMonth) {
451                isFocusMonth = mFocusDay[i];
452                mMonthNumPaint.setColor(isFocusMonth ? mFocusMonthColor : mOtherMonthColor);
453            }
454            if (mHasToday && mToday == i) {
455                mMonthNumPaint.setTextSize(MINI_TODAY_NUMBER_TEXT_SIZE);
456                mMonthNumPaint.setFakeBoldText(true);
457            }
458            int x = (2 * i + 1) * (mWidth - mPadding * 2) / (divisor) + mPadding;
459            canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint);
460            if (mHasToday && mToday == i) {
461                mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
462                mMonthNumPaint.setFakeBoldText(false);
463            }
464        }
465    }
466
467    /**
468     * Draws a horizontal line for separating the weeks. Override this method if
469     * you want custom separators.
470     *
471     * @param canvas The canvas to draw on
472     */
473    protected void drawDaySeparators(Canvas canvas) {
474        if (mHasSelectedDay) {
475            r.top = 1;
476            r.bottom = mHeight - 1;
477            r.left = mSelectedLeft + 1;
478            r.right = mSelectedRight - 1;
479            p.setStrokeWidth(MINI_TODAY_OUTLINE_WIDTH);
480            p.setStyle(Style.STROKE);
481            p.setColor(mTodayOutlineColor);
482            canvas.drawRect(r, p);
483        }
484        if (mShowWeekNum) {
485            p.setColor(mDaySeparatorColor);
486            p.setStrokeWidth(DAY_SEPARATOR_WIDTH);
487
488            int x = (mWidth - mPadding * 2) / mNumCells + mPadding;
489            canvas.drawLine(x, 0, x, mHeight, p);
490        }
491    }
492
493    @Override
494    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
495        mWidth = w;
496        updateSelectionPositions();
497    }
498
499    /**
500     * This calculates the positions for the selected day lines.
501     */
502    protected void updateSelectionPositions() {
503        if (mHasSelectedDay) {
504            int selectedPosition = mSelectedDay - mWeekStart;
505            if (selectedPosition < 0) {
506                selectedPosition += 7;
507            }
508            if (mShowWeekNum) {
509                selectedPosition++;
510            }
511            mSelectedLeft = selectedPosition * (mWidth - mPadding * 2) / mNumCells
512                    + mPadding;
513            mSelectedRight = (selectedPosition + 1) * (mWidth - mPadding * 2) / mNumCells
514                    + mPadding;
515        }
516    }
517
518    @Override
519    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
520        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
521    }
522
523    @Override
524    public boolean onHoverEvent(MotionEvent event) {
525        Context context = getContext();
526        // only send accessibility events if accessibility and exploration are
527        // on.
528        AccessibilityManager am = (AccessibilityManager) context
529                .getSystemService(Service.ACCESSIBILITY_SERVICE);
530        if (!am.isEnabled() || !am.isTouchExplorationEnabled()) {
531            return super.onHoverEvent(event);
532        }
533        if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
534            Time hover = getDayFromLocation(event.getX());
535            if (hover != null
536                    && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) {
537                Long millis = hover.toMillis(true);
538                String date = Utils.formatDateRange(context, millis, millis,
539                        DateUtils.FORMAT_SHOW_DATE);
540                AccessibilityEvent accessEvent =
541                    AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
542                accessEvent.getText().add(date);
543                sendAccessibilityEventUnchecked(accessEvent);
544                mLastHoverTime = hover;
545            }
546        }
547        return true;
548    }
549
550    Time mLastHoverTime = null;
551}