1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.calendar.month;
18 
19 import com.android.calendar.R;
20 import com.android.calendar.Utils;
21 
22 import android.app.Service;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Canvas;
26 import android.graphics.Paint;
27 import android.graphics.Paint.Align;
28 import android.graphics.Paint.Style;
29 import android.graphics.Rect;
30 import android.graphics.drawable.Drawable;
31 import android.text.format.DateUtils;
32 import android.text.format.Time;
33 import android.view.MotionEvent;
34 import android.view.View;
35 import android.view.accessibility.AccessibilityEvent;
36 import android.view.accessibility.AccessibilityManager;
37 
38 import java.security.InvalidParameterException;
39 import 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  */
49 public 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 
SimpleWeekView(Context context)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      */
setWeekParams(HashMap<String, Integer> params, String tz)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      */
initView()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      */
getFirstMonth()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      */
getLastMonth()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      */
getFirstJulianDay()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      */
getDayFromLocation(float x)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
onDraw(Canvas canvas)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      */
drawBackground(Canvas canvas)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      */
drawWeekNums(Canvas canvas)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      */
drawDaySeparators(Canvas canvas)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
onSizeChanged(int w, int h, int oldw, int oldh)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      */
updateSelectionPositions()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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)519     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
520         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
521     }
522 
523     @Override
onHoverEvent(MotionEvent event)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 }