2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.calendar;
19 import static android.provider.Calendar.EVENT_BEGIN_TIME;
20 import static android.provider.Calendar.EVENT_END_TIME;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.Resources;
27 import android.content.res.TypedArray;
28 import android.database.Cursor;
29 import android.graphics.Bitmap;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.Paint;
33 import android.graphics.Paint.Style;
34 import android.graphics.Path;
35 import android.graphics.Path.Direction;
36 import android.graphics.PorterDuff;
37 import android.graphics.Rect;
38 import android.graphics.RectF;
39 import android.graphics.Typeface;
40 import android.net.Uri;
41 import android.os.Bundle;
42 import android.os.Handler;
43 import android.provider.Calendar.Attendees;
44 import android.provider.Calendar.Calendars;
45 import android.provider.Calendar.Events;
46 import android.text.TextUtils;
47 import android.text.format.DateFormat;
48 import android.text.format.DateUtils;
49 import android.text.format.Time;
50 import android.util.Log;
51 import android.view.ContextMenu;
52 import android.view.ContextMenu.ContextMenuInfo;
53 import android.view.Gravity;
54 import android.view.KeyEvent;
55 import android.view.LayoutInflater;
56 import android.view.MenuItem;
57 import android.view.MotionEvent;
58 import android.view.View;
59 import android.view.ViewConfiguration;
60 import android.view.ViewGroup;
61 import android.view.WindowManager;
62 import android.view.accessibility.AccessibilityEvent;
63 import android.view.accessibility.AccessibilityManager;
64 import android.widget.ImageView;
65 import android.widget.PopupWindow;
66 import android.widget.TextView;
68 import java.util.ArrayList;
69 import java.util.Calendar;
70 import java.util.Locale;
71 import java.util.TimeZone;
72 import java.util.regex.Matcher;
73 import java.util.regex.Pattern;
76 * This is the base class for a set of classes that implement views (day view
77 * and week view to start with) that share some common code.
79 public class CalendarView extends View
80 implements View.OnCreateContextMenuListener, View.OnClickListener {
82 private static float mScale = 0; // Used for supporting different screen densities
83 private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event
85 private boolean mOnFlingCalled;
87 * ID of the last event which was displayed with the toast popup.
89 * This is used to prevent popping up multiple quick views for the same event, especially
90 * during calendar syncs. This becomes valid when an event is selected, either by default
91 * on starting calendar or by scrolling to an event. It becomes invalid when the user
92 * explicitly scrolls to an empty time slot, changes views, or deletes the event.
94 private long mLastPopupEventID;
96 protected CalendarApplication mCalendarApp;
97 protected CalendarActivity mParentActivity;
99 // This runs when we need to update the tz
100 private Runnable mUpdateTZ = new Runnable() {
103 String tz = Utils.getTimeZone(mContext, this);
104 // BaseDate we want to keep on the same day, so we swap tz
105 mBaseDate.timezone = tz;
106 mBaseDate.normalize(true);
107 // CurrentTime we want to keep at the same absolute time, so we
109 mCurrentTime.switchTimezone(tz);
110 mTimeZone = TimeZone.getTimeZone(tz);
112 mTitleTextView.setText(mDateRange);
115 private Context mContext;
117 private static final String[] CALENDARS_PROJECTION = new String[] {
119 Calendars.ACCESS_LEVEL, // 1
120 Calendars.OWNER_ACCOUNT, // 2
122 private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
123 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
124 private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
126 private static final String[] ATTENDEES_PROJECTION = new String[] {
128 Attendees.ATTENDEE_RELATIONSHIP, // 1
130 private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
131 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
133 private static float SMALL_ROUND_RADIUS = 3.0F;
135 private static final int FROM_NONE = 0;
136 private static final int FROM_ABOVE = 1;
137 private static final int FROM_BELOW = 2;
138 private static final int FROM_LEFT = 4;
139 private static final int FROM_RIGHT = 8;
141 private static final int ACCESS_LEVEL_NONE = 0;
142 private static final int ACCESS_LEVEL_DELETE = 1;
143 private static final int ACCESS_LEVEL_EDIT = 2;
145 private static int HORIZONTAL_SCROLL_THRESHOLD = 50;
147 private ContinueScroll mContinueScroll = new ContinueScroll();
149 static private class DayHeader{
154 private DayHeader[] dayHeaders = new DayHeader[32];
156 // Make this visible within the package for more informative debugging
158 private Time mCurrentTime;
159 //Update the current time line every five minutes if the window is left open that long
160 private static final int UPDATE_CURRENT_TIME_DELAY = 300000;
161 private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime();
162 private int mTodayJulianDay;
164 private Typeface mBold = Typeface.DEFAULT_BOLD;
165 private int mFirstJulianDay;
166 private int mLastJulianDay;
168 private int mMonthLength;
169 private int mFirstDate;
170 private int[] mEarliestStartHour; // indexed by the week day offset
171 private boolean[] mHasAllDayEvent; // indexed by the week day offset
173 private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
176 * This variable helps to avoid unnecessarily reloading events by keeping
177 * track of the start millis parameter used for the most recent loading
178 * of events. If the next reload matches this, then the events are not
179 * reloaded. To force a reload, set this to zero (this is set to zero
180 * in the method clearCachedEvents()).
182 private long mLastReloadMillis;
184 private ArrayList<Event> mEvents = new ArrayList<Event>();
185 private int mSelectionDay; // Julian day
186 private int mSelectionHour;
188 /* package private so that CalendarActivity can read it when creating new
191 boolean mSelectionAllDay;
193 private int mCellWidth;
195 // Pre-allocate these objects and re-use them
196 private Rect mRect = new Rect();
197 private RectF mRectF = new RectF();
198 private Rect mSrcRect = new Rect();
199 private Rect mDestRect = new Rect();
200 private Paint mPaint = new Paint();
201 private Paint mPaintBorder = new Paint();
202 private Paint mEventTextPaint = new Paint();
203 private Paint mSelectionPaint = new Paint();
204 private Path mPath = new Path();
206 protected boolean mDrawTextInEventRect;
207 private int mStartDay;
209 private PopupWindow mPopup;
210 private View mPopupView;
212 // The number of milliseconds to show the popup window
213 private static final int POPUP_DISMISS_DELAY = 3000;
214 private DismissPopup mDismissPopup = new DismissPopup();
216 // For drawing to an off-screen Canvas
217 private Bitmap mBitmap;
218 private Canvas mCanvas;
219 private boolean mRedrawScreen = true;
220 private boolean mRemeasure = true;
222 private final EventLoader mEventLoader;
223 protected final EventGeometry mEventGeometry;
225 private static final int DAY_GAP = 1;
226 private static final int HOUR_GAP = 1;
227 private static int SINGLE_ALLDAY_HEIGHT = 20;
228 private static int MAX_ALLDAY_HEIGHT = 72;
229 private static int ALLDAY_TOP_MARGIN = 3;
230 private static int MAX_ALLDAY_EVENT_HEIGHT = 18;
232 /* The extra space to leave above the text in all-day events */
233 private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
235 /* The extra space to leave above the text in normal events */
236 private static final int NORMAL_TEXT_TOP_MARGIN = 2;
238 private static final int HOURS_LEFT_MARGIN = 2;
239 private static final int HOURS_RIGHT_MARGIN = 4;
240 private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
242 private static int CURRENT_TIME_LINE_HEIGHT = 2;
243 private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1;
244 private static int CURRENT_TIME_MARKER_INNER_WIDTH = 6;
245 private static int CURRENT_TIME_MARKER_HEIGHT = 6;
246 private static int CURRENT_TIME_MARKER_WIDTH = 8;
247 private static int CURRENT_TIME_LINE_SIDE_BUFFER = 1;
249 /* package */ static final int MINUTES_PER_HOUR = 60;
250 /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
251 /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
252 /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
253 /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
255 private static int NORMAL_FONT_SIZE = 12;
256 private static int EVENT_TEXT_FONT_SIZE = 12;
257 private static int HOURS_FONT_SIZE = 12;
258 private static int AMPM_FONT_SIZE = 9;
259 private static int MIN_CELL_WIDTH_FOR_TEXT = 27;
260 private static final int MAX_EVENT_TEXT_LEN = 500;
261 private static float MIN_EVENT_HEIGHT = 15.0F; // in pixels
263 // This value forces the position calculator to take care of the overwap which can't be
264 // detected from the view of event time but actually is detected when rendering them.
267 // Imagine there are two events: A (from 1:00pm to 1:01pm) and B (from 1:02pm to 2:00pm).
268 // The position calculator (Event#doComputePositions()), marks them as "not overwrapped"
269 // as A finishes before B's begin time, so those events are put on the same column
270 // (or, horizontal position).
271 // From the view of renderer, however, the actual rectangle for A is larger than "1 min."
272 // for accomodating at least 1 line of text in it.
273 // As a result, A's rectangle is overwrapped by B's, and A becomes hard to be touched
274 // without trackball or DPAD (as, it is beneath B from the user' view).
275 // This values forces the original calculator to take care of the actual overwrap detected in
279 // Theoretically we can calcurate an ideal value for this purpose by making the calculator
280 // understand the relation between each event and pixel-level height of actual rectangles,
281 // but we don't do so as currently the calculator doesn't have convenient way to obtain
282 // necessary values for the calculation.
283 /* package */ static long EVENT_OVERWRAP_MARGIN_TIME = MILLIS_PER_MINUTE * 15;
285 private static int mSelectionColor;
286 private static int mPressedColor;
287 private static int mSelectedEventTextColor;
288 private static int mEventTextColor;
289 private static int mWeek_saturdayColor;
290 private static int mWeek_sundayColor;
291 private static int mCalendarDateBannerTextColor;
292 private static int mCalendarAllDayBackground;
293 private static int mCalendarAmPmLabel;
294 private static int mCalendarDateBannerBackground;
295 private static int mCalendarDateSelected;
296 private static int mCalendarGridAreaBackground;
297 private static int mCalendarGridAreaSelected;
298 private static int mCalendarGridLineHorizontalColor;
299 private static int mCalendarGridLineVerticalColor;
300 private static int mCalendarHourBackground;
301 private static int mCalendarHourLabel;
302 private static int mCalendarHourSelected;
303 private static int mCurrentTimeMarkerColor;
304 private static int mCurrentTimeLineColor;
305 private static int mCurrentTimeMarkerBorderColor;
307 private int mViewStartX;
308 private int mViewStartY;
309 private int mMaxViewStartY;
310 private int mBitmapHeight;
311 private int mViewHeight;
312 private int mViewWidth;
313 private int mGridAreaHeight;
314 private int mCellHeight;
315 private int mScrollStartY;
316 private int mPreviousDirection;
317 private int mPreviousDistanceX;
319 private int mHoursTextHeight;
320 private int mEventTextAscent;
321 private int mEventTextHeight;
322 private int mAllDayHeight;
323 private int mBannerPlusMargin;
324 private int mMaxAllDayEvents;
326 protected int mNumDays = 7;
327 private int mNumHours = 10;
328 private int mHoursWidth;
329 private int mDateStrWidth;
330 private int mFirstCell;
331 private int mFirstHour = -1;
332 private int mFirstHourOffset;
333 private String[] mHourStrs;
334 private String[] mDayStrs;
335 private String[] mDayStrs2Letter;
336 private boolean mIs24HourFormat;
338 private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN];
339 private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
340 private boolean mComputeSelectedEvents;
341 private Event mSelectedEvent;
342 private Event mPrevSelectedEvent;
343 private Rect mPrevBox = new Rect();
344 protected final Resources mResources;
345 private String mAmString;
346 private String mPmString;
347 private DeleteEventHelper mDeleteEventHelper;
349 private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
352 * The initial state of the touch mode when we enter this view.
354 private static final int TOUCH_MODE_INITIAL_STATE = 0;
357 * Indicates we just received the touch event and we are waiting to see if
358 * it is a tap or a scroll gesture.
360 private static final int TOUCH_MODE_DOWN = 1;
363 * Indicates the touch gesture is a vertical scroll
365 private static final int TOUCH_MODE_VSCROLL = 0x20;
368 * Indicates the touch gesture is a horizontal scroll
370 private static final int TOUCH_MODE_HSCROLL = 0x40;
372 private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
375 * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
377 private static final int SELECTION_HIDDEN = 0;
378 private static final int SELECTION_PRESSED = 1;
379 private static final int SELECTION_SELECTED = 2;
380 private static final int SELECTION_LONGPRESS = 3;
382 private int mSelectionMode = SELECTION_HIDDEN;
384 private boolean mScrolling = false;
386 private TimeZone mTimeZone;
387 private String mDateRange;
388 private TextView mTitleTextView;
390 // Accessibility support related members
392 private int mPrevSelectionDay;
393 private int mPrevSelectionHour;
394 private CharSequence mPrevTitleTextViewText;
395 private Bundle mTempEventBundle;
397 public CalendarView(CalendarActivity activity) {
400 mScale = getContext().getResources().getDisplayMetrics().density;
402 SINGLE_ALLDAY_HEIGHT *= mScale;
403 MAX_ALLDAY_HEIGHT *= mScale;
404 ALLDAY_TOP_MARGIN *= mScale;
405 MAX_ALLDAY_EVENT_HEIGHT *= mScale;
407 NORMAL_FONT_SIZE *= mScale;
408 EVENT_TEXT_FONT_SIZE *= mScale;
409 HOURS_FONT_SIZE *= mScale;
410 AMPM_FONT_SIZE *= mScale;
411 MIN_CELL_WIDTH_FOR_TEXT *= mScale;
412 MIN_EVENT_HEIGHT *= mScale;
414 HORIZONTAL_SCROLL_THRESHOLD *= mScale;
416 CURRENT_TIME_MARKER_HEIGHT *= mScale;
417 CURRENT_TIME_MARKER_WIDTH *= mScale;
418 CURRENT_TIME_LINE_HEIGHT *= mScale;
419 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale;
420 CURRENT_TIME_MARKER_INNER_WIDTH *= mScale;
421 CURRENT_TIME_LINE_SIDE_BUFFER *= mScale;
423 SMALL_ROUND_RADIUS *= mScale;
427 mResources = activity.getResources();
428 mEventLoader = activity.mEventLoader;
429 mEventGeometry = new EventGeometry();
430 mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
431 mEventGeometry.setHourGap(HOUR_GAP);
432 mParentActivity = activity;
433 mCalendarApp = (CalendarApplication) mParentActivity.getApplication();
434 mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */);
435 mLastPopupEventID = INVALID_EVENT_ID;
440 private void init(Context context) {
443 // Allow focus in touch mode so that we can do keyboard shortcuts
444 // even after we've entered touch mode.
445 setFocusableInTouchMode(true);
447 setOnCreateContextMenuListener(this);
449 mStartDay = Utils.getFirstDayOfWeek();
451 mTimeZone = TimeZone.getTimeZone(Utils.getTimeZone(context, mUpdateTZ));
454 mCurrentTime = new Time(Utils.getTimeZone(context, mUpdateTZ));
455 long currentTime = System.currentTimeMillis();
456 mCurrentTime.set(currentTime);
457 //The % makes it go off at the next increment of 5 minutes.
458 postDelayed(mUpdateCurrentTime,
459 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
460 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
462 mWeek_saturdayColor = mResources.getColor(R.color.week_saturday);
463 mWeek_sundayColor = mResources.getColor(R.color.week_sunday);
464 mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
465 mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background);
466 mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
467 mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background);
468 mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected);
469 mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background);
470 mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
471 mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color);
472 mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color);
473 mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background);
474 mCalendarHourLabel = mResources.getColor(R.color.calendar_hour_label);
475 mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected);
476 mSelectionColor = mResources.getColor(R.color.selection);
477 mPressedColor = mResources.getColor(R.color.pressed);
478 mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color);
479 mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
480 mCurrentTimeMarkerColor = mResources.getColor(R.color.current_time_marker);
481 mCurrentTimeLineColor = mResources.getColor(R.color.current_time_line);
482 mCurrentTimeMarkerBorderColor = mResources.getColor(R.color.current_time_marker_border);
483 mEventTextPaint.setColor(mEventTextColor);
484 mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
485 mEventTextPaint.setTextAlign(Paint.Align.LEFT);
486 mEventTextPaint.setAntiAlias(true);
488 int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
489 Paint p = mSelectionPaint;
490 p.setColor(gridLineColor);
491 p.setStyle(Style.STROKE);
492 p.setStrokeWidth(2.0f);
493 p.setAntiAlias(false);
496 p.setAntiAlias(true);
498 mPaintBorder.setColor(0xffc8c8c8);
499 mPaintBorder.setStyle(Style.STROKE);
500 mPaintBorder.setAntiAlias(true);
501 mPaintBorder.setStrokeWidth(2.0f);
503 // Allocate space for 2 weeks worth of weekday names so that we can
504 // easily start the week display at any week day.
505 mDayStrs = new String[14];
507 // Also create an array of 2-letter abbreviations.
508 mDayStrs2Letter = new String[14];
510 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
511 int index = i - Calendar.SUNDAY;
512 // e.g. Tue for Tuesday
513 mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM);
514 mDayStrs[index + 7] = mDayStrs[index];
515 // e.g. Tu for Tuesday
516 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT);
518 // If we don't have 2-letter day strings, fall back to 1-letter.
519 if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
520 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
523 mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
526 // Figure out how much space we need for the 3-letter abbrev names
527 // in the worst case.
528 p.setTextSize(NORMAL_FONT_SIZE);
529 p.setTypeface(mBold);
530 String[] dateStrs = {" 28", " 30"};
531 mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
532 mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
534 p.setTextSize(HOURS_FONT_SIZE);
536 updateIs24HourFormat();
538 mAmString = DateUtils.getAMPMString(Calendar.AM);
539 mPmString = DateUtils.getAMPMString(Calendar.PM);
540 String[] ampm = {mAmString, mPmString};
541 p.setTextSize(AMPM_FONT_SIZE);
542 mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p);
543 mHoursWidth += HOURS_MARGIN;
545 LayoutInflater inflater;
546 inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
547 mPopupView = inflater.inflate(R.layout.bubble_event, null);
548 mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
549 ViewGroup.LayoutParams.MATCH_PARENT,
550 ViewGroup.LayoutParams.WRAP_CONTENT));
551 mPopup = new PopupWindow(context);
552 mPopup.setContentView(mPopupView);
553 Resources.Theme dialogTheme = getResources().newTheme();
554 dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
555 TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
556 android.R.attr.windowBackground });
557 mPopup.setBackgroundDrawable(ta.getDrawable(0));
560 // Enable touching the popup window
561 mPopupView.setOnClickListener(this);
563 mBaseDate = new Time(Utils.getTimeZone(context, mUpdateTZ));
564 long millis = System.currentTimeMillis();
565 mBaseDate.set(millis);
567 mEarliestStartHour = new int[mNumDays];
568 mHasAllDayEvent = new boolean[mNumDays];
570 mNumHours = context.getResources().getInteger(R.integer.number_of_hours);
571 mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title);
575 * This is called when the popup window is pressed.
577 public void onClick(View v) {
578 if (v == mPopupView) {
579 // Pretend it was a trackball click because that will always
580 // jump to the "View event" screen.
581 switchViews(true /* trackball */);
585 public void updateIs24HourFormat() {
586 mIs24HourFormat = DateFormat.is24HourFormat(mParentActivity);
587 mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
591 * Returns the start of the selected time in milliseconds since the epoch.
593 * @return selected time in UTC milliseconds since the epoch.
595 long getSelectedTimeInMillis() {
596 Time time = new Time(mBaseDate);
597 time.setJulianDay(mSelectionDay);
598 time.hour = mSelectionHour;
600 // We ignore the "isDst" field because we want normalize() to figure
601 // out the correct DST value and not adjust the selected time based
602 // on the current setting of DST.
603 return time.normalize(true /* ignore isDst */);
606 Time getSelectedTime() {
607 Time time = new Time(mBaseDate);
608 time.setJulianDay(mSelectionDay);
609 time.hour = mSelectionHour;
611 // We ignore the "isDst" field because we want normalize() to figure
612 // out the correct DST value and not adjust the selected time based
613 // on the current setting of DST.
614 time.normalize(true /* ignore isDst */);
619 * Returns the start of the selected time in minutes since midnight,
620 * local time. The derived class must ensure that this is consistent
621 * with the return value from getSelectedTimeInMillis().
623 int getSelectedMinutesSinceMidnight() {
624 return mSelectionHour * MINUTES_PER_HOUR;
627 public void setSelectedDay(Time time) {
629 mSelectionHour = mBaseDate.hour;
630 mSelectedEvent = null;
631 mPrevSelectedEvent = null;
632 long millis = mBaseDate.toMillis(false /* use isDst */);
633 mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
634 mSelectedEvents.clear();
635 mComputeSelectedEvents = true;
637 // Force a recalculation of the first visible hour
640 mTitleTextView.setText(mDateRange);
642 // Force a redraw of the selection box.
643 mSelectionMode = SELECTION_SELECTED;
644 mRedrawScreen = true;
649 public Time getSelectedDay() {
650 Time time = new Time(mBaseDate);
651 time.setJulianDay(mSelectionDay);
652 time.hour = mSelectionHour;
654 // We ignore the "isDst" field because we want normalize() to figure
655 // out the correct DST value and not adjust the selected time based
656 // on the current setting of DST.
657 time.normalize(true /* ignore isDst */);
661 private void recalc() {
662 // Set the base date to the beginning of the week if we are displaying
665 int dayOfWeek = mBaseDate.weekDay;
666 int diff = dayOfWeek - mStartDay;
671 mBaseDate.monthDay -= diff;
672 mBaseDate.normalize(true /* ignore isDst */);
676 long start = mBaseDate.normalize(true /* use isDst */);
678 mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
679 mLastJulianDay = mFirstJulianDay + mNumDays - 1;
681 mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
682 mFirstDate = mBaseDate.monthDay;
684 int flags = DateUtils.FORMAT_SHOW_YEAR;
685 if (DateFormat.is24HourFormat(mParentActivity)) {
686 flags |= DateUtils.FORMAT_24HOUR;
689 mBaseDate.monthDay += mNumDays - 1;
690 end = mBaseDate.toMillis(true /* ignore isDst */);
691 mBaseDate.monthDay -= mNumDays - 1;
692 flags |= DateUtils.FORMAT_NO_MONTH_DAY;
694 flags |= DateUtils.FORMAT_SHOW_WEEKDAY
695 | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
698 mDateRange = Utils.formatDateRange(mParentActivity, start, end, flags);
700 if (!TextUtils.equals(Utils.getTimeZone(mContext, mUpdateTZ), Time.getCurrentTimezone())) {
701 flags = DateUtils.FORMAT_SHOW_TIME;
702 if (DateFormat.is24HourFormat(mParentActivity)) {
703 flags |= DateUtils.FORMAT_24HOUR;
705 start = System.currentTimeMillis();
707 String tz = Utils.getTimeZone(mContext, mUpdateTZ);
708 boolean isDST = mBaseDate.isDst != 0;
709 StringBuilder title = new StringBuilder(mDateRange);
710 title.append(" (").append(Utils.formatDateRange(mContext, start, start, flags))
712 .append(mTimeZone.getDisplayName(isDST, TimeZone.SHORT, Locale.getDefault()))
714 mDateRange = title.toString();
716 // Do not set the title here because this is called when executing
717 // initNextView() to prepare the Day view when sliding the finger
718 // horizontally but we don't always want to change the title. And
719 // if we change the title here and then change it back in the caller
720 // then we get an annoying flicker.
723 void setDetailedView(String detailedView) {
724 mDetailedView = detailedView;
728 protected void onSizeChanged(int width, int height, int oldw, int oldh) {
730 mViewHeight = height;
731 int gridAreaWidth = width - mHoursWidth;
732 mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
734 Paint p = new Paint();
735 p.setTextSize(NORMAL_FONT_SIZE);
736 int bannerTextHeight = (int) Math.abs(p.ascent());
738 p.setTextSize(HOURS_FONT_SIZE);
739 mHoursTextHeight = (int) Math.abs(p.ascent());
741 p.setTextSize(EVENT_TEXT_FONT_SIZE);
742 float ascent = -p.ascent();
743 mEventTextAscent = (int) Math.ceil(ascent);
744 float totalHeight = ascent + p.descent();
745 mEventTextHeight = (int) Math.ceil(totalHeight);
748 mBannerPlusMargin = bannerTextHeight + 14;
750 mBannerPlusMargin = 0;
753 remeasure(width, height);
756 // Measures the space needed for various parts of the view after
757 // loading new events. This can change if there are all-day events.
758 private void remeasure(int width, int height) {
760 // First, clear the array of earliest start times, and the array
761 // indicating presence of an all-day event.
762 for (int day = 0; day < mNumDays; day++) {
763 mEarliestStartHour[day] = 25; // some big number
764 mHasAllDayEvent[day] = false;
767 // Compute the space needed for the all-day events, if any.
768 // Make a pass over all the events, and keep track of the maximum
769 // number of all-day events in any one day. Also, keep track of
770 // the earliest event in each day.
771 int maxAllDayEvents = 0;
772 ArrayList<Event> events = mEvents;
773 int len = events.size();
774 for (int ii = 0; ii < len; ii++) {
775 Event event = events.get(ii);
776 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay)
779 int max = event.getColumn() + 1;
780 if (maxAllDayEvents < max) {
781 maxAllDayEvents = max;
783 int daynum = event.startDay - mFirstJulianDay;
784 int durationDays = event.endDay - event.startDay + 1;
786 durationDays += daynum;
789 if (daynum + durationDays > mNumDays) {
790 durationDays = mNumDays - daynum;
792 for (int day = daynum; durationDays > 0; day++, durationDays--) {
793 mHasAllDayEvent[day] = true;
796 int daynum = event.startDay - mFirstJulianDay;
797 int hour = event.startTime / 60;
798 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
799 mEarliestStartHour[daynum] = hour;
802 // Also check the end hour in case the event spans more than
804 daynum = event.endDay - mFirstJulianDay;
805 hour = event.endTime / 60;
806 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
807 mEarliestStartHour[daynum] = hour;
811 mMaxAllDayEvents = maxAllDayEvents;
813 mFirstCell = mBannerPlusMargin;
814 int allDayHeight = 0;
815 if (maxAllDayEvents > 0) {
816 // If there is at most one all-day event per day, then use less
817 // space (but more than the space for a single event).
818 if (maxAllDayEvents == 1) {
819 allDayHeight = SINGLE_ALLDAY_HEIGHT;
821 // Allow the all-day area to grow in height depending on the
822 // number of all-day events we need to show, up to a limit.
823 allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT;
824 if (allDayHeight > MAX_ALLDAY_HEIGHT) {
825 allDayHeight = MAX_ALLDAY_HEIGHT;
828 mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN;
830 mSelectionAllDay = false;
832 mAllDayHeight = allDayHeight;
834 mGridAreaHeight = height - mFirstCell;
835 mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours;
836 int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP;
837 int bottomSpace = mGridAreaHeight - usedGridAreaHeight;
838 mEventGeometry.setHourHeight(mCellHeight);
840 // Create an off-screen bitmap that we can draw into.
841 mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace;
842 if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 &&
844 if (mBitmap != null) {
847 mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565);
848 mCanvas = new Canvas(mBitmap);
850 mMaxViewStartY = mBitmapHeight - mGridAreaHeight;
852 if (mFirstHour == -1) {
854 mFirstHourOffset = 0;
857 // When we change the base date, the number of all-day events may
858 // change and that changes the cell height. When we switch dates,
859 // we use the mFirstHourOffset from the previous view, but that may
860 // be too large for the new view if the cell height is smaller.
861 if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
862 mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
864 mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
866 int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
867 //When we get new events we don't want to dismiss the popup unless the event changes
868 if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) {
871 mPopup.setWidth(eventAreaWidth - 20);
872 mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
876 * Initialize the state for another view. The given view is one that has
877 * its own bitmap and will use an animation to replace the current view.
878 * The current view and new view are either both Week views or both Day
879 * views. They differ in their base date.
881 * @param view the view to initialize.
883 private void initView(CalendarView view) {
884 view.mSelectionHour = mSelectionHour;
885 view.mSelectedEvents.clear();
886 view.mComputeSelectedEvents = true;
887 view.mFirstHour = mFirstHour;
888 view.mFirstHourOffset = mFirstHourOffset;
889 view.remeasure(getWidth(), getHeight());
891 view.mSelectedEvent = null;
892 view.mPrevSelectedEvent = null;
893 view.mStartDay = mStartDay;
894 if (view.mEvents.size() > 0) {
895 view.mSelectionAllDay = mSelectionAllDay;
897 view.mSelectionAllDay = false;
900 // Redraw the screen so that the selection box will be redrawn. We may
901 // have scrolled to a different part of the day in some other view
902 // so the selection box in this view may no longer be visible.
903 view.mRedrawScreen = true;
908 * Switch to another view based on what was selected (an event or a free
909 * slot) and how it was selected (by touch or by trackball).
911 * @param trackBallSelection true if the selection was made using the
914 private void switchViews(boolean trackBallSelection) {
915 Event selectedEvent = mSelectedEvent;
918 mLastPopupEventID = INVALID_EVENT_ID;
920 // This is the Week view.
921 // With touch, we always switch to Day/Agenda View
922 // With track ball, if we selected a free slot, then create an event.
923 // If we selected a specific event, switch to EventInfo view.
924 if (trackBallSelection) {
925 if (selectedEvent == null) {
926 // Switch to the EditEvent view
927 long startMillis = getSelectedTimeInMillis();
928 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
929 Intent intent = new Intent(Intent.ACTION_VIEW);
930 intent.setClassName(mParentActivity, EditEvent.class.getName());
931 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
932 intent.putExtra(EVENT_END_TIME, endMillis);
933 mParentActivity.startActivity(intent);
935 // Switch to the EventInfo view
936 Intent intent = new Intent(Intent.ACTION_VIEW);
937 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
939 intent.setData(eventUri);
940 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
941 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
942 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
943 mParentActivity.startActivity(intent);
946 // This was a touch selection. If the touch selected a single
947 // unambiguous event, then view that event. Otherwise go to
949 if (mSelectedEvents.size() == 1) {
950 // Switch to the EventInfo view
951 Intent intent = new Intent(Intent.ACTION_VIEW);
952 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
954 intent.setData(eventUri);
955 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
956 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
957 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
958 mParentActivity.startActivity(intent);
960 // Switch to the Day/Agenda view.
961 long millis = getSelectedTimeInMillis();
962 Utils.startActivity(mParentActivity, mDetailedView, millis);
966 // This is the Day view.
967 // If we selected a free slot, then create an event.
968 // If we selected an event, then go to the EventInfo view.
969 if (selectedEvent == null) {
970 // Switch to the EditEvent view
971 long startMillis = getSelectedTimeInMillis();
972 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
973 Intent intent = new Intent(Intent.ACTION_VIEW);
974 intent.setClassName(mParentActivity, EditEvent.class.getName());
975 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
976 intent.putExtra(EVENT_END_TIME, endMillis);
977 mParentActivity.startActivity(intent);
979 // Switch to the EventInfo view
980 Intent intent = new Intent(Intent.ACTION_VIEW);
981 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id);
982 intent.setData(eventUri);
983 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
984 intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
985 intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
986 mParentActivity.startActivity(intent);
992 public boolean onKeyUp(int keyCode, KeyEvent event) {
994 long duration = event.getEventTime() - event.getDownTime();
997 case KeyEvent.KEYCODE_DPAD_CENTER:
998 if (mSelectionMode == SELECTION_HIDDEN) {
999 // Don't do anything unless the selection is visible.
1003 if (mSelectionMode == SELECTION_PRESSED) {
1004 // This was the first press when there was nothing selected.
1005 // Change the selection from the "pressed" state to the
1006 // the "selected" state. We treat short-press and
1007 // long-press the same here because nothing was selected.
1008 mSelectionMode = SELECTION_SELECTED;
1009 mRedrawScreen = true;
1014 // Check the duration to determine if this was a short press
1015 if (duration < ViewConfiguration.getLongPressTimeout()) {
1016 switchViews(true /* trackball */);
1018 mSelectionMode = SELECTION_LONGPRESS;
1019 mRedrawScreen = true;
1024 case KeyEvent.KEYCODE_BACK:
1025 if (event.isTracking() && !event.isCanceled()) {
1027 mParentActivity.finish();
1032 return super.onKeyUp(keyCode, event);
1036 public boolean onKeyDown(int keyCode, KeyEvent event) {
1037 if (mSelectionMode == SELECTION_HIDDEN) {
1038 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
1039 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
1040 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
1041 // Display the selection box but don't move or select it
1042 // on this key press.
1043 mSelectionMode = SELECTION_SELECTED;
1044 mRedrawScreen = true;
1047 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
1048 // Display the selection box but don't select it
1049 // on this key press.
1050 mSelectionMode = SELECTION_PRESSED;
1051 mRedrawScreen = true;
1057 mSelectionMode = SELECTION_SELECTED;
1060 int selectionDay = mSelectionDay;
1063 case KeyEvent.KEYCODE_DEL:
1064 // Delete the selected event, if any
1065 Event selectedEvent = mSelectedEvent;
1066 if (selectedEvent == null) {
1070 mLastPopupEventID = INVALID_EVENT_ID;
1072 long begin = selectedEvent.startMillis;
1073 long end = selectedEvent.endMillis;
1074 long id = selectedEvent.id;
1075 mDeleteEventHelper.delete(begin, end, id, -1);
1077 case KeyEvent.KEYCODE_ENTER:
1078 switchViews(true /* trackball or keyboard */);
1080 case KeyEvent.KEYCODE_BACK:
1081 if (event.getRepeatCount() == 0) {
1082 event.startTracking();
1085 return super.onKeyDown(keyCode, event);
1086 case KeyEvent.KEYCODE_DPAD_LEFT:
1087 if (mSelectedEvent != null) {
1088 mSelectedEvent = mSelectedEvent.nextLeft;
1090 if (mSelectedEvent == null) {
1091 mLastPopupEventID = INVALID_EVENT_ID;
1097 case KeyEvent.KEYCODE_DPAD_RIGHT:
1098 if (mSelectedEvent != null) {
1099 mSelectedEvent = mSelectedEvent.nextRight;
1101 if (mSelectedEvent == null) {
1102 mLastPopupEventID = INVALID_EVENT_ID;
1108 case KeyEvent.KEYCODE_DPAD_UP:
1109 if (mSelectedEvent != null) {
1110 mSelectedEvent = mSelectedEvent.nextUp;
1112 if (mSelectedEvent == null) {
1113 mLastPopupEventID = INVALID_EVENT_ID;
1114 if (!mSelectionAllDay) {
1115 mSelectionHour -= 1;
1116 adjustHourSelection();
1117 mSelectedEvents.clear();
1118 mComputeSelectedEvents = true;
1124 case KeyEvent.KEYCODE_DPAD_DOWN:
1125 if (mSelectedEvent != null) {
1126 mSelectedEvent = mSelectedEvent.nextDown;
1128 if (mSelectedEvent == null) {
1129 mLastPopupEventID = INVALID_EVENT_ID;
1130 if (mSelectionAllDay) {
1131 mSelectionAllDay = false;
1134 adjustHourSelection();
1135 mSelectedEvents.clear();
1136 mComputeSelectedEvents = true;
1143 return super.onKeyDown(keyCode, event);
1146 if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
1148 CalendarView view = mParentActivity.getNextView();
1149 Time date = view.mBaseDate;
1150 date.set(mBaseDate);
1151 if (selectionDay < mFirstJulianDay) {
1152 date.monthDay -= mNumDays;
1155 date.monthDay += mNumDays;
1158 date.normalize(true /* ignore isDst */);
1159 view.mSelectionDay = selectionDay;
1162 mTitleTextView.setText(view.mDateRange);
1163 mParentActivity.switchViews(forward, 0, 0);
1166 mSelectionDay = selectionDay;
1167 mSelectedEvents.clear();
1168 mComputeSelectedEvents = true;
1171 mRedrawScreen = true;
1176 return super.onKeyDown(keyCode, event);
1179 // This is called after scrolling stops to move the selected hour
1180 // to the visible part of the screen.
1181 private void resetSelectedHour() {
1182 if (mSelectionHour < mFirstHour + 1) {
1183 mSelectionHour = mFirstHour + 1;
1184 mSelectedEvent = null;
1185 mSelectedEvents.clear();
1186 mComputeSelectedEvents = true;
1187 } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
1188 mSelectionHour = mFirstHour + mNumHours - 3;
1189 mSelectedEvent = null;
1190 mSelectedEvents.clear();
1191 mComputeSelectedEvents = true;
1195 private void initFirstHour() {
1196 mFirstHour = mSelectionHour - mNumHours / 2;
1197 if (mFirstHour < 0) {
1199 } else if (mFirstHour + mNumHours > 24) {
1200 mFirstHour = 24 - mNumHours;
1205 * Recomputes the first full hour that is visible on screen after the
1206 * screen is scrolled.
1208 private void computeFirstHour() {
1209 // Compute the first full hour that is visible on screen
1210 mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
1211 mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
1214 private void adjustHourSelection() {
1215 if (mSelectionHour < 0) {
1217 if (mMaxAllDayEvents > 0) {
1218 mPrevSelectedEvent = null;
1219 mSelectionAllDay = true;
1223 if (mSelectionHour > 23) {
1224 mSelectionHour = 23;
1227 // If the selected hour is at least 2 time slots from the top and
1228 // bottom of the screen, then don't scroll the view.
1229 if (mSelectionHour < mFirstHour + 1) {
1230 // If there are all-days events for the selected day but there
1231 // are no more normal events earlier in the day, then jump to
1232 // the all-day event area.
1233 // Exception 1: allow the user to scroll to 8am with the trackball
1234 // before jumping to the all-day event area.
1235 // Exception 2: if 12am is on screen, then allow the user to select
1236 // 12am before going up to the all-day event area.
1237 int daynum = mSelectionDay - mFirstJulianDay;
1238 if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
1239 && mFirstHour > 0 && mFirstHour < 8) {
1240 mPrevSelectedEvent = null;
1241 mSelectionAllDay = true;
1242 mSelectionHour = mFirstHour + 1;
1246 if (mFirstHour > 0) {
1248 mViewStartY -= (mCellHeight + HOUR_GAP);
1249 if (mViewStartY < 0) {
1256 if (mSelectionHour > mFirstHour + mNumHours - 3) {
1257 if (mFirstHour < 24 - mNumHours) {
1259 mViewStartY += (mCellHeight + HOUR_GAP);
1260 if (mViewStartY > mBitmapHeight - mGridAreaHeight) {
1261 mViewStartY = mBitmapHeight - mGridAreaHeight;
1264 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
1265 mViewStartY = mBitmapHeight - mGridAreaHeight;
1270 void clearCachedEvents() {
1271 mLastReloadMillis = 0;
1274 private Runnable mCancelCallback = new Runnable() {
1276 clearCachedEvents();
1280 void reloadEvents() {
1281 // Protect against this being called before this view has been
1283 if (mParentActivity == null) {
1287 mSelectedEvent = null;
1288 mPrevSelectedEvent = null;
1289 mSelectedEvents.clear();
1291 // The start date is the beginning of the week at 12am
1292 Time weekStart = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
1293 weekStart.set(mBaseDate);
1295 weekStart.minute = 0;
1296 weekStart.second = 0;
1297 long millis = weekStart.normalize(true /* ignore isDst */);
1299 // Avoid reloading events unnecessarily.
1300 if (millis == mLastReloadMillis) {
1303 mLastReloadMillis = millis;
1305 // load events in the background
1306 mParentActivity.startProgressSpinner();
1307 final ArrayList<Event> events = new ArrayList<Event>();
1308 mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() {
1312 mRedrawScreen = true;
1313 mComputeSelectedEvents = true;
1315 mParentActivity.stopProgressSpinner();
1318 }, mCancelCallback);
1322 protected void onDraw(Canvas canvas) {
1324 remeasure(getWidth(), getHeight());
1328 if (mRedrawScreen && mCanvas != null) {
1330 mRedrawScreen = false;
1333 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1335 if (mViewStartX > 0) {
1336 canvas.translate(mViewWidth - mViewStartX, 0);
1338 canvas.translate(-(mViewWidth + mViewStartX), 0);
1340 CalendarView nextView = mParentActivity.getNextView();
1342 // Prevent infinite recursive calls to onDraw().
1343 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
1345 nextView.onDraw(canvas);
1348 canvas.translate(-mViewStartX, 0);
1351 if (mBitmap != null) {
1352 drawCalendarView(canvas);
1355 // Draw the fixed areas (that don't scroll) directly to the canvas.
1356 drawAfterScroll(canvas);
1357 mComputeSelectedEvents = false;
1359 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
1363 sendAccessibilityEvents();
1366 private void drawCalendarView(Canvas canvas) {
1368 // Copy the scrollable region from the big bitmap to the canvas.
1369 Rect src = mSrcRect;
1370 Rect dest = mDestRect;
1372 src.top = mViewStartY;
1373 src.bottom = mViewStartY + mGridAreaHeight;
1375 src.right = mViewWidth;
1377 dest.top = mFirstCell;
1378 dest.bottom = mViewHeight;
1380 dest.right = mViewWidth;
1383 canvas.clipRect(dest);
1384 canvas.drawColor(0, PorterDuff.Mode.CLEAR);
1385 canvas.drawBitmap(mBitmap, src, dest, null);
1389 private void drawAfterScroll(Canvas canvas) {
1393 if (mMaxAllDayEvents != 0) {
1394 drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p);
1395 drawUpperLeftCorner(r, canvas, p);
1399 drawDayHeaderLoop(r, canvas, p);
1402 // Draw the AM and PM indicators if we're in 12 hour mode
1403 if (!mIs24HourFormat) {
1404 drawAmPm(canvas, p);
1407 // Update the popup window showing the event details, but only if
1408 // we are not scrolling and we have focus.
1409 if (!mScrolling && isFocused()) {
1410 updateEventDetails();
1414 // This isn't really the upper-left corner. It's the square area just
1415 // below the upper-left corner, above the hours and to the left of the
1417 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
1418 p.setColor(mCalendarHourBackground);
1419 r.top = mBannerPlusMargin;
1420 r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
1422 r.right = mHoursWidth;
1423 canvas.drawRect(r, p);
1426 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
1427 // Draw the horizontal day background banner
1428 p.setColor(mCalendarDateBannerBackground);
1430 r.bottom = mBannerPlusMargin;
1432 r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
1433 canvas.drawRect(r, p);
1435 // Fill the extra space on the right side with the default background
1437 r.right = mViewWidth;
1438 p.setColor(mCalendarGridAreaBackground);
1439 canvas.drawRect(r, p);
1441 // Draw a highlight on the selected day (if any), but only if we are
1442 // displaying more than one day.
1443 if (mSelectionMode != SELECTION_HIDDEN) {
1445 p.setColor(mCalendarDateSelected);
1447 r.bottom = mBannerPlusMargin;
1448 int daynum = mSelectionDay - mFirstJulianDay;
1449 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1450 r.right = r.left + mCellWidth;
1451 canvas.drawRect(r, p);
1455 p.setTextSize(NORMAL_FONT_SIZE);
1456 p.setTextAlign(Paint.Align.CENTER);
1457 int x = mHoursWidth;
1458 int deltaX = mCellWidth + DAY_GAP;
1459 int cell = mFirstJulianDay;
1462 if (mDateStrWidth < mCellWidth) {
1463 dayNames = mDayStrs;
1465 dayNames = mDayStrs2Letter;
1468 p.setTypeface(mBold);
1469 p.setAntiAlias(true);
1470 for (int day = 0; day < mNumDays; day++, cell++) {
1471 drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p);
1476 private void drawAmPm(Canvas canvas, Paint p) {
1477 p.setColor(mCalendarAmPmLabel);
1478 p.setTextSize(AMPM_FONT_SIZE);
1479 p.setTypeface(mBold);
1480 p.setAntiAlias(true);
1481 mPaint.setTextAlign(Paint.Align.RIGHT);
1482 String text = mAmString;
1483 if (mFirstHour >= 12) {
1486 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
1487 int right = mHoursWidth - HOURS_RIGHT_MARGIN;
1488 canvas.drawText(text, right, y, p);
1490 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
1491 // Also draw the "PM"
1493 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
1494 + 2 * mHoursTextHeight + HOUR_GAP;
1495 canvas.drawText(text, right, y, p);
1499 private void drawCurrentTimeMarker(int top, Canvas canvas, Paint p) {
1500 Rect r = new Rect();
1501 r.top = top - CURRENT_TIME_LINE_HEIGHT / 2;
1502 r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2;
1504 r.right = mHoursWidth;
1506 p.setColor(mCurrentTimeMarkerColor);
1507 canvas.drawRect(r, p);
1510 private void drawCurrentTimeLine(Rect r, int left, int top, Canvas canvas, Paint p) {
1511 //Do a white outline so it'll show up on a red event
1512 p.setColor(mCurrentTimeMarkerBorderColor);
1513 r.top = top - CURRENT_TIME_LINE_HEIGHT / 2 - CURRENT_TIME_LINE_BORDER_WIDTH;
1514 r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2 + CURRENT_TIME_LINE_BORDER_WIDTH;
1515 r.left = left + CURRENT_TIME_LINE_SIDE_BUFFER;
1516 r.right = r.left + mCellWidth - 2 * CURRENT_TIME_LINE_SIDE_BUFFER;
1517 canvas.drawRect(r, p);
1518 //Then draw the red line
1519 p.setColor(mCurrentTimeLineColor);
1520 r.top = top - CURRENT_TIME_LINE_HEIGHT / 2;
1521 r.bottom = top + CURRENT_TIME_LINE_HEIGHT / 2;
1522 canvas.drawRect(r, p);
1525 private void doDraw(Canvas canvas) {
1528 int lineY = mCurrentTime.hour*(mCellHeight + HOUR_GAP)
1529 + ((mCurrentTime.minute * mCellHeight) / 60)
1532 drawGridBackground(r, canvas, p);
1533 drawHours(r, canvas, p);
1536 int x = mHoursWidth;
1537 int deltaX = mCellWidth + DAY_GAP;
1538 int cell = mFirstJulianDay;
1539 for (int day = 0; day < mNumDays; day++, cell++) {
1540 drawEvents(cell, x, HOUR_GAP, canvas, p);
1542 if(cell == mTodayJulianDay) {
1543 //And the current time shows up somewhere on the screen
1544 if(lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
1545 //draw both the marker and the line
1546 drawCurrentTimeMarker(lineY, canvas, p);
1547 drawCurrentTimeLine(r, x, lineY, canvas, p);
1554 private void drawHours(Rect r, Canvas canvas, Paint p) {
1555 // Draw the background for the hour labels
1556 p.setColor(mCalendarHourBackground);
1558 r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP;
1560 r.right = mHoursWidth;
1561 canvas.drawRect(r, p);
1563 // Fill the bottom left corner with the default grid background
1565 r.bottom = mBitmapHeight;
1566 p.setColor(mCalendarGridAreaBackground);
1567 canvas.drawRect(r, p);
1569 // Draw a highlight on the selected hour (if needed)
1570 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
1571 p.setColor(mCalendarHourSelected);
1572 r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
1573 r.bottom = r.top + mCellHeight + 2 * HOUR_GAP;
1575 r.right = mHoursWidth;
1576 canvas.drawRect(r, p);
1578 boolean drawBorder = false;
1581 r.bottom -= HOUR_GAP;
1584 // Also draw the highlight on the grid
1585 p.setColor(mCalendarGridAreaSelected);
1586 int daynum = mSelectionDay - mFirstJulianDay;
1587 r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1588 r.right = r.left + mCellWidth;
1589 canvas.drawRect(r, p);
1591 // Draw a border around the highlighted grid hour.
1595 r.bottom -= HOUR_GAP;
1597 path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW);
1598 canvas.drawPath(path, mSelectionPaint);
1601 saveSelectionPosition(r.left, r.top, r.right, r.bottom);
1604 p.setColor(mCalendarHourLabel);
1605 p.setTextSize(HOURS_FONT_SIZE);
1606 p.setTypeface(mBold);
1607 p.setTextAlign(Paint.Align.RIGHT);
1608 p.setAntiAlias(true);
1610 int right = mHoursWidth - HOURS_RIGHT_MARGIN;
1611 int y = HOUR_GAP + mHoursTextHeight;
1613 for (int i = 0; i < 24; i++) {
1614 String time = mHourStrs[i];
1615 canvas.drawText(time, right, y, p);
1616 y += mCellHeight + HOUR_GAP;
1620 private void sendAccessibilityEvents() {
1621 if (!isShown() || !AccessibilityManager.getInstance(mContext).isEnabled()) {
1624 // if the title text has changed => announce period
1625 CharSequence titleTextViewText = mTitleTextView.getText();
1626 // intended use of identity comparison
1627 boolean titleChanged = titleTextViewText != mPrevTitleTextViewText;
1629 mPrevTitleTextViewText = titleTextViewText;
1630 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
1632 // if title or selection has changed => announce selection
1633 // Note: if the title has changed we want to send both events
1634 if (titleChanged || mPrevSelectionDay != mSelectionDay
1635 || mPrevSelectionHour != mSelectionHour) {
1636 mPrevSelectionDay = mSelectionDay;
1637 mPrevSelectionHour = mSelectionHour;
1638 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
1643 public void sendAccessibilityEvent(int eventType) {
1644 // we send only selection events since semantically we select
1645 // certain element and not always this view gets focus which
1646 // triggers firing of a focus accessibility event
1647 if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
1650 super.sendAccessibilityEvent(eventType);
1654 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1655 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
1656 // add the currently shown period (day/week)
1657 if (mNumDays == 1) {
1658 // for daily view the title has enough context information
1659 event.getText().add(mTitleTextView.getText());
1661 // since the title view does not contain enough context we
1662 // compute a more descriptive title for the shown time frame
1663 int flags = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH
1664 | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
1665 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
1666 if (DateFormat.is24HourFormat(mParentActivity)) {
1667 flags |= DateUtils.FORMAT_24HOUR;
1670 long start = mBaseDate.toMillis(false);
1671 long gmtOff = mBaseDate.gmtoff;
1672 int firstJulianDay = Time.getJulianDay(start, gmtOff);
1674 Time time = new Time(mBaseDate);
1675 time.setJulianDay(firstJulianDay);
1676 long startTime = time.normalize(true);
1677 time.setJulianDay(firstJulianDay + mNumDays);
1678 long endTime = time.normalize(true);
1680 String timeRange = Utils.formatDateRange(mParentActivity, startTime, endTime,
1682 event.getText().add(timeRange);
1684 } else if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
1686 // add the selection
1687 if (mNumDays == 1) {
1688 // if day view we need only hour information
1689 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
1691 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
1692 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
1694 long startTime = getSelectedTimeInMillis();
1695 long endTime = startTime + MILLIS_PER_HOUR;
1696 if (DateFormat.is24HourFormat(mParentActivity)) {
1697 flags |= DateUtils.FORMAT_24HOUR;
1699 String timeRange = Utils.formatDateRange(mParentActivity, startTime, endTime,
1701 event.getText().add(timeRange);
1703 // add the selected event data if such
1704 if (mSelectedEvent != null) {
1705 Event selectedEvent = mSelectedEvent;
1706 if (mTempEventBundle == null) {
1707 mTempEventBundle = new Bundle();
1709 Bundle bundle = mTempEventBundle;
1711 bundle.putLong("id", selectedEvent.id);
1712 bundle.putInt("color", selectedEvent.color);
1713 bundle.putCharSequence("title", selectedEvent.title);
1714 bundle.putCharSequence("location", selectedEvent.location);
1715 bundle.putBoolean("allDay", selectedEvent.allDay);
1716 bundle.putInt("startDay", selectedEvent.startDay);
1717 bundle.putInt("endDay", selectedEvent.endDay);
1718 bundle.putInt("startTime", selectedEvent.startTime);
1719 bundle.putInt("endTime", selectedEvent.endTime);
1720 bundle.putLong("startMillis", selectedEvent.startMillis);
1721 bundle.putLong("endMillis", selectedEvent.endMillis);
1722 bundle.putString("organizer", selectedEvent.organizer);
1723 bundle.putBoolean("guestsCanModify", selectedEvent.guestsCanModify);
1724 event.setParcelableData(bundle);
1728 // add day event count, events for same hour count and
1729 // the index of the selected event for the same hour
1730 int todayEventCount = 0;
1731 int sameHourEventCount = 0;
1732 int currentSameHourEventIndex = 0;
1733 int selectionHourStart = mSelectionHour * MINUTES_PER_HOUR;
1734 int selectionHourEnd = selectionHourStart + MINUTES_PER_HOUR;
1735 for (int i = 0, count = mEvents.size(); i < count; i++) {
1736 Event calendarEvent = mEvents.get(i);
1737 if (calendarEvent.endDay == mSelectionDay) {
1739 if (selectionHourStart >= calendarEvent.endTime
1740 || selectionHourEnd <= calendarEvent.startTime) {
1743 if (calendarEvent == mSelectedEvent) {
1744 currentSameHourEventIndex = sameHourEventCount;
1746 sameHourEventCount++;
1749 event.setAddedCount(todayEventCount);
1750 event.setItemCount(sameHourEventCount);
1751 event.setCurrentItemIndex(currentSameHourEventIndex);
1756 private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
1757 float xCenter = x + mCellWidth / 2.0f;
1759 if (Utils.isSaturday(day, mStartDay)) {
1760 p.setColor(mWeek_saturdayColor);
1761 } else if (Utils.isSunday(day, mStartDay)) {
1762 p.setColor(mWeek_sundayColor);
1764 p.setColor(mCalendarDateBannerTextColor);
1767 int dateNum = mFirstDate + day;
1768 if (dateNum > mMonthLength) {
1769 dateNum -= mMonthLength;
1773 // Add a leading zero if the date is a single digit
1775 dateNumStr = "0" + dateNum;
1777 dateNumStr = String.valueOf(dateNum);
1780 DayHeader header = dayHeaders[day];
1781 if (header == null || header.cell != cell) {
1782 // The day header string is regenerated on every draw during drag and fling animation.
1783 // Caching day header since formatting the string takes surprising long time.
1785 dayHeaders[day] = new DayHeader();
1786 dayHeaders[day].cell = cell;
1787 dayHeaders[day].dateString = getResources().getString(
1788 R.string.weekday_day, dateStr, dateNumStr);
1790 dateStr = dayHeaders[day].dateString;
1792 float y = mBannerPlusMargin - 7;
1793 canvas.drawText(dateStr, xCenter, y, p);
1796 private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
1797 Paint.Style savedStyle = p.getStyle();
1799 // Clear the background
1800 p.setColor(mCalendarGridAreaBackground);
1802 r.bottom = mBitmapHeight;
1804 r.right = mViewWidth;
1805 canvas.drawRect(r, p);
1807 // Draw the horizontal grid lines
1808 p.setColor(mCalendarGridLineHorizontalColor);
1809 p.setStyle(Style.STROKE);
1810 p.setStrokeWidth(0);
1811 p.setAntiAlias(false);
1812 float startX = mHoursWidth;
1813 float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays;
1815 float deltaY = mCellHeight + HOUR_GAP;
1816 for (int hour = 0; hour <= 24; hour++) {
1817 canvas.drawLine(startX, y, stopX, y, p);
1821 // Draw the vertical grid lines
1822 p.setColor(mCalendarGridLineVerticalColor);
1824 float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
1825 float deltaX = mCellWidth + DAY_GAP;
1826 float x = mHoursWidth + mCellWidth;
1827 for (int day = 0; day < mNumDays; day++) {
1828 canvas.drawLine(x, startY, x, stopY, p);
1832 // Restore the saved style.
1833 p.setStyle(savedStyle);
1834 p.setAntiAlias(true);
1837 Event getSelectedEvent() {
1838 if (mSelectedEvent == null) {
1839 // There is no event at the selected hour, so create a new event.
1840 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
1841 getSelectedMinutesSinceMidnight());
1843 return mSelectedEvent;
1846 boolean isEventSelected() {
1847 return (mSelectedEvent != null);
1850 Event getNewEvent() {
1851 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
1852 getSelectedMinutesSinceMidnight());
1855 static Event getNewEvent(int julianDay, long utcMillis,
1856 int minutesSinceMidnight) {
1857 Event event = Event.newInstance();
1858 event.startDay = julianDay;
1859 event.endDay = julianDay;
1860 event.startMillis = utcMillis;
1861 event.endMillis = event.startMillis + MILLIS_PER_HOUR;
1862 event.startTime = minutesSinceMidnight;
1863 event.endTime = event.startTime + MINUTES_PER_HOUR;
1867 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
1868 float maxWidthF = 0.0f;
1870 int len = strings.length;
1871 for (int i = 0; i < len; i++) {
1872 float width = p.measureText(strings[i]);
1873 maxWidthF = Math.max(width, maxWidthF);
1875 int maxWidth = (int) (maxWidthF + 0.5);
1876 if (maxWidth < currentMax) {
1877 maxWidth = currentMax;
1882 private void saveSelectionPosition(float left, float top, float right, float bottom) {
1883 mPrevBox.left = (int) left;
1884 mPrevBox.right = (int) right;
1885 mPrevBox.top = (int) top;
1886 mPrevBox.bottom = (int) bottom;
1889 private Rect getCurrentSelectionPosition() {
1890 Rect box = new Rect();
1891 box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
1892 box.bottom = box.top + mCellHeight + HOUR_GAP;
1893 int daynum = mSelectionDay - mFirstJulianDay;
1894 box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
1895 box.right = box.left + mCellWidth + DAY_GAP;
1899 private void drawAllDayEvents(int firstDay, int numDays,
1900 Rect r, Canvas canvas, Paint p) {
1901 p.setTextSize(NORMAL_FONT_SIZE);
1902 p.setTextAlign(Paint.Align.LEFT);
1903 Paint eventTextPaint = mEventTextPaint;
1905 // Draw the background for the all-day events area
1906 r.top = mBannerPlusMargin;
1907 r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
1908 r.left = mHoursWidth;
1909 r.right = r.left + mNumDays * (mCellWidth + DAY_GAP);
1910 p.setColor(mCalendarAllDayBackground);
1911 canvas.drawRect(r, p);
1913 // Fill the extra space on the right side with the default background
1915 r.right = mViewWidth;
1916 p.setColor(mCalendarGridAreaBackground);
1917 canvas.drawRect(r, p);
1919 // Draw the vertical grid lines
1920 p.setColor(mCalendarGridLineVerticalColor);
1921 p.setStyle(Style.STROKE);
1922 p.setStrokeWidth(0);
1923 p.setAntiAlias(false);
1924 float startY = r.top;
1925 float stopY = r.bottom;
1926 float deltaX = mCellWidth + DAY_GAP;
1927 float x = mHoursWidth + mCellWidth;
1928 for (int day = 0; day <= mNumDays; day++) {
1929 canvas.drawLine(x, startY, x, stopY, p);
1932 p.setAntiAlias(true);
1933 p.setStyle(Style.FILL);
1935 int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
1936 float left = mHoursWidth;
1937 int lastDay = firstDay + numDays - 1;
1938 ArrayList<Event> events = mEvents;
1939 int numEvents = events.size();
1940 float drawHeight = mAllDayHeight;
1941 float numRectangles = mMaxAllDayEvents;
1942 for (int i = 0; i < numEvents; i++) {
1943 Event event = events.get(i);
1946 int startDay = event.startDay;
1947 int endDay = event.endDay;
1948 if (startDay > lastDay || endDay < firstDay)
1950 if (startDay < firstDay)
1951 startDay = firstDay;
1952 if (endDay > lastDay)
1954 int startIndex = startDay - firstDay;
1955 int endIndex = endDay - firstDay;
1956 float height = drawHeight / numRectangles;
1958 // Prevent a single event from getting too big
1959 if (height > MAX_ALLDAY_EVENT_HEIGHT) {
1960 height = MAX_ALLDAY_EVENT_HEIGHT;
1963 // Leave a one-pixel space between the vertical day lines and the
1965 event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2;
1966 event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1;
1967 event.top = y + height * event.getColumn();
1969 // Multiply the height by 0.9 to leave a little gap between events
1970 event.bottom = event.top + height * 0.9f;
1972 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
1973 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
1975 // Check if this all-day event intersects the selected day
1976 if (mSelectionAllDay && mComputeSelectedEvents) {
1977 if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
1978 mSelectedEvents.add(event);
1983 if (mSelectionAllDay) {
1984 // Compute the neighbors for the list of all-day events that
1985 // intersect the selected day.
1986 computeAllDayNeighbors();
1987 if (mSelectedEvent != null) {
1988 Event event = mSelectedEvent;
1989 RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
1990 drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
1993 // Draw the highlight on the selected all-day area
1994 float top = mBannerPlusMargin + 1;
1995 float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1;
1996 int daynum = mSelectionDay - mFirstJulianDay;
1997 left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1;
1998 float right = left + mCellWidth + DAY_GAP - 1;
1999 if (mNumDays == 1) {
2000 // The Day view doesn't have a vertical line on the right.
2005 path.addRect(left, top, right, bottom, Direction.CW);
2006 canvas.drawPath(path, mSelectionPaint);
2008 // Set the selection position to zero so that when we move down
2009 // to the normal event area, we will highlight the topmost event.
2010 saveSelectionPosition(0f, 0f, 0f, 0f);
2014 private void computeAllDayNeighbors() {
2015 int len = mSelectedEvents.size();
2016 if (len == 0 || mSelectedEvent != null) {
2020 // First, clear all the links
2021 for (int ii = 0; ii < len; ii++) {
2022 Event ev = mSelectedEvents.get(ii);
2026 ev.nextRight = null;
2029 // For each event in the selected event list "mSelectedEvents", find
2030 // its neighbors in the up and down directions. This could be done
2031 // more efficiently by sorting on the Event.getColumn() field, but
2032 // the list is expected to be very small.
2034 // Find the event in the same row as the previously selected all-day
2036 int startPosition = -1;
2037 if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) {
2038 startPosition = mPrevSelectedEvent.getColumn();
2040 int maxPosition = -1;
2041 Event startEvent = null;
2042 Event maxPositionEvent = null;
2043 for (int ii = 0; ii < len; ii++) {
2044 Event ev = mSelectedEvents.get(ii);
2045 int position = ev.getColumn();
2046 if (position == startPosition) {
2048 } else if (position > maxPosition) {
2049 maxPositionEvent = ev;
2050 maxPosition = position;
2052 for (int jj = 0; jj < len; jj++) {
2056 Event neighbor = mSelectedEvents.get(jj);
2057 int neighborPosition = neighbor.getColumn();
2058 if (neighborPosition == position - 1) {
2059 ev.nextUp = neighbor;
2060 } else if (neighborPosition == position + 1) {
2061 ev.nextDown = neighbor;
2065 if (startEvent != null) {
2066 mSelectedEvent = startEvent;
2068 mSelectedEvent = maxPositionEvent;
2072 RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
2073 // If this event is selected, then use the selection color
2074 if (mSelectedEvent == event) {
2075 // Also, remember the last selected event that we drew
2076 mPrevSelectedEvent = event;
2077 p.setColor(mSelectionColor);
2078 eventTextPaint.setColor(mSelectedEventTextColor);
2080 // Use the normal color for all-day events
2081 p.setColor(event.color);
2082 eventTextPaint.setColor(mEventTextColor);
2087 rf.bottom = event.bottom;
2088 rf.left = event.left;
2089 rf.right = event.right;
2090 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
2097 private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
2098 Paint eventTextPaint = mEventTextPaint;
2099 int cellWidth = mCellWidth;
2100 int cellHeight = mCellHeight;
2102 // Use the selected hour as the selection region
2103 Rect selectionArea = mRect;
2104 selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
2105 selectionArea.bottom = selectionArea.top + cellHeight;
2106 selectionArea.left = left;
2107 selectionArea.right = selectionArea.left + cellWidth;
2109 ArrayList<Event> events = mEvents;
2110 int numEvents = events.size();
2111 EventGeometry geometry = mEventGeometry;
2113 for (int i = 0; i < numEvents; i++) {
2114 Event event = events.get(i);
2115 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
2119 if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents
2120 && geometry.eventIntersectsSelection(event, selectionArea)) {
2121 mSelectedEvents.add(event);
2124 RectF rf = drawEventRect(event, canvas, p, eventTextPaint);
2125 drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
2128 if (date == mSelectionDay && !mSelectionAllDay && isFocused()
2129 && mSelectionMode != SELECTION_HIDDEN) {
2131 if (mSelectedEvent != null) {
2132 RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint);
2133 drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
2138 // Computes the "nearest" neighbor event in four directions (left, right,
2139 // up, down) for each of the events in the mSelectedEvents array.
2140 private void computeNeighbors() {
2141 int len = mSelectedEvents.size();
2142 if (len == 0 || mSelectedEvent != null) {
2146 // First, clear all the links
2147 for (int ii = 0; ii < len; ii++) {
2148 Event ev = mSelectedEvents.get(ii);
2152 ev.nextRight = null;
2155 Event startEvent = mSelectedEvents.get(0);
2156 int startEventDistance1 = 100000; // any large number
2157 int startEventDistance2 = 100000; // any large number
2158 int prevLocation = FROM_NONE;
2164 Rect box = getCurrentSelectionPosition();
2165 if (mPrevSelectedEvent != null) {
2166 prevTop = (int) mPrevSelectedEvent.top;
2167 prevBottom = (int) mPrevSelectedEvent.bottom;
2168 prevLeft = (int) mPrevSelectedEvent.left;
2169 prevRight = (int) mPrevSelectedEvent.right;
2170 // Check if the previously selected event intersects the previous
2171 // selection box. (The previously selected event may be from a
2172 // much older selection box.)
2173 if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
2174 || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
2175 mPrevSelectedEvent = null;
2176 prevTop = mPrevBox.top;
2177 prevBottom = mPrevBox.bottom;
2178 prevLeft = mPrevBox.left;
2179 prevRight = mPrevBox.right;
2181 // Clip the top and bottom to the previous selection box.
2182 if (prevTop < mPrevBox.top) {
2183 prevTop = mPrevBox.top;
2185 if (prevBottom > mPrevBox.bottom) {
2186 prevBottom = mPrevBox.bottom;
2190 // Just use the previously drawn selection box
2191 prevTop = mPrevBox.top;
2192 prevBottom = mPrevBox.bottom;
2193 prevLeft = mPrevBox.left;
2194 prevRight = mPrevBox.right;
2197 // Figure out where we came from and compute the center of that area.
2198 if (prevLeft >= box.right) {
2199 // The previously selected event was to the right of us.
2200 prevLocation = FROM_RIGHT;
2201 prevCenter = (prevTop + prevBottom) / 2;
2202 } else if (prevRight <= box.left) {
2203 // The previously selected event was to the left of us.
2204 prevLocation = FROM_LEFT;
2205 prevCenter = (prevTop + prevBottom) / 2;
2206 } else if (prevBottom <= box.top) {
2207 // The previously selected event was above us.
2208 prevLocation = FROM_ABOVE;
2209 prevCenter = (prevLeft + prevRight) / 2;
2210 } else if (prevTop >= box.bottom) {
2211 // The previously selected event was below us.
2212 prevLocation = FROM_BELOW;
2213 prevCenter = (prevLeft + prevRight) / 2;
2216 // For each event in the selected event list "mSelectedEvents", search
2217 // all the other events in that list for the nearest neighbor in 4
2219 for (int ii = 0; ii < len; ii++) {
2220 Event ev = mSelectedEvents.get(ii);
2222 int startTime = ev.startTime;
2223 int endTime = ev.endTime;
2224 int left = (int) ev.left;
2225 int right = (int) ev.right;
2226 int top = (int) ev.top;
2227 if (top < box.top) {
2230 int bottom = (int) ev.bottom;
2231 if (bottom > box.bottom) {
2232 bottom = box.bottom;
2235 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
2236 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2237 if (DateFormat.is24HourFormat(mParentActivity)) {
2238 flags |= DateUtils.FORMAT_24HOUR;
2240 String timeRange = Utils.formatDateRange(mParentActivity,
2241 ev.startMillis, ev.endMillis, flags);
2242 Log.i("Cal", "left: " + left + " right: " + right + " top: " + top
2243 + " bottom: " + bottom + " ev: " + timeRange + " " + ev.title);
2245 int upDistanceMin = 10000; // any large number
2246 int downDistanceMin = 10000; // any large number
2247 int leftDistanceMin = 10000; // any large number
2248 int rightDistanceMin = 10000; // any large number
2249 Event upEvent = null;
2250 Event downEvent = null;
2251 Event leftEvent = null;
2252 Event rightEvent = null;
2254 // Pick the starting event closest to the previously selected event,
2255 // if any. distance1 takes precedence over distance2.
2258 if (prevLocation == FROM_ABOVE) {
2259 if (left >= prevCenter) {
2260 distance1 = left - prevCenter;
2261 } else if (right <= prevCenter) {
2262 distance1 = prevCenter - right;
2264 distance2 = top - prevBottom;
2265 } else if (prevLocation == FROM_BELOW) {
2266 if (left >= prevCenter) {
2267 distance1 = left - prevCenter;
2268 } else if (right <= prevCenter) {
2269 distance1 = prevCenter - right;
2271 distance2 = prevTop - bottom;
2272 } else if (prevLocation == FROM_LEFT) {
2273 if (bottom <= prevCenter) {
2274 distance1 = prevCenter - bottom;
2275 } else if (top >= prevCenter) {
2276 distance1 = top - prevCenter;
2278 distance2 = left - prevRight;
2279 } else if (prevLocation == FROM_RIGHT) {
2280 if (bottom <= prevCenter) {
2281 distance1 = prevCenter - bottom;
2282 } else if (top >= prevCenter) {
2283 distance1 = top - prevCenter;
2285 distance2 = prevLeft - right;
2287 if (distance1 < startEventDistance1
2288 || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
2290 startEventDistance1 = distance1;
2291 startEventDistance2 = distance2;
2294 // For each neighbor, figure out if it is above or below or left
2295 // or right of me and compute the distance.
2296 for (int jj = 0; jj < len; jj++) {
2300 Event neighbor = mSelectedEvents.get(jj);
2301 int neighborLeft = (int) neighbor.left;
2302 int neighborRight = (int) neighbor.right;
2303 if (neighbor.endTime <= startTime) {
2304 // This neighbor is entirely above me.
2305 // If we overlap the same column, then compute the distance.
2306 if (neighborLeft < right && neighborRight > left) {
2307 int distance = startTime - neighbor.endTime;
2308 if (distance < upDistanceMin) {
2309 upDistanceMin = distance;
2311 } else if (distance == upDistanceMin) {
2312 int center = (left + right) / 2;
2313 int currentDistance = 0;
2314 int currentLeft = (int) upEvent.left;
2315 int currentRight = (int) upEvent.right;
2316 if (currentRight <= center) {
2317 currentDistance = center - currentRight;
2318 } else if (currentLeft >= center) {
2319 currentDistance = currentLeft - center;
2322 int neighborDistance = 0;
2323 if (neighborRight <= center) {
2324 neighborDistance = center - neighborRight;
2325 } else if (neighborLeft >= center) {
2326 neighborDistance = neighborLeft - center;
2328 if (neighborDistance < currentDistance) {
2329 upDistanceMin = distance;
2334 } else if (neighbor.startTime >= endTime) {
2335 // This neighbor is entirely below me.
2336 // If we overlap the same column, then compute the distance.
2337 if (neighborLeft < right && neighborRight > left) {
2338 int distance = neighbor.startTime - endTime;
2339 if (distance < downDistanceMin) {
2340 downDistanceMin = distance;
2341 downEvent = neighbor;
2342 } else if (distance == downDistanceMin) {
2343 int center = (left + right) / 2;
2344 int currentDistance = 0;
2345 int currentLeft = (int) downEvent.left;
2346 int currentRight = (int) downEvent.right;
2347 if (currentRight <= center) {
2348 currentDistance = center - currentRight;
2349 } else if (currentLeft >= center) {
2350 currentDistance = currentLeft - center;
2353 int neighborDistance = 0;
2354 if (neighborRight <= center) {
2355 neighborDistance = center - neighborRight;
2356 } else if (neighborLeft >= center) {
2357 neighborDistance = neighborLeft - center;
2359 if (neighborDistance < currentDistance) {
2360 downDistanceMin = distance;
2361 downEvent = neighbor;
2367 if (neighborLeft >= right) {
2368 // This neighbor is entirely to the right of me.
2369 // Take the closest neighbor in the y direction.
2370 int center = (top + bottom) / 2;
2372 int neighborBottom = (int) neighbor.bottom;
2373 int neighborTop = (int) neighbor.top;
2374 if (neighborBottom <= center) {
2375 distance = center - neighborBottom;
2376 } else if (neighborTop >= center) {
2377 distance = neighborTop - center;
2379 if (distance < rightDistanceMin) {
2380 rightDistanceMin = distance;
2381 rightEvent = neighbor;
2382 } else if (distance == rightDistanceMin) {
2383 // Pick the closest in the x direction
2384 int neighborDistance = neighborLeft - right;
2385 int currentDistance = (int) rightEvent.left - right;
2386 if (neighborDistance < currentDistance) {
2387 rightDistanceMin = distance;
2388 rightEvent = neighbor;
2391 } else if (neighborRight <= left) {
2392 // This neighbor is entirely to the left of me.
2393 // Take the closest neighbor in the y direction.
2394 int center = (top + bottom) / 2;
2396 int neighborBottom = (int) neighbor.bottom;
2397 int neighborTop = (int) neighbor.top;
2398 if (neighborBottom <= center) {
2399 distance = center - neighborBottom;
2400 } else if (neighborTop >= center) {
2401 distance = neighborTop - center;
2403 if (distance < leftDistanceMin) {
2404 leftDistanceMin = distance;
2405 leftEvent = neighbor;
2406 } else if (distance == leftDistanceMin) {
2407 // Pick the closest in the x direction
2408 int neighborDistance = left - neighborRight;
2409 int currentDistance = left - (int) leftEvent.right;
2410 if (neighborDistance < currentDistance) {
2411 leftDistanceMin = distance;
2412 leftEvent = neighbor;
2417 ev.nextUp = upEvent;
2418 ev.nextDown = downEvent;
2419 ev.nextLeft = leftEvent;
2420 ev.nextRight = rightEvent;
2422 mSelectedEvent = startEvent;
2426 private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
2428 int color = event.color;
2430 // Fade visible boxes if event was declined.
2431 boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
2433 int alpha = color & 0xff000000;
2434 color &= 0x00ffffff;
2435 int red = (color & 0x00ff0000) >> 16;
2436 int green = (color & 0x0000ff00) >> 8;
2437 int blue = (color & 0x0000ff);
2438 color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
2439 color += 0x7F7F7F + alpha;
2442 // If this event is selected, then use the selection color
2443 if (mSelectedEvent == event) {
2444 if (mSelectionMode == SELECTION_PRESSED || mSelectionMode == SELECTION_SELECTED) {
2445 // Also, remember the last selected event that we drew
2446 mPrevSelectedEvent = event;
2447 p.setColor(mSelectionColor);
2448 eventTextPaint.setColor(mSelectedEventTextColor);
2449 } else if (mSelectionMode == SELECTION_LONGPRESS) {
2450 p.setColor(mSelectionColor);
2451 eventTextPaint.setColor(mSelectedEventTextColor);
2454 eventTextPaint.setColor(mEventTextColor);
2458 eventTextPaint.setColor(mEventTextColor);
2464 rf.bottom = event.bottom;
2465 rf.left = event.left;
2466 rf.right = event.right - 1;
2468 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
2470 // Draw a darker border
2471 float[] hsv = new float[3];
2472 Color.colorToHSV(p.getColor(), hsv);
2475 mPaintBorder.setColor(Color.HSVToColor(hsv));
2476 canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, mPaintBorder);
2484 private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");
2486 // Sanitize a string before passing it to drawText or else we get little
2487 // squares. For newlines and tabs before a comma, delete the character.
2488 // Otherwise, just replace them with a space.
2489 private String drawTextSanitizer(String string) {
2490 Matcher m = drawTextSanitizerFilter.matcher(string);
2491 string = m.replaceAll(",").replace('\n', ' ').replace('\n', ' ');
2495 private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
2496 if (!mDrawTextInEventRect) {
2500 float width = rf.right - rf.left;
2501 float height = rf.bottom - rf.top;
2503 // Leave one pixel extra space between lines
2504 int lineHeight = mEventTextHeight + 1;
2506 // If the rectangle is too small for text, then return
2507 if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) {
2511 // Truncate the event title to a known (large enough) limit
2512 String text = event.getTitleAndLocation();
2514 text = drawTextSanitizer(text);
2516 int len = text.length();
2517 if (len > MAX_EVENT_TEXT_LEN) {
2518 text = text.substring(0, MAX_EVENT_TEXT_LEN);
2519 len = MAX_EVENT_TEXT_LEN;
2522 // Figure out how much space the event title will take, and create a
2523 // String fragment that will fit in the rectangle. Use multiple lines,
2525 p.getTextWidths(text, mCharWidths);
2526 String fragment = text;
2527 float top = rf.top + mEventTextAscent + topMargin;
2530 // Leave one pixel extra space at the bottom
2531 while (start < len && height >= (lineHeight + 1)) {
2532 boolean lastLine = (height < 2 * lineHeight + 1);
2533 // Skip leading spaces at the beginning of each line
2535 char c = text.charAt(start);
2536 if (c != ' ') break;
2538 } while (start < len);
2542 for (int ii = start; ii < len; ii++) {
2543 char c = text.charAt(ii);
2545 // If we found the end of a word, then remember the ending
2550 sum += mCharWidths[ii];
2551 // If adding this character would exceed the width and this
2552 // isn't the last line, then break the line at the previous
2553 // word. If there was no previous word, then break this word.
2555 if (end > start && !lastLine) {
2556 // There was a previous word on this line.
2557 fragment = text.substring(start, end);
2562 // This is the only word and it is too long to fit on
2563 // the line (or this is the last line), so take as many
2564 // characters of this word as will fit.
2565 fragment = text.substring(start, ii);
2571 // If sum <= width, then we can fit the rest of the text on
2574 fragment = text.substring(start, len);
2578 canvas.drawText(fragment, rf.left + 1, top, p);
2581 height -= lineHeight;
2585 private void updateEventDetails() {
2586 if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
2587 || mSelectionMode == SELECTION_LONGPRESS) {
2591 if (mLastPopupEventID == mSelectedEvent.id) {
2595 mLastPopupEventID = mSelectedEvent.id;
2597 // Remove any outstanding callbacks to dismiss the popup.
2598 getHandler().removeCallbacks(mDismissPopup);
2600 Event event = mSelectedEvent;
2601 TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
2602 titleView.setText(event.title);
2604 ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
2605 imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
2607 imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
2608 imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
2612 flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE |
2613 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
2615 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
2616 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
2617 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
2619 if (DateFormat.is24HourFormat(mParentActivity)) {
2620 flags |= DateUtils.FORMAT_24HOUR;
2622 String timeRange = Utils.formatDateRange(mParentActivity,
2623 event.startMillis, event.endMillis, flags);
2624 TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
2625 timeView.setText(timeRange);
2627 TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
2628 final boolean empty = TextUtils.isEmpty(event.location);
2629 whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
2630 if (!empty) whereView.setText(event.location);
2632 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
2633 postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
2635 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
2638 // The following routines are called from the parent activity when certain
2639 // touch events occur.
2641 void doDown(MotionEvent ev) {
2642 mTouchMode = TOUCH_MODE_DOWN;
2644 mOnFlingCalled = false;
2645 getHandler().removeCallbacks(mContinueScroll);
2648 void doSingleTapUp(MotionEvent ev) {
2649 int x = (int) ev.getX();
2650 int y = (int) ev.getY();
2651 int selectedDay = mSelectionDay;
2652 int selectedHour = mSelectionHour;
2654 boolean validPosition = setSelectionFromPosition(x, y);
2655 if (!validPosition) {
2656 // return if the touch wasn't on an area of concern
2660 mSelectionMode = SELECTION_SELECTED;
2661 mRedrawScreen = true;
2664 boolean launchNewView = false;
2665 if (mSelectedEvent != null) {
2666 // If the tap is on an event, launch the "View event" view
2667 launchNewView = true;
2668 } else if (mSelectedEvent == null && selectedDay == mSelectionDay
2669 && selectedHour == mSelectionHour) {
2670 // If the tap is on an already selected hour slot,
2671 // then launch the Day/Agenda view. Otherwise, just select the hour
2673 launchNewView = true;
2676 if (launchNewView) {
2677 switchViews(false /* not the trackball */);
2681 void doLongPress(MotionEvent ev) {
2682 int x = (int) ev.getX();
2683 int y = (int) ev.getY();
2685 boolean validPosition = setSelectionFromPosition(x, y);
2686 if (!validPosition) {
2687 // return if the touch wasn't on an area of concern
2691 mSelectionMode = SELECTION_LONGPRESS;
2692 mRedrawScreen = true;
2697 void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
2698 // Use the distance from the current point to the initial touch instead
2699 // of deltaX and deltaY to avoid accumulating floating-point rounding
2700 // errors. Also, we don't need floats, we can use ints.
2701 int distanceX = (int) e1.getX() - (int) e2.getX();
2702 int distanceY = (int) e1.getY() - (int) e2.getY();
2704 // If we haven't figured out the predominant scroll direction yet,
2706 if (mTouchMode == TOUCH_MODE_DOWN) {
2707 int absDistanceX = Math.abs(distanceX);
2708 int absDistanceY = Math.abs(distanceY);
2709 mScrollStartY = mViewStartY;
2710 mPreviousDistanceX = 0;
2711 mPreviousDirection = 0;
2713 // If the x distance is at least twice the y distance, then lock
2714 // the scroll horizontally. Otherwise scroll vertically.
2715 if (absDistanceX >= 2 * absDistanceY) {
2716 mTouchMode = TOUCH_MODE_HSCROLL;
2717 mViewStartX = distanceX;
2718 initNextView(-mViewStartX);
2720 mTouchMode = TOUCH_MODE_VSCROLL;
2722 } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2723 // We are already scrolling horizontally, so check if we
2724 // changed the direction of scrolling so that the other week
2726 mViewStartX = distanceX;
2727 if (distanceX != 0) {
2728 int direction = (distanceX > 0) ? 1 : -1;
2729 if (direction != mPreviousDirection) {
2730 // The user has switched the direction of scrolling
2731 // so re-init the next view
2732 initNextView(-mViewStartX);
2733 mPreviousDirection = direction;
2737 // If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
2738 // then change the title to the new day (or week), but only
2739 // if we haven't already changed the title.
2740 if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) {
2741 if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) {
2742 CalendarView view = mParentActivity.getNextView();
2743 mTitleTextView.setText(view.mDateRange);
2745 } else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
2746 if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) {
2747 CalendarView view = mParentActivity.getNextView();
2748 mTitleTextView.setText(view.mDateRange);
2751 if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD
2752 || mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
2753 mTitleTextView.setText(mDateRange);
2756 mPreviousDistanceX = distanceX;
2759 if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
2760 mViewStartY = mScrollStartY + distanceY;
2761 if (mViewStartY < 0) {
2763 } else if (mViewStartY > mMaxViewStartY) {
2764 mViewStartY = mMaxViewStartY;
2771 if (mSelectionMode != SELECTION_HIDDEN) {
2772 mSelectionMode = SELECTION_HIDDEN;
2773 mRedrawScreen = true;
2778 void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
2779 mTouchMode = TOUCH_MODE_INITIAL_STATE;
2780 mSelectionMode = SELECTION_HIDDEN;
2781 mOnFlingCalled = true;
2782 int deltaX = (int) e2.getX() - (int) e1.getX();
2783 int distanceX = Math.abs(deltaX);
2784 int deltaY = (int) e2.getY() - (int) e1.getY();
2785 int distanceY = Math.abs(deltaY);
2787 if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) {
2788 boolean switchForward = initNextView(deltaX);
2789 CalendarView view = mParentActivity.getNextView();
2790 mTitleTextView.setText(view.mDateRange);
2791 mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth);
2796 // Continue scrolling vertically
2797 mContinueScroll.init((int) velocityY / 20);
2798 post(mContinueScroll);
2801 private boolean initNextView(int deltaX) {
2802 // Change the view to the previous day or week
2803 CalendarView view = mParentActivity.getNextView();
2804 Time date = view.mBaseDate;
2805 date.set(mBaseDate);
2806 boolean switchForward;
2808 date.monthDay -= mNumDays;
2809 view.mSelectionDay = mSelectionDay - mNumDays;
2810 switchForward = false;
2812 date.monthDay += mNumDays;
2813 view.mSelectionDay = mSelectionDay + mNumDays;
2814 switchForward = true;
2816 date.normalize(true /* ignore isDst */);
2818 view.layout(getLeft(), getTop(), getRight(), getBottom());
2819 view.reloadEvents();
2820 return switchForward;
2824 public boolean onTouchEvent(MotionEvent ev) {
2825 int action = ev.getAction();
2828 case MotionEvent.ACTION_DOWN:
2829 mParentActivity.mGestureDetector.onTouchEvent(ev);
2832 case MotionEvent.ACTION_MOVE:
2833 mParentActivity.mGestureDetector.onTouchEvent(ev);
2836 case MotionEvent.ACTION_UP:
2837 mParentActivity.mGestureDetector.onTouchEvent(ev);
2838 if (mOnFlingCalled) {
2841 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
2842 mTouchMode = TOUCH_MODE_INITIAL_STATE;
2843 if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) {
2844 // The user has gone beyond the threshold so switch views
2845 mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth);
2849 // Not beyond the threshold so invalidate which will cause
2850 // the view to snap back. Also call recalc() to ensure
2851 // that we have the correct starting date and title.
2853 mTitleTextView.setText(mDateRange);
2859 // If we were scrolling, then reset the selected hour so that it
2863 resetSelectedHour();
2864 mRedrawScreen = true;
2869 // This case isn't expected to happen.
2870 case MotionEvent.ACTION_CANCEL:
2871 mParentActivity.mGestureDetector.onTouchEvent(ev);
2873 resetSelectedHour();
2877 if (mParentActivity.mGestureDetector.onTouchEvent(ev)) {
2880 return super.onTouchEvent(ev);
2884 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
2887 // If the trackball is held down, then the context menu pops up and
2888 // we never get onKeyUp() for the long-press. So check for it here
2889 // and change the selection to the long-press state.
2890 if (mSelectionMode != SELECTION_LONGPRESS) {
2891 mSelectionMode = SELECTION_LONGPRESS;
2892 mRedrawScreen = true;
2896 final long startMillis = getSelectedTimeInMillis();
2897 int flags = DateUtils.FORMAT_SHOW_TIME
2898 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT
2899 | DateUtils.FORMAT_SHOW_WEEKDAY;
2900 final String title = Utils.formatDateRange(mParentActivity, startMillis, startMillis,
2902 menu.setHeaderTitle(title);
2904 int numSelectedEvents = mSelectedEvents.size();
2905 if (mNumDays == 1) {
2908 // If there is a selected event, then allow it to be viewed and
2910 if (numSelectedEvents >= 1) {
2911 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
2912 item.setOnMenuItemClickListener(mContextMenuHandler);
2913 item.setIcon(android.R.drawable.ic_menu_info_details);
2915 int accessLevel = getEventAccessLevel(mParentActivity, mSelectedEvent);
2916 if (accessLevel == ACCESS_LEVEL_EDIT) {
2917 item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
2918 item.setOnMenuItemClickListener(mContextMenuHandler);
2919 item.setIcon(android.R.drawable.ic_menu_edit);
2920 item.setAlphabeticShortcut('e');
2923 if (accessLevel >= ACCESS_LEVEL_DELETE) {
2924 item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
2925 item.setOnMenuItemClickListener(mContextMenuHandler);
2926 item.setIcon(android.R.drawable.ic_menu_delete);
2929 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2930 item.setOnMenuItemClickListener(mContextMenuHandler);
2931 item.setIcon(android.R.drawable.ic_menu_add);
2932 item.setAlphabeticShortcut('n');
2934 // Otherwise, if the user long-pressed on a blank hour, allow
2935 // them to create an event. They can also do this by tapping.
2936 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2937 item.setOnMenuItemClickListener(mContextMenuHandler);
2938 item.setIcon(android.R.drawable.ic_menu_add);
2939 item.setAlphabeticShortcut('n');
2944 // If there is a selected event, then allow it to be viewed and
2946 if (numSelectedEvents >= 1) {
2947 item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
2948 item.setOnMenuItemClickListener(mContextMenuHandler);
2949 item.setIcon(android.R.drawable.ic_menu_info_details);
2951 int accessLevel = getEventAccessLevel(mParentActivity, mSelectedEvent);
2952 if (accessLevel == ACCESS_LEVEL_EDIT) {
2953 item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
2954 item.setOnMenuItemClickListener(mContextMenuHandler);
2955 item.setIcon(android.R.drawable.ic_menu_edit);
2956 item.setAlphabeticShortcut('e');
2959 if (accessLevel >= ACCESS_LEVEL_DELETE) {
2960 item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
2961 item.setOnMenuItemClickListener(mContextMenuHandler);
2962 item.setIcon(android.R.drawable.ic_menu_delete);
2965 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2966 item.setOnMenuItemClickListener(mContextMenuHandler);
2967 item.setIcon(android.R.drawable.ic_menu_add);
2968 item.setAlphabeticShortcut('n');
2970 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
2971 item.setOnMenuItemClickListener(mContextMenuHandler);
2972 item.setIcon(android.R.drawable.ic_menu_day);
2973 item.setAlphabeticShortcut('d');
2975 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
2976 item.setOnMenuItemClickListener(mContextMenuHandler);
2977 item.setIcon(android.R.drawable.ic_menu_agenda);
2978 item.setAlphabeticShortcut('a');
2980 // No events are selected
2981 item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
2982 item.setOnMenuItemClickListener(mContextMenuHandler);
2983 item.setIcon(android.R.drawable.ic_menu_add);
2984 item.setAlphabeticShortcut('n');
2986 item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
2987 item.setOnMenuItemClickListener(mContextMenuHandler);
2988 item.setIcon(android.R.drawable.ic_menu_day);
2989 item.setAlphabeticShortcut('d');
2991 item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
2992 item.setOnMenuItemClickListener(mContextMenuHandler);
2993 item.setIcon(android.R.drawable.ic_menu_agenda);
2994 item.setAlphabeticShortcut('a');
3001 private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
3002 public boolean onMenuItemClick(MenuItem item) {
3003 switch (item.getItemId()) {
3004 case MenuHelper.MENU_EVENT_VIEW: {
3005 if (mSelectedEvent != null) {
3006 long id = mSelectedEvent.id;
3007 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
3008 Intent intent = new Intent(Intent.ACTION_VIEW);
3009 intent.setData(eventUri);
3010 intent.setClassName(mParentActivity, EventInfoActivity.class.getName());
3011 intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
3012 intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
3013 mParentActivity.startActivity(intent);
3017 case MenuHelper.MENU_EVENT_EDIT: {
3018 if (mSelectedEvent != null) {
3019 long id = mSelectedEvent.id;
3020 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
3021 Intent intent = new Intent(Intent.ACTION_EDIT);
3022 intent.setData(eventUri);
3023 intent.setClassName(mParentActivity, EditEvent.class.getName());
3024 intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
3025 intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
3026 mParentActivity.startActivity(intent);
3030 case MenuHelper.MENU_DAY: {
3031 long startMillis = getSelectedTimeInMillis();
3032 Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
3035 case MenuHelper.MENU_AGENDA: {
3036 long startMillis = getSelectedTimeInMillis();
3037 Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
3040 case MenuHelper.MENU_EVENT_CREATE: {
3041 long startMillis = getSelectedTimeInMillis();
3042 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
3043 Intent intent = new Intent(Intent.ACTION_VIEW);
3044 intent.setClassName(mParentActivity, EditEvent.class.getName());
3045 intent.putExtra(EVENT_BEGIN_TIME, startMillis);
3046 intent.putExtra(EVENT_END_TIME, endMillis);
3047 intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay);
3048 mParentActivity.startActivity(intent);
3051 case MenuHelper.MENU_EVENT_DELETE: {
3052 if (mSelectedEvent != null) {
3053 Event selectedEvent = mSelectedEvent;
3054 long begin = selectedEvent.startMillis;
3055 long end = selectedEvent.endMillis;
3056 long id = selectedEvent.id;
3057 mDeleteEventHelper.delete(begin, end, id, -1);
3069 private static int getEventAccessLevel(Context context, Event e) {
3070 ContentResolver cr = context.getContentResolver();
3072 int visibility = Calendars.NO_ACCESS;
3073 int relationship = Attendees.RELATIONSHIP_ORGANIZER;
3075 // Get the calendar id for this event
3076 Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
3077 new String[] { Events.CALENDAR_ID },
3078 null /* selection */,
3079 null /* selectionArgs */,
3082 if (cursor == null) {
3083 return ACCESS_LEVEL_NONE;
3086 if (cursor.getCount() == 0) {
3088 return ACCESS_LEVEL_NONE;
3091 cursor.moveToFirst();
3092 long calId = cursor.getLong(0);
3095 Uri uri = Calendars.CONTENT_URI;
3096 String where = String.format(CALENDARS_WHERE, calId);
3097 cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
3099 String calendarOwnerAccount = null;
3100 if (cursor != null) {
3101 cursor.moveToFirst();
3102 visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
3103 calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
3107 if (visibility < Calendars.CONTRIBUTOR_ACCESS) {
3108 return ACCESS_LEVEL_NONE;
3111 if (e.guestsCanModify) {
3112 return ACCESS_LEVEL_EDIT;
3115 if (!TextUtils.isEmpty(calendarOwnerAccount) &&
3116 calendarOwnerAccount.equalsIgnoreCase(e.organizer)) {
3117 return ACCESS_LEVEL_EDIT;
3120 return ACCESS_LEVEL_DELETE;
3124 * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
3125 * If the touch position is not within the displayed grid, then this
3126 * method returns false.
3128 * @param x the x position of the touch
3129 * @param y the y position of the touch
3130 * @return true if the touch position is valid
3132 private boolean setSelectionFromPosition(int x, int y) {
3133 if (x < mHoursWidth) {
3137 int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
3138 if (day >= mNumDays) {
3141 day += mFirstJulianDay;
3143 if (y < mFirstCell + mFirstHourOffset) {
3144 mSelectionAllDay = true;
3146 hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
3148 mSelectionHour = hour;
3149 mSelectionAllDay = false;
3151 mSelectionDay = day;
3152 findSelectedEvent(x, y);
3153 // Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day
3154 // + " hour: " + hour
3155 // + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset);
3156 // if (mSelectedEvent != null) {
3157 // Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title);
3158 // for (Event ev : mSelectedEvents) {
3159 // int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
3160 // | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
3161 // String timeRange = Utils.formatDateRange(mParentActivity,
3162 // ev.startMillis, ev.endMillis, flags);
3164 // Log.i("Cal", " " + timeRange + " " + ev.title);
3170 private void findSelectedEvent(int x, int y) {
3171 int date = mSelectionDay;
3172 int cellWidth = mCellWidth;
3173 ArrayList<Event> events = mEvents;
3174 int numEvents = events.size();
3175 int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP);
3177 mSelectedEvent = null;
3179 mSelectedEvents.clear();
3180 if (mSelectionAllDay) {
3182 float minYdistance = 10000.0f; // any large number
3183 Event closestEvent = null;
3184 float drawHeight = mAllDayHeight;
3185 int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
3186 for (int i = 0; i < numEvents; i++) {
3187 Event event = events.get(i);
3188 if (!event.allDay) {
3192 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
3193 float numRectangles = event.getMaxColumns();
3194 float height = drawHeight / numRectangles;
3195 if (height > MAX_ALLDAY_EVENT_HEIGHT) {
3196 height = MAX_ALLDAY_EVENT_HEIGHT;
3198 float eventTop = yOffset + height * event.getColumn();
3199 float eventBottom = eventTop + height;
3200 if (eventTop < y && eventBottom > y) {
3201 // If the touch is inside the event rectangle, then
3203 mSelectedEvents.add(event);
3204 closestEvent = event;
3207 // Find the closest event
3208 if (eventTop >= y) {
3209 yDistance = eventTop - y;
3211 yDistance = y - eventBottom;
3213 if (yDistance < minYdistance) {
3214 minYdistance = yDistance;
3215 closestEvent = event;
3220 mSelectedEvent = closestEvent;
3224 // Adjust y for the scrollable bitmap
3225 y += mViewStartY - mFirstCell;
3227 // Use a region around (x,y) for the selection region
3228 Rect region = mRect;
3229 region.left = x - 10;
3230 region.right = x + 10;
3231 region.top = y - 10;
3232 region.bottom = y + 10;
3234 EventGeometry geometry = mEventGeometry;
3236 for (int i = 0; i < numEvents; i++) {
3237 Event event = events.get(i);
3238 // Compute the event rectangle.
3239 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
3243 // If the event intersects the selection region, then add it to
3245 if (geometry.eventIntersectsSelection(event, region)) {
3246 mSelectedEvents.add(event);
3250 // If there are any events in the selected region, then assign the
3251 // closest one to mSelectedEvent.
3252 if (mSelectedEvents.size() > 0) {
3253 int len = mSelectedEvents.size();
3254 Event closestEvent = null;
3255 float minDist = mViewWidth + mViewHeight; // some large distance
3256 for (int index = 0; index < len; index++) {
3257 Event ev = mSelectedEvents.get(index);
3258 float dist = geometry.pointToEvent(x, y, ev);
3259 if (dist < minDist) {
3264 mSelectedEvent = closestEvent;
3266 // Keep the selected hour and day consistent with the selected
3267 // event. They could be different if we touched on an empty hour
3268 // slot very close to an event in the previous hour slot. In
3269 // that case we will select the nearby event.
3270 int startDay = mSelectedEvent.startDay;
3271 int endDay = mSelectedEvent.endDay;
3272 if (mSelectionDay < startDay) {
3273 mSelectionDay = startDay;
3274 } else if (mSelectionDay > endDay) {
3275 mSelectionDay = endDay;
3278 int startHour = mSelectedEvent.startTime / 60;
3280 if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
3281 endHour = (mSelectedEvent.endTime - 1) / 60;
3283 endHour = mSelectedEvent.endTime / 60;
3286 if (mSelectionHour < startHour) {
3287 mSelectionHour = startHour;
3288 } else if (mSelectionHour > endHour) {
3289 mSelectionHour = endHour;
3294 // Encapsulates the code to continue the scrolling after the
3295 // finger is lifted. Instead of stopping the scroll immediately,
3296 // the scroll continues to "free spin" and gradually slows down.
3297 private class ContinueScroll implements Runnable {
3302 private static final float FRICTION_COEF = 0.7F;
3303 private static final long FREE_SPIN_MILLIS = 180;
3304 private static final int MAX_DELTA = 60;
3305 private static final int SCROLL_REPEAT_INTERVAL = 30;
3307 public void init(int deltaY) {
3311 } else if (deltaY < 0) {
3314 mAbsDeltaY = Math.abs(deltaY);
3316 // Limit the maximum speed
3317 if (mAbsDeltaY > MAX_DELTA) {
3318 mAbsDeltaY = MAX_DELTA;
3320 mFloatDeltaY = mAbsDeltaY;
3321 mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS;
3322 // Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY
3323 // + " mViewStartY: " + mViewStartY);
3327 long time = System.currentTimeMillis();
3329 // Start out with a frictionless "free spin"
3330 if (time > mFreeSpinTime) {
3331 // If the delta is small, then apply a fixed deceleration.
3333 if (mAbsDeltaY <= 10) {
3336 mFloatDeltaY *= FRICTION_COEF;
3337 mAbsDeltaY = (int) mFloatDeltaY;
3340 if (mAbsDeltaY < 0) {
3345 if (mSignDeltaY == 1) {
3346 mViewStartY -= mAbsDeltaY;
3348 mViewStartY += mAbsDeltaY;
3350 // Log.i("Cal", " scroll: mAbsDeltaY: " + mAbsDeltaY
3351 // + " mViewStartY: " + mViewStartY);
3353 if (mViewStartY < 0) {
3356 } else if (mViewStartY > mMaxViewStartY) {
3357 mViewStartY = mMaxViewStartY;
3363 if (mAbsDeltaY > 0) {
3364 postDelayed(this, SCROLL_REPEAT_INTERVAL);
3368 resetSelectedHour();
3369 mRedrawScreen = true;
3377 * Cleanup the pop-up and timers.
3379 public void cleanup() {
3380 // Protect against null-pointer exceptions
3381 if (mPopup != null) {
3384 mLastPopupEventID = INVALID_EVENT_ID;
3385 Handler handler = getHandler();
3386 if (handler != null) {
3387 handler.removeCallbacks(mDismissPopup);
3388 handler.removeCallbacks(mUpdateCurrentTime);
3393 mRedrawScreen = false;
3395 // clear the cached values for accessibility support
3396 mPrevSelectionDay = 0;
3397 mPrevSelectionHour = 0;
3398 mPrevTitleTextViewText = null;
3402 * Restart the update timer
3404 public void updateView() {
3406 post(mUpdateCurrentTime);
3409 @Override protected void onDetachedFromWindow() {
3411 if (mBitmap != null) {
3415 super.onDetachedFromWindow();
3418 class DismissPopup implements Runnable {
3420 // Protect against null-pointer exceptions
3421 if (mPopup != null) {
3427 class UpdateCurrentTime implements Runnable {
3429 long currentTime = System.currentTimeMillis();
3430 mCurrentTime.set(currentTime);
3431 //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
3432 postDelayed(mUpdateCurrentTime,
3433 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY));
3434 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff);
3435 mRedrawScreen = true;